Platform-wide visual overhaul, team invites, analytics improvements, and deployment hardening
UI overhaul applying jury dashboard design patterns across all pages: - Stat cards with border-l-4 accent + icon pills on admin, observer, mentor, applicant dashboards and reports - Card section headers with color-coded icon pills throughout - Hover lift effects (translate-y + shadow) on cards and list items - Gradient progress bars (brand-teal to brand-blue) platform-wide - AnimatedCard stagger animations on all dashboard sections - Auth pages with gradient accent strip and polished icon containers - EmptyState component upgraded with rounded icon pill containers - Replaced AI-looking icons (Brain/Sparkles/Bot/Wand2/Cpu) with descriptive alternatives across 12 files - Removed gradient overlay from jury dashboard header - Quick actions restyled as card links with group hover effects Backend improvements: - Team member invite emails with account setup flow and notification logging - Analytics routers accept edition-wide queries (programId) in addition to roundId - Round detail endpoint returns inline progress data (eliminates extra getProgress call) - Award voting endpoints parallelized with Promise.all - Bulk invite supports optional sendInvitation flag - AwardVote composite index migration for query performance Infrastructure: - Docker entrypoint with migration retry loop (configurable retries/delay) - docker-compose pull_policy: always for automatic image refresh - Simplified deploy/update scripts using docker compose up -d --pull always - Updated deployment documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { Search, Loader2, Plus, Package } from 'lucide-react'
|
||||
import { Search, Loader2, Plus, Package, CheckCircle2 } from 'lucide-react'
|
||||
import { getCountryName } from '@/lib/countries'
|
||||
|
||||
interface AssignProjectsDialogProps {
|
||||
@@ -65,7 +65,6 @@ export function AssignProjectsDialog({
|
||||
const { data, isLoading } = trpc.project.list.useQuery(
|
||||
{
|
||||
programId,
|
||||
notInRoundId: roundId,
|
||||
search: debouncedSearch || undefined,
|
||||
page: 1,
|
||||
perPage: 5000,
|
||||
@@ -87,23 +86,28 @@ export function AssignProjectsDialog({
|
||||
})
|
||||
|
||||
const projects = data?.projects ?? []
|
||||
const alreadyInRound = new Set(
|
||||
projects.filter((p) => p.round?.id === roundId).map((p) => p.id)
|
||||
)
|
||||
const assignableProjects = projects.filter((p) => !alreadyInRound.has(p.id))
|
||||
|
||||
const toggleProject = useCallback((id: string) => {
|
||||
if (alreadyInRound.has(id)) return
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) next.delete(id)
|
||||
else next.add(id)
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
}, [alreadyInRound])
|
||||
|
||||
const toggleAll = useCallback(() => {
|
||||
if (selectedIds.size === projects.length) {
|
||||
if (selectedIds.size === assignableProjects.length) {
|
||||
setSelectedIds(new Set())
|
||||
} else {
|
||||
setSelectedIds(new Set(projects.map((p) => p.id)))
|
||||
setSelectedIds(new Set(assignableProjects.map((p) => p.id)))
|
||||
}
|
||||
}, [selectedIds.size, projects])
|
||||
}, [selectedIds.size, assignableProjects])
|
||||
|
||||
const handleAssign = () => {
|
||||
if (selectedIds.size === 0) return
|
||||
@@ -144,9 +148,9 @@ export function AssignProjectsDialog({
|
||||
) : projects.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Package className="h-12 w-12 text-muted-foreground/50" />
|
||||
<p className="mt-2 font-medium">No available projects</p>
|
||||
<p className="mt-2 font-medium">No projects found</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
All program projects are already in this round.
|
||||
{debouncedSearch ? 'No projects match your search.' : 'This program has no projects yet.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -154,11 +158,15 @@ export function AssignProjectsDialog({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={selectedIds.size === projects.length && projects.length > 0}
|
||||
checked={assignableProjects.length > 0 && selectedIds.size === assignableProjects.length}
|
||||
disabled={assignableProjects.length === 0}
|
||||
onCheckedChange={toggleAll}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{selectedIds.size} of {projects.length} selected
|
||||
{selectedIds.size} of {assignableProjects.length} assignable selected
|
||||
{alreadyInRound.size > 0 && (
|
||||
<span className="ml-1">({alreadyInRound.size} already in round)</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,34 +182,54 @@ export function AssignProjectsDialog({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{projects.map((project) => (
|
||||
<TableRow
|
||||
key={project.id}
|
||||
className={selectedIds.has(project.id) ? 'bg-muted/50' : 'cursor-pointer'}
|
||||
onClick={() => toggleProject(project.id)}
|
||||
>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedIds.has(project.id)}
|
||||
onCheckedChange={() => toggleProject(project.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{project.title}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{project.teamName || '—'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{project.country ? (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{getCountryName(project.country)}
|
||||
</Badge>
|
||||
) : '—'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{projects.map((project) => {
|
||||
const isInRound = alreadyInRound.has(project.id)
|
||||
return (
|
||||
<TableRow
|
||||
key={project.id}
|
||||
className={
|
||||
isInRound
|
||||
? 'opacity-60'
|
||||
: selectedIds.has(project.id)
|
||||
? 'bg-muted/50'
|
||||
: 'cursor-pointer'
|
||||
}
|
||||
onClick={() => toggleProject(project.id)}
|
||||
>
|
||||
<TableCell>
|
||||
{isInRound ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<Checkbox
|
||||
checked={selectedIds.has(project.id)}
|
||||
onCheckedChange={() => toggleProject(project.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
{project.title}
|
||||
{isInRound && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
In round
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{project.teamName || '—'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{project.country ? (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{getCountryName(project.country)}
|
||||
</Badge>
|
||||
) : '—'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import {
|
||||
Sparkles,
|
||||
FileText,
|
||||
RefreshCw,
|
||||
Loader2,
|
||||
CheckCircle2,
|
||||
@@ -119,7 +119,7 @@ export function EvaluationSummaryCard({
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
<FileText className="h-5 w-5" />
|
||||
AI Evaluation Summary
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
@@ -128,7 +128,7 @@ export function EvaluationSummaryCard({
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col items-center justify-center py-6 text-center">
|
||||
<Sparkles className="h-10 w-10 text-muted-foreground/50 mb-3" />
|
||||
<FileText className="h-10 w-10 text-muted-foreground/50 mb-3" />
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
No summary generated yet. Click below to analyze submitted evaluations.
|
||||
</p>
|
||||
@@ -136,7 +136,7 @@ export function EvaluationSummaryCard({
|
||||
{isGenerating ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{isGenerating ? 'Generating...' : 'Generate Summary'}
|
||||
</Button>
|
||||
@@ -155,7 +155,7 @@ export function EvaluationSummaryCard({
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
<FileText className="h-5 w-5" />
|
||||
AI Evaluation Summary
|
||||
</CardTitle>
|
||||
<CardDescription className="flex items-center gap-2 mt-1">
|
||||
|
||||
@@ -27,7 +27,8 @@ import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { UserAvatar } from '@/components/shared/user-avatar'
|
||||
import { UserActions, UserMobileActions } from '@/components/admin/user-actions'
|
||||
import { Pagination } from '@/components/shared/pagination'
|
||||
import { Plus, Users, Search } from 'lucide-react'
|
||||
import { Plus, Users, Search, Mail, Loader2 } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { formatRelativeTime } from '@/lib/utils'
|
||||
type RoleValue = 'SUPER_ADMIN' | 'PROGRAM_ADMIN' | 'JURY_MEMBER' | 'MENTOR' | 'OBSERVER'
|
||||
|
||||
@@ -60,6 +61,39 @@ const roleColors: Record<string, 'default' | 'outline' | 'secondary'> = {
|
||||
SUPER_ADMIN: 'destructive' as 'default',
|
||||
}
|
||||
|
||||
function InlineSendInvite({ userId, userEmail }: { userId: string; userEmail: string }) {
|
||||
const utils = trpc.useUtils()
|
||||
const sendInvitation = trpc.user.sendInvitation.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success(`Invitation sent to ${userEmail}`)
|
||||
utils.user.list.invalidate()
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message || 'Failed to send invitation')
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-xs gap-1 px-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
sendInvitation.mutate({ userId })
|
||||
}}
|
||||
disabled={sendInvitation.isPending}
|
||||
>
|
||||
{sendInvitation.isPending ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<Mail className="h-3 w-3" />
|
||||
)}
|
||||
Send Invite
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export function MembersContent() {
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
@@ -124,7 +158,7 @@ export function MembersContent() {
|
||||
<Button asChild>
|
||||
<Link href="/admin/members/invite">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Invite Member
|
||||
Add Member
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -223,9 +257,14 @@ export function MembersContent() {
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={statusColors[user.status] || 'secondary'}>
|
||||
{statusLabels[user.status] || user.status}
|
||||
</Badge>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={statusColors[user.status] || 'secondary'}>
|
||||
{statusLabels[user.status] || user.status}
|
||||
</Badge>
|
||||
{user.status === 'NONE' && (
|
||||
<InlineSendInvite userId={user.id} userEmail={user.email} />
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{user.lastLoginAt ? (
|
||||
@@ -272,9 +311,14 @@ export function MembersContent() {
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={statusColors[user.status] || 'secondary'}>
|
||||
{statusLabels[user.status] || user.status}
|
||||
</Badge>
|
||||
<div className="flex flex-col items-end gap-1.5">
|
||||
<Badge variant={statusColors[user.status] || 'secondary'}>
|
||||
{statusLabels[user.status] || user.status}
|
||||
</Badge>
|
||||
{user.status === 'NONE' && (
|
||||
<InlineSendInvite userId={user.id} userEmail={user.email} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
|
||||
@@ -5,6 +5,7 @@ import Link from 'next/link'
|
||||
import type { Route } from 'next'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { signOut } from 'next-auth/react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
@@ -33,7 +34,7 @@ import {
|
||||
Trophy,
|
||||
User,
|
||||
MessageSquare,
|
||||
Wand2,
|
||||
LayoutTemplate,
|
||||
} from 'lucide-react'
|
||||
import { getInitials } from '@/lib/utils'
|
||||
import { Logo } from '@/components/shared/logo'
|
||||
@@ -41,6 +42,7 @@ import { EditionSelector } from '@/components/shared/edition-selector'
|
||||
import { useEdition } from '@/contexts/edition-context'
|
||||
import { UserAvatar } from '@/components/shared/user-avatar'
|
||||
import { NotificationBell } from '@/components/shared/notification-bell'
|
||||
import { LanguageSwitcher } from '@/components/shared/language-switcher'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
|
||||
@@ -120,7 +122,7 @@ const adminNavigation: NavItem[] = [
|
||||
{
|
||||
name: 'Apply Page',
|
||||
href: '/admin/programs',
|
||||
icon: Wand2,
|
||||
icon: LayoutTemplate,
|
||||
activeMatch: 'apply-settings',
|
||||
},
|
||||
{
|
||||
@@ -145,6 +147,7 @@ const roleLabels: Record<string, string> = {
|
||||
|
||||
export function AdminSidebar({ user }: AdminSidebarProps) {
|
||||
const pathname = usePathname()
|
||||
const tAuth = useTranslations('auth')
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||
const { status: sessionStatus } = useSession()
|
||||
const isAuthenticated = sessionStatus === 'authenticated'
|
||||
@@ -170,6 +173,7 @@ export function AdminSidebar({ user }: AdminSidebarProps) {
|
||||
<div className="fixed top-0 left-0 right-0 z-40 flex h-16 items-center justify-between border-b bg-card px-4 lg:hidden">
|
||||
<Logo showText textSuffix="Admin" />
|
||||
<div className="flex items-center gap-2">
|
||||
<LanguageSwitcher />
|
||||
<NotificationBell />
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -204,7 +208,8 @@ export function AdminSidebar({ user }: AdminSidebarProps) {
|
||||
{/* Logo + Notification */}
|
||||
<div className="flex h-16 items-center justify-between border-b px-6">
|
||||
<Logo showText textSuffix="Admin" />
|
||||
<div className="hidden lg:block">
|
||||
<div className="hidden lg:flex items-center gap-1">
|
||||
<LanguageSwitcher />
|
||||
<NotificationBell />
|
||||
</div>
|
||||
</div>
|
||||
@@ -344,7 +349,7 @@ export function AdminSidebar({ user }: AdminSidebarProps) {
|
||||
className="flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-2 text-destructive focus:bg-destructive/10 focus:text-destructive"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
<span>Sign out</span>
|
||||
<span>{tAuth('signOut')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -2,35 +2,37 @@
|
||||
|
||||
import { Home, Users, FileText, MessageSquare } from 'lucide-react'
|
||||
import { RoleNav, type NavItem, type RoleNavUser } from '@/components/layouts/role-nav'
|
||||
|
||||
const navigation: NavItem[] = [
|
||||
{
|
||||
name: 'Dashboard',
|
||||
href: '/applicant',
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
name: 'Team',
|
||||
href: '/applicant/team',
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
name: 'Documents',
|
||||
href: '/applicant/documents',
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
name: 'Mentor',
|
||||
href: '/applicant/mentor',
|
||||
icon: MessageSquare,
|
||||
},
|
||||
]
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
interface ApplicantNavProps {
|
||||
user: RoleNavUser
|
||||
}
|
||||
|
||||
export function ApplicantNav({ user }: ApplicantNavProps) {
|
||||
const t = useTranslations('nav')
|
||||
const navigation: NavItem[] = [
|
||||
{
|
||||
name: t('dashboard'),
|
||||
href: '/applicant',
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
name: t('team'),
|
||||
href: '/applicant/team',
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
name: t('documents'),
|
||||
href: '/applicant/documents',
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
name: t('mentoring'),
|
||||
href: '/applicant/mentor',
|
||||
icon: MessageSquare,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<RoleNav
|
||||
navigation={navigation}
|
||||
|
||||
@@ -1,32 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { BookOpen, ClipboardList, GitCompare, Home } from 'lucide-react'
|
||||
import { BookOpen, ClipboardList, GitCompare, Home, Trophy } from 'lucide-react'
|
||||
import { RoleNav, type NavItem, type RoleNavUser } from '@/components/layouts/role-nav'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
const navigation: NavItem[] = [
|
||||
{
|
||||
name: 'Dashboard',
|
||||
href: '/jury',
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
name: 'My Assignments',
|
||||
href: '/jury/assignments',
|
||||
icon: ClipboardList,
|
||||
},
|
||||
{
|
||||
name: 'Compare',
|
||||
href: '/jury/compare',
|
||||
icon: GitCompare,
|
||||
},
|
||||
{
|
||||
name: 'Learning Hub',
|
||||
href: '/jury/learning',
|
||||
icon: BookOpen,
|
||||
},
|
||||
]
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
interface JuryNavProps {
|
||||
user: RoleNavUser
|
||||
@@ -65,6 +43,35 @@ function RemainingBadge() {
|
||||
}
|
||||
|
||||
export function JuryNav({ user }: JuryNavProps) {
|
||||
const t = useTranslations('nav')
|
||||
const navigation: NavItem[] = [
|
||||
{
|
||||
name: t('dashboard'),
|
||||
href: '/jury',
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
name: t('assignments'),
|
||||
href: '/jury/assignments',
|
||||
icon: ClipboardList,
|
||||
},
|
||||
{
|
||||
name: t('awards'),
|
||||
href: '/jury/awards',
|
||||
icon: Trophy,
|
||||
},
|
||||
{
|
||||
name: t('compare'),
|
||||
href: '/jury/compare',
|
||||
icon: GitCompare,
|
||||
},
|
||||
{
|
||||
name: t('learningHub'),
|
||||
href: '/jury/learning',
|
||||
icon: BookOpen,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<RoleNav
|
||||
navigation={navigation}
|
||||
|
||||
@@ -2,30 +2,32 @@
|
||||
|
||||
import { BookOpen, Home, Users } from 'lucide-react'
|
||||
import { RoleNav, type NavItem, type RoleNavUser } from '@/components/layouts/role-nav'
|
||||
|
||||
const navigation: NavItem[] = [
|
||||
{
|
||||
name: 'Dashboard',
|
||||
href: '/mentor',
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
name: 'My Mentees',
|
||||
href: '/mentor/projects',
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
name: 'Resources',
|
||||
href: '/mentor/resources',
|
||||
icon: BookOpen,
|
||||
},
|
||||
]
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
interface MentorNavProps {
|
||||
user: RoleNavUser
|
||||
}
|
||||
|
||||
export function MentorNav({ user }: MentorNavProps) {
|
||||
const t = useTranslations('nav')
|
||||
const navigation: NavItem[] = [
|
||||
{
|
||||
name: t('dashboard'),
|
||||
href: '/mentor',
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
name: t('myProjects'),
|
||||
href: '/mentor/projects',
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
name: t('learningHub'),
|
||||
href: '/mentor/resources',
|
||||
icon: BookOpen,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<RoleNav
|
||||
navigation={navigation}
|
||||
|
||||
@@ -2,25 +2,27 @@
|
||||
|
||||
import { BarChart3, Home } from 'lucide-react'
|
||||
import { RoleNav, type NavItem, type RoleNavUser } from '@/components/layouts/role-nav'
|
||||
|
||||
const navigation: NavItem[] = [
|
||||
{
|
||||
name: 'Dashboard',
|
||||
href: '/observer',
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
name: 'Reports',
|
||||
href: '/observer/reports',
|
||||
icon: BarChart3,
|
||||
},
|
||||
]
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
interface ObserverNavProps {
|
||||
user: RoleNavUser
|
||||
}
|
||||
|
||||
export function ObserverNav({ user }: ObserverNavProps) {
|
||||
const t = useTranslations('nav')
|
||||
const navigation: NavItem[] = [
|
||||
{
|
||||
name: t('dashboard'),
|
||||
href: '/observer',
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
name: t('reports'),
|
||||
href: '/observer/reports',
|
||||
icon: BarChart3,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<RoleNav
|
||||
navigation={navigation}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { signOut, useSession } from 'next-auth/react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { UserAvatar } from '@/components/shared/user-avatar'
|
||||
@@ -21,6 +22,7 @@ import { LogOut, Menu, Moon, Settings, Sun, User, X } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { Logo } from '@/components/shared/logo'
|
||||
import { NotificationBell } from '@/components/shared/notification-bell'
|
||||
import { LanguageSwitcher } from '@/components/shared/language-switcher'
|
||||
|
||||
export type NavItem = {
|
||||
name: string
|
||||
@@ -49,6 +51,8 @@ function isNavItemActive(pathname: string, href: string, basePath: string): bool
|
||||
|
||||
export function RoleNav({ navigation, roleName, user, basePath, statusBadge }: RoleNavProps) {
|
||||
const pathname = usePathname()
|
||||
const tCommon = useTranslations('common')
|
||||
const tAuth = useTranslations('auth')
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||
const { status: sessionStatus } = useSession()
|
||||
const isAuthenticated = sessionStatus === 'authenticated'
|
||||
@@ -107,6 +111,7 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge }: R
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<LanguageSwitcher />
|
||||
<NotificationBell />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -130,7 +135,7 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge }: R
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={"/settings/profile" as Route} className="flex cursor-pointer items-center">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Profile Settings
|
||||
{tCommon('settings')}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -139,7 +144,7 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge }: R
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Sign out
|
||||
{tAuth('signOut')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -191,7 +196,7 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge }: R
|
||||
onClick={() => signOut({ callbackUrl: '/login' })}
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Sign out
|
||||
{tAuth('signOut')}
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
ChevronRight,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
|
||||
const PER_PAGE_OPTIONS = [10, 20, 50]
|
||||
@@ -121,9 +122,9 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
|
||||
</div>
|
||||
|
||||
{/* Observer Notice */}
|
||||
<div className="rounded-lg border-2 border-blue-300 bg-blue-50 px-4 py-3">
|
||||
<div className="rounded-lg border border-blue-200 bg-blue-50/50 px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-blue-100">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-blue-100 p-2.5">
|
||||
<Eye className="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -175,65 +176,95 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
|
||||
</div>
|
||||
) : stats ? (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card className="transition-all hover:shadow-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Programs</CardTitle>
|
||||
<FolderKanban className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.programCount}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{stats.activeRoundCount} active round{stats.activeRoundCount !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AnimatedCard index={0}>
|
||||
<Card className="border-l-4 border-l-blue-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Programs</p>
|
||||
<p className="text-2xl font-bold mt-1">{stats.programCount}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{stats.activeRoundCount} active round{stats.activeRoundCount !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-blue-50 p-3">
|
||||
<FolderKanban className="h-5 w-5 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
<Card className="transition-all hover:shadow-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Projects</CardTitle>
|
||||
<ClipboardList className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.projectCount}</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{selectedRoundId !== 'all' ? 'In selected round' : 'Across all rounds'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AnimatedCard index={1}>
|
||||
<Card className="border-l-4 border-l-emerald-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Projects</p>
|
||||
<p className="text-2xl font-bold mt-1">{stats.projectCount}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{selectedRoundId !== 'all' ? 'In selected round' : 'Across all rounds'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-emerald-50 p-3">
|
||||
<ClipboardList className="h-5 w-5 text-emerald-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
<Card className="transition-all hover:shadow-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Jury Members</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.jurorCount}</div>
|
||||
<p className="text-xs text-muted-foreground">Active members</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AnimatedCard index={2}>
|
||||
<Card className="border-l-4 border-l-violet-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Jury Members</p>
|
||||
<p className="text-2xl font-bold mt-1">{stats.jurorCount}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">Active members</p>
|
||||
</div>
|
||||
<div className="rounded-xl bg-violet-50 p-3">
|
||||
<Users className="h-5 w-5 text-violet-600" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
<Card className="transition-all hover:shadow-md">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Evaluations</CardTitle>
|
||||
<CheckCircle2 className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.submittedEvaluations}</div>
|
||||
<div className="mt-2">
|
||||
<Progress value={stats.completionRate} className="h-2" />
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{stats.completionRate}% completion rate
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<AnimatedCard index={3}>
|
||||
<Card className="border-l-4 border-l-brand-teal transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Evaluations</p>
|
||||
<p className="text-2xl font-bold mt-1">{stats.submittedEvaluations}</p>
|
||||
<div className="mt-2">
|
||||
<Progress value={stats.completionRate} className="h-2" gradient />
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{stats.completionRate}% completion rate
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl bg-brand-teal/10 p-3">
|
||||
<CheckCircle2 className="h-5 w-5 text-brand-teal" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Projects Table */}
|
||||
<AnimatedCard index={4}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>All Projects</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2.5">
|
||||
<div className="rounded-lg bg-emerald-500/10 p-1.5">
|
||||
<ClipboardList className="h-4 w-4 text-emerald-500" />
|
||||
</div>
|
||||
All Projects
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{projectsData ? `${projectsData.total} project${projectsData.total !== 1 ? 's' : ''} found` : 'Loading projects...'}
|
||||
</CardDescription>
|
||||
@@ -395,12 +426,19 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
|
||||
{/* Score Distribution */}
|
||||
{stats && stats.scoreDistribution.some((b) => b.count > 0) && (
|
||||
<AnimatedCard index={5}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Score Distribution</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2.5">
|
||||
<div className="rounded-lg bg-amber-500/10 p-1.5">
|
||||
<BarChart3 className="h-4 w-4 text-amber-500" />
|
||||
</div>
|
||||
Score Distribution
|
||||
</CardTitle>
|
||||
<CardDescription>Distribution of global scores across evaluations</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -424,13 +462,20 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
)}
|
||||
|
||||
{/* Recent Rounds */}
|
||||
{recentRounds.length > 0 && (
|
||||
<AnimatedCard index={6}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Rounds</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2.5">
|
||||
<div className="rounded-lg bg-violet-500/10 p-1.5">
|
||||
<BarChart3 className="h-4 w-4 text-violet-500" />
|
||||
</div>
|
||||
Recent Rounds
|
||||
</CardTitle>
|
||||
<CardDescription>Overview of the latest voting rounds</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -470,6 +515,7 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { z } from 'zod'
|
||||
import { toast } from 'sonner'
|
||||
import { Bot, Loader2, Zap, AlertCircle, RefreshCw, Brain } from 'lucide-react'
|
||||
import { Cog, Loader2, Zap, AlertCircle, RefreshCw, SlidersHorizontal } from 'lucide-react'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -264,7 +264,7 @@ export function AISettingsForm({ settings }: AISettingsFormProps) {
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
{model.isReasoning && (
|
||||
<Brain className="h-3 w-3 text-purple-500" />
|
||||
<SlidersHorizontal className="h-3 w-3 text-purple-500" />
|
||||
)}
|
||||
<span>{model.name}</span>
|
||||
</div>
|
||||
@@ -278,7 +278,7 @@ export function AISettingsForm({ settings }: AISettingsFormProps) {
|
||||
<FormDescription>
|
||||
{form.watch('ai_model')?.startsWith('o') ? (
|
||||
<span className="flex items-center gap-1 text-purple-600">
|
||||
<Brain className="h-3 w-3" />
|
||||
<SlidersHorizontal className="h-3 w-3" />
|
||||
Reasoning model - optimized for complex analysis tasks
|
||||
</span>
|
||||
) : (
|
||||
@@ -323,7 +323,7 @@ export function AISettingsForm({ settings }: AISettingsFormProps) {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Bot className="mr-2 h-4 w-4" />
|
||||
<Cog className="mr-2 h-4 w-4" />
|
||||
Save AI Settings
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
Zap,
|
||||
TrendingUp,
|
||||
Activity,
|
||||
Brain,
|
||||
SlidersHorizontal,
|
||||
Filter,
|
||||
Users,
|
||||
Award,
|
||||
@@ -26,7 +26,7 @@ const ACTION_ICONS: Record<string, typeof Zap> = {
|
||||
ASSIGNMENT: Users,
|
||||
FILTERING: Filter,
|
||||
AWARD_ELIGIBILITY: Award,
|
||||
MENTOR_MATCHING: Brain,
|
||||
MENTOR_MATCHING: SlidersHorizontal,
|
||||
}
|
||||
|
||||
const ACTION_LABELS: Record<string, string> = {
|
||||
@@ -235,7 +235,7 @@ export function AIUsageCard() {
|
||||
variant="outline"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Brain className="h-3 w-3" />
|
||||
<SlidersHorizontal className="h-3 w-3" />
|
||||
<span>{model}</span>
|
||||
<span className="text-muted-foreground">
|
||||
{(data as { costFormatted?: string }).costFormatted}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import {
|
||||
Bot,
|
||||
Cog,
|
||||
Palette,
|
||||
Mail,
|
||||
HardDrive,
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
import { AISettingsForm } from './ai-settings-form'
|
||||
import { AIUsageCard } from './ai-usage-card'
|
||||
import { BrandingSettingsForm } from './branding-settings-form'
|
||||
@@ -195,7 +196,7 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
|
||||
</TabsTrigger>
|
||||
{isSuperAdmin && (
|
||||
<TabsTrigger value="ai" className="gap-2 shrink-0">
|
||||
<Bot className="h-4 w-4" />
|
||||
<Cog className="h-4 w-4" />
|
||||
AI
|
||||
</TabsTrigger>
|
||||
)}
|
||||
@@ -275,7 +276,7 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
|
||||
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
|
||||
{isSuperAdmin && (
|
||||
<TabsTrigger value="ai" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
|
||||
<Bot className="h-4 w-4" />
|
||||
<Cog className="h-4 w-4" />
|
||||
AI
|
||||
</TabsTrigger>
|
||||
)}
|
||||
@@ -308,6 +309,7 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
|
||||
|
||||
{isSuperAdmin && (
|
||||
<TabsContent value="ai" className="space-y-6">
|
||||
<AnimatedCard>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>AI Configuration</CardTitle>
|
||||
@@ -319,11 +321,13 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
|
||||
<AISettingsForm settings={aiSettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
<AIUsageCard />
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="tags">
|
||||
<AnimatedCard>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
@@ -353,9 +357,11 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="branding">
|
||||
<AnimatedCard>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Platform Branding</CardTitle>
|
||||
@@ -367,10 +373,12 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
|
||||
<BrandingSettingsForm settings={brandingSettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
</TabsContent>
|
||||
|
||||
{isSuperAdmin && (
|
||||
<TabsContent value="email">
|
||||
<AnimatedCard>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Email Configuration</CardTitle>
|
||||
@@ -382,10 +390,12 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
|
||||
<EmailSettingsForm settings={emailSettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="notifications">
|
||||
<AnimatedCard>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Notification Email Settings</CardTitle>
|
||||
@@ -397,10 +407,12 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
|
||||
<NotificationSettingsForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
</TabsContent>
|
||||
|
||||
{isSuperAdmin && (
|
||||
<TabsContent value="storage">
|
||||
<AnimatedCard>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>File Storage</CardTitle>
|
||||
@@ -412,11 +424,13 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
|
||||
<StorageSettingsForm settings={storageSettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{isSuperAdmin && (
|
||||
<TabsContent value="security">
|
||||
<AnimatedCard>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Security Settings</CardTitle>
|
||||
@@ -428,10 +442,12 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
|
||||
<SecuritySettingsForm settings={securitySettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="defaults">
|
||||
<AnimatedCard>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Default Settings</CardTitle>
|
||||
@@ -443,9 +459,11 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
|
||||
<DefaultsSettingsForm settings={defaultsSettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="digest" className="space-y-6">
|
||||
<AnimatedCard>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Digest Configuration</CardTitle>
|
||||
@@ -457,9 +475,11 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
|
||||
<DigestSettingsSection settings={digestSettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="analytics" className="space-y-6">
|
||||
<AnimatedCard>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Analytics & Reports</CardTitle>
|
||||
@@ -471,9 +491,11 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
|
||||
<AnalyticsSettingsSection settings={analyticsSettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="audit" className="space-y-6">
|
||||
<AnimatedCard>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Audit & Security</CardTitle>
|
||||
@@ -485,9 +507,11 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
|
||||
<AuditSettingsSection settings={auditSecuritySettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="localization" className="space-y-6">
|
||||
<AnimatedCard>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Localization</CardTitle>
|
||||
@@ -499,6 +523,7 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
|
||||
<LocalizationSettingsSection settings={localizationSettings} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</AnimatedCard>
|
||||
</TabsContent>
|
||||
</div>{/* end content area */}
|
||||
</div>{/* end lg:flex */}
|
||||
@@ -506,7 +531,7 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
|
||||
|
||||
{/* Quick Links to sub-pages */}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<Card>
|
||||
<Card className="transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<LayoutTemplate className="h-4 w-4" />
|
||||
@@ -528,7 +553,7 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
|
||||
</Card>
|
||||
|
||||
{isSuperAdmin && (
|
||||
<Card>
|
||||
<Card className="transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Webhook className="h-4 w-4" />
|
||||
|
||||
@@ -28,7 +28,9 @@ export function EmptyState({
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Icon className="h-12 w-12 text-muted-foreground/50" />
|
||||
<div className="rounded-2xl bg-muted/60 p-4">
|
||||
<Icon className="h-8 w-8 text-muted-foreground/70" />
|
||||
</div>
|
||||
<h3 className="mt-4 font-medium">{title}</h3>
|
||||
{description && (
|
||||
<p className="mt-1 max-w-sm text-sm text-muted-foreground">
|
||||
|
||||
@@ -6,8 +6,10 @@ import { cn } from '@/lib/utils'
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
|
||||
gradient?: boolean
|
||||
}
|
||||
>(({ className, value, gradient, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
@@ -17,7 +19,12 @@ const Progress = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
className={cn(
|
||||
'h-full w-full flex-1 transition-all',
|
||||
gradient
|
||||
? 'bg-gradient-to-r from-brand-teal to-brand-blue'
|
||||
: 'bg-primary'
|
||||
)}
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
|
||||
Reference in New Issue
Block a user