base
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user