@@ -181,6 +181,9 @@ const translations = {
banUser : 'Ban User' ,
banUser : 'Ban User' ,
unbanUser : 'Unban User' ,
unbanUser : 'Unban User' ,
isBanned : 'Banned' ,
isBanned : 'Banned' ,
blockUser : 'Block User' ,
unblockUser : 'Unblock User' ,
blockedTooltip : 'Blocked' ,
authSecurity : 'Security' ,
authSecurity : 'Security' ,
bio : 'Bio' ,
bio : 'Bio' ,
messagesSent : 'Messages' ,
messagesSent : 'Messages' ,
@@ -348,6 +351,9 @@ const translations = {
banUser : 'Забанить' ,
banUser : 'Забанить' ,
unbanUser : 'Разбанить' ,
unbanUser : 'Разбанить' ,
isBanned : 'Забанен' ,
isBanned : 'Забанен' ,
blockUser : 'Заблокировать' ,
unblockUser : 'Разблокировать' ,
blockedTooltip : 'Заблокирован' ,
authSecurity : 'Безопасность' ,
authSecurity : 'Безопасность' ,
bio : 'О себе' ,
bio : 'О себе' ,
messagesSent : 'Сообщения' ,
messagesSent : 'Сообщения' ,
@@ -541,7 +547,7 @@ export default function AdminPage() {
const [ tzSearch , setTzSearch ] = useState ( '' ) ;
const [ tzSearch , setTzSearch ] = useState ( '' ) ;
const [ showTzDropdown , setShowTzDropdown ] = useState ( false ) ;
const [ showTzDropdown , setShowTzDropdown ] = useState ( false ) ;
const [ toast , setToast ] = useState < { message : string , type : 'success' | 'error' } | null > ( null ) ;
const [ toast , setToast ] = useState < { message : string , type : 'success' | 'error' } | null > ( null ) ;
const showToast = ( message : string , type : 'success' | 'error' = 'success' ) = > {
const showToast = ( message : string , type : 'success' | 'error' = 'success' ) = > {
setToast ( { message , type } ) ;
setToast ( { message , type } ) ;
@@ -551,7 +557,7 @@ export default function AdminPage() {
const generatePassword = ( ) = > {
const generatePassword = ( ) = > {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()' ;
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()' ;
let pass = '' ;
let pass = '' ;
for ( let i = 0 ; i < 12 ; i + + ) pass + = chars [ Math.floor ( Math.random ( ) * chars.length ) ] ;
for ( let i = 0 ; i < 12 ; i ++ ) pass += chars [ Math . floor ( Math . random ( ) * chars . length ) ] ;
return pass ;
return pass ;
} ;
} ;
@@ -625,21 +631,21 @@ export default function AdminPage() {
try {
try {
const res = await httpClient . request < Stats > ( '/admin/dashboard' ) ;
const res = await httpClient . request < Stats > ( '/admin/dashboard' ) ;
setStats ( res ) ;
setStats ( res ) ;
} catch { }
} catch { }
} ;
} ;
const fetchSettings = async ( ) = > {
const fetchSettings = async ( ) = > {
try {
try {
const res = await httpClient . request < Conf > ( '/admin/settings' ) ;
const res = await httpClient . request < Conf > ( '/admin/settings' ) ;
setConfig ( res ) ;
setConfig ( res ) ;
} catch { }
} catch { }
} ;
} ;
const fetchTimezones = async ( ) = > {
const fetchTimezones = async ( ) = > {
try {
try {
const res = await httpClient . request < TimezoneDto [ ] > ( '/admin/timezones' ) ;
const res = await httpClient . request < TimezoneDto [ ] > ( '/admin/timezones' ) ;
setTimezones ( res ) ;
setTimezones ( res ) ;
} catch { }
} catch { }
} ;
} ;
const saveSettings = async ( ) = > {
const saveSettings = async ( ) = > {
@@ -789,7 +795,7 @@ export default function AdminPage() {
try {
try {
const res = await httpClient . request < AppUser [ ] > ( ` /admin/users?query= ${ encodeURIComponent ( q ) } ` ) ;
const res = await httpClient . request < AppUser [ ] > ( ` /admin/users?query= ${ encodeURIComponent ( q ) } ` ) ;
setUsers ( res ) ;
setUsers ( res ) ;
} catch { } finally {
} catch { } finally {
setIsSearching ( false ) ;
setIsSearching ( false ) ;
}
}
} ;
} ;
@@ -798,7 +804,7 @@ export default function AdminPage() {
try {
try {
const res = await httpClient . request < AppUser > ( ` /admin/users/ ${ id } ` ) ;
const res = await httpClient . request < AppUser > ( ` /admin/users/ ${ id } ` ) ;
setSelectedUser ( res ) ;
setSelectedUser ( res ) ;
} catch { }
} catch { }
} ;
} ;
const handleLogin = async ( e : React.FormEvent ) = > {
const handleLogin = async ( e : React.FormEvent ) = > {
@@ -837,8 +843,8 @@ export default function AdminPage() {
< div className = "flex justify-between items-center mb-6" >
< div className = "flex justify-between items-center mb-6" >
< h1 className = "text-2xl font-bold text-accent" > { t . loginTitle } < / h1 >
< h1 className = "text-2xl font-bold text-accent" > { t . loginTitle } < / h1 >
< div className = "flex gap-2" >
< div className = "flex gap-2" >
< button type = "button" onClick = { ( ) = > setLang ( 'en' ) } className = { ` text-sm ${ lang === 'en' ? 'text-accent font-bold' : 'text-gray-500' } ` } > EN < / button >
< button type = "button" onClick = { ( ) = > setLang ( 'en' ) } className = { ` text-sm ${ lang === 'en' ? 'text-accent font-bold' : 'text-gray-500' } ` } > EN < / button >
< button type = "button" onClick = { ( ) = > setLang ( 'ru' ) } className = { ` text-sm ${ lang === 'ru' ? 'text-accent font-bold' : 'text-gray-500' } ` } > RU < / button >
< button type = "button" onClick = { ( ) = > setLang ( 'ru' ) } className = { ` text-sm ${ lang === 'ru' ? 'text-accent font-bold' : 'text-gray-500' } ` } > RU < / button >
< / div >
< / div >
< / div >
< / div >
< input
< input
@@ -914,7 +920,7 @@ export default function AdminPage() {
< div className = "flex justify-between items-end mb-6" >
< div className = "flex justify-between items-end mb-6" >
< div >
< div >
< h3 className = "text-gray-400 text-[10px] mb-1 flex items-center gap-2 uppercase tracking-[0.2em] font-black" >
< h3 className = "text-gray-400 text-[10px] mb-1 flex items-center gap-2 uppercase tracking-[0.2em] font-black" >
< Database className = "w-4 h-4 text-accent" / > { t . storageUsed }
< Database className = "w-4 h-4 text-accent" / > { t . storageUsed }
< / h3 >
< / h3 >
< div className = "text-4xl font-black flex items-baseline gap-2" >
< div className = "text-4xl font-black flex items-baseline gap-2" >
{ formatBytes ( stats . storageUsedBytes ) }
{ formatBytes ( stats . storageUsedBytes ) }
@@ -940,7 +946,7 @@ export default function AdminPage() {
< div className = "bg-surface border border-white/10 p-8 rounded-3xl lg:col-span-1" >
< div className = "bg-surface border border-white/10 p-8 rounded-3xl lg:col-span-1" >
< h3 className = "text-gray-400 text-[10px] mb-4 flex items-center gap-2 uppercase tracking-[0.2em] font-black" >
< h3 className = "text-gray-400 text-[10px] mb-4 flex items-center gap-2 uppercase tracking-[0.2em] font-black" >
< Users className = "w-4 h-4 text-accent" / > { t . totalRegistered }
< Users className = "w-4 h-4 text-accent" / > { t . totalRegistered }
< / h3 >
< / h3 >
< div className = "text-5xl font-black mb-2 tracking-tighter" > { stats . totalUsers } < / div >
< div className = "text-5xl font-black mb-2 tracking-tighter" > { stats . totalUsers } < / div >
< div className = "flex items-center gap-2" >
< div className = "flex items-center gap-2" >
@@ -984,7 +990,7 @@ export default function AdminPage() {
< / h1 >
< / h1 >
< div className = "ml-auto flex gap-2" >
< div className = "ml-auto flex gap-2" >
< button onClick = { fetchSettings } className = "p-2.5 hover:bg-white/5 rounded-xl text-gray-400 transition-all active:scale-95" >
< button onClick = { fetchSettings } className = "p-2.5 hover:bg-white/5 rounded-xl text-gray-400 transition-all active:scale-95" >
< Activity className = "w-5 h-5" / >
< Activity className = "w-5 h-5" / >
< / button >
< / button >
< button onClick = { saveSettings } className = "bg-accent hover:bg-accentLight text-black font-bold px-8 py-2.5 rounded-xl transition-all shadow-lg shadow-accent/20 active:scale-95" >
< button onClick = { saveSettings } className = "bg-accent hover:bg-accentLight text-black font-bold px-8 py-2.5 rounded-xl transition-all shadow-lg shadow-accent/20 active:scale-95" >
{ t . saveChanges }
{ t . saveChanges }
@@ -999,18 +1005,18 @@ export default function AdminPage() {
< div className = "grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-8" >
< div className = "grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-8" >
< label className = "flex flex-col gap-1.5 group" >
< label className = "flex flex-col gap-1.5 group" >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1 group-focus-within:text-accent transition-colors" > { t . domainUrl } < / span >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1 group-focus-within:text-accent transition-colors" > { t . domainUrl } < / span >
< input className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 focus:bg-black/60 transition-all font-mono text-sm" value = { config . system . domainUrl } onChange = { e = > setConfig ( { . . . config , system : { . . . config . system , domainUrl : e.target.value } } ) } / >
< input className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 focus:bg-black/60 transition-all font-mono text-sm" value = { config . system . domainUrl } onChange = { e = > setConfig ( { . . . config , system : { . . . config . system , domainUrl : e.target.value } } ) } / >
< Hint > { t . hints . domainUrl } < / Hint >
< Hint > { t . hints . domainUrl } < / Hint >
< / label >
< / label >
< label className = "flex flex-col gap-1.5 group" >
< label className = "flex flex-col gap-1.5 group" >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1 group-focus-within:text-accent transition-colors" > { t . adminPublicPath } < / span >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1 group-focus-within:text-accent transition-colors" > { t . adminPublicPath } < / span >
< input className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 focus:bg-black/60 transition-all font-mono text-sm" value = { config . system . adminRoute } onChange = { e = > setConfig ( { . . . config , system : { . . . config . system , adminRoute : e.target.value } } ) } placeholder = "/admin" / >
< input className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 focus:bg-black/60 transition-all font-mono text-sm" value = { config . system . adminRoute } onChange = { e = > setConfig ( { . . . config , system : { . . . config . system , adminRoute : e.target.value } } ) } placeholder = "/admin" / >
< Hint > { t . hints . adminRoute } < / Hint >
< Hint > { t . hints . adminRoute } < / Hint >
< / label >
< / label >
< div className = "flex flex-col gap-2 bg-white/[0.02] p-4 rounded-2xl border border-white/5" >
< div className = "flex flex-col gap-2 bg-white/[0.02] p-4 rounded-2xl border border-white/5" >
< div className = "flex items-center justify-between" >
< div className = "flex items-center justify-between" >
< span className = "text-sm text-gray-200 font-semibold" > { t . enableReg } < / span >
< span className = "text-sm text-gray-200 font-semibold" > { t . enableReg } < / span >
< Toggle checked = { config . system . enableRegistration } onChange = { v = > setConfig ( { . . . config , system : { . . . config . system , enableRegistration : v } } ) } / >
< Toggle checked = { config . system . enableRegistration } onChange = { v = > setConfig ( { . . . config , system : { . . . config . system , enableRegistration : v } } ) } / >
< / div >
< / div >
< Hint > { t . hints . enableRegistration } < / Hint >
< Hint > { t . hints . enableRegistration } < / Hint >
< / div >
< / div >
@@ -1022,7 +1028,7 @@ export default function AdminPage() {
value = { config . system . serverTimezone }
value = { config . system . serverTimezone }
onFocus = { ( ) = > setShowTzDropdown ( true ) }
onFocus = { ( ) = > setShowTzDropdown ( true ) }
onChange = { e = > {
onChange = { e = > {
setConfig ( { . . . config , system : { . . . config . system , serverTimezone : e.target.value } } ) ;
setConfig ( { . . . config , system : { . . . config . system , serverTimezone : e.target.value } } ) ;
setTzSearch ( e . target . value ) ;
setTzSearch ( e . target . value ) ;
} }
} }
placeholder = "UTC"
placeholder = "UTC"
@@ -1043,7 +1049,7 @@ export default function AdminPage() {
< button
< button
key = { tz . id }
key = { tz . id }
onClick = { ( ) = > {
onClick = { ( ) = > {
setConfig ( { . . . config , system : { . . . config . system , serverTimezone : tz.id } } ) ;
setConfig ( { . . . config , system : { . . . config . system , serverTimezone : tz.id } } ) ;
setTzSearch ( '' ) ;
setTzSearch ( '' ) ;
setShowTzDropdown ( false ) ;
setShowTzDropdown ( false ) ;
} }
} }
@@ -1073,35 +1079,35 @@ export default function AdminPage() {
< div className = "bg-surface border border-white/10 rounded-2xl p-8 flex flex-col gap-8" >
< div className = "bg-surface border border-white/10 rounded-2xl p-8 flex flex-col gap-8" >
< div className = "flex items-center justify-between" >
< div className = "flex items-center justify-between" >
< h3 className = "text-xl font-semibold opacity-30 uppercase tracking-widest text-sm" > { t . stories } < / h3 >
< h3 className = "text-xl font-semibold opacity-30 uppercase tracking-widest text-sm" > { t . stories } < / h3 >
< Toggle checked = { config . stories . enabled } onChange = { v = > setConfig ( { . . . config , stories : { . . . config . stories , enabled : v } } ) } / >
< Toggle checked = { config . stories . enabled } onChange = { v = > setConfig ( { . . . config , stories : { . . . config . stories , enabled : v } } ) } / >
< / div >
< / div >
{ config . stories . enabled && (
{ config . stories . enabled && (
< div className = "grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-10 pt-4 border-t border-white/10" >
< div className = "grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-10 pt-4 border-t border-white/10" >
< label className = "flex flex-col gap-1.5 group" >
< label className = "flex flex-col gap-1.5 group" >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1" > { t . maxStories } < / span >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1" > { t . maxStories } < / span >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 transition-all" value = { config . stories . maxStoriesPerPeriod } onChange = { e = > setConfig ( { . . . config , stories : { . . . config . stories , maxStoriesPerPeriod : parseInt ( e . target . value ) || 0 } } ) } / >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 transition-all" value = { config . stories . maxStoriesPerPeriod } onChange = { e = > setConfig ( { . . . config , stories : { . . . config . stories , maxStoriesPerPeriod : parseInt ( e . target . value ) || 0 } } ) } / >
< Hint > { t . hints . maxStories } < / Hint >
< Hint > { t . hints . maxStories } < / Hint >
< / label >
< / label >
< label className = "flex flex-col gap-1.5 group" >
< label className = "flex flex-col gap-1.5 group" >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1" > { t . storyLifetime } < / span >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1" > { t . storyLifetime } < / span >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 transition-all" value = { config . stories . storyLifetimeHours } onChange = { e = > setConfig ( { . . . config , stories : { . . . config . stories , storyLifetimeHours : parseInt ( e . target . value ) || 0 } } ) } / >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 transition-all" value = { config . stories . storyLifetimeHours } onChange = { e = > setConfig ( { . . . config , stories : { . . . config . stories , storyLifetimeHours : parseInt ( e . target . value ) || 0 } } ) } / >
< Hint > { t . hints . storyLifetime } < / Hint >
< Hint > { t . hints . storyLifetime } < / Hint >
< / label >
< / label >
< div className = "flex flex-col gap-2 bg-white/[0.02] p-4 rounded-2xl border border-white/5" >
< div className = "flex flex-col gap-2 bg-white/[0.02] p-4 rounded-2xl border border-white/5" >
< div className = "flex items-center justify-between" >
< div className = "flex items-center justify-between" >
< span className = "text-sm text-gray-200 font-semibold" > { t . textStories } < / span >
< span className = "text-sm text-gray-200 font-semibold" > { t . textStories } < / span >
< Toggle checked = { config . stories . textStoriesEnabled } onChange = { v = > setConfig ( { . . . config , stories : { . . . config . stories , textStoriesEnabled : v } } ) } / >
< Toggle checked = { config . stories . textStoriesEnabled } onChange = { v = > setConfig ( { . . . config , stories : { . . . config . stories , textStoriesEnabled : v } } ) } / >
< / div >
< / div >
< Hint > { t . hints . textStoryDuration } < / Hint >
< Hint > { t . hints . textStoryDuration } < / Hint >
< / div >
< / div >
< label className = "flex flex-col gap-1.5 group" >
< label className = "flex flex-col gap-1.5 group" >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1" > { t . mediaStoryDuration } < / span >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1" > { t . mediaStoryDuration } < / span >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 transition-all" value = { config . stories . mediaStoryMaxDurationSeconds } onChange = { e = > setConfig ( { . . . config , stories : { . . . config . stories , mediaStoryMaxDurationSeconds : parseInt ( e . target . value ) || 0 } } ) } / >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 transition-all" value = { config . stories . mediaStoryMaxDurationSeconds } onChange = { e = > setConfig ( { . . . config , stories : { . . . config . stories , mediaStoryMaxDurationSeconds : parseInt ( e . target . value ) || 0 } } ) } / >
< Hint > { t . hints . mediaStoryDuration } < / Hint >
< Hint > { t . hints . mediaStoryDuration } < / Hint >
< / label >
< / label >
< label className = "flex flex-col gap-1.5 group" >
< label className = "flex flex-col gap-1.5 group" >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1" > { t . maxMediaSize } < / span >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1" > { t . maxMediaSize } < / span >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 transition-all" value = { bytesToMb ( config . stories . maxMediaSizeBytes ) } onChange = { e = > setConfig ( { . . . config , stories : { . . . config . stories , maxMediaSizeBytes : mbToBytes ( parseInt ( e . target . value ) || 0 ) } } ) } / >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 transition-all" value = { bytesToMb ( config . stories . maxMediaSizeBytes ) } onChange = { e = > setConfig ( { . . . config , stories : { . . . config . stories , maxMediaSizeBytes : mbToBytes ( parseInt ( e . target . value ) || 0 ) } } ) } / >
< Hint > { t . hints . maxMediaSize } < / Hint >
< Hint > { t . hints . maxMediaSize } < / Hint >
< / label >
< / label >
< / div >
< / div >
@@ -1118,13 +1124,13 @@ export default function AdminPage() {
< div className = "flex flex-col gap-2 bg-white/[0.02] p-4 rounded-2xl border border-white/5" >
< div className = "flex flex-col gap-2 bg-white/[0.02] p-4 rounded-2xl border border-white/5" >
< div className = "flex items-center justify-between" >
< div className = "flex items-center justify-between" >
< span className = "text-sm text-gray-200 font-semibold" > { t . supportGroups } < / span >
< span className = "text-sm text-gray-200 font-semibold" > { t . supportGroups } < / span >
< Toggle checked = { config . chats . supportGroups } onChange = { v = > setConfig ( { . . . config , chats : { . . . config . chats , supportGroups : v } } ) } / >
< Toggle checked = { config . chats . supportGroups } onChange = { v = > setConfig ( { . . . config , chats : { . . . config . chats , supportGroups : v } } ) } / >
< / div >
< / div >
< Hint > { t . hints . supportGroups } < / Hint >
< Hint > { t . hints . supportGroups } < / Hint >
< / div >
< / div >
< label className = "flex flex-col gap-1.5 group" >
< label className = "flex flex-col gap-1.5 group" >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1" > { t . maxGroupMembers } < / span >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1" > { t . maxGroupMembers } < / span >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 transition-all" value = { config . chats . maxGroupParticipants } onChange = { e = > setConfig ( { . . . config , chats : { . . . config . chats , maxGroupParticipants : parseInt ( e . target . value ) || 0 } } ) } / >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 transition-all" value = { config . chats . maxGroupParticipants } onChange = { e = > setConfig ( { . . . config , chats : { . . . config . chats , maxGroupParticipants : parseInt ( e . target . value ) || 0 } } ) } / >
< Hint > { t . hints . maxGroupMembers } < / Hint >
< Hint > { t . hints . maxGroupMembers } < / Hint >
< / label >
< / label >
< / div >
< / div >
@@ -1132,21 +1138,21 @@ export default function AdminPage() {
< div className = "flex flex-col gap-2 bg-white/[0.02] p-4 rounded-2xl border border-white/5" >
< div className = "flex flex-col gap-2 bg-white/[0.02] p-4 rounded-2xl border border-white/5" >
< div className = "flex items-center justify-between" >
< div className = "flex items-center justify-between" >
< span className = "text-sm text-gray-200 font-semibold" > { t . autoClean } < / span >
< span className = "text-sm text-gray-200 font-semibold" > { t . autoClean } < / span >
< Toggle checked = { config . chats . enableAutoClean } onChange = { v = > setConfig ( { . . . config , chats : { . . . config . chats , enableAutoClean : v } } ) } / >
< Toggle checked = { config . chats . enableAutoClean } onChange = { v = > setConfig ( { . . . config , chats : { . . . config . chats , enableAutoClean : v } } ) } / >
< / div >
< / div >
< Hint > { t . hints . autoClean } < / Hint >
< Hint > { t . hints . autoClean } < / Hint >
< / div >
< / div >
< div className = "flex flex-col gap-2 bg-white/[0.02] p-4 rounded-2xl border border-white/5" >
< div className = "flex flex-col gap-2 bg-white/[0.02] p-4 rounded-2xl border border-white/5" >
< div className = "flex items-center justify-between" >
< div className = "flex items-center justify-between" >
< span className = "text-sm text-gray-200 font-semibold" > { t . chatToGroup } < / span >
< span className = "text-sm text-gray-200 font-semibold" > { t . chatToGroup } < / span >
< Toggle checked = { config . chats . allowChatToGroupConversion } onChange = { v = > setConfig ( { . . . config , chats : { . . . config . chats , allowChatToGroupConversion : v } } ) } / >
< Toggle checked = { config . chats . allowChatToGroupConversion } onChange = { v = > setConfig ( { . . . config , chats : { . . . config . chats , allowChatToGroupConversion : v } } ) } / >
< / div >
< / div >
< Hint > { t . hints . chatToGroup } < / Hint >
< Hint > { t . hints . chatToGroup } < / Hint >
< / div >
< / div >
< div className = "flex flex-col gap-2 bg-white/[0.02] p-4 rounded-2xl border border-white/5" >
< div className = "flex flex-col gap-2 bg-white/[0.02] p-4 rounded-2xl border border-white/5" >
< div className = "flex items-center justify-between" >
< div className = "flex items-center justify-between" >
< span className = "text-sm text-gray-200 font-semibold" > { t . enableFolders } < / span >
< span className = "text-sm text-gray-200 font-semibold" > { t . enableFolders } < / span >
< Toggle checked = { config . chats . enableFolders } onChange = { v = > setConfig ( { . . . config , chats : { . . . config . chats , enableFolders : v } } ) } / >
< Toggle checked = { config . chats . enableFolders } onChange = { v = > setConfig ( { . . . config , chats : { . . . config . chats , enableFolders : v } } ) } / >
< / div >
< / div >
< Hint > { t . hints . enableFolders } < / Hint >
< Hint > { t . hints . enableFolders } < / Hint >
< / div >
< / div >
@@ -1163,17 +1169,17 @@ export default function AdminPage() {
< div className = "flex flex-col gap-8" >
< div className = "flex flex-col gap-8" >
< label className = "flex flex-col gap-1.5 group" >
< label className = "flex flex-col gap-1.5 group" >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1" > { t . dailyLimit } < / span >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1" > { t . dailyLimit } < / span >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 transition-all font-mono" value = { config . messages . dailyMessageLimitPerUser } onChange = { e = > setConfig ( { . . . config , messages : { . . . config . messages , dailyMessageLimitPerUser : parseInt ( e . target . value ) || 0 } } ) } / >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 transition-all font-mono" value = { config . messages . dailyMessageLimitPerUser } onChange = { e = > setConfig ( { . . . config , messages : { . . . config . messages , dailyMessageLimitPerUser : parseInt ( e . target . value ) || 0 } } ) } / >
< Hint > { t . hints . dailyLimit } < / Hint >
< Hint > { t . hints . dailyLimit } < / Hint >
< / label >
< / label >
< label className = "flex flex-col gap-1.5 group" >
< label className = "flex flex-col gap-1.5 group" >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1" > { t . chatLimit } < / span >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1" > { t . chatLimit } < / span >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 transition-all font-mono" value = { config . messages . chatMessageLimit } onChange = { e = > setConfig ( { . . . config , messages : { . . . config . messages , chatMessageLimit : parseInt ( e . target . value ) || 0 } } ) } / >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 transition-all font-mono" value = { config . messages . chatMessageLimit } onChange = { e = > setConfig ( { . . . config , messages : { . . . config . messages , chatMessageLimit : parseInt ( e . target . value ) || 0 } } ) } / >
< Hint > { t . hints . chatLimit } < / Hint >
< Hint > { t . hints . chatLimit } < / Hint >
< / label >
< / label >
< label className = "flex flex-col gap-1.5 group" >
< label className = "flex flex-col gap-1.5 group" >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1" > { t . maxFileSize } < / span >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1" > { t . maxFileSize } < / span >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 transition-all font-mono" value = { bytesToMb ( config . messages . maxFileSize ) } onChange = { e = > setConfig ( { . . . config , messages : { . . . config . messages , maxFileSize : mbToBytes ( parseInt ( e . target . value ) || 0 ) } } ) } / >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent/50 transition-all font-mono" value = { bytesToMb ( config . messages . maxFileSize ) } onChange = { e = > setConfig ( { . . . config , messages : { . . . config . messages , maxFileSize : mbToBytes ( parseInt ( e . target . value ) || 0 ) } } ) } / >
< Hint > { t . hints . maxFileSize } < / Hint >
< Hint > { t . hints . maxFileSize } < / Hint >
< / label >
< / label >
< / div >
< / div >
@@ -1195,7 +1201,7 @@ export default function AdminPage() {
< div key = { item . k } className = "flex flex-col gap-1 bg-white/[0.01] p-2 px-3 rounded-xl border border-white/[0.03]" >
< div key = { item . k } className = "flex flex-col gap-1 bg-white/[0.01] p-2 px-3 rounded-xl border border-white/[0.03]" >
< div className = "flex items-center justify-between" >
< div className = "flex items-center justify-between" >
< span className = "text-sm text-gray-300 font-medium" > { item . l } < / span >
< span className = "text-sm text-gray-300 font-medium" > { item . l } < / span >
< Toggle checked = { ( config . messages as any ) [ item . k ] } onChange = { v = > setConfig ( { . . . config , messages : { . . . config . messages , [ item . k ] : v } } ) } / >
< Toggle checked = { ( config . messages as any ) [ item . k ] } onChange = { v = > setConfig ( { . . . config , messages : { . . . config . messages , [ item . k ] : v } } ) } / >
< / div >
< / div >
{ item . h && < Hint > { item . h } < / Hint > }
{ item . h && < Hint > { item . h } < / Hint > }
< / div >
< / div >
@@ -1212,7 +1218,7 @@ export default function AdminPage() {
< h3 className = "text-xl font-bold text-accent uppercase tracking-tighter flex items-center gap-2" >
< h3 className = "text-xl font-bold text-accent uppercase tracking-tighter flex items-center gap-2" >
< Globe className = "w-5 h-5" / > { t . webRtc }
< Globe className = "w-5 h-5" / > { t . webRtc }
< / h3 >
< / h3 >
< Toggle checked = { config . webRtc . enabled } onChange = { v = > setConfig ( { . . . config , webRtc : { . . . config . webRtc , enabled : v } } ) } / >
< Toggle checked = { config . webRtc . enabled } onChange = { v = > setConfig ( { . . . config , webRtc : { . . . config . webRtc , enabled : v } } ) } / >
< / div >
< / div >
< Hint > { t . hints . webRtc } < / Hint >
< Hint > { t . hints . webRtc } < / Hint >
@@ -1221,14 +1227,14 @@ export default function AdminPage() {
< div className = "bg-white/[0.03] p-4 rounded-2xl border border-white/5 flex flex-col gap-3" >
< div className = "bg-white/[0.03] p-4 rounded-2xl border border-white/5 flex flex-col gap-3" >
< div className = "flex items-center justify-between" >
< div className = "flex items-center justify-between" >
< span className = "text-xs font-bold text-gray-400 uppercase" > { t . videoCalls } < / span >
< span className = "text-xs font-bold text-gray-400 uppercase" > { t . videoCalls } < / span >
< Toggle checked = { config . webRtc . enableVideoCalls } onChange = { v = > setConfig ( { . . . config , webRtc : { . . . config . webRtc , enableVideoCalls : v } } ) } / >
< Toggle checked = { config . webRtc . enableVideoCalls } onChange = { v = > setConfig ( { . . . config , webRtc : { . . . config . webRtc , enableVideoCalls : v } } ) } / >
< / div >
< / div >
< Hint > { t . hints . videoCalls } < / Hint >
< Hint > { t . hints . videoCalls } < / Hint >
< / div >
< / div >
< div className = "bg-white/[0.03] p-4 rounded-2xl border border-white/5 flex flex-col gap-3" >
< div className = "bg-white/[0.03] p-4 rounded-2xl border border-white/5 flex flex-col gap-3" >
< div className = "flex items-center justify-between" >
< div className = "flex items-center justify-between" >
< span className = "text-xs font-bold text-gray-400 uppercase" > { t . screenSharing } < / span >
< span className = "text-xs font-bold text-gray-400 uppercase" > { t . screenSharing } < / span >
< Toggle checked = { config . webRtc . enableScreenSharing } onChange = { v = > setConfig ( { . . . config , webRtc : { . . . config . webRtc , enableScreenSharing : v } } ) } / >
< Toggle checked = { config . webRtc . enableScreenSharing } onChange = { v = > setConfig ( { . . . config , webRtc : { . . . config . webRtc , enableScreenSharing : v } } ) } / >
< / div >
< / div >
< Hint > { t . hints . screenSharing } < / Hint >
< Hint > { t . hints . screenSharing } < / Hint >
< / div >
< / div >
@@ -1244,22 +1250,22 @@ export default function AdminPage() {
< div className = "grid grid-cols-1 md:grid-cols-2 gap-8" >
< div className = "grid grid-cols-1 md:grid-cols-2 gap-8" >
< label className = "flex flex-col gap-1.5 group" >
< label className = "flex flex-col gap-1.5 group" >
< span className = "text-[10px] font-black uppercase text-gray-500 ml-1 group-focus-within:text-accent transition-colors" > { t . turnHost } < / span >
< span className = "text-[10px] font-black uppercase text-gray-500 ml-1 group-focus-within:text-accent transition-colors" > { t . turnHost } < / span >
< input className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent font-mono text-xs" value = { config . webRtc . turnHost } onChange = { e = > setConfig ( { . . . config , webRtc : { . . . config . webRtc , turnHost : e.target.value } } ) } placeholder = "turn.example.com" / >
< input className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent font-mono text-xs" value = { config . webRtc . turnHost } onChange = { e = > setConfig ( { . . . config , webRtc : { . . . config . webRtc , turnHost : e.target.value } } ) } placeholder = "turn.example.com" / >
< Hint > { t . hints . turnHost } < / Hint >
< Hint > { t . hints . turnHost } < / Hint >
< / label >
< / label >
< label className = "flex flex-col gap-1.5 group" >
< label className = "flex flex-col gap-1.5 group" >
< span className = "text-[10px] font-black uppercase text-gray-500 ml-1 group-focus-within:text-accent transition-colors" > { t . turnPort } < / span >
< span className = "text-[10px] font-black uppercase text-gray-500 ml-1 group-focus-within:text-accent transition-colors" > { t . turnPort } < / span >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent font-mono text-xs" value = { config . webRtc . turnPort } onChange = { e = > setConfig ( { . . . config , webRtc : { . . . config . webRtc , turnPort : parseInt ( e . target . value ) || 0 } } ) } / >
< input type = "number" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent font-mono text-xs" value = { config . webRtc . turnPort } onChange = { e = > setConfig ( { . . . config , webRtc : { . . . config . webRtc , turnPort : parseInt ( e . target . value ) || 0 } } ) } / >
< Hint > { t . hints . turnPort } < / Hint >
< Hint > { t . hints . turnPort } < / Hint >
< / label >
< / label >
< label className = "flex flex-col gap-1.5 group" >
< label className = "flex flex-col gap-1.5 group" >
< span className = "text-[10px] font-black uppercase text-gray-500 ml-1 group-focus-within:text-accent transition-colors" > { t . turnUser } < / span >
< span className = "text-[10px] font-black uppercase text-gray-500 ml-1 group-focus-within:text-accent transition-colors" > { t . turnUser } < / span >
< input className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent font-mono text-xs" value = { config . webRtc . turnUser } onChange = { e = > setConfig ( { . . . config , webRtc : { . . . config . webRtc , turnUser : e.target.value } } ) } / >
< input className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent font-mono text-xs" value = { config . webRtc . turnUser } onChange = { e = > setConfig ( { . . . config , webRtc : { . . . config . webRtc , turnUser : e.target.value } } ) } / >
< Hint > { t . hints . turnUser } < / Hint >
< Hint > { t . hints . turnUser } < / Hint >
< / label >
< / label >
< label className = "flex flex-col gap-1.5 group" >
< label className = "flex flex-col gap-1.5 group" >
< span className = "text-[10px] font-black uppercase text-gray-500 ml-1 group-focus-within:text-accent transition-colors" > { t . turnSecret } < / span >
< span className = "text-[10px] font-black uppercase text-gray-500 ml-1 group-focus-within:text-accent transition-colors" > { t . turnSecret } < / span >
< input type = "password" name = "turnSecret" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent font-mono text-xs" value = { config . webRtc . turnSecret } onChange = { e = > setConfig ( { . . . config , webRtc : { . . . config . webRtc , turnSecret : e.target.value } } ) } / >
< input type = "password" name = "turnSecret" className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-accent font-mono text-xs" value = { config . webRtc . turnSecret } onChange = { e = > setConfig ( { . . . config , webRtc : { . . . config . webRtc , turnSecret : e.target.value } } ) } / >
< Hint > { t . hints . turnSecret } < / Hint >
< Hint > { t . hints . turnSecret } < / Hint >
< / label >
< / label >
< / div >
< / div >
@@ -1279,7 +1285,7 @@ export default function AdminPage() {
< h3 className = "text-xl font-bold flex items-center gap-3" >
< h3 className = "text-xl font-bold flex items-center gap-3" >
< div className = "w-6 h-6 bg-purple-500 rounded-md" / > { t . klipy }
< div className = "w-6 h-6 bg-purple-500 rounded-md" / > { t . klipy }
< / h3 >
< / h3 >
< Toggle checked = { config . klipy . enabled } onChange = { v = > setConfig ( { . . . config , klipy : { . . . config . klipy , enabled : v } } ) } / >
< Toggle checked = { config . klipy . enabled } onChange = { v = > setConfig ( { . . . config , klipy : { . . . config . klipy , enabled : v } } ) } / >
< / div >
< / div >
< Hint > { t . hints . klipy } < / Hint >
< Hint > { t . hints . klipy } < / Hint >
{ config . klipy . enabled && (
{ config . klipy . enabled && (
@@ -1287,12 +1293,12 @@ export default function AdminPage() {
< div className = "grid grid-cols-1 md:grid-cols-2 gap-8" >
< div className = "grid grid-cols-1 md:grid-cols-2 gap-8" >
< label className = "flex flex-col gap-1.5 group" >
< label className = "flex flex-col gap-1.5 group" >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1 group-focus-within:text-purple-400 transition-colors" > { t . appName } < / span >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1 group-focus-within:text-purple-400 transition-colors" > { t . appName } < / span >
< input className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-purple-500/50 transition-all font-mono text-sm" value = { config . klipy . appName } onChange = { e = > setConfig ( { . . . config , klipy : { . . . config . klipy , appName : e.target.value } } ) } / >
< input className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-purple-500/50 transition-all font-mono text-sm" value = { config . klipy . appName } onChange = { e = > setConfig ( { . . . config , klipy : { . . . config . klipy , appName : e.target.value } } ) } / >
< Hint > { t . hints . appName } < / Hint >
< Hint > { t . hints . appName } < / Hint >
< / label >
< / label >
< label className = "flex flex-col gap-1.5 group" >
< label className = "flex flex-col gap-1.5 group" >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1 group-focus-within:text-purple-400 transition-colors" > { t . apiKey } < / span >
< span className = "text-xs font-bold text-gray-500 uppercase ml-1 group-focus-within:text-purple-400 transition-colors" > { t . apiKey } < / span >
< input className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-purple-500/50 transition-all font-mono text-sm" value = { config . klipy . apiKey } onChange = { e = > setConfig ( { . . . config , klipy : { . . . config . klipy , apiKey : e.target.value } } ) } type = "password" / >
< input className = "bg-black/40 border border-white/10 rounded-xl px-4 py-3.5 outline-none text-white focus:border-purple-500/50 transition-all font-mono text-sm" value = { config . klipy . apiKey } onChange = { e = > setConfig ( { . . . config , klipy : { . . . config . klipy , apiKey : e.target.value } } ) } type = "password" / >
< Hint > { t . hints . apiKey } < / Hint >
< Hint > { t . hints . apiKey } < / Hint >
< / label >
< / label >
< / div >
< / div >
@@ -1318,7 +1324,7 @@ export default function AdminPage() {
< div className = "bg-white/[0.03] p-5 rounded-2xl border border-white/5 flex flex-col gap-4" >
< div className = "bg-white/[0.03] p-5 rounded-2xl border border-white/5 flex flex-col gap-4" >
< div className = "flex items-center justify-between" >
< div className = "flex items-center justify-between" >
< span className = "text-sm font-semibold text-gray-200" > { t . telegramImport } < / span >
< span className = "text-sm font-semibold text-gray-200" > { t . telegramImport } < / span >
< Toggle checked = { config . import . enableTelegramImport } onChange = { v = > setConfig ( { . . . config , import : { . . . config . import , enableTelegramImport : v } } ) } / >
< Toggle checked = { config . import . enableTelegramImport } onChange = { v = > setConfig ( { . . . config , import : { . . . config . import , enableTelegramImport : v } } ) } / >
< / div >
< / div >
< Hint > { t . hints . import } < / Hint >
< Hint > { t . hints . import } < / Hint >
< p className = "text-xs text-gray-500 italic mt-2" >
< p className = "text-xs text-gray-500 italic mt-2" >
@@ -1334,7 +1340,7 @@ export default function AdminPage() {
< div className = "bg-surface border border-white/10 rounded-2xl p-8 flex flex-col gap-8" >
< div className = "bg-surface border border-white/10 rounded-2xl p-8 flex flex-col gap-8" >
< div className = "flex items-center justify-between" >
< div className = "flex items-center justify-between" >
< h3 className = "text-xl font-semibold opacity-30 uppercase tracking-widest text-sm" > { t . federation } < / h3 >
< h3 className = "text-xl font-semibold opacity-30 uppercase tracking-widest text-sm" > { t . federation } < / h3 >
< Toggle checked = { config . federation . enabled } onChange = { v = > setConfig ( { . . . config , federation : { . . . config . federation , enabled : v } } ) } / >
< Toggle checked = { config . federation . enabled } onChange = { v = > setConfig ( { . . . config , federation : { . . . config . federation , enabled : v } } ) } / >
< / div >
< / div >
{ config . federation . enabled && (
{ config . federation . enabled && (
@@ -1361,7 +1367,7 @@ export default function AdminPage() {
{ config . federation . allowedDomains . map ( d = > (
{ config . federation . allowedDomains . map ( d = > (
< div key = { d . domain } className = "flex items-center justify-between p-4 rounded-xl bg-black/30 border border-white/5 group hover:border-white/10 transition-all" >
< div key = { d . domain } className = "flex items-center justify-between p-4 rounded-xl bg-black/30 border border-white/5 group hover:border-white/10 transition-all" >
< span className = "text-sm font-mono text-gray-300" > { d . domain } < / span >
< span className = "text-sm font-mono text-gray-300" > { d . domain } < / span >
< button onClick = { ( ) = > removeDomain ( d . domain ) } className = "text-gray-600 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-all p-1" > < Trash2 className = "w-4 h-4" / > < / button >
< button onClick = { ( ) = > removeDomain ( d . domain ) } className = "text-gray-600 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-all p-1" > < Trash2 className = "w-4 h-4" / > < / button >
< / div >
< / div >
) ) }
) ) }
{ config . federation . allowedDomains . length === 0 && (
{ config . federation . allowedDomains . length === 0 && (
@@ -1393,17 +1399,21 @@ export default function AdminPage() {
{ users . length === 0 && ! isSearching && < div className = "text-center py-10 text-gray-500" > { t . noUsersFound } < / div > }
{ users . length === 0 && ! isSearching && < div className = "text-center py-10 text-gray-500" > { t . noUsersFound } < / div > }
{ users . map ( u = > (
{ users . map ( u = > (
< button key = { u . id } onClick = { ( ) = > fetchUserDetails ( u . id ) } className = { ` flex items-center gap-3 p-3 rounded-xl transition-all text-left group ${ selectedUser ? . id === u . id ? 'bg-accent/10' : 'hover:bg-white/5' } ` } >
< button key = { u . id } onClick = { ( ) = > fetchUserDetails ( u . id ) } className = { ` flex items-center gap-3 p-3 rounded-xl transition-all text-left group ${ selectedUser ? . id === u . id ? 'bg-accent/10' : 'hover:bg-white/5' } ` } >
< div className = "relative" >
< div className = "w-10 h-10 rounded-full bg-accent/20 flex items-center justify-center text-accent font-bold shrink-0" >
< div className = "w-10 h-10 rounded-full bg-accent/20 flex items-center justify-center text-accent font-bold shrink-0" >
{ u . avatar ? < img src = { u . avatar } className = "w-full h-full rounded-full" / > : u . username . charAt ( 0 ) . toUpperCase ( ) }
{ u . avatar ? < img src = { u . avatar } className = "w-full h-full rounded-full" / > : u . username . charAt ( 0 ) . toUpperCase ( ) }
< / div >
< / div >
{ u . isBanned && (
< div className = "absolute -bottom-0.5 -left-0.5 w-4 h-4 bg-surface rounded-full flex items-center justify-center" title = { t . blockedTooltip } >
< XCircle className = "w-3.5 h-3.5 text-red-500" / >
< / div >
) }
< / div >
< div className = "flex-1 min-w-0" >
< div className = "flex-1 min-w-0" >
< div className = "font-semibold truncate text-white" > { u . displayName } < / div >
< div className = "font-semibold truncate text-white" > { u . displayName } < / div >
< div className = "text-xs text-gray-500 truncate" > @ { u . username } < / div >
< div className = "text-xs text-gray-500 truncate" > @ { u . username } < / div >
< / div >
< / div >
< div className = "flex items-center gap-2" >
< div className = "flex items-center gap-2" >
{ u . isBanned && (
< span className = "text-[10px] bg-red-500/20 text-red-400 px-2 py-0.5 rounded-full font-bold" > { t . isBanned . toUpperCase ( ) } < / span >
) }
{ u . isOnline ? (
{ u . isOnline ? (
< span className = "text-[10px] bg-green-500/20 text-green-400 px-2 py-0.5 rounded-full font-bold" > { t . online . toUpperCase ( ) } < / span >
< span className = "text-[10px] bg-green-500/20 text-green-400 px-2 py-0.5 rounded-full font-bold" > { t . online . toUpperCase ( ) } < / span >
) : (
) : (
@@ -1429,14 +1439,20 @@ export default function AdminPage() {
{ selectedUser ? (
{ selectedUser ? (
< motion.div initial = { { opacity : 0 , x : 20 } } animate = { { opacity : 1 , x : 0 } } className = "w-80 bg-surface border border-white/10 rounded-2xl p-6 flex flex-col gap-6 shrink-0 sticky top-0 overflow-y-auto max-h-full" >
< motion.div initial = { { opacity : 0 , x : 20 } } animate = { { opacity : 1 , x : 0 } } className = "w-80 bg-surface border border-white/10 rounded-2xl p-6 flex flex-col gap-6 shrink-0 sticky top-0 overflow-y-auto max-h-full" >
< div className = "flex flex-col items-center gap-4 text-center pb-6 border-b border-white/10" >
< div className = "flex flex-col items-center gap-4 text-center pb-6 border-b border-white/10" >
< div className = "relative" >
< div className = "w-20 h-20 rounded-full bg-accent/20 flex items-center justify-center text-2xl font-bold text-accent" >
< div className = "w-20 h-20 rounded-full bg-accent/20 flex items-center justify-center text-2xl font-bold text-accent" >
{ selectedUser . avatar ? < img src = { selectedUser . avatar } className = "w-full h-full rounded-full" / > : selectedUser . username . charAt ( 0 ) . toUpperCase ( ) }
{ selectedUser . avatar ? < img src = { selectedUser . avatar } className = "w-full h-full rounded-full" / > : selectedUser . username . charAt ( 0 ) . toUpperCase ( ) }
< / div >
< / div >
{ selectedUser . isBanned && (
< div className = "absolute -bottom-1 -left-1 w-6 h-6 bg-surface rounded-full flex items-center justify-center" title = { t . blockedTooltip } >
< XCircle className = "w-5 h-5 text-red-500" / >
< / div >
) }
< / div >
< div >
< div >
< h2 className = "text-xl font-bold text-white leading-tight" > { selectedUser . displayName } < / h2 >
< h2 className = "text-xl font-bold text-white leading-tight" > { selectedUser . displayName } < / h2 >
< div className = "text-gray-500 text-sm" > @ { selectedUser . username } < / div >
< div className = "text-gray-500 text-sm" > @ { selectedUser . username } < / div >
< / div >
< / div >
{ selectedUser . isBanned && < span className = "text-xs bg-red-500 text-white px-2 py-1 rounded-lg font-bold uppercase tracking-wider" > { t . isBanned } < / span > }
< / div >
< / div >
< div className = "flex flex-col gap-4" >
< div className = "flex flex-col gap-4" >
@@ -1462,11 +1478,11 @@ export default function AdminPage() {
) }
) }
{ selectedUser . isBanned ? (
{ selectedUser . isBanned ? (
< button onClick = { ( ) = > handleUnbanUser ( selectedUser . id ) } className = "w-full py-2.5 rounded-xl bg-green-500/10 hover:bg-green-500/20 text-green-500 font-bold transition-all border border-green-500/20 text-sm mt-2" >
< button onClick = { ( ) = > handleUnbanUser ( selectedUser . id ) } className = "w-full py-2.5 rounded-xl bg-green-500/10 hover:bg-green-500/20 text-green-500 font-bold transition-all border border-green-500/20 text-sm mt-2" >
{ t . unban }
{ t . unblockUser }
< / button >
< / button >
) : (
) : (
< button onClick = { ( ) = > handleBanUser ( selectedUser . id ) } className = "w-full py-2.5 rounded-xl bg-red-500/10 hover:bg-red-500/20 text-red-500 font-bold transition-all border border-red-500/20 text-sm mt-2" >
< button onClick = { ( ) = > handleBanUser ( selectedUser . id ) } className = "w-full py-2.5 rounded-xl bg-red-500/10 hover:bg-red-500/20 text-red-500 font-bold transition-all border border-red-500/20 text-sm mt-2" >
{ t . ban }
{ t . blockUser }
< / button >
< / button >
) }
) }
< button onClick = { ( ) = > handleDeleteUser ( selectedUser . id ) } className = "w-full py-2.5 rounded-xl bg-red-500/10 hover:bg-red-500/20 text-red-500 font-bold transition-all border border-red-500/20 text-sm mt-4" >
< button onClick = { ( ) = > handleDeleteUser ( selectedUser . id ) } className = "w-full py-2.5 rounded-xl bg-red-500/10 hover:bg-red-500/20 text-red-500 font-bold transition-all border border-red-500/20 text-sm mt-4" >
@@ -1488,22 +1504,22 @@ export default function AdminPage() {
{ showAddUserModal && (
{ showAddUserModal && (
< div className = "fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm" onClick = { ( ) = > setShowAddUserModal ( false ) } >
< div className = "fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm" onClick = { ( ) = > setShowAddUserModal ( false ) } >
< motion.div initial = { { opacity : 0 , scale : 0.95 } } animate = { { opacity : 1 , scale : 1 } } onClick = { e = > e . stopPropagation ( ) } className = "w-full max-w-sm bg-surface border border-white/10 rounded-2xl p-6 shadow-2xl relative" >
< motion.div initial = { { opacity : 0 , scale : 0.95 } } animate = { { opacity : 1 , scale : 1 } } onClick = { e = > e . stopPropagation ( ) } className = "w-full max-w-sm bg-surface border border-white/10 rounded-2xl p-6 shadow-2xl relative" >
< button onClick = { ( ) = > setShowAddUserModal ( false ) } className = "absolute top-4 right-4 text-gray-500 hover:text-white" > < XCircle className = "w-5 h-5" / > < / button >
< button onClick = { ( ) = > setShowAddUserModal ( false ) } className = "absolute top-4 right-4 text-gray-500 hover:text-white" > < XCircle className = "w-5 h-5" / > < / button >
< h2 className = "text-xl font-bold text-white mb-4" > { t . addUser } < / h2 >
< h2 className = "text-xl font-bold text-white mb-4" > { t . addUser } < / h2 >
< div className = "flex flex-col gap-4" >
< div className = "flex flex-col gap-4" >
< label className = "flex flex-col gap-1 text-xs text-gray-400" >
< label className = "flex flex-col gap-1 text-xs text-gray-400" >
{ t . username }
{ t . username }
< input className = "bg-black/50 border border-white/10 rounded-xl px-4 py-2.5 outline-none text-white focus:border-accent" value = { newUser . username } onChange = { e = > setNewUser ( { . . . newUser , username : e.target.value.toLowerCase ( ) } ) } placeholder = { t . usernamePlaceholder } / >
< input className = "bg-black/50 border border-white/10 rounded-xl px-4 py-2.5 outline-none text-white focus:border-accent" value = { newUser . username } onChange = { e = > setNewUser ( { . . . newUser , username : e.target.value.toLowerCase ( ) } ) } placeholder = { t . usernamePlaceholder } / >
< / label >
< / label >
< label className = "flex flex-col gap-1 text-xs text-gray-400" >
< label className = "flex flex-col gap-1 text-xs text-gray-400" >
{ t . displayName }
{ t . displayName }
< input className = "bg-black/50 border border-white/10 rounded-xl px-4 py-2.5 outline-none text-white focus:border-accent" value = { newUser . displayName } onChange = { e = > setNewUser ( { . . . newUser , displayName : e.target.value } ) } placeholder = { t . displayNamePlaceholder } / >
< input className = "bg-black/50 border border-white/10 rounded-xl px-4 py-2.5 outline-none text-white focus:border-accent" value = { newUser . displayName } onChange = { e = > setNewUser ( { . . . newUser , displayName : e.target.value } ) } placeholder = { t . displayNamePlaceholder } / >
< / label >
< / label >
< label className = "flex flex-col gap-1 text-xs text-gray-400" >
< label className = "flex flex-col gap-1 text-xs text-gray-400" >
{ t . password }
{ t . password }
< div className = "flex gap-2" >
< div className = "flex gap-2" >
< input className = "bg-black/50 border border-white/10 rounded-xl px-4 py-2.5 outline-none text-white focus:border-accent flex-1 font-mono" value = { newUser . password } onChange = { e = > setNewUser ( { . . . newUser , password : e.target.value } ) } placeholder = { t . passwordPlaceholder } / >
< input className = "bg-black/50 border border-white/10 rounded-xl px-4 py-2.5 outline-none text-white focus:border-accent flex-1 font-mono" value = { newUser . password } onChange = { e = > setNewUser ( { . . . newUser , password : e.target.value } ) } placeholder = { t . passwordPlaceholder } / >
< button onClick = { ( ) = > setNewUser ( { . . . newUser , password : generatePassword ( ) } ) } className = "bg-white/5 hover:bg-white/10 px-3 rounded-xl text-[10px] transition-colors" > { t . generate } < / button >
< button onClick = { ( ) = > setNewUser ( { . . . newUser , password : generatePassword ( ) } ) } className = "bg-white/5 hover:bg-white/10 px-3 rounded-xl text-[10px] transition-colors" > { t . generate } < / button >
< / div >
< / div >
< / label >
< / label >
< button onClick = { handleAddUser } disabled = { ! newUser . username || ! newUser . password } className = "bg-accent text-black font-bold py-2.5 rounded-xl hover:bg-accentLight transition-all mt-2 disabled:opacity-50" >
< button onClick = { handleAddUser } disabled = { ! newUser . username || ! newUser . password } className = "bg-accent text-black font-bold py-2.5 rounded-xl hover:bg-accentLight transition-all mt-2 disabled:opacity-50" >
@@ -1517,7 +1533,7 @@ export default function AdminPage() {
{ /* Global Toast */ }
{ /* Global Toast */ }
{ toast && (
{ toast && (
< div className = "fixed top-4 right-4 z-[9999] px-5 py-3 rounded-2xl bg-surface border border-white/10 text-white shadow-2xl flex items-center gap-2" >
< div className = "fixed top-4 right-4 z-[9999] px-5 py-3 rounded-2xl bg-surface border border-white/10 text-white shadow-2xl flex items-center gap-2" >
{ toast . type === 'success' ? < CheckCircle className = "text-green-400 w-5 h-5" / > : < XCircle className = "text-red-400 w-5 h-5" / > }
{ toast . type === 'success' ? < CheckCircle className = "text-green-400 w-5 h-5" / > : < XCircle className = "text-red-400 w-5 h-5" / > }
{ toast . message }
{ toast . message }
< / div >
< / div >
) }
) }