"use client"; import { Pencil, Plus, Save, Search, Trash2, X } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { addBlacklistWord as addBlacklistWordAction, createPrefix, deletePrefix as deletePrefixAction, removeBlacklistWord as removeBlacklistWordAction, updatePrefix, updatePrefixSettings, } from "@/actions/prefixes"; import { useConfirmDialog } from "@/components/admin/confirm-dialog"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; 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; editPrefix: UserPrefix | null; }) { const [form, setForm] = useState({ 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 (

{editPrefix ? "Edit Prefix" : "Add Prefix"}

{/* Preview */}
{form.icon && {form.icon}} {hasMultiColor ? ( {"{"} {[...(form.text || "...")] .map((char, i) => ({ char, n: i })) .map(({ char, n }) => ( {char} ))} {"}"} ) : ( {"{"} {form.text || "..."} {"}"} )} {form.username || "Username"}
{!editPrefix && (
Username setForm({ ...form, username: e.target.value })} />
)}
Text setForm({ ...form, text: e.target.value })} /> {form.text.length}/15
Color (HEX or multi: #FF0000,#00FF00)
setForm({ ...form, color: e.target.value })} />
Icon
Effect
setForm({ ...form, active: e.target.checked })} />
); } export function PrefixesClient({ canEdit }: { canEdit: boolean }) { const { run, isPending } = useServerAction(); const { confirm, dialog: confirmDialog } = useConfirmDialog(); const [prefixes, setPrefixes] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [blacklist, setBlacklist] = useState([]); const [newWord, setNewWord] = useState(""); const [settings, setSettings] = useState({ max_length: "15", min_rank_to_buy: "1", price_credits: "5", price_points: "0", points_type: "0", }); const [dialogOpen, setDialogOpen] = useState(false); const [editPrefix, setEditPrefix] = useState(null); const fetchPrefixes = useCallback(async () => { const params = new URLSearchParams({ page: String(page) }); if (searchQuery) params.set("q", searchQuery); const res = await fetch(`/api/admin/prefixes?${params}`); if (!res.ok) return; const data = await res.json(); if (data.ok) { setPrefixes(data.prefixes); setTotalPages(data.pages); } }, [page, searchQuery]); const fetchBlacklist = useCallback(async () => { const res = await fetch("/api/admin/prefixes/blacklist"); if (!res.ok) return; const data = await res.json(); if (data.ok) setBlacklist(data.words); }, []); const fetchSettings = useCallback(async () => { const res = await fetch("/api/admin/prefixes/settings"); if (!res.ok) return; const data = await res.json(); if (data.ok) setSettings(data.settings); }, []); useEffect(() => { fetchPrefixes(); }, [fetchPrefixes]); useEffect(() => { fetchBlacklist(); fetchSettings(); }, [fetchBlacklist, fetchSettings]); const handleDeletePrefix = async (id: number) => { const ok = await confirm({ title: "Delete prefix", description: "Are you sure you want to delete this prefix?", confirmLabel: "Delete", cancelLabel: "Cancel", }); if (!ok) return; run(() => deletePrefixAction({ id }), { successMessage: "Prefix deleted!", onSuccess: () => { fetchPrefixes(); }, }); }; const handleSavePrefix = async (data: PrefixFormData, editId?: number) => { if (editId) { run( () => updatePrefix({ id: editId, text: data.text, color: data.color, icon: data.icon, effect: data.effect, active: data.active ? 1 : 0, }), { successMessage: "Prefix updated!", onSuccess: () => { fetchPrefixes(); }, }, ); } else { run( () => createPrefix({ username: data.username, text: data.text, color: data.color, icon: data.icon, effect: data.effect, active: data.active ? 1 : 0, }), { successMessage: "Prefix created!", onSuccess: () => { fetchPrefixes(); }, }, ); } }; const openAddDialog = () => { setEditPrefix(null); setDialogOpen(true); }; const openEditDialog = (prefix: UserPrefix) => { setEditPrefix(prefix); setDialogOpen(true); }; const handleAddBlacklistWord = async () => { if (!newWord.trim()) return; run(() => addBlacklistWordAction({ word: newWord.trim() }), { successMessage: "Word added to blacklist!", onSuccess: () => { setNewWord(""); fetchBlacklist(); }, }); }; const handleRemoveBlacklistWord = async (id: number) => { run(() => removeBlacklistWordAction({ id }), { successMessage: "Word removed from blacklist!", onSuccess: () => { fetchBlacklist(); }, }); }; const saveSettings = async () => { run( () => updatePrefixSettings({ settings: settings as unknown as Record, }), { successMessage: "Settings saved!", }, ); }; return ( <> {confirmDialog} setDialogOpen(false)} onSave={handleSavePrefix} /> User Prefixes Blacklist Settings
{ setSearchQuery(e.target.value); setPage(1); }} />
{canEdit && ( )}
ID User Prefix Color Icon Effect Active Date {canEdit && Actions} {prefixes.map((prefix) => { const colors = prefix.color.split(","); const hasMultiColor = colors.length > 1 && new Set(colors).size > 1; const prefixIcon = prefix.icon || ""; return ( {prefix.id} {prefix.username || `#${prefix.user_id}`} {hasMultiColor ? ( <> {"{"} {[...prefix.text].map((char, i) => ( {char} ))} {"}"} ) : ( {"{"} {prefix.text} {"}"} )}
{colors.slice(0, 5).map((c, i) => (
))} {colors.length > 5 && ( +{colors.length - 5} )}
{prefixIcon || "-"} {prefix.effect ? PRESET_EFFECTS.find((fx) => fx.id === prefix.effect) ?.label || prefix.effect : "-"} {prefix.active ? "Yes" : "No"} {prefix.created_at ? new Date(prefix.created_at).toLocaleDateString() : "—"} {canEdit && (
)} ); })} {prefixes.length === 0 && ( No prefixes found )}
{totalPages > 1 && (
Page {page} of {totalPages}
)}
Forbidden Words {canEdit && (
setNewWord(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleAddBlacklistWord() } />
)}
{blacklist.map((word) => (
{word.word} {canEdit && ( )}
))} {blacklist.length === 0 && (

No blacklist words

)}
Prefix Settings
Max Length setSettings({ ...settings, max_length: e.target.value }) } />
Minimum Rank setSettings({ ...settings, min_rank_to_buy: e.target.value, }) } />
Price (Credits) setSettings({ ...settings, price_credits: e.target.value, }) } />
Price (Points) setSettings({ ...settings, price_points: e.target.value }) } />
Points Type setSettings({ ...settings, points_type: e.target.value }) } />

0 = Duckets, 5 = Diamonds, etc.

{canEdit && ( )}
); }