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:
159
src/components/shared/status-tracker.tsx
Normal file
159
src/components/shared/status-tracker.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CheckCircle, Circle, Clock } from 'lucide-react'
|
||||
|
||||
interface TimelineItem {
|
||||
status: string
|
||||
label: string
|
||||
date: Date | string | null
|
||||
completed: boolean
|
||||
}
|
||||
|
||||
interface StatusTrackerProps {
|
||||
timeline: TimelineItem[]
|
||||
currentStatus: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function StatusTracker({
|
||||
timeline,
|
||||
currentStatus,
|
||||
className,
|
||||
}: StatusTrackerProps) {
|
||||
const formatDate = (date: Date | string | null) => {
|
||||
if (!date) return null
|
||||
const d = typeof date === 'string' ? new Date(date) : date
|
||||
return d.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('relative', className)}>
|
||||
<div className="space-y-0">
|
||||
{timeline.map((item, index) => {
|
||||
const isCompleted = item.completed
|
||||
const isCurrent =
|
||||
isCompleted && !timeline[index + 1]?.completed
|
||||
const isPending = !isCompleted
|
||||
|
||||
return (
|
||||
<div key={item.status} className="relative flex gap-4">
|
||||
{/* Vertical line */}
|
||||
{index < timeline.length - 1 && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-[15px] top-[32px] h-full w-0.5',
|
||||
isCompleted ? 'bg-primary' : 'bg-muted'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Icon */}
|
||||
<div className="relative z-10 flex h-8 w-8 shrink-0 items-center justify-center">
|
||||
{isCompleted ? (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-8 w-8 items-center justify-center rounded-full',
|
||||
isCurrent
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-primary/20 text-primary'
|
||||
)}
|
||||
>
|
||||
{isCurrent ? (
|
||||
<Clock className="h-4 w-4" />
|
||||
) : (
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-muted bg-background">
|
||||
<Circle className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 pb-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<p
|
||||
className={cn(
|
||||
'font-medium',
|
||||
isPending && 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</p>
|
||||
{isCurrent && (
|
||||
<span className="text-xs bg-primary/10 text-primary px-2 py-0.5 rounded-full">
|
||||
Current
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.date && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatDate(item.date)}
|
||||
</p>
|
||||
)}
|
||||
{isPending && !isCurrent && (
|
||||
<p className="text-sm text-muted-foreground">Pending</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Compact horizontal version
|
||||
interface StatusBarProps {
|
||||
status: string
|
||||
statuses: { value: string; label: string }[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function StatusBar({ status, statuses, className }: StatusBarProps) {
|
||||
const currentIndex = statuses.findIndex((s) => s.value === status)
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center gap-2', className)}>
|
||||
{statuses.map((s, index) => {
|
||||
const isCompleted = index <= currentIndex
|
||||
const isCurrent = index === currentIndex
|
||||
|
||||
return (
|
||||
<div key={s.value} className="flex items-center gap-2">
|
||||
{index > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
'h-0.5 w-8',
|
||||
isCompleted ? 'bg-primary' : 'bg-muted'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium',
|
||||
isCurrent
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: isCompleted
|
||||
? 'bg-primary/20 text-primary'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{isCompleted && !isCurrent && (
|
||||
<CheckCircle className="h-3 w-3" />
|
||||
)}
|
||||
{s.label}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user