import { motion, useScroll, useTransform, type MotionValue } from "framer-motion"; import { useEffect, useRef, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import LangToggle from "../components/LangToggle"; import { useT } from "../i18n"; import "./presentation.css"; type Progress = MotionValue; type Range = [number, number]; const TEAM = [ { roleKey: "tower1", name: "Txema Puch" }, { roleKey: "pilot", name: "Mónica Gómez" }, { roleKey: "tower2", name: "Roberto Molero" }, ] as const; const MOBILE_BREAKPOINT_QUERY = "(max-width: 900px)"; function useIsMobile(): boolean { const [isMobile, setIsMobile] = useState(false); useEffect(() => { if (typeof window === "undefined" || !window.matchMedia) return; const mql = window.matchMedia(MOBILE_BREAKPOINT_QUERY); const update = () => setIsMobile(mql.matches); update(); mql.addEventListener("change", update); return () => mql.removeEventListener("change", update); }, []); return isMobile; } function useReveal(progress: Progress, range: Range, first = false): MotionValue { const [start, end] = range; const pad = Math.min(0.04, (end - start) / 3); if (first) { return useTransform(progress, [start, end - pad, end], [1, 1, 0]); } return useTransform(progress, [start, start + pad, end - pad, end], [0, 1, 1, 0]); } function ImageScene({ progress, range, image, kicker, title, sub, first, }: { progress: Progress; range: Range; image: string; kicker: string; title: string; sub?: string; first?: boolean; }) { const opacity = useReveal(progress, range, first); return (
{kicker}

{title}

{sub &&

{sub}

}
); } function WindowScene({ progress, range, closing, kicker, title, }: { progress: Progress; range: Range; closing: boolean; kicker: string; title: string; }) { const opacity = useReveal(progress, range); const shadeFrom: Range = [range[0] + 0.02, range[0] + 0.07]; const shadeY = useTransform(progress, shadeFrom, closing ? ["-100%", "0%"] : ["0%", "-100%"]); return (
{kicker}

{title}

); } function ExplainPanel({ progress, range, title, body, }: { progress: Progress; range: Range; title: string; body: string; }) { const opacity = useReveal(progress, range); const y = useTransform(progress, range, [28, -28]); return (

{title}

{body}

); } function Explain({ progress, range, panelRanges, header, panels, }: { progress: Progress; range: Range; panelRanges: Range[]; header: string; panels: { title: string; body: string }[]; }) { const opacity = useReveal(progress, range); return (
{header}
{panels.map((panel, index) => ( ))}
); } const INTRO_PANEL_RANGES: Range[] = [ [0.51, 0.62], [0.62, 0.74], [0.74, 0.86], ]; export default function Presentation() { const t = useT(); const s = t.scenes; const trackRef = useRef(null); const navigate = useNavigate(); const handedOffRef = useRef(false); const { scrollYProgress } = useScroll({ target: trackRef, offset: ["start start", "end end"], }); useEffect(() => { const unsub = scrollYProgress.on("change", (value) => { if (value > 0.96 && !handedOffRef.current) { handedOffRef.current = true; navigate("/dashboard"); } }); return () => unsub(); }, [scrollYProgress, navigate]); return (
); } export function PresentationFinal() { const t = useT(); const s = t.scenes; const trackRef = useRef(null); const { scrollYProgress } = useScroll({ target: trackRef, offset: ["start start", "end end"], }); const [creditsVisible, setCreditsVisible] = useState(false); useEffect(() => { const unsub = scrollYProgress.on("change", (value) => { setCreditsVisible(value > 0.88); }); return () => unsub(); }, [scrollYProgress]); return (
); } function Credits({ progress, range, roles, teamHeader, thanks, subtitle, visible, }: { progress: Progress; range: Range; roles: { tower1: string; pilot: string; tower2: string }; teamHeader: string; thanks: string; subtitle: string; visible: boolean; }) { const [start, end] = range; const pad = Math.min(0.04, (end - start) / 3); const opacity = useTransform(progress, [start, start + pad], [0, 1]); const textOpacity = useTransform(progress, [start + 0.03, start + 0.1], [0, 1]); const isMobile = useIsMobile(); const roleFontSize = isMobile ? 11 : 12; const nameFontSize = isMobile ? 18 : 22; const teamBlockGap = isMobile ? 16 : 22; const logoWidth = isMobile ? "min(440px, 85vw)" : "min(440px, 70vw)"; return (
SADAR

{subtitle}

{teamHeader}
{TEAM.map((member, idx) => (
{roles[member.roleKey]}
{member.name}
))}
{thanks}
); }