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
45 lines
1.5 KiB
TypeScript
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>
|
|
);
|
|
}
|