mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 20:51:17 +00:00
110 lines
4.9 KiB
TypeScript
110 lines
4.9 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useEffectEvent, useRef, useState } from "react"
|
|
import { MonitorIcon, SmartphoneIcon, TabletIcon } from "lucide-react"
|
|
import { cn } from "@/lib/utils"
|
|
import { DEVICES, PREVIEW_CONTENT, PREVIEW_FOCUS, PREVIEW_READY, type Device } from "../../../lib/preview"
|
|
import type { TemplateContent } from "../../../lib/types"
|
|
|
|
const icons = { desktop: MonitorIcon, tablet: TabletIcon, mobile: SmartphoneIcon }
|
|
|
|
// Die Vorlage läuft in echter Gerätebreite im iframe (damit Media Queries greifen) und wird
|
|
// passend herunterskaliert. Änderungen gehen per postMessage hinein, ohne Neuladen.
|
|
// `focus` (Baukasten): Baustein, zu dem die Vorschau scrollen soll; `n` erlaubt erneutes Springen zum selben Baustein.
|
|
export function PreviewFrame({ templateKey, variantId, content, focus }: {
|
|
templateKey: string
|
|
variantId: string
|
|
content: TemplateContent
|
|
focus?: { id: string; n: number } | null
|
|
}) {
|
|
const frameRef = useRef<HTMLIFrameElement>(null)
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
const [size, setSize] = useState({ width: 0, height: 0 })
|
|
const [device, setDevice] = useState<Device>("desktop")
|
|
|
|
const post = useEffectEvent(() => {
|
|
frameRef.current?.contentWindow?.postMessage({ type: PREVIEW_CONTENT, content }, window.location.origin)
|
|
})
|
|
|
|
useEffect(() => {
|
|
post()
|
|
}, [content])
|
|
|
|
useEffect(() => {
|
|
if (!focus) return
|
|
// Kurz warten, damit ein gerade hinzugefügter Baustein in der Vorschau schon gerendert ist
|
|
const timer = setTimeout(() => {
|
|
frameRef.current?.contentWindow?.postMessage({ type: PREVIEW_FOCUS, id: focus.id }, window.location.origin)
|
|
}, 120)
|
|
return () => clearTimeout(timer)
|
|
}, [focus])
|
|
|
|
useEffect(() => {
|
|
function onMessage(event: MessageEvent) {
|
|
if (event.origin === window.location.origin && event.source === frameRef.current?.contentWindow && event.data?.type === PREVIEW_READY) {
|
|
post()
|
|
}
|
|
}
|
|
window.addEventListener("message", onMessage)
|
|
return () => window.removeEventListener("message", onMessage)
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const container = containerRef.current
|
|
if (!container) return
|
|
const observer = new ResizeObserver(([entry]) => setSize({ width: entry.contentRect.width, height: entry.contentRect.height }))
|
|
observer.observe(container)
|
|
return () => observer.disconnect()
|
|
}, [])
|
|
|
|
const deviceWidth = DEVICES[device].width
|
|
const inset = device === "desktop" ? 0 : 16
|
|
const scale = size.width ? Math.min(1, (size.width - inset * 2) / deviceWidth) : 1
|
|
|
|
return (
|
|
<section className="flex min-h-[75vh] flex-col overflow-hidden rounded-xl border bg-surface lg:min-h-0">
|
|
<div className="flex items-center justify-between gap-2 border-b bg-card px-2 py-1.5">
|
|
<div className="flex gap-1">
|
|
{(Object.keys(DEVICES) as Device[]).map((key) => {
|
|
const Icon = icons[key]
|
|
return (
|
|
<button
|
|
key={key}
|
|
type="button"
|
|
aria-pressed={device === key}
|
|
onClick={() => setDevice(key)}
|
|
className={cn(
|
|
"flex cursor-pointer items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs transition-colors",
|
|
device === key ? "bg-surface font-medium" : "text-muted-foreground hover:text-foreground"
|
|
)}
|
|
>
|
|
<Icon className="size-3.5" />
|
|
{DEVICES[key].label}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
<span className="font-mono text-[11px] text-muted-foreground">
|
|
{deviceWidth}px · {Math.round(scale * 100)}%
|
|
</span>
|
|
</div>
|
|
<div ref={containerRef} className="relative min-h-0 flex-1 overflow-hidden">
|
|
{size.width > 0 && (
|
|
<iframe
|
|
ref={frameRef}
|
|
src={`/preview/${templateKey}?live=1&variant=${variantId}`}
|
|
title="Vorschau der Variante"
|
|
className={cn("absolute origin-top-left border-0 bg-white", inset > 0 && "rounded-2xl shadow-xl ring-1 ring-black/10")}
|
|
style={{
|
|
width: deviceWidth,
|
|
height: (size.height - inset * 2) / scale,
|
|
top: inset,
|
|
left: Math.max(0, (size.width - deviceWidth * scale) / 2),
|
|
transform: `scale(${scale})`,
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|