Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 120 additions & 35 deletions packages/frontend/src/pages/Admin.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ShieldCheck } from 'lucide-react'
import { ShieldCheck, RotateCcw } from 'lucide-react'
import Skeleton from '@/components/ui/Skeleton'
import Button from '@/components/ui/Button'
import GlobalTogglesSection from '@/components/Features/GlobalTogglesSection'
Expand Down Expand Up @@ -28,65 +28,150 @@ export default function AdminPage() {

if (!isAuthenticated) {
return (
<main className='p-6 flex flex-col items-center justify-center min-h-[60vh] gap-6'>
<ShieldCheck className='w-12 h-12 text-lucky-purple' />
<div className='text-center space-y-2'>
<h1 className='type-h1 text-lucky-text-primary'>Admin Panel</h1>
<p className='text-lucky-text-secondary'>
Sign in with Discord to access the admin panel.
</p>
<main className='flex flex-col items-center justify-center min-h-[64vh] gap-8 bg-lucky-bg-primary px-4'>
<div className='flex flex-col items-center gap-4 text-center max-w-sm'>
<div className='p-3 rounded-lg bg-lucky-surface-elevated'>
<ShieldCheck
className='w-8 h-8 text-lucky-brand'
aria-hidden='true'
/>
</div>
<div className='space-y-2'>
<h1 className='text-xl font-semibold text-lucky-text-primary' style={{ fontFamily: 'Sora' }}>
Admin Panel
</h1>
<p className='text-sm text-lucky-text-secondary' style={{ fontFamily: 'Manrope' }}>
Sign in with Discord to access the admin panel.
</p>
</div>
</div>
<Button onClick={login}>Sign in with Discord</Button>
<Button onClick={login} className='w-fit'>
Sign in with Discord
</Button>
</main>
)
}

if (!isDeveloper) {
return (
<main className='p-6 flex flex-col items-center justify-center min-h-[60vh] gap-4'>
<ShieldCheck className='w-12 h-12 text-lucky-red' />
<div className='text-center space-y-2'>
<h1 className='type-h1 text-lucky-text-primary'>Access Denied</h1>
<p className='text-lucky-text-secondary'>
This page is restricted to bot administrators.
</p>
<main className='flex flex-col items-center justify-center min-h-[64vh] gap-6 bg-lucky-bg-primary px-4'>
<div className='flex flex-col items-center gap-4 text-center max-w-sm'>
<div className='p-3 rounded-lg bg-lucky-surface-elevated'>
<ShieldCheck
className='w-8 h-8 text-lucky-error'
aria-hidden='true'
/>
</div>
<div className='space-y-2'>
<h1 className='text-xl font-semibold text-lucky-text-primary' style={{ fontFamily: 'Sora' }}>
Access Denied
</h1>
<p className='text-sm text-lucky-text-secondary' style={{ fontFamily: 'Manrope' }}>
This page is restricted to bot administrators.
</p>
</div>
</div>
</main>
)
}

if (isLoading) {
return (
<main className='p-6 space-y-6'>
<Skeleton className='h-10 w-48' />
<main className='p-4 md:p-6 bg-lucky-bg-primary space-y-6 min-h-screen'>
<div className='space-y-4'>
{[1, 2, 3].map((i) => (
<Skeleton key={i} className='h-24 w-full' />
))}
<Skeleton className='h-8 w-48' />
<div className='space-y-3'>
{[1, 2, 3].map((i) => (
<Skeleton key={i} className='h-10 w-full' />
))}
</div>
</div>
</main>
)
}

return (
<main className='p-4 md:p-6 space-y-10'>
<header className='flex items-center gap-3'>
<ShieldCheck className='w-7 h-7 text-lucky-purple' aria-hidden='true' />
<h1 className='type-h1 text-lucky-text-primary'>Admin Panel</h1>
<main className='bg-lucky-bg-primary min-h-screen'>
{/* Header — compact, density-first */}
<header className='border-b border-lucky-border-soft bg-lucky-surface-sidebar px-4 md:px-6 py-4'>
<div className='flex items-center justify-between gap-3'>
<div className='flex items-center gap-3'>
<ShieldCheck
className='w-6 h-6 text-lucky-brand flex-shrink-0'
aria-hidden='true'
/>
<h1 className='text-lg font-semibold text-lucky-text-primary' style={{ fontFamily: 'Sora' }}>
Admin Panel
</h1>
</div>
{loadError && (
<Button
variant='ghost'
size='sm'
onClick={retryLoad}
className='text-lucky-text-secondary hover:text-lucky-text-primary'
aria-label='Retry loading features'
>
<RotateCcw className='w-4 h-4' />
</Button>
)}
</div>
</header>

{loadError && <FeatureErrorBanner loadError={loadError} retryLoad={retryLoad} />}
{/* Content area — enterprise admin density */}
<div className='px-4 md:px-6 py-6 space-y-6'>
{loadError && (
<FeatureErrorBanner
loadError={loadError}
retryLoad={retryLoad}
/>
)}

<section aria-labelledby='global-toggles-heading'>
<GlobalTogglesSection
toggles={globalToggles}
provider={globalToggleProvider}
writable={globalTogglesWritable}
onToggle={handleGlobalToggle}
/>
</section>
{/* Global Toggles Section — grid density control */}
<section
aria-labelledby='global-toggles-heading'
className='space-y-4'
>
<div className='space-y-1'>
<h2
id='global-toggles-heading'
className='text-base font-semibold text-lucky-text-primary'
style={{ fontFamily: 'Sora' }}
>
Global Feature Toggles
</h2>
<p className='text-xs text-lucky-text-tertiary' style={{ fontFamily: 'Manrope' }}>
Manage feature flags across all servers
</p>
</div>
<GlobalTogglesSection
toggles={globalToggles}
provider={globalToggleProvider}
writable={globalTogglesWritable}
onToggle={handleGlobalToggle}
/>
</section>

<BotGuildsSection />
{/* Bot Guilds Section — enterprise list density */}
<section
aria-labelledby='bot-guilds-heading'
className='space-y-4'
>
<div className='space-y-1'>
<h2
id='bot-guilds-heading'
className='text-base font-semibold text-lucky-text-primary'
style={{ fontFamily: 'Sora' }}
>
Server Management
</h2>
<p className='text-xs text-lucky-text-tertiary' style={{ fontFamily: 'Manrope' }}>
View and manage bot activity across all servers
</p>
</div>
<BotGuildsSection />
</section>
</div>
</main>
)
}
172 changes: 102 additions & 70 deletions packages/frontend/src/pages/Config.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState, lazy, Suspense } from 'react'
import { useNavigate } from 'react-router-dom'
import { Music, MessageSquare, Shield } from 'lucide-react'
import { Music, MessageSquare, Shield, ArrowLeft } from 'lucide-react'
import Button from '@/components/ui/Button'
import { usePageMetadata } from '@/hooks/usePageMetadata'
import { useGuildSelection } from '@/hooks/useGuildSelection'
Expand Down Expand Up @@ -51,84 +51,116 @@ export default function ConfigPage() {

if (!selectedGuild) {
return (
<main className='space-y-6'>
<header>
<h1 className='type-h1 text-lucky-text-primary mb-2'>
Configuration
</h1>
<p className='text-lucky-text-secondary'>
Please select a server to configure
</p>
<main className='bg-lucky-bg-primary min-h-screen px-4 md:px-6 py-6'>
<header className='space-y-4'>
<div className='space-y-1'>
<h1
className='text-2xl font-semibold text-lucky-text-primary'
style={{ fontFamily: 'Sora' }}
>
Configuration
</h1>
<p
className='text-sm text-lucky-text-secondary'
style={{ fontFamily: 'Manrope' }}
>
Please select a server to configure
</p>
</div>
</header>
</main>
)
}

return (
<main className='space-y-6'>
<header>
<h1 className='type-h1 text-lucky-text-primary mb-2'>
Configuration
</h1>
<p className='text-lucky-text-secondary'>
Configure modules and commands for your servers
</p>
<main className='bg-lucky-bg-primary min-h-screen'>
{/* Header */}
<header className='border-b border-lucky-border-soft bg-lucky-surface-sidebar px-4 md:px-6 py-4'>
<div className='space-y-1'>
<h1
className='text-xl font-semibold text-lucky-text-primary'
style={{ fontFamily: 'Sora' }}
>
Configuration
</h1>
<p
className='text-xs text-lucky-text-tertiary'
style={{ fontFamily: 'Manrope' }}
>
Server: <span className='text-lucky-text-secondary font-medium'>{selectedGuild.name}</span>
</p>
</div>
</header>

{!selectedModule ? (
<section aria-labelledby='modules-heading'>
<h2 id='modules-heading' className='sr-only'>
Available Modules
</h2>
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'>
{modules.map((module) => (
<button
key={module.id}
type='button'
className='surface-card group w-full p-6 text-left transition-all duration-200 hover:border-lucky-border-strong hover:bg-lucky-bg-active focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-lucky-brand/30 focus-visible:ring-offset-2 focus-visible:ring-offset-background'
onClick={() => handleModuleClick(module.id)}
>
<div className='flex items-start gap-4'>
<div
className='rounded-lg bg-lucky-brand/15 p-3 text-lucky-brand transition-colors group-hover:bg-lucky-brand/20'
aria-hidden='true'
{/* Content */}
<div className='px-4 md:px-6 py-6'>
{!selectedModule ? (
/* Module selection grid — Polaris resource-list density */
<section aria-labelledby='modules-heading'>
<h2 id='modules-heading' className='sr-only'>
Available Configuration Modules
</h2>
<div className='grid grid-cols-1 md:grid-cols-2 gap-4'>
{modules.map((module) => {
const Icon = module.icon
return (
<button
key={module.id}
type='button'
className='group relative flex items-start gap-4 p-4 rounded-lg border border-lucky-border-soft bg-lucky-surface-panel transition-colors duration-120 hover:border-lucky-border-strong hover:bg-lucky-surface-elevated focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-lucky-brand focus-visible:ring-offset-2 focus-visible:ring-offset-lucky-bg-primary'
onClick={() => handleModuleClick(module.id)}
style={{ transitionDuration: '120ms', transitionTimingFunction: 'cubic-bezier(0.2, 0, 0, 1)' }}
>
<module.icon className='h-6 w-6' />
</div>
<div className='flex-1'>
<h3 className='type-h2 text-lucky-text-primary mb-1'>
{module.name}
</h3>
<p className='text-sm text-lucky-text-secondary'>
{module.description}
</p>
</div>
</div>
</button>
))}
</div>
</section>
) : (
<section aria-labelledby='module-config-heading'>
<div className='mb-4'>
<Button
variant='ghost'
onClick={() => setSelectedModule(null)}
aria-label='Back to module selection'
>
← Back
</Button>
</div>
<Suspense fallback={<LoadingSpinner />}>
{selectedModule === 'music' && (
<MusicConfig guildId={selectedGuild.id} />
)}
{selectedModule === 'commands' && (
<CommandsConfig guildId={selectedGuild.id} />
)}
</Suspense>
</section>
)}
<div className='flex-shrink-0 p-2 rounded-lg bg-lucky-surface-highlight group-hover:bg-lucky-brand/15 transition-colors duration-120' style={{ transitionDuration: '120ms', transitionTimingFunction: 'cubic-bezier(0.2, 0, 0, 1)' }} aria-hidden='true'>
<Icon className='w-5 h-5 text-lucky-brand' />
</div>
<div className='flex-1 text-left space-y-1'>
<h3
className='text-base font-semibold text-lucky-text-primary'
style={{ fontFamily: 'Sora' }}
>
{module.name}
</h3>
<p
className='text-xs text-lucky-text-secondary leading-snug'
style={{ fontFamily: 'Manrope' }}
>
{module.description}
</p>
</div>
<div className='flex-shrink-0 text-lucky-text-tertiary group-hover:text-lucky-text-secondary transition-colors duration-120' style={{ transitionDuration: '120ms', transitionTimingFunction: 'cubic-bezier(0.2, 0, 0, 1)' }}>
→
</div>
</button>
)
})}
</div>
</section>
) : (
/* Module config view */
<section aria-labelledby='module-config-heading'>
<div className='mb-6 flex items-center gap-2'>
<Button
variant='ghost'
size='sm'
onClick={() => setSelectedModule(null)}
className='gap-2 text-lucky-text-secondary hover:text-lucky-text-primary'
>
<ArrowLeft className='w-4 h-4' />
← Back
</Button>
</div>
<Suspense fallback={<div className='flex justify-center py-12'><LoadingSpinner /></div>}>
{selectedModule === 'music' && (
<MusicConfig guildId={selectedGuild.id} />
)}
{selectedModule === 'commands' && (
<CommandsConfig guildId={selectedGuild.id} />
)}
</Suspense>
</section>
)}
</div>
</main>
)
}
Loading