dorfteich/apps/web/src/watches/WatchesSection.tsx
Claude Fable 5 83fa23bbf9
All checks were successful
CI / Lint, typecheck, test (push) Successful in 3m35s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m44s
CD / Deploy to Test (push) Successful in 15s
CD / Smoke tests against Test (push) Successful in 1m17s
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Successful in 5m36s
CI / Import/export fidelity gate (push) Successful in 46s
Polish round 2: content footer, dismissable menus, manual versions, substring search, icon actions in settings (M10 follow-up)
- content footer: the collab status is an icon (wifi/off/refresh, localized
  tooltip + visually-hidden text, class/data-status hooks kept for e2e) on
  the left, the legal links right-aligned; read mode drops the editor
  frame and its inner padding, edit mode keeps it
- menus (page overflow, user, notifications bell, pond switcher) close on
  outside click and Escape via a shared useDismissable hook; the bell got
  its missing tooltip
- side panels (labels, history) stack vertically in one column
- edit mode gains a Save-version icon (prompt for the name, POST
  /pages/:id/versions); the history panel lists contributors by display
  name — more than three collapse to two plus an expandable ellipsis
  (PageVersionView.contributors resolved server-side, deleted users drop
  out)
- search finds partial words via a LIKE fallback next to the tsquery
  (FTS matches still rank first; regression-pinned in the db pack), and
  the recent-searches list has a clear button
- pond owners create labels directly in the label picker (plus a
  permanent link to the full manager); add/remove/delete buttons across
  the pond settings (members, access rules, labels, files) and the
  watch/unwatch toggles in pond/user settings are icon buttons now —
  class hooks and accessible names unchanged for the e2e packs

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

61 lines
2.1 KiB
TypeScript

import type { WatchListView } from '@dorfteich/shared';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { EyeOff } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { apiDelete, apiGet } from '../lib/api';
/**
* The account's watch list (issue #93): everything the user follows, with
* links to the targets and an unwatch action per entry.
*/
export function WatchesSection(): React.JSX.Element {
const { t } = useTranslation('watches');
const queryClient = useQueryClient();
const list = useQuery({
queryKey: ['watches'],
queryFn: () => apiGet<WatchListView>('/users/me/watches'),
});
const unwatch = async (targetType: string, targetId: string): Promise<void> => {
await apiDelete(`/watches/${targetType}/${targetId}`);
await queryClient.invalidateQueries({ queryKey: ['watches'] });
await queryClient.invalidateQueries({ queryKey: ['watch', targetType, targetId] });
};
return (
<section className="settings-section">
<h2>{t('settings.title')}</h2>
{list.data && list.data.watches.length === 0 && <p>{t('settings.empty')}</p>}
{list.data && list.data.watches.length > 0 && (
<ul className="watches-list">
{list.data.watches.map((watch) => (
<li key={watch.id}>
<Link
to={
watch.targetType === 'pond'
? `/p/${watch.pondSlug}`
: `/p/${watch.pondSlug}/${watch.slug}`
}
>
{watch.name}
</Link>
<span className="watches-list__type">{t(`settings.types.${watch.targetType}`)}</span>
<button
type="button"
className="icon-button watches-list__unwatch"
aria-label={t('settings.unwatch')}
title={t('settings.unwatch')}
onClick={() => void unwatch(watch.targetType, watch.targetId)}
>
<EyeOff aria-hidden />
</button>
</li>
))}
</ul>
)}
</section>
);
}