@@ -1,7 +1,7 @@
" use client" ;
' use client'
import { Pencil , Plus , Save , Search , Trash2 , X } from " lucide-react" ;
import { useCallback , useEffect , useState } from " react" ;
import { Pencil , Plus , Save , Search , Trash2 , X } from ' lucide-react'
import { useCallback , useEffect , useState } from ' react'
import {
addBlacklistWord as addBlacklistWordAction ,
createPrefix ,
@@ -9,91 +9,98 @@ import {
removeBlacklistWord as removeBlacklistWordAction ,
updatePrefix ,
updatePrefixSettings ,
} from " @/actions/prefixes" ;
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" ;
} from ' @/actions/prefixes'
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 ;
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 ;
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 ;
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 ;
username : string
text : string
color : string
icon : string
effect : string
active : boolean
}
const PRESET_EFFECTS = [
{ id : "" , label : " Nessuno" } ,
{ id : " glow" , label : " ✨ Bagliore" } ,
{ id : " shadow" , label : " 🌑 Ombra" } ,
{ id : " italic" , label : " 𝑰 Corsivo" } ,
{ id : " outline" , label : " 🔲 Contorno" } ,
{ id : " pulse" , label : " 💫 Pulsante" } ,
{ id : " bold-glow" , label : " 💡 Neon" } ,
] ;
{ id : '' , label : ' Nessuno' } ,
{ id : ' glow' , label : ' ✨ Bagliore' } ,
{ id : ' shadow' , label : ' 🌑 Ombra' } ,
{ id : ' italic' , label : ' 𝑰 Corsivo' } ,
{ id : ' outline' , label : ' 🔲 Contorno' } ,
{ id : ' pulse' , label : ' 💫 Pulsante' } ,
{ id : ' bold-glow' , label : ' 💡 Neon' } ,
]
const PRESET_ICONS = [
{ emoji : "" , label : " Nessuna" } ,
{ emoji : "👑" , label : " Corona" } ,
{ emoji : "⭐" , label : " Stella" } ,
{ emoji : "❤️" , label : " Cuore" } ,
{ emoji : "💎" , label : " Diamante" } ,
{ emoji : "🔥" , label : " Fuoco" } ,
{ emoji : "⚡" , label : " Fulmine" } ,
{ emoji : "🛡️" , label : " Scudo" } ,
{ emoji : "💀" , label : " Teschio" } ,
{ emoji : "🎵" , label : " Musica" } ,
{ emoji : "💠" , label : " Gemma" } ,
{ emoji : "⚔️" , label : " Spade" } ,
{ emoji : "🏆" , label : " Trofeo" } ,
{ emoji : "🚀" , label : " Razzo" } ,
{ emoji : "👻" , label : " Fantasma" } ,
{ emoji : "🐾" , label : " Zampa" } ,
{ emoji : "🍃" , label : " Foglia" } ,
{ emoji : "😎" , label : " Cool" } ,
{ emoji : "🐺" , label : " Lupo" } ,
{ emoji : "🦁" , label : " Leone" } ,
{ emoji : "🐉" , label : " Drago" } ,
{ emoji : "🦄" , label : " Unicorno" } ,
{ emoji : "🌙" , label : " Luna" } ,
{ emoji : "☀️" , label : " Sole" } ,
{ emoji : "❄️" , label : " Neve" } ,
{ emoji : "🌹" , label : " Rosa" } ,
{ emoji : "🎮" , label : " Controller" } ,
{ emoji : "💰" , label : " Soldi" } ,
{ emoji : "🔮" , label : " Sfera" } ,
{ emoji : " 🏴☠️" , label : " Pirata" } ,
] ;
{ emoji : '' , label : ' Nessuna' } ,
{ emoji : '👑' , label : ' Corona' } ,
{ emoji : '⭐' , label : ' Stella' } ,
{ emoji : '❤️' , label : ' Cuore' } ,
{ emoji : '💎' , label : ' Diamante' } ,
{ emoji : '🔥' , label : ' Fuoco' } ,
{ emoji : '⚡' , label : ' Fulmine' } ,
{ emoji : '🛡️' , label : ' Scudo' } ,
{ emoji : '💀' , label : ' Teschio' } ,
{ emoji : '🎵' , label : ' Musica' } ,
{ emoji : '💠' , label : ' Gemma' } ,
{ emoji : '⚔️' , label : ' Spade' } ,
{ emoji : '🏆' , label : ' Trofeo' } ,
{ emoji : '🚀' , label : ' Razzo' } ,
{ emoji : '👻' , label : ' Fantasma' } ,
{ emoji : '🐾' , label : ' Zampa' } ,
{ emoji : '🍃' , label : ' Foglia' } ,
{ emoji : '😎' , label : ' Cool' } ,
{ emoji : '🐺' , label : ' Lupo' } ,
{ emoji : '🦁' , label : ' Leone' } ,
{ emoji : '🐉' , label : ' Drago' } ,
{ emoji : '🦄' , label : ' Unicorno' } ,
{ emoji : '🌙' , label : ' Luna' } ,
{ emoji : '☀️' , label : ' Sole' } ,
{ emoji : '❄️' , label : ' Neve' } ,
{ emoji : '🌹' , label : ' Rosa' } ,
{ emoji : '🎮' , label : ' Controller' } ,
{ emoji : '💰' , label : ' Soldi' } ,
{ emoji : '🔮' , label : ' Sfera' } ,
{ emoji : ' 🏴☠️' , label : ' Pirata' } ,
]
function PrefixDialog ( {
isOpen ,
@@ -101,59 +108,65 @@ function PrefixDialog({
onSave ,
editPrefix ,
} : {
isOpen : boolean ;
onClose : ( ) = > void ;
onSave : ( data : PrefixFormData , editId? : number ) = > Promise < void > ;
editPrefix : UserPrefix | null ;
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 : "" ,
username : '' ,
text : '' ,
color : ' #FFFFFF' ,
icon : '' ,
effect : '' ,
active : false ,
} ) ;
const [ saving , setSaving ] = useState ( false ) ;
} )
const [ saving , setSaving ] = useState ( false )
// biome-ignore lint/correctness/useExhaustiveDependencies: explicitly chosen here, see surrounding code
useEffect ( ( ) = > {
if ( editPrefix ) {
setForm ( {
username : editPrefix.username || "" ,
username : editPrefix.username || '' ,
text : editPrefix.text ,
color : editPrefix.color ,
icon : editPrefix.icon || "" ,
effect : editPrefix.effect || "" ,
icon : editPrefix.icon || '' ,
effect : editPrefix.effect || '' ,
active : editPrefix.active ,
} ) ;
} )
} else {
setForm ( { username : "" , text : "" , color : " #FFFFFF" , icon : "" , effect : "" , active : false } ) ;
setForm ( { username : '' , text : '' , color : ' #FFFFFF' , icon : '' , effect : '' , active : false } )
}
} , [ editPrefix , isOpen ] ) ;
} , [ editPrefix , isOpen ] )
if ( ! isOpen ) return null ;
if ( ! isOpen ) return null
const handleSave = async ( ) = > {
setSaving ( true ) ;
setSaving ( true )
try {
await onSave ( form , editPrefix ? . id ) ;
onClose ( ) ;
await onSave ( form , editPrefix ? . id )
onClose ( )
} finally {
setSaving ( false ) ;
setSaving ( false )
}
} ;
}
// Parse multi-color for preview
const colors = form . color . split ( "," ) ;
const hasMultiColor = colors . length > 1 && new Set ( colors ) . size > 1 ;
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-black/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 ? "Modifica Prefisso" : "Aggiungi Prefisso" } < / h3 >
< button type = "button" onClick = { onClose } className = "text-muted-foreground hover:text-foreground" >
< h3 className = "text-lg font-semibold" >
{ editPrefix ? 'Modifica Prefisso' : 'Aggiungi Prefisso' }
< / h3 >
< button
type = "button"
onClick = { onClose }
className = "text-muted-foreground hover:text-foreground"
>
< X className = "h-5 w-5" / >
< / button >
< / div >
@@ -164,24 +177,24 @@ function PrefixDialog({
{ form . icon && < span className = "mr-0.5" > { form . icon } < / span > }
{ hasMultiColor ? (
< span >
{ "{" }
{ [ . . . ( form . text || " ..." ) ] . map ( ( char , i ) = > (
{ '{' }
{ [ . . . ( form . text || ' ...' ) ] . map ( ( char , i ) = > (
// biome-ignore lint/suspicious/noArrayIndexKey: char position drives color slot
< span key = { i } style = { { color : colors [ Math . min ( i , colors . length - 1 ) ] } } >
{ char }
< / span >
) ) }
{ "}" }
{ '}' }
< / span >
) : (
< span style = { { color : colors [ 0 ] || " #FFFFFF" } } >
{ "{" }
{ form . text || " ..." }
{ "}" }
< span style = { { color : colors [ 0 ] || ' #FFFFFF' } } >
{ '{' }
{ form . text || ' ...' }
{ '}' }
< / span >
) }
< / span >
< span className = "ml-2 text-lg text-muted-foreground" > { form . username || " Username" } < / span >
< span className = "ml-2 text-lg text-muted-foreground" > { form . username || ' Username' } < / span >
< / div >
{ ! editPrefix && (
@@ -216,7 +229,7 @@ function PrefixDialog({
/ >
< div
className = "w-8 h-8 rounded border shrink-0"
style = { { backgroundColor : colors [ 0 ] || " #FFFFFF" } }
style = { { backgroundColor : colors [ 0 ] || ' #FFFFFF' } }
/ >
< / div >
< / div >
@@ -272,76 +285,76 @@ function PrefixDialog({
onClick = { handleSave }
>
< Save className = "h-4 w-4 mr-1" / >
{ saving ? " Salvando..." : " Salva" }
{ saving ? ' Salvando...' : ' Salva' }
< / Button >
< / div >
< / div >
< / div >
) ;
)
}
export function PrefixesClient ( { canEdit } : { canEdit : boolean } ) {
const { run , isPending } = useServerAction ( ) ;
const [ prefixes , setPrefixes ] = useState < UserPrefix [ ] > ( [ ] ) ;
const [ searchQuery , setSearchQuery ] = useState ( "" ) ;
const [ page , setPage ] = useState ( 1 ) ;
const [ totalPages , setTotalPages ] = useState ( 1 ) ;
const { run , isPending } = useServerAction ( )
const [ prefixes , setPrefixes ] = useState < UserPrefix [ ] > ( [ ] )
const [ searchQuery , setSearchQuery ] = useState ( '' )
const [ page , setPage ] = useState ( 1 )
const [ totalPages , setTotalPages ] = useState ( 1 )
const [ blacklist , setBlacklist ] = useState < BlacklistWord [ ] > ( [ ] ) ;
const [ newWord , setNewWord ] = useState ( "" ) ;
const [ blacklist , setBlacklist ] = useState < BlacklistWord [ ] > ( [ ] )
const [ newWord , setNewWord ] = useState ( '' )
const [ settings , setSettings ] = useState < PrefixSettings > ( {
max_length : "15" ,
min_rank_to_buy : "1" ,
price_credits : "5" ,
price_points : "0" ,
points_type : "0" ,
} ) ;
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 < UserPrefix | null > ( null ) ;
const [ dialogOpen , setDialogOpen ] = useState ( false )
const [ editPrefix , setEditPrefix ] = useState < UserPrefix | null > ( 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 } ` ) ;
const data = await res . json ( ) ;
const params = new URLSearchParams ( { page : String ( page ) } )
if ( searchQuery ) params . set ( 'q' , searchQuery )
const res = await fetch ( ` /api/admin/prefixes? ${ params } ` )
const data = await res . json ( )
if ( data . ok ) {
setPrefixes ( data . prefixes ) ;
setTotalPages ( data . pages ) ;
setPrefixes ( data . prefixes )
setTotalPages ( data . pages )
}
} , [ page , searchQuery ] ) ;
} , [ page , searchQuery ] )
const fetchBlacklist = useCallback ( async ( ) = > {
const res = await fetch ( " /api/admin/prefixes/blacklist" ) ;
const data = await res . json ( ) ;
if ( data . ok ) setBlacklist ( data . words ) ;
} , [ ] ) ;
const res = await fetch ( ' /api/admin/prefixes/blacklist' )
const data = await res . json ( )
if ( data . ok ) setBlacklist ( data . words )
} , [ ] )
const fetchSettings = useCallback ( async ( ) = > {
const res = await fetch ( " /api/admin/prefixes/settings" ) ;
const data = await res . json ( ) ;
if ( data . ok ) setSettings ( data . settings ) ;
} , [ ] ) ;
const res = await fetch ( ' /api/admin/prefixes/settings' )
const data = await res . json ( )
if ( data . ok ) setSettings ( data . settings )
} , [ ] )
useEffect ( ( ) = > {
fetchPrefixes ( ) ;
} , [ fetchPrefixes ] ) ;
fetchPrefixes ( )
} , [ fetchPrefixes ] )
useEffect ( ( ) = > {
fetchBlacklist ( ) ;
fetchSettings ( ) ;
} , [ fetchBlacklist , fetchSettings ] ) ;
fetchBlacklist ( )
fetchSettings ( )
} , [ fetchBlacklist , fetchSettings ] )
const handleDeletePrefix = async ( id : number ) = > {
if ( ! confirm ( " Sei sicuro di voler eliminare questo prefisso?" ) ) return ;
if ( ! confirm ( ' Sei sicuro di voler eliminare questo prefisso?' ) ) return
run ( ( ) = > deletePrefixAction ( { id } ) , {
successMessage : " Prefisso eliminato!" ,
successMessage : ' Prefisso eliminato!' ,
onSuccess : ( ) = > {
fetchPrefixes ( ) ;
fetchPrefixes ( )
} ,
} ) ;
} ;
} )
}
const handleSavePrefix = async ( data : PrefixFormData , editId? : number ) = > {
if ( editId ) {
@@ -356,12 +369,12 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
active : data.active ? 1 : 0 ,
} ) ,
{
successMessage : " Prefisso aggiornato!" ,
successMessage : ' Prefisso aggiornato!' ,
onSuccess : ( ) = > {
fetchPrefixes ( ) ;
fetchPrefixes ( )
} ,
} ,
) ;
)
} else {
run (
( ) = >
@@ -374,50 +387,50 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
active : data.active ? 1 : 0 ,
} ) ,
{
successMessage : " Prefisso creato!" ,
successMessage : ' Prefisso creato!' ,
onSuccess : ( ) = > {
fetchPrefixes ( ) ;
fetchPrefixes ( )
} ,
} ,
) ;
)
}
} ;
}
const openAddDialog = ( ) = > {
setEditPrefix ( null ) ;
setDialogOpen ( true ) ;
} ;
setEditPrefix ( null )
setDialogOpen ( true )
}
const openEditDialog = ( prefix : UserPrefix ) = > {
setEditPrefix ( prefix ) ;
setDialogOpen ( true ) ;
} ;
setEditPrefix ( prefix )
setDialogOpen ( true )
}
const handleAddBlacklistWord = async ( ) = > {
if ( ! newWord . trim ( ) ) return ;
if ( ! newWord . trim ( ) ) return
run ( ( ) = > addBlacklistWordAction ( { word : newWord.trim ( ) } ) , {
successMessage : " Parola aggiunta alla blacklist!" ,
successMessage : ' Parola aggiunta alla blacklist!' ,
onSuccess : ( ) = > {
setNewWord ( "" ) ;
fetchBlacklist ( ) ;
setNewWord ( '' )
fetchBlacklist ( )
} ,
} ) ;
} ;
} )
}
const handleRemoveBlacklistWord = async ( id : number ) = > {
run ( ( ) = > removeBlacklistWordAction ( { id } ) , {
successMessage : " Parola rimossa dalla blacklist!" ,
successMessage : ' Parola rimossa dalla blacklist!' ,
onSuccess : ( ) = > {
fetchBlacklist ( ) ;
fetchBlacklist ( )
} ,
} ) ;
} ;
} )
}
const saveSettings = async ( ) = > {
run ( ( ) = > updatePrefixSettings ( { settings : settings as unknown as Record < string , string > } ) , {
successMessage : " Impostazioni salvate!" ,
} ) ;
} ;
successMessage : ' Impostazioni salvate!' ,
} )
}
return (
< >
@@ -444,8 +457,8 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
placeholder = "Cerca per utente o testo..."
value = { searchQuery }
onChange = { ( e ) = > {
setSearchQuery ( e . target . value ) ;
setPage ( 1 ) ;
setSearchQuery ( e . target . value )
setPage ( 1 )
} }
/ >
< / div >
@@ -476,19 +489,19 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
< / TableHeader >
< TableBody >
{ prefixes . map ( ( prefix ) = > {
const colors = prefix . color . split ( "," ) ;
const hasMultiColor = colors . length > 1 && new Set ( colors ) . size > 1 ;
const prefixIcon = prefix . icon || "" ;
const colors = prefix . color . split ( ',' )
const hasMultiColor = colors . length > 1 && new Set ( colors ) . size > 1
const prefixIcon = prefix . icon || ''
return (
< TableRow key = { prefix . id } >
< TableCell > { prefix . id } < / TableCell >
< TableCell > { prefix . username || ` # ${ prefix . user_id } ` } < / TableCell >
< TableCell >
< span style = { { fontWeight : " bold" } } >
< span style = { { fontWeight : ' bold' } } >
{ hasMultiColor ? (
< >
{ "{" }
{ '{' }
{ [ . . . prefix . text ] . map ( ( char , i ) = > (
< span
// biome-ignore lint/suspicious/noArrayIndexKey: char position drives color slot
@@ -500,13 +513,13 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
{ char }
< / span >
) ) }
{ "}" }
{ '}' }
< / >
) : (
< span style = { { color : colors [ 0 ] || " #FFFFFF" } } >
{ "{" }
< span style = { { color : colors [ 0 ] || ' #FFFFFF' } } >
{ '{' }
{ prefix . text }
{ "}" }
{ '}' }
< / span >
) }
< / span >
@@ -523,24 +536,31 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
/ >
) ) }
{ colors . length > 5 && (
< span className = "text-xs text-muted-foreground" > + { colors . length - 5 } < / span >
< span className = "text-xs text-muted-foreground" >
+ { colors . length - 5 }
< / span >
) }
< / div >
< / TableCell >
< TableCell className = "text-center text-lg" > { prefixIcon || "-" } < / TableCell >
< TableCell className = "text-center text-lg" > { prefixIcon || '-' } < / TableCell >
< TableCell className = "text-xs" >
{ prefix . effect
? PRESET_EFFECTS . find ( ( fx ) = > fx . id === prefix . effect ) ? . label || prefix . effect
: "-" }
? PRESET_EFFECTS . find ( ( fx ) = > fx . id === prefix . effect ) ? . label ||
prefix . effect
: '-' }
< / TableCell >
< TableCell > { prefix . active ? "Si" : "No" } < / TableCell >
< TableCell > { prefix . active ? 'Si' : 'No' } < / TableCell >
< TableCell className = "text-xs" >
{ new Date ( prefix . created_at ) . toLocaleDateString ( ) }
< / TableCell >
{ canEdit && (
< TableCell >
< div className = "flex items-center gap-1" >
< Button size = "icon" variant = "outline" onClick = { ( ) = > openEditDialog ( prefix ) } >
< Button
size = "icon"
variant = "outline"
onClick = { ( ) = > openEditDialog ( prefix ) }
>
< Pencil className = "h-4 w-4" / >
< / Button >
< Button
@@ -555,7 +575,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
< / TableCell >
) }
< / TableRow >
) ;
)
} ) }
{ prefixes . length === 0 && (
< TableRow >
@@ -569,7 +589,12 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
{ totalPages > 1 && (
< div className = "flex items-center justify-center gap-2" >
< Button disabled = { page <= 1 } size = "sm" variant = "outline" onClick = { ( ) = > setPage ( page - 1 ) } >
< Button
disabled = { page <= 1 }
size = "sm"
variant = "outline"
onClick = { ( ) = > setPage ( page - 1 ) }
>
Precedente
< / Button >
< span className = "text-sm" >
@@ -599,7 +624,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
placeholder = "Aggiungi parola..."
value = { newWord }
onChange = { ( e ) = > setNewWord ( e . target . value ) }
onKeyDown = { ( e ) = > e . key === " Enter" && handleAddBlacklistWord ( ) }
onKeyDown = { ( e ) = > e . key === ' Enter' && handleAddBlacklistWord ( ) }
/ >
< Button disabled = { isPending } onClick = { handleAddBlacklistWord } >
< Plus className = "h-4 w-4 mr-1" / >
@@ -699,5 +724,5 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
< / TabsContent >
< / Tabs >
< / >
) ;
)
}