192 lines
7.6 KiB
TypeScript
192 lines
7.6 KiB
TypeScript
'use client'
|
|
|
|
import { FlaskConical, Plus, ChevronDown, Trash2, Layout } from 'lucide-react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { renameCanvas, deleteCanvas, createCanvas } from '@/app/actions/canvas-actions'
|
|
import { useRouter } from 'next/navigation'
|
|
import { useState, useTransition } from 'react'
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu"
|
|
import { cn } from '@/lib/utils'
|
|
import { toast } from 'sonner'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
|
|
interface LabHeaderProps {
|
|
canvases: any[]
|
|
currentCanvasId: string | null
|
|
onCreateCanvas: () => Promise<void>
|
|
}
|
|
|
|
export function LabHeader({ canvases, currentCanvasId, onCreateCanvas }: LabHeaderProps) {
|
|
const router = useRouter()
|
|
const { t, language } = useLanguage()
|
|
const [isPending, startTransition] = useTransition()
|
|
const [isEditing, setIsEditing] = useState(false)
|
|
|
|
const currentCanvas = canvases.find(c => c.id === currentCanvasId)
|
|
|
|
const handleRename = async (id: string, newName: string) => {
|
|
if (!newName || newName === currentCanvas?.name) {
|
|
setIsEditing(false)
|
|
return
|
|
}
|
|
|
|
try {
|
|
await renameCanvas(id, newName)
|
|
toast.success(t('labHeader.renamed'))
|
|
router.refresh()
|
|
} catch (e) {
|
|
toast.error(t('labHeader.renameError'))
|
|
}
|
|
setIsEditing(false)
|
|
}
|
|
|
|
const handleCreate = async () => {
|
|
startTransition(async () => {
|
|
try {
|
|
const newCanvas = await createCanvas(language)
|
|
router.push(`/lab?id=${newCanvas.id}`)
|
|
toast.success(t('labHeader.created'))
|
|
} catch (e) {
|
|
toast.error(t('labHeader.createFailed'))
|
|
}
|
|
})
|
|
}
|
|
|
|
const handleDelete = async (id: string, name: string) => {
|
|
if (window.confirm(`${t('labHeader.deleteSpace')} "${name}" ?`)) {
|
|
try {
|
|
await deleteCanvas(id)
|
|
toast.success(t('labHeader.deleted'))
|
|
router.push('/lab')
|
|
router.refresh()
|
|
} catch (e) {
|
|
toast.error(t('labHeader.deleteError'))
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<header className="h-16 border-b bg-white/80 dark:bg-[#1a1c22]/80 backdrop-blur-md flex items-center justify-between px-6 z-20 shrink-0 sticky top-0">
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-2 pe-4 border-e">
|
|
<div className="p-2 bg-primary/10 rounded-xl">
|
|
<FlaskConical className="h-4 w-4 text-primary" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-sm font-bold tracking-tight">{t('labHeader.title')}</h1>
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse" />
|
|
<p className="text-[10px] text-muted-foreground uppercase tracking-widest font-semibold">{t('labHeader.live')}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Project Switcher */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" className="h-10 flex items-center gap-2 hover:bg-muted/50 rounded-xl px-3 transition-all active:scale-95">
|
|
<Layout className="h-4 w-4 text-muted-foreground" />
|
|
<div className="flex flex-col items-start gap-0.5">
|
|
<span className="text-[10px] text-muted-foreground uppercase font-bold leading-none">{t('labHeader.currentProject')}</span>
|
|
<span className="text-sm font-semibold truncate max-w-[150px]">{currentCanvas?.name || t('labHeader.choose')}</span>
|
|
</div>
|
|
<ChevronDown className="h-4 w-4 text-muted-foreground ms-2" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-[280px] p-2 rounded-2xl shadow-xl border-muted/20">
|
|
<DropdownMenuLabel className="text-xs text-muted-foreground px-2 py-1.5 flex justify-between items-center">
|
|
{t('labHeader.yourSpaces')}
|
|
<span className="text-[10px] bg-muted px-1.5 py-0.5 rounded-full font-mono">{canvases.length}</span>
|
|
</DropdownMenuLabel>
|
|
<div className="space-y-1 mt-1">
|
|
{canvases.map(c => (
|
|
<div key={c.id} className="group/item flex items-center gap-1">
|
|
<DropdownMenuItem
|
|
className={cn(
|
|
"flex-1 flex flex-col items-start gap-0.5 rounded-xl cursor-pointer p-3 transition-all",
|
|
c.id === currentCanvasId ? "bg-primary/5 text-primary border border-primary/20" : "hover:bg-muted"
|
|
)}
|
|
onClick={() => router.push(`/lab?id=${c.id}`)}
|
|
>
|
|
<div className="flex items-center gap-2 w-full justify-between">
|
|
<span className="font-semibold text-sm">{c.name}</span>
|
|
{c.id === currentCanvasId && <span className="w-2 h-2 rounded-full bg-primary" />}
|
|
</div>
|
|
<span className="text-[10px] text-muted-foreground">{t('labHeader.updated')} {new Date(c.updatedAt).toLocaleDateString()}</span>
|
|
</DropdownMenuItem>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 opacity-0 group-hover/item:opacity-100 transition-opacity hover:text-destructive hover:bg-destructive/10"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
handleDelete(c.id, c.name)
|
|
}}
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<DropdownMenuSeparator className="my-2" />
|
|
<DropdownMenuItem
|
|
onClick={handleCreate}
|
|
disabled={isPending}
|
|
className="flex items-center gap-2 text-primary font-medium p-3 rounded-xl cursor-pointer hover:bg-primary/5"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
{t('labHeader.newSpace')}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
{/* Inline Rename — click on project name to edit */}
|
|
{currentCanvas && (
|
|
<div className="ms-2 flex items-center gap-2">
|
|
{isEditing ? (
|
|
<input
|
|
autoFocus
|
|
className="bg-muted px-3 py-1.5 rounded-lg text-sm font-medium focus:ring-2 focus:ring-primary/20 outline-none w-[200px]"
|
|
defaultValue={currentCanvas.name}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') handleRename(currentCanvas.id, e.currentTarget.value)
|
|
if (e.key === 'Escape') setIsEditing(false)
|
|
}}
|
|
onBlur={(e) => handleRename(currentCanvas.id, e.target.value)}
|
|
/>
|
|
) : (
|
|
<button
|
|
onClick={() => setIsEditing(true)}
|
|
className="text-sm font-semibold text-foreground hover:text-primary transition-colors"
|
|
title={t('labHeader.rename') || 'Rename'}
|
|
>
|
|
{currentCanvas.name}
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
{currentCanvas && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => handleDelete(currentCanvas.id, currentCanvas.name)}
|
|
className="text-muted-foreground hover:text-destructive hover:bg-destructive/5 rounded-xl transition-all"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</header>
|
|
)
|
|
}
|