dorfteich/apps/web/src/pages/PageStatusBar.tsx
Claude Fable 5 c858f12592 #134 Statuszeile zwischen Navigation und Artikel
Neue schlanke Statuszeile (letzte Aktualisierung · Wortzahl · geschätzte
Lesezeit) zwischen Seitenkopf und Artikel — im authentifizierten
Lesemodus und in der öffentlichen Ansicht.

- Geteilte Komponente `PageStatusBar` (Datum via Intl in der aktiven
  Sprache, Lesezeit = ceil(Wörter/200), Singular/Plural, Lesezeit
  ausgeblendet bei 0 Wörtern).
- `countWords`/`htmlToText`-Helfer in lib/word-count.ts.
- Authentifiziert (`PageEditorPage`, nur Lesemodus): Wortzahl aus dem
  vorhandenen Markdown-Export (geteilter Query-Key ['page-markdown']),
  `updatedAt` direkt von `page.data`.
- Öffentlich (`PublicPageView`): Wortzahl aus dem server-gerenderten HTML
  per DOMParser — kein Editor-Bundle nötig; kein Backend-Change.
- i18n common.statusbar (de+en), CSS `.page-statusbar` (middot-getrennt,
  gedämpft). Gates grün (typecheck/lint/i18n:check); visuell verifiziert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
2026-07-19 01:04:11 +02:00

45 lines
1.5 KiB
TypeScript

import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
/** Average silent reading speed; reading time = ceil(words / WPM). */
const WORDS_PER_MINUTE = 200;
/**
* A slim status line shown between the page header and the article body
* (issue #134): last update, word count, and estimated reading time. Rendered
* both in the authenticated read view and the anonymous public view, so it
* takes already-derived values as props rather than reaching into the editor.
*/
export function PageStatusBar({
updatedAt,
wordCount,
}: {
updatedAt: string;
wordCount: number;
}): React.JSX.Element {
const { t, i18n } = useTranslation('common');
const updated = useMemo(() => {
const date = new Date(updatedAt);
if (Number.isNaN(date.getTime())) return null;
return new Intl.DateTimeFormat(i18n.language, {
dateStyle: 'medium',
timeStyle: 'short',
}).format(date);
}, [updatedAt, i18n.language]);
const minutes = Math.max(1, Math.ceil(wordCount / WORDS_PER_MINUTE));
return (
<div className="page-statusbar" aria-label={t('statusbar.label')}>
{updated && (
<span className="page-statusbar__item">{t('statusbar.updated', { date: updated })}</span>
)}
<span className="page-statusbar__item">{t('statusbar.words', { count: wordCount })}</span>
{wordCount > 0 && (
<span className="page-statusbar__item">{t('statusbar.readingTime', { minutes })}</span>
)}
</div>
);
}