dorfteich/apps/web/src/pages/PublicPageView.tsx
Claude Fable 5 418aafd5ec
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 4m43s
CI / Build container images (pull_request) Successful in 1m11s
CI / Auth e2e pack (pull_request) Successful in 7m14s
CI / Import/export fidelity gate (pull_request) Successful in 56s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
#163: Dokumentsprache und Seitentitel der SPA
i18n spiegelt die aktive Sprache auf <html lang> (Init + languageChanged;
der User-Locale-Wechsel in auth-context läuft über dasselbe Event). Neuer
useDocumentTitle-Hook setzt je Route einen sprechenden Titel
(Seite — Teich — Dorfteich), verdrahtet in allen Routen-Komponenten;
dynamische Titel folgen den geladenen Daten.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq
2026-07-21 13:52:59 +02:00

66 lines
2.6 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { PublicComments } from '../comments/CommentsSection';
import { ApiError, apiGet } from '../lib/api';
import { useDocumentTitle } from '../lib/use-document-title';
import { countWords, htmlToText } from '../lib/word-count';
import { NotFoundPage } from './NotFoundPage';
import { PageStatusBar } from './PageStatusBar';
interface PublicPageContent {
pondName: string;
pondSlug: string;
title: string;
slug: string;
html: string;
updatedAt: string;
}
/**
* Read-only public page view (issue #56): renders a page that a `public` grant
* opens to anonymous visitors, from the server-derived HTML — deliberately
* WITHOUT importing the collaborative editor, so anonymous readers never load
* the editor bundle. A non-public page 404s (the api hides its existence).
*/
export function PublicPageView(): React.JSX.Element {
const { t } = useTranslation('public');
const { pondSlug = '', pageSlug = '' } = useParams<{ pondSlug: string; pageSlug: string }>();
const query = useQuery({
queryKey: ['public-page', pondSlug, pageSlug],
queryFn: () => apiGet<PublicPageContent>(`/public/${pondSlug}/${pageSlug}/content`),
enabled: Boolean(pondSlug && pageSlug),
retry: false,
});
useDocumentTitle(query.data?.title, query.data?.pondName);
// Word count for the status line (#134), derived from the server-rendered
// HTML — no editor bundle needed. Kept before the early returns so the hook
// order stays stable.
const wordCount = useMemo(
() => countWords(htmlToText(query.data?.html ?? '')),
[query.data?.html],
);
if (query.error instanceof ApiError && query.error.status === 404) return <NotFoundPage />;
if (query.isLoading || !query.data) return <div aria-busy="true" />;
const page = query.data;
return (
<article className="public-page">
<p className="public-page__badge">{t('readOnlyBadge')}</p>
<p className="public-page__pond">{page.pondName}</p>
<h1 className="public-page__title">{page.title}</h1>
<PageStatusBar updatedAt={page.updatedAt} wordCount={wordCount} />
{/* The HTML comes from the server's content cache (issue #24), derived
from the sanitized editor schema — safe to render. */}
<div className="public-page__body" dangerouslySetInnerHTML={{ __html: page.html }} />
{/* Existing comments, read-only for anonymous visitors (issue #133). */}
<PublicComments pondSlug={pondSlug} pageSlug={pageSlug} />
</article>
);
}