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>
This commit is contained in:
178
src/components/shared/edition-selector.tsx
Normal file
178
src/components/shared/edition-selector.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user