refactor(cms): separate dashboard data and editor components with focused UX fixes
CI / check (push) Successful in 1m32s
CI / deploy (push) Successful in 1m20s
CI / e2e (push) Successful in 21s

This commit is contained in:
Simo committed 2026-09-05 18:46:03 +02:00
1 parent 3ae4f21217
commit e17346ab9f
11 files changed
+1212 -1118

No files matched your search

+239
View File
@@ -0,0 +1,239 @@
"use client";
import { Save, X } from "lucide-react";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
PRESET_EFFECTS,
PRESET_ICONS,
type PrefixFormData,
type UserPrefix,
} from "./prefix-model";
export function PrefixDialog({
isOpen,
onClose,
onSave,
editPrefix,
}: {
isOpen: boolean;
onClose: () => void;
onSave: (data: PrefixFormData, editId?: number) => Promise<void>;
editPrefix: UserPrefix | null;
}) {
const [form, setForm] = useState<PrefixFormData>({
username: "",
text: "",
color: "#FFFFFF",
icon: "",
effect: "",
active: false,
});
const [saving, setSaving] = useState(false);
// biome-ignore lint/correctness/useExhaustiveDependencies: explicitly chosen here, see surrounding code
useEffect(() => {
if (editPrefix) {
setForm({
username: editPrefix.username || "",
text: editPrefix.text,
color: editPrefix.color,
icon: editPrefix.icon || "",
effect: editPrefix.effect || "",
active: editPrefix.active,
});
} else {
setForm({
username: "",
text: "",
color: "#FFFFFF",
icon: "",
effect: "",
active: false,
});
}
}, [editPrefix, isOpen]);
if (!isOpen) return null;
const handleSave = async () => {
setSaving(true);
try {
await onSave(form, editPrefix?.id);
onClose();
} finally {
setSaving(false);
}
};
// Parse multi-color for preview
const colors = form.color.split(",");
const hasMultiColor = colors.length > 1 && new Set(colors).size > 1;
return (
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open && !saving) onClose();
}}
disablePointerDismissal
>
<DialogContent
showCloseButton={false}
className="block max-h-[90dvh] overflow-y-auto sm:max-w-md space-y-4"
>
<div className="flex items-center justify-between">
<DialogTitle className="text-lg font-semibold">
{editPrefix ? "Edit Prefix" : "Add Prefix"}
</DialogTitle>
<button
type="button"
disabled={saving}
onClick={onClose}
className="text-muted-foreground hover:text-foreground"
>
<span className="sr-only">Close prefix editor</span>
<X className="h-5 w-5" />
</button>
</div>
{/* Preview */}
<div className="flex items-center justify-center p-3 rounded-lg bg-muted min-h-[40px]">
<span className="text-lg font-bold">
{form.icon && <span className="mr-0.5">{form.icon}</span>}
{hasMultiColor ? (
<span>
{"{"}
{[...(form.text || "...")]
.map((char, i) => ({ char, n: i }))
.map(({ char, n }) => (
<span
key={`char-${n}`}
style={{ color: colors[Math.min(n, colors.length - 1)] }}
>
{char}
</span>
))}
{"}"}
</span>
) : (
<span style={{ color: colors[0] || "#FFFFFF" }}>
{"{"}
{form.text || "..."}
{"}"}
</span>
)}
</span>
<span className="ml-2 text-lg text-muted-foreground">
{form.username || "Username"}
</span>
</div>
{!editPrefix && (
<div className="space-y-2">
<span className="text-sm font-medium">Username</span>
<Input
placeholder="Username..."
aria-label="Username"
value={form.username}
onChange={(e) => setForm({ ...form, username: e.target.value })}
/>
</div>
)}
<div className="space-y-2">
<span className="text-sm font-medium">Text</span>
<Input
maxLength={15}
placeholder="Prefix text..."
aria-label="Prefix text"
value={form.text}
onChange={(e) => setForm({ ...form, text: e.target.value })}
/>
<span className="text-xs text-muted-foreground">
{form.text.length}/15
</span>
</div>
<div className="space-y-2">
<span className="text-sm font-medium">
Color (HEX or multi: #FF0000,#00FF00)
</span>
<div className="flex items-center gap-2">
<Input
placeholder="#FFFFFF o #FF0000,#00FF00,..."
aria-label="Prefix color"
value={form.color}
onChange={(e) => setForm({ ...form, color: e.target.value })}
/>
<div
className="w-8 h-8 rounded border shrink-0"
style={{ backgroundColor: colors[0] || "#FFFFFF" }}
/>
</div>
</div>
<div className="space-y-2">
<span className="text-sm font-medium">Icon</span>
<select
className="w-full px-3 py-2 rounded border bg-background text-sm"
aria-label="Prefix icon"
value={form.icon}
onChange={(e) => setForm({ ...form, icon: e.target.value })}
>
{PRESET_ICONS.map((icon) => (
<option key={icon.label} value={icon.emoji}>
{icon.emoji ? `${icon.emoji} ${icon.label}` : icon.label}
</option>
))}
</select>
</div>
<div className="space-y-2">
<span className="text-sm font-medium">Effect</span>
<select
className="w-full px-3 py-2 rounded border bg-background text-sm"
aria-label="Prefix effect"
value={form.effect}
onChange={(e) => setForm({ ...form, effect: e.target.value })}
>
{PRESET_EFFECTS.map((fx) => (
<option key={fx.id} value={fx.id}>
{fx.label}
</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<input
checked={form.active}
id="prefix-active"
type="checkbox"
onChange={(e) => setForm({ ...form, active: e.target.checked })}
/>
<label className="text-sm" htmlFor="prefix-active">
Active
</label>
</div>
<div className="flex justify-end gap-2 pt-2">
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button
disabled={
saving ||
!form.text.trim() ||
(!editPrefix && !form.username.trim())
}
onClick={handleSave}
>
<Save className="h-4 w-4 mr-1" />
{saving ? "Saving..." : "Save"}
</Button>
</div>
</DialogContent>
</Dialog>
);
}