This commit is contained in:
2026-06-30 17:09:52 +02:00
commit 0806be135d
54 changed files with 10482 additions and 0 deletions
+198
View File
@@ -0,0 +1,198 @@
import { useState } from 'react'
import { motion } from 'framer-motion'
import {
LayoutDashboard,
KanbanSquare,
Target,
Bug,
Users,
Plus,
ChevronDown,
LogOut,
Settings,
Gamepad2
} from 'lucide-react'
import { useApp } from '../lib/AppContext'
import { supa } from '../lib/supabase'
import { ROLE_LABELS } from '../lib/types'
import { Avatar, Modal } from './ui'
export type Page = 'dashboard' | 'kanban' | 'milestones' | 'bugs' | 'members'
const NAV: { id: Page; label: string; icon: typeof LayoutDashboard }[] = [
{ id: 'dashboard', label: 'Tableau de bord', icon: LayoutDashboard },
{ id: 'kanban', label: 'Tâches (Kanban)', icon: KanbanSquare },
{ id: 'milestones', label: 'Jalons & planning', icon: Target },
{ id: 'bugs', label: 'Bugs', icon: Bug },
{ id: 'members', label: 'Équipe', icon: Users }
]
export default function Layout({
page,
setPage,
children
}: {
page: Page
setPage: (p: Page) => void
children: React.ReactNode
}): JSX.Element {
const { profile, projects, currentProject, setCurrentProject, refreshProjects, signOut } = useApp()
const [picker, setPicker] = useState(false)
const [showNew, setShowNew] = useState(false)
return (
<div className="flex h-full">
{/* Barre latérale */}
<aside className="flex w-64 shrink-0 flex-col border-r border-border bg-panel">
<div className="flex items-center gap-2 px-4 py-4 text-accent">
<Gamepad2 size={22} />
<span className="font-semibold text-white">GameDev Tracker</span>
</div>
{/* Sélecteur de projet */}
<div className="relative px-3">
<button
className="flex w-full items-center justify-between rounded-lg border border-border bg-panel2 px-3 py-2 text-left text-sm hover:border-accent"
onClick={() => setPicker((v) => !v)}
>
<span className="truncate">
<span className="block text-[10px] uppercase text-gray-500">Projet</span>
<span className="font-medium text-white">{currentProject?.name ?? 'Aucun projet'}</span>
</span>
<ChevronDown size={16} className="text-gray-400" />
</button>
{picker && (
<div className="absolute left-3 right-3 z-20 mt-1 rounded-lg border border-border bg-panel2 py-1 shadow-xl">
{projects.map((p) => (
<button
key={p.id}
className="block w-full px-3 py-2 text-left text-sm hover:bg-border"
onClick={() => {
setCurrentProject(p)
setPicker(false)
}}
>
{p.name}
</button>
))}
<button
className="flex w-full items-center gap-2 border-t border-border px-3 py-2 text-left text-sm text-accent2 hover:bg-border"
onClick={() => {
setShowNew(true)
setPicker(false)
}}
>
<Plus size={14} /> Nouveau projet
</button>
</div>
)}
</div>
<nav className="mt-4 flex-1 space-y-1 px-3">
{NAV.map((n) => (
<button
key={n.id}
onClick={() => setPage(n.id)}
className={`relative flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors ${
page === n.id ? 'text-white' : 'text-gray-400 hover:bg-panel2 hover:text-white'
}`}
>
{page === n.id && (
<motion.span
layoutId="nav-active"
className="absolute inset-0 rounded-lg bg-accent/20"
transition={{ type: 'spring', stiffness: 380, damping: 30 }}
/>
)}
<n.icon size={18} className="relative z-10" />
<span className="relative z-10">{n.label}</span>
</button>
))}
</nav>
{/* Pied : utilisateur */}
<div className="border-t border-border p-3">
<div className="flex items-center gap-2">
<Avatar profile={profile} />
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium text-white">{profile?.full_name}</div>
<div className="truncate text-xs text-gray-500">
{profile ? ROLE_LABELS[profile.role] : ''}
</div>
</div>
<button onClick={signOut} title="Se déconnecter" className="text-gray-400 hover:text-rose-400">
<LogOut size={18} />
</button>
</div>
</div>
</aside>
{/* Contenu */}
<main className="flex-1 overflow-hidden">
{currentProject ? (
children
) : (
<div className="flex h-full flex-col items-center justify-center gap-4 text-gray-400">
<Settings size={40} className="text-gray-600" />
<p>Aucun projet pour le moment.</p>
<button className="btn-primary" onClick={() => setShowNew(true)}>
<Plus size={16} /> Créer le premier projet
</button>
</div>
)}
</main>
{showNew && <NewProjectModal onClose={() => setShowNew(false)} onCreated={refreshProjects} />}
</div>
)
}
function NewProjectModal({
onClose,
onCreated
}: {
onClose: () => void
onCreated: () => Promise<void>
}): JSX.Element {
const { profile, setCurrentProject } = useApp()
const [name, setName] = useState('')
const [desc, setDesc] = useState('')
const [busy, setBusy] = useState(false)
const create = async (): Promise<void> => {
if (!name.trim()) return
setBusy(true)
const { data } = await supa()
.from('projects')
.insert({ name: name.trim(), description: desc.trim() || null, created_by: profile?.id })
.select()
.single()
await onCreated()
if (data) setCurrentProject(data)
setBusy(false)
onClose()
}
return (
<Modal title="Nouveau projet" onClose={onClose}>
<label className="label">Nom du jeu / projet</label>
<input
className="input mb-3"
autoFocus
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Mon super jeu"
/>
<label className="label">Description (optionnel)</label>
<textarea
className="input mb-4 h-24 resize-none"
value={desc}
onChange={(e) => setDesc(e.target.value)}
placeholder="Genre, plateforme, pitch..."
/>
<button className="btn-primary w-full" onClick={create} disabled={busy || !name.trim()}>
Créer
</button>
</Modal>
)
}