// ============================================================
// Shared UI - FreshWave Digital Studio
// ============================================================
const { useState, useEffect, useRef } = React;
// Accent color helpers ----------------------------------------------------
const ACCENT_VAR = {
cyan: 'var(--cyan)',
magenta: 'var(--magenta)',
'vapor-purple': 'var(--vapor-purple)',
sunset: 'var(--sunset)',
};
const accentHsl = (a, alpha) =>
alpha == null ? `hsl(${ACCENT_VAR[a] || ACCENT_VAR.cyan})` : `hsl(${ACCENT_VAR[a] || ACCENT_VAR.cyan} / ${alpha})`;
const FW_CONFIG = window.FRESHWAVE_CONFIG || {};
const FW_THEME_URL = (FW_CONFIG.themeUrl || '/wp-content/themes/freshwave').replace(/\/$/, '');
const fwAsset = (path) => FW_THEME_URL ? `${FW_THEME_URL}/${path.replace(/^\//, '')}` : path;
// Navigate helper (hash router) -------------------------------------------
const go = (path) => { window.location.hash = path; };
// Click sparks -------------------------------------------------------------
const SPARK_COLORS = ['var(--cyan)', 'var(--magenta)', 'var(--vapor-purple)', 'var(--sunset)'];
function triggerButtonSparks(event, force = false) {
const target = event.currentTarget || event.target;
if (!target || target.disabled || target.getAttribute('aria-disabled') === 'true') return;
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
if (!force && Math.random() > 0.58) return;
const rect = target.getBoundingClientRect();
const x = event.clientX || rect.left + rect.width / 2;
const y = event.clientY || rect.top + rect.height / 2;
const burst = document.createElement('span');
burst.className = 'fw-spark-burst';
burst.style.left = `${x}px`;
burst.style.top = `${y}px`;
const count = force ? 10 : 5 + Math.floor(Math.random() * 4);
for (let i = 0; i < count; i += 1) {
const spark = document.createElement('i');
const angle = -165 + Math.random() * 330;
const color = SPARK_COLORS[Math.floor(Math.random() * SPARK_COLORS.length)];
spark.style.setProperty('--spark-color', `hsl(${color})`);
spark.style.setProperty('--spark-angle', `${angle}deg`);
spark.style.setProperty('--spark-distance', `${30 + Math.random() * 54}px`);
spark.style.setProperty('--spark-drop', `${-8 + Math.random() * 18}px`);
spark.style.setProperty('--spark-width', `${6 + Math.random() * 9}px`);
spark.style.setProperty('--spark-speed', `${520 + Math.random() * 180}ms`);
spark.style.setProperty('--spark-delay', `${Math.random() * 45}ms`);
burst.appendChild(spark);
}
document.body.appendChild(burst);
window.setTimeout(() => burst.remove(), 820);
}
// Minimal icon set (simple arrows / marks only) ---------------------------
function Icon({ name, className = 'h-4 w-4', stroke = 2, style }) {
const common = {
className, style, viewBox: '0 0 24 24', fill: 'none',
stroke: 'currentColor', strokeWidth: stroke, strokeLinecap: 'round', strokeLinejoin: 'round',
};
switch (name) {
case 'arrow-right': return ();
case 'arrow-up-right': return ();
case 'arrow-left': return ();
case 'check': return ();
case 'menu': return ();
case 'close': return ();
case 'spinner': return ();
case 'layers': return ();
default: return null;
}
}
// Button ------------------------------------------------------------------
function Button({ children, variant = 'default', size = 'md', className = '', as = 'button', onPointerDown, ...props }) {
const base = 'inline-flex items-center justify-center gap-2 rounded-md font-heading uppercase tracking-wider transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:opacity-50 disabled:pointer-events-none select-none';
const sizes = {
md: 'h-12 px-6 text-xs',
lg: 'h-14 px-8 text-sm',
sm: 'h-11 px-4 text-xs',
};
const variants = {
default: 'bg-primary text-primary-foreground glow-rest-cyan hover:shadow-[0_0_30px_hsl(var(--cyan)/0.7)] hover:-translate-y-0.5',
secondary: 'bg-secondary text-secondary-foreground glow-rest-magenta hover:shadow-[0_0_30px_hsl(var(--magenta)/0.7)] hover:-translate-y-0.5',
outline: 'border border-border bg-transparent text-foreground hover:border-primary hover:text-primary hover:shadow-[0_0_18px_hsl(var(--cyan)/0.35)]',
ghost: 'bg-transparent text-foreground hover:bg-muted',
};
const Tag = as;
const handlePointerDown = (event) => {
triggerButtonSparks(event);
if (onPointerDown) onPointerDown(event);
};
return (
{children}
);
}
// Striped placeholder (user drops photos later) ---------------------------
function Placeholder({ label, accent = 'cyan', className = '', glow = false, children }) {
return (
);
}
function LogoImage({ variant = 'emblem', alt = 'FreshWave logo', className = '' }) {
const [failed, setFailed] = useState(false);
const src = variant === 'full' ? fwAsset('assets/freshwave-logo.png') : fwAsset('assets/fw-emblem.png');
if (!failed) {
return
setFailed(true)} />;
}
if (variant === 'full') {
return (
FRESHWAVE
);
}
return (
FW
);
}
// Section eyebrow ---------------------------------------------------------
function Eyebrow({ children, accent = 'cyan' }) {
return (
{children}
);
}
const TRUST_POINTS = ['No payment today', 'Quote within 72h', 'Material help included'];
function TrustStrip({ className = '' }) {
return (
{TRUST_POINTS.map((point, idx) => (
{point}
))}
);
}
function BeforePrintPanel({ className = '' }) {
const checks = ['Price locked first', 'Timeline confirmed', 'Reference details checked'];
return (
FreshWave promise
Before we print
We confirm price, size, finish, timeline and reference details first. You only move forward when the quote feels right.
{checks.map((check, idx) => (
{check}
))}
);
}
// Header ------------------------------------------------------------------
const NAV = [
{ label: 'Process', path: '#/process' },
{ label: 'Favorites', path: '#/work' },
{ label: 'Sleeper Picks', path: '#/sleepers' },
{ label: 'About', path: '#/about' },
{ label: 'Contact', path: '#/contact' },
];
function Header({ route }) {
const [open, setOpen] = useState(false);
const headerNav = NAV;
const isActive = (p) => {
if (p === '#/work') return route === '/work';
if (p === '#/sleepers') return route === '/sleepers' || route.startsWith('/sleepers/');
if (p === '#/process') return route === '/process';
if (p === '#/about') return route === '/about';
if (p === '#/contact') return route === '/contact';
return false;
};
const navTo = (p) => { setOpen(false); go(p.replace('#', '')); };
return (
{open && (
)}
);
}
// Footer ------------------------------------------------------------------
function Footer() {
const connectEmail = FW_CONFIG.contactEmail || 'customerservice@freshwavefinds.shop';
const socialLinks = ['Instagram', 'TikTok', 'YouTube', 'Makerworld', 'Whatnot', 'Patreon', 'Discord', 'WhatsApp'];
return (
);
}
// Stats strip -------------------------------------------------------------
function StatsStrip() {
return (
);
}
// Service card ------------------------------------------------------------
function ServiceCard({ s, onOpen }) {
return (
);
}
// Project card ------------------------------------------------------------
function ProjectCard({ project, onOpen }) {
return (
);
}
// Material swatch ---------------------------------------------------------
function MaterialSwatch({ m }) {
return (
);
}
const INQUIRY_ENDPOINT = FW_CONFIG.inquiryEndpoint || 'api/submit-inquiry.php';
const INQUIRY_EMAIL = FW_CONFIG.contactEmail || 'customerservice@freshwavefinds.shop';
function inquirySubject(payload) {
const kind = payload.kind || 'inquiry';
const label = kind === 'preload' ? 'Sleeper item request' : kind === 'custom' ? 'Custom request' : 'Contact inquiry';
return `FreshWave ${label} - ${payload.name || 'New lead'}`;
}
function inquiryBody(payload) {
const ordered = [
['Type', payload.kind],
['Name', payload.name],
['Email', payload.email],
['Project', payload.project],
['Sleeper item', payload.preload],
['Category', payload.category],
['Material', payload.material],
['Finish', payload.finish],
['Size', payload.size],
['Quantity', payload.quantity],
['Budget', payload.budget],
['Estimate', payload.estimate],
['Message', payload.message],
['Notes', payload.notes],
];
return ordered
.filter(([, value]) => value !== undefined && value !== null && String(value).trim() !== '')
.map(([label, value]) => `${label}: ${value}`)
.join('\n');
}
function buildInquiryMailto(payload) {
return `mailto:${INQUIRY_EMAIL}?subject=${encodeURIComponent(inquirySubject(payload))}&body=${encodeURIComponent(inquiryBody(payload))}`;
}
async function submitInquiry(payload) {
const headers = { 'Content-Type': 'application/json' };
if (FW_CONFIG.nonce) headers['X-WP-Nonce'] = FW_CONFIG.nonce;
const response = await fetch(INQUIRY_ENDPOINT, {
method: 'POST',
headers,
body: JSON.stringify({
source: 'freshwave-site',
submittedAt: new Date().toISOString(),
...payload,
}),
});
const result = await response.json().catch(() => ({}));
if (!response.ok || !result.ok) {
throw new Error(result.error || 'Submission failed');
}
return result;
}
Object.assign(window, {
Icon, Button, Placeholder, LogoImage, Eyebrow, Header, Footer, StatsStrip, ServiceCard, ProjectCard, MaterialSwatch,
TrustStrip, BeforePrintPanel, accentHsl, go, useState, useEffect, useRef, submitInquiry, buildInquiryMailto, fwAsset, triggerButtonSparks,
});