From 27e409fb76d89c0334839ea6f7befe31ba76239b Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Thu, 27 Nov 2025 13:11:11 +0100 Subject: [PATCH] chore: update devbook.md to mark completion of layout, UI components, and homepage; update dev.db --- dev.db | Bin 81920 -> 81920 bytes devbook.md | 20 +++--- src/app/api/sessions/route.ts | 71 +++++++++++++++++++ src/app/sessions/new/page.tsx | 103 +++++++++++++++++++++++++++ src/app/sessions/page.tsx | 105 ++++++++++++++++++++++++++++ src/components/ui/Badge.tsx | 50 ++++++++++++++ src/components/ui/Button.tsx | 77 +++++++++++++++++++++ src/components/ui/Card.tsx | 66 ++++++++++++++++++ src/components/ui/Input.tsx | 44 ++++++++++++ src/components/ui/Modal.tsx | 123 +++++++++++++++++++++++++++++++++ src/components/ui/Textarea.tsx | 45 ++++++++++++ src/components/ui/index.ts | 7 ++ 12 files changed, 701 insertions(+), 10 deletions(-) create mode 100644 src/app/api/sessions/route.ts create mode 100644 src/app/sessions/new/page.tsx create mode 100644 src/app/sessions/page.tsx create mode 100644 src/components/ui/Badge.tsx create mode 100644 src/components/ui/Button.tsx create mode 100644 src/components/ui/Card.tsx create mode 100644 src/components/ui/Input.tsx create mode 100644 src/components/ui/Modal.tsx create mode 100644 src/components/ui/Textarea.tsx create mode 100644 src/components/ui/index.ts diff --git a/dev.db b/dev.db index cbae5090c0f4a89827527010b8f5cc5a32cb80b4..adf03906cede589d961917383167b73d4e2d8b76 100644 GIT binary patch delta 228 zcmZo@U~On%ogmG~KT*b+k$+>t5`Q*E{s;#Ch|Pime*B@0s*D`=((3N^?(WICnHj0( zCY6Q;Kv0}fkZ)#@myw=PSsYxFSX7c)6j+*?SCW~h;F4dIm~3QVWU6ausB2^%VrXP# zXlP|%qGxVyu5DmoWneJb(O%L7uL>NJn}6AV;upc@hD8Ahn=Y^jY-Vxz!_UaInK9tM K{GtQ_2m%1tFF(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 */} +