// Brigzy redesign — interactive bits const { useState, useEffect, useRef } = React; // Skip hover-only effects (cursor spotlight, magnetic buttons, 3D tilt) on touch devices — // they require a hover-capable pointer to make sense. const IS_TOUCH = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(hover: none) and (pointer: coarse)").matches; /* ---------- API ENDPOINTS ---------- */ const SUPABASE_FN_BASE = "https://dygjwtljgzfyoqdklcrk.supabase.co/functions/v1"; const WAITLIST_SIGNUP_URL = `${SUPABASE_FN_BASE}/waitlist-signup`; const LANDING_STATS_URL = `${SUPABASE_FN_BASE}/landing-stats`; /* ---------- TWEAKS ---------- */ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "accent": "#7c3aed" }/*EDITMODE-END*/; const ACCENT_OPTIONS = [ "#7c3aed", // Brigzy purple (default) "#a78bfa", // light lavender "#e8704a", // terracotta "#14110d", // ink (monochrome) ]; function applyTweaks(t) { document.documentElement.style.setProperty("--accent", t.accent); const hex = t.accent.replace("#", ""); const r = parseInt(hex.slice(0,2), 16); const g = parseInt(hex.slice(2,4), 16); const b = parseInt(hex.slice(4,6), 16); document.documentElement.style.setProperty("--accent-soft", `rgba(${r}, ${g}, ${b}, 0.08)`); } /* ---------- THEME TOGGLE ---------- Initial class is set by the inline bootstrap script in . This wires the button click + listens for system preference changes (only when the user has not set an explicit override). */ function setupThemeToggle() { const THEME_KEY = "brigzy-theme"; const btn = document.getElementById("themeToggle"); if (!btn) return; btn.addEventListener("click", () => { const nowDark = !document.documentElement.classList.contains("dark"); document.documentElement.classList.toggle("dark", nowDark); try { localStorage.setItem(THEME_KEY, nowDark ? "dark" : "light"); } catch (_) {} }); // Follow system preference for users who haven't overridden. if (window.matchMedia) { const mq = window.matchMedia("(prefers-color-scheme: dark)"); const onChange = (e) => { let hasOverride = false; try { hasOverride = !!localStorage.getItem(THEME_KEY); } catch (_) {} if (!hasOverride) document.documentElement.classList.toggle("dark", e.matches); }; if (mq.addEventListener) mq.addEventListener("change", onChange); else if (mq.addListener) mq.addListener(onChange); // Safari < 14 fallback } } /* ---------- LIVE WAITLIST COUNTER ---------- */ // Fetches real waitlist count from Supabase landing-stats edge function. // Refreshes every 30s (matches function's Cache-Control). Flashes on increment. function LiveWaitlistCounter({ initialCount = null }) { const [count, setCount] = useState(initialCount); const [flash, setFlash] = useState(false); const prevRef = useRef(initialCount); useEffect(() => { let mounted = true; const fetchCount = async () => { try { const res = await fetch(LANDING_STATS_URL); if (!res.ok) return; const data = await res.json(); if (!mounted || typeof data.waitlist !== "number") return; if (prevRef.current !== null && data.waitlist > prevRef.current) { setFlash(true); setTimeout(() => { if (mounted) setFlash(false); }, 600); } prevRef.current = data.waitlist; setCount(data.waitlist); } catch (_) { /* silent fail — keep last known value */ } }; fetchCount(); const id = setInterval(fetchCount, 30000); return () => { mounted = false; clearInterval(id); }; }, []); if (count === null) { return ···; } return {count.toLocaleString("sk-SK")}; } /* ---------- WAITLIST FORM ---------- */ function WaitlistForm({ variant }) { const [email, setEmail] = useState(""); const [website, setWebsite] = useState(""); // honeypot — bots fill, humans don't const [state, setState] = useState("idle"); // idle | loading | done const [errorMsg, setErrorMsg] = useState(""); const [signupPosition, setSignupPosition] = useState(null); const btnRef = useRef(null); // Magnetic effect useEffect(() => { if (IS_TOUCH) return; const btn = btnRef.current; if (!btn) return; const onMove = (e) => { const r = btn.getBoundingClientRect(); const x = e.clientX - (r.left + r.width / 2); const y = e.clientY - (r.top + r.height / 2); const dist = Math.hypot(x, y); if (dist < 80) { btn.style.transform = `translate(${x * 0.25}px, ${y * 0.25}px)`; } else { btn.style.transform = ""; } }; const onLeave = () => { btn.style.transform = ""; }; window.addEventListener("mousemove", onMove); btn.addEventListener("mouseleave", onLeave); return () => { window.removeEventListener("mousemove", onMove); btn.removeEventListener("mouseleave", onLeave); }; }, []); const handle = async (e) => { e.preventDefault(); if (!email.includes("@") || state === "loading") return; setState("loading"); setErrorMsg(""); try { const res = await fetch(WAITLIST_SIGNUP_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email.trim(), website }), }); const data = await res.json().catch(() => ({})); if (res.ok && data.ok) { if (typeof data.count === "number") setSignupPosition(data.count); setState("done"); return; } // Map known error codes if (data.error === "invalid_email") { setErrorMsg("Tento email nemôžeme prijať. Skontroluj ho prosím."); } else { setErrorMsg("Niečo sa pokazilo. Skús to ešte raz o chvíľu."); } setState("idle"); } catch (err) { setErrorMsg("Pripojenie zlyhalo. Skontroluj internet a skús znova."); setState("idle"); } }; if (state === "done") { return (
Si na zozname. Ozveme sa hneď, ako spustíme.
{signupPosition ? <>MIESTO #{signupPosition.toLocaleString("sk-SK")} · ĎAKUJEME : <>ĎAKUJEME · BUDEŠ MEDZI PRVÝMI }
); } return (
setEmail(e.target.value)} disabled={state === "loading"} required /> {/* Honeypot — hidden from real users, traps bots */} setWebsite(e.target.value)} tabIndex={-1} autoComplete="off" aria-hidden="true" style={{ position: "absolute", left: "-9999px", top: 0, opacity: 0, pointerEvents: "none", height: 0, width: 0 }} />
{errorMsg && (
{errorMsg}
)}
ĽUDÍ UŽ ČAKÁ · BEZ SPAMU
); } /* ---------- FAQ ---------- */ const FAQ_ITEMS = [ { q: "Je Brigzy zadarmo?", a: "Áno. Stiahnutie a používanie appky je zadarmo pre pracovníkov aj firmy. Pri úspešnom dohodnutí brigády si Brigzy berie malú províziu, ktorá pokrýva platobnú bránu a podporu — žiadne mesačné poplatky, žiadne skryté čísla." }, { q: "Kedy bude appka dostupná?", a: "Spúšťame už čoskoro — v App Store aj Google Play. Ľudia z waitlistu dostanú prístup ako prví, približne 2-3 týždne pred verejným launchom." }, { q: "Ako fungujú platby?", a: "Brigzy používa escrow systém. Firma vloží platbu pred začiatkom práce a tá sa pracovníkovi uvoľní až po dokončení. Ak je niečo zle, máme tu support, ktorý spor vyrieši." }, { q: "Pre koho je Brigzy určené?", a: "Pre študentov hľadajúcich brigádu, ľudí na materskej, profesionálov s voľnými dňami — aj pre firmy, kaviarne, sklady alebo eventy, ktoré potrebujú rýchlu výpomoc bez agentúrnych marží." }, { q: "Ako je to s mojím súkromím?", a: "Tvoje dáta sú šifrované a chránené podľa GDPR. Nikdy ich neposúvame tretím stranám bez tvojho súhlasu. Viac v Ochrane súkromia." }, { q: "Môžem zrušiť účet a zmazať dáta?", a: "Kedykoľvek. V nastaveniach máš jedno tlačidlo, ktoré všetko zruší a vymaže — bez emailov, bez prosenia, bez týždňového čakania." }, ]; function FAQ() { const [open, setOpen] = useState(0); return (
{FAQ_ITEMS.map((item, i) => (
{item.a}
))}
); } /* ---------- MARQUEE ---------- */ const CATEGORIES = ["Gastro", "Sklady", "Kaviarne", "Eventy", "Doučovanie", "Doručovanie", "IT brigády", "Stavebníctvo", "Maloobchod", "Letné tábory", "Festivaly", "Marketing", "Stánky"]; function Marquee() { const items = [...CATEGORIES, ...CATEGORIES]; return <>{items.map((c, i) => {c})}; } /* ---------- PHONE CARD CYCLER ---------- */ const JOBS = [ { co: "Bistro Vesna", title: "Barista — víkend", dist: "0.4 km", hours: "8h", pay: "€11", tag: "urgent", tagText: "Dnes" }, { co: "Hala Logistika SK", title: "Triedenie balíkov", dist: "2.1 km", hours: "6h", pay: "€9", tag: "new", tagText: "Nové" }, { co: "Festival Pohoda", title: "Hostess / Stánok", dist: "3.7 km", hours: "10h", pay: "€13", tag: "urgent", tagText: "Hneď" }, { co: "Mestská knižnica", title: "Doučovanie ZŠ", dist: "1.2 km", hours: "4h", pay: "€10", tag: "new", tagText: "Nové" }, { co: "Café Štúrova", title: "Čašník — večery", dist: "0.8 km", hours: "5h", pay: "€12", tag: "urgent", tagText: "Dnes" }, ]; function PhoneCardCycler() { const [idx, setIdx] = useState(0); useEffect(() => { const id = setInterval(() => setIdx(i => (i + 1) % JOBS.length), 2800); return () => clearInterval(id); }, []); // show two cards: current and next const a = JOBS[idx]; const b = JOBS[(idx + 1) % JOBS.length]; const Card = ({ job, k }) => (
{job.co}
{job.title}
{job.tagText}
↗ {job.dist} ○ {job.hours}
{job.pay}/h
Uchádzať sa
); return ( <> ); } /* ---------- TWEAKS APPLIED AT BOOT (tweaks panel removed for production) ---------- */ /* ---------- MOUNT ---------- */ const faqRoot = document.getElementById("faq-list"); if (faqRoot) ReactDOM.createRoot(faqRoot).render(); const heroRoot = document.getElementById("hero-waitlist"); if (heroRoot) ReactDOM.createRoot(heroRoot).render(); const ctaRoot = document.getElementById("cta-waitlist"); if (ctaRoot) ReactDOM.createRoot(ctaRoot).render(); const marqueeRoot = document.getElementById("marquee-track"); if (marqueeRoot) ReactDOM.createRoot(marqueeRoot).render(); const phoneRoot = document.getElementById("phone-stack"); if (phoneRoot) ReactDOM.createRoot(phoneRoot).render(); applyTweaks(TWEAK_DEFAULTS); setupThemeToggle(); /* ---------- COOKIE BANNER ---------- */ (function setupCookieBanner() { const banner = document.getElementById("cookieBanner"); if (!banner) return; const KEY = "brigzy-cookies"; if (localStorage.getItem(KEY)) return; // already decided // Reveal after a short delay so it doesn't fight the hero animation setTimeout(() => banner.classList.add("visible"), 1800); banner.querySelectorAll("[data-cookie-action]").forEach(btn => { btn.addEventListener("click", () => { const accepted = btn.getAttribute("data-cookie-action") === "accept"; localStorage.setItem(KEY, accepted ? "accepted" : "declined"); banner.classList.remove("visible"); }); }); })(); /* ---------- CURSOR SPOTLIGHT + DROPLET (SPEED) + ORB PARALLAX ---------- */ const spotlight = IS_TOUCH ? null : document.getElementById("spotlight"); const droplet = IS_TOUCH ? null : document.getElementById("droplet"); const orb1 = document.getElementById("orb-1"); const orb2 = document.getElementById("orb-2"); const phoneGlow = IS_TOUCH ? null : document.getElementById("phone-glow"); const phone = IS_TOUCH ? null : document.getElementById("phone"); let mouseX = window.innerWidth / 2; let mouseY = window.innerHeight / 2; let curX = mouseX, curY = mouseY; let lastMx = mouseX, lastMy = mouseY; let velocity = 0; let dropletX = mouseX, dropletY = mouseY; let dropletOpacity = 0; document.addEventListener("mousemove", (e) => { const ddx = e.clientX - lastMx; const ddy = e.clientY - lastMy; const speed = Math.hypot(ddx, ddy); velocity = velocity * 0.7 + speed * 0.3; lastMx = e.clientX; lastMy = e.clientY; mouseX = e.clientX; mouseY = e.clientY; document.body.classList.add("cursor-active"); }); document.addEventListener("mouseleave", () => { document.body.classList.remove("cursor-active"); }); function rafLoop() { curX += (mouseX - curX) * 0.12; curY += (mouseY - curY) * 0.12; velocity *= 0.9; if (spotlight) { const speedScale = 1 + Math.min(velocity / 80, 0.35); const angle = Math.atan2(mouseY - curY, mouseX - curX) * 180 / Math.PI; spotlight.style.left = curX + "px"; spotlight.style.top = curY + "px"; spotlight.style.transform = `translate(-50%, -50%) rotate(${angle}deg) scaleX(${speedScale}) scaleY(${2 - speedScale})`; } if (droplet) { const targetOpacity = Math.min(velocity / 30, 0.7); dropletOpacity += (targetOpacity - dropletOpacity) * 0.15; dropletX += (mouseX - dropletX) * 0.22; dropletY += (mouseY - dropletY) * 0.22; const stretch = 1 + Math.min(velocity / 25, 1.4); const angle = Math.atan2(mouseY - dropletY, mouseX - dropletX) * 180 / Math.PI; droplet.style.left = dropletX + "px"; droplet.style.top = dropletY + "px"; droplet.style.opacity = dropletOpacity; droplet.style.transform = `translate(-50%, -50%) rotate(${angle}deg) scaleX(${stretch}) scaleY(${1 / Math.sqrt(stretch)})`; } const dx = (mouseX / window.innerWidth - 0.5); const dy = (mouseY / window.innerHeight - 0.5); if (orb1) orb1.style.transform = `translate(${dx * -40}px, ${dy * -30}px)`; if (orb2) orb2.style.transform = `translate(${dx * 30}px, ${dy * 40}px)`; if (phone && phoneGlow) { const r = phone.getBoundingClientRect(); const px = (mouseX - (r.left + r.width / 2)) / (r.width / 2); const py = (mouseY - (r.top + r.height / 2)) / (r.height / 2); const inRange = Math.abs(px) < 3 && Math.abs(py) < 3; if (inRange) { phone.style.transform = `perspective(1200px) rotateY(${Math.max(-12, Math.min(12, px * 8))}deg) rotateX(${Math.max(-8, Math.min(8, -py * 6))}deg) rotate(0deg)`; phoneGlow.style.transform = `translate(calc(-50% + ${px * 30}px), calc(-50% + ${py * 30}px))`; } else { phone.style.transform = ""; } } requestAnimationFrame(rafLoop); } if (!IS_TOUCH) rafLoop(); /* ---------- MAGNETIC NAV CTA ---------- */ (function magnetizeNavCta() { if (IS_TOUCH) return; const btn = document.querySelector(".nav-cta"); if (!btn) return; const onMove = (e) => { const r = btn.getBoundingClientRect(); const x = e.clientX - (r.left + r.width / 2); const y = e.clientY - (r.top + r.height / 2); const dist = Math.hypot(x, y); if (dist < 60) { btn.style.transform = `translate(${x * 0.15}px, ${y * 0.15}px)`; } else { btn.style.transform = ""; } }; const onLeave = () => { btn.style.transform = ""; }; window.addEventListener("mousemove", onMove); btn.addEventListener("mouseleave", onLeave); })(); /* ---------- 3D TILT ON CARDS ---------- */ function attachTilt(selector, intensity = 6) { document.querySelectorAll(selector).forEach(card => { card.addEventListener("mousemove", (e) => { const r = card.getBoundingClientRect(); const x = (e.clientX - r.left) / r.width - 0.5; const y = (e.clientY - r.top) / r.height - 0.5; card.style.transform = `perspective(1000px) rotateY(${x * intensity}deg) rotateX(${-y * intensity}deg) translateY(-3px)`; }); card.addEventListener("mouseleave", () => { card.style.transform = ""; }); }); } if (!IS_TOUCH) { attachTilt(".feature", 3); attachTilt(".step", 4); } /* ---------- PRE KOHO: CARD EXPAND/COLLAPSE ---------- */ (function setupPreKoho() { const cards = document.querySelectorAll(".pk-card"); if (!cards.length) return; let openSide = null; const setState = (side, isOpen) => { const card = document.getElementById("pkCard" + side); const detail = document.getElementById("pkDetail" + side); if (!card || !detail) return; card.classList.toggle("active", isOpen); card.setAttribute("aria-expanded", isOpen ? "true" : "false"); detail.classList.toggle("open", isOpen); if (isOpen) detail.removeAttribute("hidden"); else detail.setAttribute("hidden", ""); }; const toggle = (side) => { const other = side === "A" ? "B" : "A"; if (openSide === side) { setState(side, false); openSide = null; return; } if (openSide === other) setState(other, false); setState(side, true); openSide = side; // Scroll the open detail into view after the panel animates open. setTimeout(() => { const detail = document.getElementById("pkDetail" + side); if (detail) detail.scrollIntoView({ behavior: "smooth", block: "nearest" }); }, 120); }; cards.forEach(card => { const side = card.dataset.side; if (!side) return; card.addEventListener("click", () => toggle(side)); card.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(side); } }); }); document.querySelectorAll(".pk-close").forEach(btn => { btn.addEventListener("click", (e) => { e.stopPropagation(); const side = btn.dataset.close; if (side && openSide === side) { setState(side, false); openSide = null; } }); }); // Waitlist CTA inside an open detail panel: do a controlled scroll instead of native anchor // navigation, because the open panel + the pending scrollIntoView from toggle() can interfere // with the browser's smooth-scroll target and the user lands short of the section. document.querySelectorAll(".pk-detail .pk-cta").forEach(a => { a.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); const href = a.getAttribute("href"); const target = href && document.querySelector(href); if (!target) return; const y = target.getBoundingClientRect().top + window.scrollY; try { history.replaceState(null, "", href); } catch (_) {} window.scrollTo({ top: y, behavior: "smooth" }); }); }); })(); /* ---------- SCROLL REVEAL ---------- */ const sections = document.querySelectorAll("section"); sections.forEach(s => s.classList.add("reveal")); const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); } }); }, { threshold: 0.08, rootMargin: "0px 0px -60px 0px" }); sections.forEach(s => io.observe(s)); document.querySelector(".hero")?.classList.add("in"); // Play hero h1 word reveal only when page is visible (avoids paused animations leaving text hidden) (function playHeroWords() { const h1 = document.querySelector(".hero h1"); if (!h1) return; const start = () => h1.classList.add("anim"); if (document.visibilityState === "visible") { requestAnimationFrame(start); } else { document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") start(); }, { once: true }); } })();