Full Next.js 15 platform with tRPC, Prisma, PostgreSQL, NextAuth. Includes production Dockerfile (multi-stage, port 7600), docker-compose with registry-based image pull, Gitea Actions CI workflow, nginx config for portal.monaco-opc.com, deployment scripts, and DEPLOYMENT.md guide. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
179 lines
6.7 KiB
TypeScript
179 lines
6.7 KiB
TypeScript
'use client'
|
|
|
|
import { Check, ChevronDown } from 'lucide-react'
|
|
import { cn } from '@/lib/utils'
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandItem,
|
|
CommandList,
|
|
} from '@/components/ui/command'
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from '@/components/ui/popover'
|
|
import { useEdition } from '@/contexts/edition-context'
|
|
import { useState } from 'react'
|
|
|
|
const statusConfig: Record<string, { bg: string; text: string; dot: string }> = {
|
|
DRAFT: {
|
|
bg: 'bg-amber-50 dark:bg-amber-950/50',
|
|
text: 'text-amber-700 dark:text-amber-400',
|
|
dot: 'bg-amber-500',
|
|
},
|
|
ACTIVE: {
|
|
bg: 'bg-emerald-50 dark:bg-emerald-950/50',
|
|
text: 'text-emerald-700 dark:text-emerald-400',
|
|
dot: 'bg-emerald-500',
|
|
},
|
|
ARCHIVED: {
|
|
bg: 'bg-slate-100 dark:bg-slate-800/50',
|
|
text: 'text-slate-600 dark:text-slate-400',
|
|
dot: 'bg-slate-400',
|
|
},
|
|
}
|
|
|
|
export function EditionSelector() {
|
|
const { currentEdition, editions, setCurrentEdition, isLoading } = useEdition()
|
|
const [open, setOpen] = useState(false)
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center gap-3 px-1 py-2">
|
|
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-gradient-to-br from-brand-blue/10 to-brand-teal/10 animate-pulse">
|
|
<span className="text-lg font-bold text-muted-foreground/50">--</span>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<div className="h-3 w-16 rounded bg-muted animate-pulse" />
|
|
<div className="h-2.5 w-10 rounded bg-muted animate-pulse" />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (editions.length === 0) {
|
|
return (
|
|
<div className="flex items-center gap-3 px-1 py-2">
|
|
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-muted/50 border border-dashed border-muted-foreground/20">
|
|
<span className="text-lg font-bold text-muted-foreground/40">?</span>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium text-muted-foreground">No editions</p>
|
|
<p className="text-xs text-muted-foreground/60">Create one to start</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const status = currentEdition ? statusConfig[currentEdition.status] : statusConfig.DRAFT
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<button
|
|
role="combobox"
|
|
aria-expanded={open}
|
|
className={cn(
|
|
'group flex w-full items-center gap-3 rounded-xl px-1 py-2 text-left transition-all duration-200',
|
|
'hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
open && 'bg-muted/50'
|
|
)}
|
|
>
|
|
{/* Year Badge */}
|
|
<div className="relative flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-brand-blue shadow-xs transition-transform duration-200 group-hover:scale-[1.02]">
|
|
<span className="text-lg font-bold tracking-tight text-white">
|
|
{currentEdition ? String(currentEdition.year).slice(-2) : '--'}
|
|
</span>
|
|
{/* Status indicator dot */}
|
|
<div className={cn(
|
|
'absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-white',
|
|
status.dot
|
|
)} />
|
|
</div>
|
|
|
|
{/* Text */}
|
|
<div className="min-w-0 flex-1">
|
|
<p className="truncate text-sm font-semibold text-slate-900 dark:text-slate-100">
|
|
{currentEdition ? currentEdition.year : 'Select'}
|
|
</p>
|
|
<p className="truncate text-xs text-slate-500 dark:text-slate-400">
|
|
{currentEdition?.status === 'ACTIVE' ? 'Current Edition' : currentEdition?.status?.toLowerCase()}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Chevron */}
|
|
<ChevronDown className={cn(
|
|
'h-4 w-4 shrink-0 text-muted-foreground/60 transition-transform duration-200',
|
|
open && 'rotate-180'
|
|
)} />
|
|
</button>
|
|
</PopoverTrigger>
|
|
|
|
<PopoverContent
|
|
className="w-[240px] p-0 shadow-lg border-border/50"
|
|
align="start"
|
|
sideOffset={8}
|
|
>
|
|
<Command className="rounded-lg">
|
|
<CommandList className="max-h-[280px]">
|
|
<CommandEmpty className="py-6 text-center text-sm text-muted-foreground">
|
|
No editions found
|
|
</CommandEmpty>
|
|
<CommandGroup className="p-1.5">
|
|
{editions.map((edition) => {
|
|
const editionStatus = statusConfig[edition.status] || statusConfig.DRAFT
|
|
const isSelected = currentEdition?.id === edition.id
|
|
|
|
return (
|
|
<CommandItem
|
|
key={edition.id}
|
|
value={`${edition.name} ${edition.year}`}
|
|
onSelect={() => {
|
|
setCurrentEdition(edition.id)
|
|
setOpen(false)
|
|
}}
|
|
className={cn(
|
|
'group/item flex items-center gap-3 rounded-lg px-2.5 py-2.5 cursor-pointer transition-colors',
|
|
isSelected ? 'bg-slate-100 dark:bg-slate-800' : 'hover:bg-slate-50 dark:hover:bg-slate-800/50'
|
|
)}
|
|
>
|
|
{/* Year badge in dropdown */}
|
|
<div className={cn(
|
|
'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg font-bold text-sm transition-colors',
|
|
isSelected
|
|
? 'bg-brand-blue text-white'
|
|
: 'bg-slate-200 text-slate-600 dark:bg-slate-700 dark:text-slate-300'
|
|
)}>
|
|
{String(edition.year).slice(-2)}
|
|
</div>
|
|
|
|
{/* Edition info */}
|
|
<div className="min-w-0 flex-1">
|
|
<p className="truncate text-sm font-semibold text-slate-900 dark:text-slate-100">
|
|
{edition.year}
|
|
</p>
|
|
<div className="flex items-center gap-1.5">
|
|
<div className={cn('h-1.5 w-1.5 rounded-full', editionStatus.dot)} />
|
|
<span className="text-xs text-slate-500 dark:text-slate-400 capitalize">
|
|
{edition.status.toLowerCase()}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Check mark */}
|
|
{isSelected && (
|
|
<Check className="h-4 w-4 shrink-0 text-brand-blue" />
|
|
)}
|
|
</CommandItem>
|
|
)
|
|
})}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
}
|