// 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 (
{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) => (
setOpen(open === i ? -1 : i)} aria-expanded={open === i}>
{item.q}
))}
);
}
/* ---------- 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.dist}
○ {job.hours}
);
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 });
}
})();