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
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
66 lines
2.6 KiB
TypeScript
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>
|
|
);
|
|
}
|