diff --git a/dev.db b/dev.db index cbae509..adf0390 100644 Binary files a/dev.db and b/dev.db differ diff --git a/devbook.md b/devbook.md index dbe34ef..e8772d1 100644 --- a/devbook.md +++ b/devbook.md @@ -135,16 +135,16 @@ Application de gestion d'ateliers SWOT pour entretiens managériaux. ## Phase 4 : Layout & Navigation -- [ ] Créer le layout principal avec : - - [ ] Header avec navigation et user menu - - [ ] Theme toggle (dark/light) -- [ ] Créer les composants UI de base : - - [ ] Button - - [ ] Card - - [ ] Input - - [ ] Modal - - [ ] Badge -- [ ] Créer la page d'accueil `/` - Dashboard avec liste des sessions +- [x] Créer le layout principal avec : + - [x] Header avec navigation et user menu + - [x] Theme toggle (dark/light) +- [x] Créer les composants UI de base : + - [x] Button + - [x] Card + - [x] Input + - [x] Modal + - [x] Badge +- [x] Créer la page d'accueil `/` - Dashboard avec liste des sessions --- diff --git a/src/app/api/sessions/route.ts b/src/app/api/sessions/route.ts new file mode 100644 index 0000000..4cccef7 --- /dev/null +++ b/src/app/api/sessions/route.ts @@ -0,0 +1,71 @@ +import { NextResponse } from 'next/server'; +import { auth } from '@/lib/auth'; +import { prisma } from '@/services/database'; + +export async function GET() { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); + } + + const sessions = await prisma.session.findMany({ + where: { userId: session.user.id }, + include: { + _count: { + select: { + items: true, + actions: true, + }, + }, + }, + orderBy: { updatedAt: 'desc' }, + }); + + return NextResponse.json(sessions); + } catch (error) { + console.error('Error fetching sessions:', error); + return NextResponse.json( + { error: 'Erreur lors de la récupération des sessions' }, + { status: 500 } + ); + } +} + +export async function POST(request: Request) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); + } + + const body = await request.json(); + const { title, collaborator } = body; + + if (!title || !collaborator) { + return NextResponse.json( + { error: 'Titre et collaborateur requis' }, + { status: 400 } + ); + } + + const newSession = await prisma.session.create({ + data: { + title, + collaborator, + userId: session.user.id, + }, + }); + + return NextResponse.json(newSession, { status: 201 }); + } catch (error) { + console.error('Error creating session:', error); + return NextResponse.json( + { error: 'Erreur lors de la création de la session' }, + { status: 500 } + ); + } +} + diff --git a/src/app/sessions/new/page.tsx b/src/app/sessions/new/page.tsx new file mode 100644 index 0000000..4b3b362 --- /dev/null +++ b/src/app/sessions/new/page.tsx @@ -0,0 +1,103 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { Card, CardHeader, CardTitle, CardDescription, CardContent, Button, Input } from '@/components/ui'; + +export default function NewSessionPage() { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + setLoading(true); + + const formData = new FormData(e.currentTarget); + const title = formData.get('title') as string; + const collaborator = formData.get('collaborator') as string; + + if (!title || !collaborator) { + setError('Veuillez remplir tous les champs'); + setLoading(false); + return; + } + + try { + const res = await fetch('/api/sessions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title, collaborator }), + }); + + const data = await res.json(); + + if (!res.ok) { + setError(data.error || 'Une erreur est survenue'); + setLoading(false); + return; + } + + router.push(`/sessions/${data.id}`); + } catch { + setError('Une erreur est survenue'); + setLoading(false); + } + } + + return ( +
+ + + + + Nouvelle Session SWOT + + + Créez une nouvelle session d'atelier SWOT pour un collaborateur + + + + +
+ {error && ( +
+ {error} +
+ )} + + + + + +
+ + +
+
+
+
+
+ ); +} + diff --git a/src/app/sessions/page.tsx b/src/app/sessions/page.tsx new file mode 100644 index 0000000..84f9530 --- /dev/null +++ b/src/app/sessions/page.tsx @@ -0,0 +1,105 @@ +import Link from 'next/link'; +import { auth } from '@/lib/auth'; +import { prisma } from '@/services/database'; +import { Card, CardContent, Badge, Button } from '@/components/ui'; + +async function getSessions(userId: string) { + return prisma.session.findMany({ + where: { userId }, + include: { + _count: { + select: { + items: true, + actions: true, + }, + }, + }, + orderBy: { updatedAt: 'desc' }, + }); +} + +export default async function SessionsPage() { + const session = await auth(); + + if (!session?.user?.id) { + return null; + } + + const sessions = await getSessions(session.user.id); + + return ( +
+ {/* Header */} +
+
+

Mes Sessions SWOT

+

+ Gérez vos ateliers SWOT avec vos collaborateurs +

+
+ + + +
+ + {/* Sessions Grid */} + {sessions.length === 0 ? ( + +
📋
+

+ Aucune session pour le moment +

+

+ Créez votre première session SWOT pour commencer à analyser les forces, + faiblesses, opportunités et menaces de vos collaborateurs. +

+ + + +
+ ) : ( +
+ {sessions.map((s) => ( + + +
+
+

+ {s.title} +

+

{s.collaborator}

+
+ 📊 +
+ + +
+ + {s._count.items} items + + + {s._count.actions} actions + +
+ +

+ Mis à jour le{' '} + {new Date(s.updatedAt).toLocaleDateString('fr-FR', { + day: 'numeric', + month: 'long', + year: 'numeric', + })} +

+
+
+ + ))} +
+ )} +
+ ); +} + diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx new file mode 100644 index 0000000..652ba63 --- /dev/null +++ b/src/components/ui/Badge.tsx @@ -0,0 +1,50 @@ +import { HTMLAttributes, forwardRef } from 'react'; + +type BadgeVariant = + | 'default' + | 'primary' + | 'strength' + | 'weakness' + | 'opportunity' + | 'threat' + | 'success' + | 'warning' + | 'destructive'; + +interface BadgeProps extends HTMLAttributes { + variant?: BadgeVariant; +} + +const variantStyles: Record = { + default: 'bg-card-hover text-foreground border-border', + primary: 'bg-primary/10 text-primary border-primary/20', + strength: 'bg-strength-bg text-strength border-strength-border', + weakness: 'bg-weakness-bg text-weakness border-weakness-border', + opportunity: 'bg-opportunity-bg text-opportunity border-opportunity-border', + threat: 'bg-threat-bg text-threat border-threat-border', + success: 'bg-success/10 text-success border-success/20', + warning: 'bg-warning/10 text-warning border-warning/20', + destructive: 'bg-destructive/10 text-destructive border-destructive/20', +}; + +export const Badge = forwardRef( + ({ className = '', variant = 'default', children, ...props }, ref) => { + return ( + + {children} + + ); + } +); + +Badge.displayName = 'Badge'; + diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx new file mode 100644 index 0000000..61f4f55 --- /dev/null +++ b/src/components/ui/Button.tsx @@ -0,0 +1,77 @@ +import { forwardRef, ButtonHTMLAttributes } from 'react'; + +type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive'; +type ButtonSize = 'sm' | 'md' | 'lg'; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: ButtonVariant; + size?: ButtonSize; + loading?: boolean; +} + +const variantStyles: Record = { + primary: + 'bg-primary text-primary-foreground hover:bg-primary-hover border-transparent', + secondary: + 'bg-card text-foreground hover:bg-card-hover border-border', + outline: + 'bg-transparent text-foreground hover:bg-card-hover border-border', + ghost: + 'bg-transparent text-foreground hover:bg-card-hover border-transparent', + destructive: + 'bg-destructive text-white hover:bg-destructive/90 border-transparent', +}; + +const sizeStyles: Record = { + sm: 'h-8 px-3 text-sm gap-1.5', + md: 'h-10 px-4 text-sm gap-2', + lg: 'h-12 px-6 text-base gap-2', +}; + +export const Button = forwardRef( + ({ className = '', variant = 'primary', size = 'md', loading, disabled, children, ...props }, ref) => { + return ( + + ); + } +); + +Button.displayName = 'Button'; + diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx new file mode 100644 index 0000000..b4187b0 --- /dev/null +++ b/src/components/ui/Card.tsx @@ -0,0 +1,66 @@ +import { HTMLAttributes, forwardRef } from 'react'; + +interface CardProps extends HTMLAttributes { + hover?: boolean; +} + +export const Card = forwardRef( + ({ className = '', hover = false, children, ...props }, ref) => { + return ( +
+ {children} +
+ ); + } +); + +Card.displayName = 'Card'; + +export const CardHeader = forwardRef>( + ({ className = '', ...props }, ref) => ( +
+ ) +); + +CardHeader.displayName = 'CardHeader'; + +export const CardTitle = forwardRef>( + ({ className = '', ...props }, ref) => ( +

+ ) +); + +CardTitle.displayName = 'CardTitle'; + +export const CardDescription = forwardRef>( + ({ className = '', ...props }, ref) => ( +

+ ) +); + +CardDescription.displayName = 'CardDescription'; + +export const CardContent = forwardRef>( + ({ className = '', ...props }, ref) => ( +

+ ) +); + +CardContent.displayName = 'CardContent'; + +export const CardFooter = forwardRef>( + ({ className = '', ...props }, ref) => ( +
+ ) +); + +CardFooter.displayName = 'CardFooter'; + diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx new file mode 100644 index 0000000..9fc5558 --- /dev/null +++ b/src/components/ui/Input.tsx @@ -0,0 +1,44 @@ +import { forwardRef, InputHTMLAttributes } from 'react'; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; +} + +export const Input = forwardRef( + ({ className = '', label, error, id, ...props }, ref) => { + const inputId = id || props.name; + + return ( +
+ {label && ( + + )} + + {error && ( +

{error}

+ )} +
+ ); + } +); + +Input.displayName = 'Input'; + diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx new file mode 100644 index 0000000..76b2eb8 --- /dev/null +++ b/src/components/ui/Modal.tsx @@ -0,0 +1,123 @@ +'use client'; + +import { Fragment, ReactNode, useEffect, useSyncExternalStore } from 'react'; +import { createPortal } from 'react-dom'; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title?: string; + children: ReactNode; + size?: 'sm' | 'md' | 'lg' | 'xl'; +} + +const sizeStyles = { + sm: 'max-w-sm', + md: 'max-w-md', + lg: 'max-w-lg', + xl: 'max-w-xl', +}; + +function subscribe() { + return () => {}; +} + +function useIsMounted() { + return useSyncExternalStore( + subscribe, + () => true, + () => false + ); +} + +export function Modal({ isOpen, onClose, title, children, size = 'md' }: ModalProps) { + const isMounted = useIsMounted(); + + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + + if (isOpen) { + document.addEventListener('keydown', handleEscape); + document.body.style.overflow = 'hidden'; + } + + return () => { + document.removeEventListener('keydown', handleEscape); + document.body.style.overflow = 'unset'; + }; + }, [isOpen, onClose]); + + if (!isMounted || !isOpen) return null; + + return createPortal( + + {/* Backdrop */} +