Some checks failed
CD / Build and push images (push) Successful in 3m54s
CD / Deploy to Test (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m8s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Failing after 11s
CD / Promote to Int (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m40s
CI / Import/export fidelity gate (push) Successful in 54s
The pond switcher grows a "+ New pond" entry with an inline form (name + optional description, quota errors surfaced translated); the pond settings of shared ponds end in a danger section that moves the pond to the site-level trash after typing its name to confirm. Personal ponds keep hiding the section. .button--danger is now a solid red button (also fixes the admin restore button, which showed red text on the accent-green background). Manuals no longer call these actions API-only; covered by a members-pack e2e test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
144 lines
4.6 KiB
TypeScript
144 lines
4.6 KiB
TypeScript
import type { PondView } from '@dorfteich/shared';
|
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { useRef, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Link, useNavigate } from 'react-router-dom';
|
|
|
|
import { useAuth } from '../auth/auth-context';
|
|
import { FormError } from '../components/forms';
|
|
import { apiGet, apiPost } from '../lib/api';
|
|
import { useDismissable } from '../lib/use-dismissable';
|
|
import { useCurrentPondRoute } from './use-pond-route';
|
|
|
|
/**
|
|
* Top-bar dropdown to switch between the signed-in user's ponds (issue #26)
|
|
* — and to create a new shared pond right from the menu. Creation is
|
|
* quota-gated server-side (`additional shared ponds per user`); a rejected
|
|
* attempt surfaces the translated error instead of hiding the option.
|
|
*/
|
|
export function PondSwitcher(): React.JSX.Element | null {
|
|
const { t } = useTranslation();
|
|
const { user } = useAuth();
|
|
const { pondSlug } = useCurrentPondRoute();
|
|
const navigate = useNavigate();
|
|
const queryClient = useQueryClient();
|
|
const [open, setOpen] = useState(false);
|
|
const [creating, setCreating] = useState(false);
|
|
const [name, setName] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [error, setError] = useState<unknown>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
|
|
const close = (): void => {
|
|
setOpen(false);
|
|
setCreating(false);
|
|
setError(null);
|
|
};
|
|
useDismissable(menuRef, open, close);
|
|
|
|
const ponds = useQuery({
|
|
queryKey: ['ponds'],
|
|
queryFn: () => apiGet<PondView[]>('/ponds'),
|
|
enabled: Boolean(user),
|
|
});
|
|
|
|
if (!ponds.data || ponds.data.length === 0) return null;
|
|
const current = ponds.data.find((pond) => pond.slug === pondSlug);
|
|
|
|
const create = async (event: React.FormEvent): Promise<void> => {
|
|
event.preventDefault();
|
|
setError(null);
|
|
setBusy(true);
|
|
try {
|
|
const pond = await apiPost<PondView>('/ponds', {
|
|
name: name.trim(),
|
|
description: description.trim(),
|
|
});
|
|
await queryClient.invalidateQueries({ queryKey: ['ponds'] });
|
|
close();
|
|
setName('');
|
|
setDescription('');
|
|
navigate(`/p/${pond.slug}`);
|
|
} catch (err) {
|
|
setError(err);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="user-menu" ref={menuRef}>
|
|
<button
|
|
type="button"
|
|
className="user-menu__trigger pond-switcher__trigger"
|
|
aria-haspopup="menu"
|
|
aria-expanded={open}
|
|
aria-label={t('layout.pondSwitcher.label')}
|
|
onClick={() => (open ? close() : setOpen(true))}
|
|
>
|
|
{current?.name ?? t('layout.pondSwitcher.trigger')}
|
|
</button>
|
|
{open && !creating && (
|
|
<div className="user-menu__list" role="menu">
|
|
{ponds.data.map((pond) => (
|
|
<Link key={pond.id} role="menuitem" to={`/p/${pond.slug}`} onClick={close}>
|
|
{pond.name}
|
|
</Link>
|
|
))}
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
className="pond-switcher__create"
|
|
onClick={() => setCreating(true)}
|
|
>
|
|
{t('pond.create.menuItem')}
|
|
</button>
|
|
</div>
|
|
)}
|
|
{open && creating && (
|
|
<form className="user-menu__list pond-switcher__form" onSubmit={(e) => void create(e)}>
|
|
<h3>{t('pond.create.title')}</h3>
|
|
<FormError error={error} />
|
|
<label>
|
|
{t('pond.create.nameLabel')}
|
|
<input
|
|
type="text"
|
|
value={name}
|
|
required
|
|
maxLength={80}
|
|
autoFocus
|
|
onChange={(e) => setName(e.target.value)}
|
|
/>
|
|
</label>
|
|
<label>
|
|
{t('pond.create.descriptionLabel')}
|
|
<input
|
|
type="text"
|
|
value={description}
|
|
maxLength={500}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
/>
|
|
</label>
|
|
<p className="pond-switcher__hint">{t('pond.create.quotaHint')}</p>
|
|
<div className="pond-switcher__actions">
|
|
<button type="submit" className="button" disabled={busy || name.trim() === ''}>
|
|
{t('pond.create.submit')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="button button--outline"
|
|
onClick={() => {
|
|
setCreating(false);
|
|
setError(null);
|
|
}}
|
|
>
|
|
{t('pond.create.cancel')}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|