"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 (
)
}
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 (
)
}
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 (
)
}
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 (
)
}