/* =========================================================
App — Fran Ruiz portfolio composer
========================================================= */
const { useEffect, useState } = React;
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"theme": "light",
"lang": "en",
"accent": "#e8624a"
}/*EDITMODE-END*/;
// Override the default language with whatever the user picked on a
// previous page — we persist it under "fran-lang" so language follows
// the user across index ↔ contact ↔ project navigations.
const STORED_LANG =
typeof localStorage !== "undefined" && localStorage.getItem("fran-lang");
const RUNTIME_DEFAULTS = STORED_LANG
? { ...TWEAK_DEFAULTS, lang: STORED_LANG }
: TWEAK_DEFAULTS;
const ACCENT_OPTIONS = [
"#e8624a", // terracotta (default — from Fimed)
"#5fa7d4", // sky blue (JP)
"#a3b59a", // sage green
"#f4ae5b", // warm sand (Garden Brasas)
"#9fc9be", // mint (Bodegas)
"#1a1a1a", // ink (monochrome)
];
const apply = (t) => {
const r = document.documentElement;
r.setAttribute("data-theme", t.theme);
r.setAttribute("data-lang", t.lang);
r.style.setProperty("--accent", t.accent);
};
const App = () => {
const [tweaks, setTweak] = useTweaks(RUNTIME_DEFAULTS);
const lang = tweaks.lang;
const t = T[lang] || T.en;
useEffect(() => { apply(tweaks); }, [tweaks]);
// Scroll reveal (manual fallback) — opts the page into hide-then-reveal animation
useEffect(() => {
document.documentElement.setAttribute("data-anim", "");
// If the animation timeline isn't advancing (e.g. sandboxed iframe),
// disable hide-then-reveal so content stays visible.
const t0 = document.timeline?.currentTime ?? 0;
setTimeout(() => {
const t1 = document.timeline?.currentTime ?? 0;
if (t0 === t1) document.documentElement.removeAttribute("data-anim");
}, 200);
const check = () => {
const els = document.querySelectorAll(".reveal:not(.in), .reveal-stagger:not(.in)");
els.forEach((el) => {
const r = el.getBoundingClientRect();
if (r.top < window.innerHeight + 80 && r.bottom > -80) {
el.classList.add("in");
}
});
};
// Run check more aggressively at start to catch initial in-view content
check();
requestAnimationFrame(check);
setTimeout(check, 50);
setTimeout(check, 200);
setTimeout(check, 500);
window.addEventListener("scroll", check, { passive: true });
window.addEventListener("resize", check);
const id = setInterval(check, 400);
// Safety net: after 2.5s, reveal anything still hidden regardless of viewport
const safety = setTimeout(() => {
document.querySelectorAll(".reveal:not(.in), .reveal-stagger:not(.in)").forEach((el) => el.classList.add("in"));
}, 2500);
return () => {
window.removeEventListener("scroll", check);
window.removeEventListener("resize", check);
clearInterval(id);
clearTimeout(safety);
};
}, []);
const setLang = (v) => {
setTweak("lang", v);
try { localStorage.setItem("fran-lang", v); } catch (e) {}
};
const toggleTheme = () => setTweak("theme", tweaks.theme === "dark" ? "light" : "dark");
return (
<>
setTweak("theme", v)}
options={[{ value: "light", label: "Claro" }, { value: "dark", label: "Oscuro" }]}
/>
setTweak("accent", v)}
options={ACCENT_OPTIONS}
/>
setTweak("lang", v)}
options={[
{ value: "en", label: "EN" },
{ value: "de", label: "DE" },
{ value: "es", label: "ES" },
]}
/>
>
);
};
ReactDOM.createRoot(document.getElementById("root")).render();