mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 07:28:45 -07:00
Implement Play Menu Tags management: add endpoints for retrieving and updating tags; enhance database schema and UI integration
This commit is contained in:
@@ -10,8 +10,20 @@ import { useToast } from '../components/Toast';
|
||||
import { Confirm } from '../components/Confirm';
|
||||
import { RefreshCw, Trash, Upload } from '../components/Icons';
|
||||
|
||||
interface PlayMenuTagSettings {
|
||||
pinnedTags: string[];
|
||||
popularTags: string[];
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export function Rooms() {
|
||||
const { data, loading, error, refresh } = useApi<Room[]>('/rooms');
|
||||
const {
|
||||
data: playMenuTags,
|
||||
loading: tagsLoading,
|
||||
error: tagsError,
|
||||
refresh: refreshPlayMenuTags,
|
||||
} = useApi<PlayMenuTagSettings>('/settings/play-menu-tags');
|
||||
const [filter, setFilter] = useState('');
|
||||
const [filterKind, setFilterKind] = useState<'all' | 'original' | 'custom' | 'dorm'>('all');
|
||||
const [pendingDelete, setPendingDelete] = useState<Room | null>(null);
|
||||
@@ -64,6 +76,13 @@ export function Rooms() {
|
||||
</>}
|
||||
/>
|
||||
|
||||
<PlayMenuTagsPanel
|
||||
tags={playMenuTags}
|
||||
loading={tagsLoading}
|
||||
error={tagsError}
|
||||
onSaved={refreshPlayMenuTags}
|
||||
/>
|
||||
|
||||
<div className="card overflow-hidden">
|
||||
<div className="flex flex-wrap items-center gap-2 border-b border-ink-800 px-3 py-2.5">
|
||||
<input value={filter} onChange={e => setFilter(e.target.value)} placeholder="Filter by name…" className="input max-w-xs" />
|
||||
@@ -156,6 +175,102 @@ export function Rooms() {
|
||||
);
|
||||
}
|
||||
|
||||
function PlayMenuTagsPanel({
|
||||
tags,
|
||||
loading,
|
||||
error,
|
||||
onSaved,
|
||||
}: {
|
||||
tags: PlayMenuTagSettings | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
onSaved: () => void;
|
||||
}) {
|
||||
const [pinnedText, setPinnedText] = useState('');
|
||||
const [popularText, setPopularText] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const toast = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
if (!tags) return;
|
||||
setPinnedText(tags.pinnedTags.join('\n'));
|
||||
setPopularText(tags.popularTags.join('\n'));
|
||||
}, [tags]);
|
||||
|
||||
const parseTags = (text: string) =>
|
||||
text
|
||||
.split(/[\n,]+/)
|
||||
.map(t => t.trim().replace(/^#+/, '').toLowerCase())
|
||||
.filter(Boolean);
|
||||
|
||||
const save = async (reset = false) => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await api<PlayMenuTagSettings>('/settings/play-menu-tags', {
|
||||
method: 'POST',
|
||||
body: reset
|
||||
? { PinnedTags: [], PopularTags: [] }
|
||||
: { PinnedTags: parseTags(pinnedText), PopularTags: parseTags(popularText) },
|
||||
});
|
||||
toast.push(reset ? 'Play menu tags reset.' : 'Play menu tags saved.', 'success');
|
||||
onSaved();
|
||||
} catch (e) {
|
||||
toast.push((e as Error).message, 'error');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card !p-4 mb-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-ink-50">Play menu tags</h2>
|
||||
<p className="mt-1 text-xs text-ink-400">
|
||||
Filter chips shown in the watch Play menu. Use one tag per line or commas; leave off the #.
|
||||
</p>
|
||||
{tags && (
|
||||
<p className="mt-1 text-[11px] text-ink-500">
|
||||
Last changed {new Date(tags.updatedAt).toLocaleString()}.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => save(true)} disabled={busy || loading} className="btn-secondary text-xs">
|
||||
Defaults
|
||||
</button>
|
||||
<button onClick={() => save(false)} disabled={busy || loading} className="btn-primary text-xs">
|
||||
{busy ? 'Saving...' : 'Save tags'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div className="mt-3 text-sm text-danger">{error}</div>}
|
||||
{loading && !tags && <div className="mt-3 text-xs text-ink-400">Loading tags...</div>}
|
||||
{tags && (
|
||||
<div className="mt-4 grid gap-3 md:grid-cols-2">
|
||||
<label className="block">
|
||||
<span className="label">Pinned tags</span>
|
||||
<textarea
|
||||
value={pinnedText}
|
||||
onChange={e => setPinnedText(e.target.value)}
|
||||
className="input mt-1 min-h-36 font-mono text-xs"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="label">Popular tags</span>
|
||||
<textarea
|
||||
value={popularText}
|
||||
onChange={e => setPopularText(e.target.value)}
|
||||
className="input mt-1 min-h-36 font-mono text-xs"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Per-room hard-delete modal with two typed-name confirmations ─────────
|
||||
// The previous bulk "purge all custom" button was removed — too easy to
|
||||
// click by accident. Each room now gets its own purge flow that requires
|
||||
|
||||
Reference in New Issue
Block a user