Files
MOPC-Portal/src/components/shared/edition-selector.tsx
Matt a606292aaa Initial commit: MOPC platform with Docker deployment setup
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>
2026-01-30 13:41:32 +01:00

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>
)
}