// ============================================================ // 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 (
{children || (
{label}
)}
); } 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 {alt} 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 (
{STATS.map((s) => (
{s.value}
{s.label}
))}
); } // Service card ------------------------------------------------------------ function ServiceCard({ s, onOpen }) { return ( ); } // Project card ------------------------------------------------------------ function ProjectCard({ project, onOpen }) { return ( ); } // Material swatch --------------------------------------------------------- function MaterialSwatch({ m }) { return (
{m.name}
{m.note}
); } 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, });