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

+9 -291
View File
@@ -1,6 +1,6 @@
"use client";
import { Pencil, Plus, Save, Search, Trash2, X } from "lucide-react";
import { Pencil, Plus, Save, Search, Trash2 } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import {
addBlacklistWord as addBlacklistWordAction,
@@ -25,296 +25,14 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useServerAction } from "@/hooks/use-server-action";
interface UserPrefix {
id: number;
user_id: number;
text: string;
color: string;
icon: string;
effect: string;
active: boolean;
created_at: string;
username?: string;
}
interface BlacklistWord {
id: number;
word: string;
created_at: string;
}
interface PrefixSettings {
max_length: string;
min_rank_to_buy: string;
price_credits: string;
price_points: string;
points_type: string;
}
interface PrefixFormData {
username: string;
text: string;
color: string;
icon: string;
effect: string;
active: boolean;
}
const PRESET_EFFECTS = [
{ id: "", label: "None" },
{ id: "glow", label: "✨ Glow" },
{ id: "shadow", label: "🌑 Shadow" },
{ id: "italic", label: "𝑰 Italic" },
{ id: "outline", label: "🔲 Outline" },
{ id: "pulse", label: "💫 Pulse" },
{ id: "bold-glow", label: "💡 Neon" },
];
const PRESET_ICONS = [
{ emoji: "", label: "None" },
{ emoji: "👑", label: "Crown" },
{ emoji: "⭐", label: "Star" },
{ emoji: "❤️", label: "Heart" },
{ emoji: "💎", label: "Diamond" },
{ emoji: "🔥", label: "Fire" },
{ emoji: "⚡", label: "Lightning" },
{ emoji: "🛡️", label: "Shield" },
{ emoji: "💀", label: "Skull" },
{ emoji: "🎵", label: "Music" },
{ emoji: "💠", label: "Gem" },
{ emoji: "⚔️", label: "Swords" },
{ emoji: "🏆", label: "Trophy" },
{ emoji: "🚀", label: "Rocket" },
{ emoji: "👻", label: "Ghost" },
{ emoji: "🐾", label: "Paw" },
{ emoji: "🍃", label: "Leaf" },
{ emoji: "😎", label: "Cool" },
{ emoji: "🐺", label: "Wolf" },
{ emoji: "🦁", label: "Lion" },
{ emoji: "🐉", label: "Dragon" },
{ emoji: "🦄", label: "Unicorn" },
{ emoji: "🌙", label: "Moon" },
{ emoji: "☀️", label: "Sun" },
{ emoji: "❄️", label: "Snow" },
{ emoji: "🌹", label: "Rose" },
{ emoji: "🎮", label: "Controller" },
{ emoji: "💰", label: "Money" },
{ emoji: "🔮", label: "Orb" },
{ emoji: "🏴‍☠️", label: "Pirate" },
];
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 (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-foreground/50">
<div className="bg-background rounded-lg shadow-lg w-full max-w-md p-6 space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">
{editPrefix ? "Edit Prefix" : "Add Prefix"}
</h3>
<button
type="button"
onClick={onClose}
className="text-muted-foreground hover:text-foreground"
>
<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..."
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..."
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,..."
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"
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"
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>
</div>
</div>
);
}
import { PrefixDialog } from "./prefix-dialog";
import {
type BlacklistWord,
PRESET_EFFECTS,
type PrefixFormData,
type PrefixSettings,
type UserPrefix,
} from "./prefix-model";
export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
const { run, isPending } = useServerAction();
const { confirm, dialog: confirmDialog } = useConfirmDialog();