dorfteich/apps/web/src/pages/PondSettingsPage.tsx
Claude Fable 5 418aafd5ec
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
#163: Dokumentsprache und Seitentitel der SPA
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
2026-07-21 13:52:59 +02:00

161 lines
6.0 KiB
TypeScript

import type { PondView } from '@dorfteich/shared';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { useAuth } from '../auth/auth-context';
import { SettingsLayout } from '../components/SettingsLayout';
import { ApiOptInSetting } from '../api-tokens/ApiOptInSetting';
import { CommentPolicySetting } from '../comments/CommentPolicySetting';
import { WatchToggle } from '../watches/WatchToggle';
import { FormError } from '../components/forms';
import { AppearanceManager } from '../fonts/AppearanceManager';
import { LabelManager } from '../labels/LabelManager';
import { PhantomPagesView } from '../links/PhantomPagesView';
import { AccessRulesManager } from '../access/AccessRulesManager';
import { EffectivePermissionsInspector } from '../access/EffectivePermissionsInspector';
import { PondFileManager } from '../files/PondFileManager';
import { apiGet } from '../lib/api';
import { VaultImportSection } from '../import/VaultImportSection';
import { SidebarViewSetting } from '../layout/SidebarViewSetting';
import { MemberManager } from '../members/MemberManager';
import { DeletePondSection } from '../ponds/DeletePondSection';
import { PondPluginSettings } from '../plugins/PondPluginSettings';
import { useDocumentTitle } from '../lib/use-document-title';
/**
* Pond settings (issues #44/#54). Hosts the 'Members' and 'Labels' sections;
* future pond-level configuration (fonts, etc.) joins it here. Member
* management is Pond-Admin-gated in the api (the MemberManager shows the list
* to any member and hides the controls otherwise); label management needs
* modify rights. The sidebar links here for the pond owner (Site Admins and
* other members can still navigate directly).
*/
export function PondSettingsPage(): React.JSX.Element {
const { t } = useTranslation('labels');
const { t: tLinks } = useTranslation('links');
const { t: tMembers } = useTranslation('members');
const { t: tErrors } = useTranslation('errors');
const { t: tFiles } = useTranslation('files');
const { t: tExport } = useTranslation('export');
const { t: tComments } = useTranslation('comments');
const { t: tApiTokens } = useTranslation('apiTokens');
const { t: tFont } = useTranslation('font');
const { t: tCommon } = useTranslation();
const { t: tImport } = useTranslation('import');
const { pondSlug = '' } = useParams<{ pondSlug: string }>();
const { user } = useAuth();
const pond = useQuery({
queryKey: ['pond', pondSlug],
queryFn: () => apiGet<PondView>(`/ponds/${pondSlug}`),
});
useDocumentTitle(tCommon('settings:title'), pond.data?.name);
if (pond.error) return <FormError error={pond.error} />;
if (!pond.data) return <></>;
const canModify = Boolean(user && (user.isSiteAdmin || user.id === pond.data.ownerId));
return (
<div className="pond-settings-page">
<div className="pond-settings-page__header">
<h1>{pond.data.name}</h1>
<WatchToggle targetType="pond" targetId={pond.data.id} variant="icon" />
</div>
<SettingsLayout>
<section>
<h2>{tMembers('title')}</h2>
<MemberManager pondId={pond.data.id} />
</section>
<AccessRulesManager pondId={pond.data.id} />
<EffectivePermissionsInspector pondId={pond.data.id} />
<section>
<h2>{t('settings.title')}</h2>
{canModify ? (
<LabelManager pondId={pond.data.id} />
) : (
<p className="form-banner form-banner--error" role="alert">
{tErrors('forbidden')}
</p>
)}
</section>
{canModify && (
<section>
<h2>{tLinks('missing.title')}</h2>
<PhantomPagesView pondId={pond.data.id} pondSlug={pondSlug} />
</section>
)}
{canModify && (
<section>
<h2>{tImport('vault.title')}</h2>
<VaultImportSection pondId={pond.data.id} pondSlug={pondSlug} />
</section>
)}
{canModify && (
<section>
<h2>{tFiles('manager.title')}</h2>
<PondFileManager pondId={pond.data.id} />
</section>
)}
{canModify && (
<section className="appearance-section">
<h2>{tFont('heading')}</h2>
<AppearanceManager
pondId={pond.data.id}
pondSlug={pondSlug}
fonts={pond.data.settings.fonts}
/>
</section>
)}
{canModify && <PondPluginSettings pondId={pond.data.id} />}
{canModify && (
<section>
<h2>{tCommon('layout.sidebar.view.defaultTitle')}</h2>
<SidebarViewSetting
pondId={pond.data.id}
pondSlug={pondSlug}
value={pond.data.settings.sidebarView}
/>
</section>
)}
{canModify && (
<section>
<h2>{tComments('policy.title')}</h2>
<CommentPolicySetting
pondId={pond.data.id}
pondSlug={pondSlug}
value={pond.data.settings.commentPolicy}
/>
</section>
)}
{canModify && (
<section>
<h2>{tApiTokens('pond.title')}</h2>
<ApiOptInSetting
pondId={pond.data.id}
pondSlug={pondSlug}
value={pond.data.settings.apiEnabled}
mcpValue={pond.data.settings.mcpEnabled}
/>
</section>
)}
<section className="pond-export">
<h2>{tExport('pond.heading')}</h2>
<p className="pond-export__hint">{tExport('pond.hint')}</p>
<a
className="button"
href={`/api/v1/ponds/${pond.data.id}/export/markdown`}
download={`${pond.data.slug}.zip`}
>
{tExport('pond.zip')}
</a>
</section>
{canModify && pond.data.type === 'shared' && (
<DeletePondSection pondId={pond.data.id} pondName={pond.data.name} />
)}
</SettingsLayout>
</div>
);
}