122 lines
4.0 KiB
TypeScript
122 lines
4.0 KiB
TypeScript
/**
|
|
* WEB-3: Lokale Vorschau der öffentlichen Webseite (/webseite/vorschau).
|
|
*
|
|
* Zeigt die vom Backend live gerenderte Seite (GET /api/preview/…) in einem
|
|
* iframe — exakt das HTML/CSS, das später veröffentlicht wird. Navigation
|
|
* INNERHALB der Vorschau funktioniert über die relativen Links der
|
|
* gerenderten Seite selbst; zusätzlich gibt es einen Seiten-Wechsler,
|
|
* einen Smartphone/Desktop-Umschalter und „Neu laden“.
|
|
* Nur veröffentlichte Seiten werden gerendert (SiteRenderer überspringt
|
|
* Entwürfe) — der Hinweis dazu steht über der Vorschau.
|
|
* Veröffentlichen selbst ist WEB-2 (gated) — hier gibt es bewusst keinen
|
|
* Publish-Knopf.
|
|
*/
|
|
import { useMemo, useState } from 'react'
|
|
import { Link, useSearchParams } from 'react-router-dom'
|
|
import { de } from '../strings/de'
|
|
import { listPages, previewUrl } from '../api/pages'
|
|
import { useApi } from '../hooks/useApi'
|
|
import './webseiteVorschau.css'
|
|
|
|
type Viewport = 'phone' | 'desktop'
|
|
|
|
export default function WebseiteVorschauPage() {
|
|
const t = de.pages.webseite.vorschau
|
|
const [params] = useSearchParams()
|
|
|
|
const pages = useApi(() => listPages(), [])
|
|
const published = useMemo(
|
|
() => (pages.data ?? []).filter((p) => p.status === 'Published'),
|
|
[pages.data],
|
|
)
|
|
|
|
const requested = params.get('seite')
|
|
const [selected, setSelected] = useState<string | null>(requested)
|
|
const slug =
|
|
(selected && published.some((p) => p.slug === selected) ? selected : null) ??
|
|
(published.some((p) => p.slug === 'start') ? 'start' : (published[0]?.slug ?? null))
|
|
|
|
const [viewport, setViewport] = useState<Viewport>('phone')
|
|
const [reloadKey, setReloadKey] = useState(0)
|
|
|
|
if (pages.loading) return <p className="muted">{de.common.loading}</p>
|
|
if (pages.error) {
|
|
return (
|
|
<section className="page">
|
|
<h2>{t.title}</h2>
|
|
<div className="alert alert--error">
|
|
<span>{pages.error}</span>
|
|
<button type="button" className="btn" onClick={pages.reload}>
|
|
{de.common.retry}
|
|
</button>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<section className="page vorschau-page">
|
|
<header className="page-head">
|
|
<div>
|
|
<h2>{t.title}</h2>
|
|
<p className="muted">{t.intro}</p>
|
|
</div>
|
|
<div className="head-actions">
|
|
<Link to="/webseite" className="btn">
|
|
{de.pages.webseite.back}
|
|
</Link>
|
|
</div>
|
|
</header>
|
|
|
|
{slug === null ? (
|
|
<p className="muted">{t.empty}</p>
|
|
) : (
|
|
<>
|
|
<div className="vorschau-toolbar">
|
|
<label className="field">
|
|
<span>{t.pageSelect}</span>
|
|
<select value={slug} onChange={(e) => setSelected(e.target.value)}>
|
|
{published.map((p) => (
|
|
<option key={p.id} value={p.slug}>
|
|
{p.title}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<div className="vorschau-viewports" role="group" aria-label={`${t.viewPhone} / ${t.viewDesktop}`}>
|
|
<button
|
|
type="button"
|
|
className={viewport === 'phone' ? 'btn btn--primary' : 'btn'}
|
|
onClick={() => setViewport('phone')}
|
|
>
|
|
{t.viewPhone}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={viewport === 'desktop' ? 'btn btn--primary' : 'btn'}
|
|
onClick={() => setViewport('desktop')}
|
|
>
|
|
{t.viewDesktop}
|
|
</button>
|
|
</div>
|
|
|
|
<button type="button" className="btn" onClick={() => setReloadKey((k) => k + 1)}>
|
|
{t.reload}
|
|
</button>
|
|
</div>
|
|
|
|
<div className={viewport === 'phone' ? 'vorschau-frame vorschau-frame--phone' : 'vorschau-frame'}>
|
|
<iframe
|
|
key={`${slug}:${reloadKey}`}
|
|
className="vorschau-iframe"
|
|
title={t.frameTitle}
|
|
src={previewUrl(slug)}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</section>
|
|
)
|
|
}
|