dorfteich/apps/web/src/comments/use-comments.ts
Claude Fable 5 65f30a5231
Some checks failed
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
Move page actions into the TopBar as self-hosted icon buttons (#101)
- lucide-react (MIT, tree-shaken, compiled into the bundle — no runtime
  requests; fonts.spec's off-origin assertion covers the page route)
- page-actions slot: TopBar registers a DOM element via context, the
  active page portals its actions into it, TopBar stays page-agnostic
- PageActions: mode toggle, watch (WatchToggle icon variant), comments
  (unread badge kept), attachments, plugin page tools, labels, history
  as icon buttons with localized aria-label+tooltip (de+en), plus an
  overflow menu for markdown copy/download, docx/odt/pdf export and the
  destructive delete (confirm kept)
- page header keeps only the title; the editor-shell tools row is gone;
  panel state lives in PageEditorPage now
- hamburger/search/bell adopt the same icon set
- e2e: content/export open the overflow menu; class hooks
  (editor-page__mode-toggle, editor-shell__*-toggle,
  editor-page__labels-toggle, editor-page__export) kept stable

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-12 04:39:42 +02:00

62 lines
2.0 KiB
TypeScript

import type { PageCommentsView } from '@dorfteich/shared';
import { useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';
import { apiGet } from '../lib/api';
/**
* Page comments data (issue #92). One query per page, shared between the
* panel and the toggle button's unread badge. "Unread" is a purely local
* notion: newer than the last time this browser opened the panel
* (localStorage — no server round-trip, per the issue's design).
*/
export const commentsQueryKey = (pageId: string): [string, string] => ['comments', pageId];
export function useComments(pageId: string | undefined): UseQueryResult<PageCommentsView> {
return useQuery({
queryKey: commentsQueryKey(pageId ?? ''),
queryFn: () => apiGet<PageCommentsView>(`/pages/${pageId}/comments`),
enabled: Boolean(pageId),
});
}
export function useInvalidateComments(pageId: string): () => Promise<void> {
const queryClient = useQueryClient();
return () => queryClient.invalidateQueries({ queryKey: commentsQueryKey(pageId) });
}
const seenKey = (pageId: string): string => `dorfteich.comments.seen.${pageId}`;
export function markCommentsSeen(pageId: string): void {
try {
localStorage.setItem(seenKey(pageId), new Date().toISOString());
} catch {
// Storage full/blocked: the badge simply stays — never break the page.
}
}
/** Comments newer than the last visit, not authored by the viewer. */
export function unreadCount(
view: PageCommentsView | undefined,
pageId: string,
viewerId: string | undefined,
): number {
if (!view) return 0;
let since = 0;
try {
const stored = localStorage.getItem(seenKey(pageId));
since = stored ? new Date(stored).getTime() : 0;
} catch {
since = 0;
}
let count = 0;
for (const thread of view.threads) {
for (const comment of [thread.root, ...thread.replies]) {
if (new Date(comment.createdAt).getTime() > since && comment.author?.id !== viewerId) {
count += 1;
}
}
}
return count;
}