"use client" import { useEffect, useRef, useState } from "react" import { Icon } from "@iconify/react" import { LandingReveal } from "@/components/landing/landing-reveal" import { ProductSectionHeading } from "@/components/landing/product/product-section-heading" import type { ProductCrossPlatformSection as ProductCrossPlatformSectionData } from "@/components/landing/product/product-data" import { demoMailPreviewSrc, type DemoMailPreviewLayout, } from "@/lib/demo/demo-mail-preview" import { demoDriveLayoutPreviewSrc, type DemoDriveLayoutPreview, } from "@/lib/demo/demo-drive-layout-preview" import { ScaledPreviewIframe } from "@/components/landing/product/scaled-preview-iframe" import { cn } from "@/lib/utils" export type ProductCrossPlatformDemoApp = "mail" | "drive" | "meet" type DeviceLayout = DemoMailPreviewLayout | DemoDriveLayoutPreview const STAGE_DURATION_MS = 4200 /** Hauteur fixe du socle — évite le saut de layout entre mobile / tablette / bureau. */ const SHOWCASE_HEIGHT_PX = 620 /** Padding bezel DeviceShell (`p-2`). */ const DEVICE_BEZEL_PX = 8 const MONITOR_STAND_PX = 20 type DeviceStage = { id: DeviceLayout label: string platform: string icon: string /** Viewport logique rendu dans l'iframe (media queries Ultimail). */ viewportWidth: number viewportHeight: number /** Largeur du cadre affiché sur la page (px). La hauteur est dérivée du ratio. */ frameWidth: number radius: number notch?: boolean monitor?: boolean } const DEVICE_STAGES: DeviceStage[] = [ { id: "phone", label: "Mobile", platform: "iOS & Android", icon: "mdi:cellphone", viewportWidth: 390, viewportHeight: 844, frameWidth: 264, radius: 40, notch: true, }, { id: "tablet", label: "Tablette", platform: "iPad paysage", icon: "mdi:tablet", viewportWidth: 1024, viewportHeight: 768, frameWidth: 520, radius: 24, }, { id: "desktop", label: "Bureau", platform: "Écran 4:3", icon: "mdi:monitor", viewportWidth: 1280, viewportHeight: 960, frameWidth: 640, radius: 12, monitor: true, }, ] function stageScale(stage: DeviceStage) { return stage.frameWidth / stage.viewportWidth } function statusBarHeight(stage: DeviceStage) { if (!stage.notch) return 0 const scale = stageScale(stage) return Math.max(24, Math.round(32 * scale)) } /** Hauteur de la zone iframe — ratio identique au viewport (aucune coupe). */ function contentAreaHeight(stage: DeviceStage) { return Math.round((stage.frameWidth * stage.viewportHeight) / stage.viewportWidth) } /** Hauteur totale du cadre = zone iframe + barre de statut éventuelle. */ function frameHeight(stage: DeviceStage) { return contentAreaHeight(stage) + statusBarHeight(stage) } function IosBatteryIcon({ size }: { size: number }) { const width = Math.round(size * 2.1) return ( ) } function DeviceStatusBar({ stage }: { stage: DeviceStage }) { if (!stage.notch) return null const scale = stageScale(stage) const height = statusBarHeight(stage) const fontSize = Math.max(9, Math.round(11 * scale)) const iconSize = Math.max(9, Math.round(11 * scale)) const cornerRadius = Math.max(stage.radius - 10, 10) /** Inset coins arrondis — plus à droite (courbure visible). */ const padLeft = Math.max(22, Math.round(28 * scale)) const padRight = Math.max(36, Math.round(cornerRadius * 1.12)) return (
9:41
) } function deviceOuterSize(stage: DeviceStage) { return { width: stage.frameWidth + DEVICE_BEZEL_PX * 2, height: frameHeight(stage) + DEVICE_BEZEL_PX * 2 + (stage.monitor ? MONITOR_STAND_PX : 0), } } function ScaledProductDemoIframe({ stage, ready, active, demoApp, }: { stage: DeviceStage ready: boolean active: boolean demoApp: ProductCrossPlatformDemoApp }) { const demoSrc = demoApp === "drive" ? demoDriveLayoutPreviewSrc(stage.id) : demoMailPreviewSrc(stage.id) const title = demoApp === "drive" ? `UltiDrive — vue ${stage.label}` : `Ultimail — vue ${stage.label}` const iframe = ( ) // Sans encoche (tablette / bureau) : l'iframe remplit directement la boîte // écran en absolute inset-0 — même structure que la démo docs (qui est nette), // sans chaîne flex qui introduirait des sous-pixels (bandes au bord). if (!stage.notch) { return (
{iframe}
) } // Avec encoche (mobile) : barre de statut en haut, iframe en dessous. return (
{iframe}
) } const MEET_PEOPLE = [ { name: "Léa", color: "#34A853" }, { name: "Vincent", color: "#4285F4" }, { name: "Thomas", color: "#EA4335" }, { name: "Camille", color: "#FBBC04" }, { name: "Sofia", color: "#A142F4" }, { name: "Karim", color: "#00ACC1" }, ] as const const MEET_CONTROLS = ["mdi:microphone", "mdi:video", "mdi:monitor-share", "mdi:message-outline"] as const /** Visio placeholder statique (pas d'iframe) rendu dans le device frame pour UltiMeet. */ function MeetDevicePlaceholder({ stage, accent, }: { stage: DeviceStage accent: string }) { const cols = stage.id === "phone" ? 2 : 3 const people = stage.id === "phone" ? MEET_PEOPLE.slice(0, 4) : MEET_PEOPLE const avatarSize = stage.id === "desktop" ? "size-14" : stage.id === "tablet" ? "size-12" : "size-9" const buttonSize = stage.id === "phone" ? "size-7" : "size-9" const iconSize = stage.id === "phone" ? "size-4" : "size-5" return (
Atelier Nord — point hebdo 12:47
{people.map((person, index) => (
{person.name.charAt(0)} {person.name}
))}
{MEET_CONTROLS.map((icon) => ( ))}
) } function DeviceShell({ stage, children, }: { stage: DeviceStage children: React.ReactNode }) { if (stage.monitor) { return (
{children}
) } return (
{stage.notch ? (
) : null}
{children}
) } function MorphingDeviceShowcase({ accent, demoApp, }: { accent: string demoApp: ProductCrossPlatformDemoApp }) { const sectionRef = useRef(null) const [visible, setVisible] = useState(false) const [stageIndex, setStageIndex] = useState(0) const [autoPlay, setAutoPlay] = useState(true) const [reduceMotion, setReduceMotion] = useState(false) const stage = DEVICE_STAGES[stageIndex]! useEffect(() => { setReduceMotion( typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches ) }, []) useEffect(() => { const node = sectionRef.current if (!node) return const observer = new IntersectionObserver( (entries) => { if (entries.some((entry) => entry.isIntersecting)) { setVisible(true) observer.disconnect() } }, { rootMargin: "120px 0px" } ) observer.observe(node) return () => observer.disconnect() }, []) useEffect(() => { if (!visible || reduceMotion || !autoPlay) return const timer = window.setInterval(() => { setStageIndex((current) => (current + 1) % DEVICE_STAGES.length) }, STAGE_DURATION_MS) return () => window.clearInterval(timer) }, [visible, reduceMotion, autoPlay]) return (
{DEVICE_STAGES.map((item, index) => { const active = index === stageIndex const outer = deviceOuterSize(item) return (
{demoApp === "meet" ? ( ) : ( )}
) })}
{DEVICE_STAGES.map((item, index) => { const active = index === stageIndex return ( ) })}

{stage.platform} {" — "} même interface, adaptée à la taille de l'écran

) } export function ProductCrossPlatformSection({ section, accent, demoApp = "mail", }: { section: ProductCrossPlatformSectionData accent: string demoApp?: ProductCrossPlatformDemoApp }) { return (
    {section.features.map((feature) => (
  • {feature.title}

    {feature.description}

  • ))}
) }