From cc7e73ce7bd3bfd34f61b1411a44f9828722b2d2 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Tue, 17 Feb 2026 09:43:08 +0100 Subject: [PATCH] feat: refactor workshop management by centralizing workshop data and improving session navigation across components --- src/app/motivators/[id]/page.tsx | 5 +- src/app/page.tsx | 93 ++---------- src/app/sessions/NewWorkshopDropdown.tsx | 15 +- src/app/sessions/WorkshopTabs.tsx | 68 +++------ src/app/sessions/[id]/page.tsx | 5 +- src/app/sessions/page.tsx | 32 +--- src/app/weather/[id]/page.tsx | 5 +- src/app/weekly-checkin/[id]/page.tsx | 5 +- src/app/year-review/[id]/page.tsx | 5 +- src/components/layout/Header.tsx | 76 ++-------- src/lib/workshops.ts | 185 +++++++++++++++++++++++ 11 files changed, 264 insertions(+), 230 deletions(-) create mode 100644 src/lib/workshops.ts diff --git a/src/app/motivators/[id]/page.tsx b/src/app/motivators/[id]/page.tsx index e85f8e6..45248ca 100644 --- a/src/app/motivators/[id]/page.tsx +++ b/src/app/motivators/[id]/page.tsx @@ -1,6 +1,7 @@ import { notFound } from 'next/navigation'; import Link from 'next/link'; import { auth } from '@/lib/auth'; +import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops'; import { getMotivatorSessionById } from '@/services/moving-motivators'; import { MotivatorBoard, MotivatorLiveWrapper } from '@/components/moving-motivators'; import { Badge, CollaboratorDisplay } from '@/components/ui'; @@ -29,8 +30,8 @@ export default async function MotivatorSessionPage({ params }: MotivatorSessionP {/* Header */}
- - Moving Motivators + + {getWorkshop('motivators').label} / {session.title} diff --git a/src/app/page.tsx b/src/app/page.tsx index 716b2d1..542fe84 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,5 @@ import Link from 'next/link'; +import { WORKSHOPS, getSessionsTabUrl } from '@/lib/workshops'; export default function Home() { return ( @@ -21,85 +22,19 @@ export default function Home() { Choisissez votre atelier
- {/* SWOT Workshop Card */} - - - {/* Moving Motivators Workshop Card */} - - - {/* Year Review Workshop Card */} - - - {/* Weekly Check-in Workshop Card */} - - - {/* Weather Workshop Card */} - + {WORKSHOPS.map((w) => ( + + ))}
diff --git a/src/app/sessions/NewWorkshopDropdown.tsx b/src/app/sessions/NewWorkshopDropdown.tsx index ff08db2..adbbe8a 100644 --- a/src/app/sessions/NewWorkshopDropdown.tsx +++ b/src/app/sessions/NewWorkshopDropdown.tsx @@ -3,14 +3,7 @@ import { useState } from 'react'; import Link from 'next/link'; import { Button } from '@/components/ui'; - -const WORKSHOPS = [ - { href: '/sessions/new', icon: '📊', label: 'SWOT', desc: 'Forces, faiblesses, opportunités' }, - { href: '/motivators/new', icon: '🎯', label: 'Moving Motivators', desc: 'Motivations intrinsèques' }, - { href: '/year-review/new', icon: '📅', label: 'Year Review', desc: "Bilan de l'année" }, - { href: '/weekly-checkin/new', icon: '📝', label: 'Weekly Check-in', desc: 'Suivi hebdomadaire' }, - { href: '/weather/new', icon: '🌤️', label: 'Météo d\'équipe', desc: 'Humeur et énergie' }, -]; +import { WORKSHOPS } from '@/lib/workshops'; export function NewWorkshopDropdown() { const [open, setOpen] = useState(false); @@ -39,15 +32,15 @@ export function NewWorkshopDropdown() {
{WORKSHOPS.map((w) => ( setOpen(false)} > {w.icon}
{w.label}
-
{w.desc}
+
{w.description}
))} diff --git a/src/app/sessions/WorkshopTabs.tsx b/src/app/sessions/WorkshopTabs.tsx index 55d8a8c..d8eecab 100644 --- a/src/app/sessions/WorkshopTabs.tsx +++ b/src/app/sessions/WorkshopTabs.tsx @@ -17,26 +17,18 @@ import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving import { deleteYearReviewSession, updateYearReviewSession } from '@/actions/year-review'; import { deleteWeeklyCheckInSession, updateWeeklyCheckInSession } from '@/actions/weekly-checkin'; import { deleteWeatherSession, updateWeatherSession } from '@/actions/weather'; +import { + type WorkshopTabType, + type WorkshopTypeId, + WORKSHOPS, + VALID_TAB_PARAMS, + getWorkshop, + getSessionPath, +} from '@/lib/workshops'; -type WorkshopType = 'all' | 'swot' | 'motivators' | 'year-review' | 'weekly-checkin' | 'weather' | 'byPerson'; - -const VALID_TABS: WorkshopType[] = [ - 'all', - 'swot', - 'motivators', - 'year-review', - 'weekly-checkin', - 'weather', - 'byPerson', -]; - -const TYPE_TABS: { value: WorkshopType; icon: string; label: string }[] = [ - { value: 'all', icon: '📋', label: 'Tous' }, - { value: 'swot', icon: '📊', label: 'SWOT' }, - { value: 'motivators', icon: '🎯', label: 'Motivators' }, - { value: 'year-review', icon: '📅', label: 'Year Review' }, - { value: 'weekly-checkin', icon: '📝', label: 'Check-in' }, - { value: 'weather', icon: '🌤️', label: 'Météo' }, +const TYPE_TABS = [ + { value: 'all' as const, icon: '📋', label: 'Tous' }, + ...WORKSHOPS.map((w) => ({ value: w.id, icon: w.icon, label: w.labelShort })), ]; interface ShareUser { @@ -212,10 +204,12 @@ export function WorkshopTabs({ // Get tab from URL or default to 'all' const tabParam = searchParams.get('tab'); - const activeTab: WorkshopType = - tabParam && VALID_TABS.includes(tabParam as WorkshopType) ? (tabParam as WorkshopType) : 'all'; + const activeTab: WorkshopTabType = + tabParam && VALID_TAB_PARAMS.includes(tabParam as WorkshopTabType) + ? (tabParam as WorkshopTabType) + : 'all'; - const setActiveTab = (tab: WorkshopType) => { + const setActiveTab = (tab: WorkshopTabType) => { const params = new URLSearchParams(searchParams.toString()); if (tab === 'all') { params.delete('tab'); @@ -362,8 +356,8 @@ function TypeFilterDropdown({ onOpenChange, counts, }: { - activeTab: WorkshopType; - setActiveTab: (t: WorkshopType) => void; + activeTab: WorkshopTabType; + setActiveTab: (t: WorkshopTabType) => void; open: boolean; onOpenChange: (v: boolean) => void; counts: Record; @@ -493,20 +487,12 @@ function SessionCard({ session }: { session: AnySession }) { : (session as MotivatorSession).participant ); + const workshop = getWorkshop(session.workshopType as WorkshopTypeId); const isSwot = session.workshopType === 'swot'; const isYearReview = session.workshopType === 'year-review'; const isWeeklyCheckIn = session.workshopType === 'weekly-checkin'; const isWeather = session.workshopType === 'weather'; - const href = isSwot - ? `/sessions/${session.id}` - : isYearReview - ? `/year-review/${session.id}` - : isWeeklyCheckIn - ? `/weekly-checkin/${session.id}` - : isWeather - ? `/weather/${session.id}` - : `/motivators/${session.id}`; - const icon = isSwot ? '📊' : isYearReview ? '📅' : isWeeklyCheckIn ? '📝' : isWeather ? '🌤️' : '🎯'; + const href = getSessionPath(session.workshopType as WorkshopTypeId, session.id); const participant = isSwot ? (session as SwotSession).collaborator : isYearReview @@ -516,15 +502,7 @@ function SessionCard({ session }: { session: AnySession }) { : isWeather ? (session as WeatherSession).user.name || (session as WeatherSession).user.email : (session as MotivatorSession).participant; - const accentColor = isSwot - ? '#06b6d4' - : isYearReview - ? '#f59e0b' - : isWeeklyCheckIn - ? '#10b981' - : isWeather - ? '#3b82f6' - : '#8b5cf6'; + const accentColor = workshop.accentColor; const handleDelete = () => { startTransition(async () => { @@ -582,7 +560,7 @@ function SessionCard({ session }: { session: AnySession }) { setShowEditModal(true); }; - const editParticipantLabel = isSwot ? 'Collaborateur' : isWeather ? '' : 'Participant'; + const editParticipantLabel = workshop.participantLabel; return ( <> @@ -597,7 +575,7 @@ function SessionCard({ session }: { session: AnySession }) { {/* Header: Icon + Title + Role badge */}
- {icon} + {workshop.icon}

{session.title}

{!session.isOwner && (
- - SWOT + + {getWorkshop('swot').labelShort} / {session.title} diff --git a/src/app/sessions/page.tsx b/src/app/sessions/page.tsx index 8cf9a2a..fa37acc 100644 --- a/src/app/sessions/page.tsx +++ b/src/app/sessions/page.tsx @@ -6,6 +6,7 @@ import { getYearReviewSessionsByUserId } from '@/services/year-review'; import { getWeeklyCheckInSessionsByUserId } from '@/services/weekly-checkin'; import { getWeatherSessionsByUserId } from '@/services/weather'; import { Card } from '@/components/ui'; +import { withWorkshopType } from '@/lib/workshops'; import { WorkshopTabs } from './WorkshopTabs'; import { NewWorkshopDropdown } from './NewWorkshopDropdown'; @@ -45,31 +46,12 @@ export default async function SessionsPage() { getWeatherSessionsByUserId(session.user.id), ]); - // Add type to each session for unified display - const allSwotSessions = swotSessions.map((s) => ({ - ...s, - workshopType: 'swot' as const, - })); - - const allMotivatorSessions = motivatorSessions.map((s) => ({ - ...s, - workshopType: 'motivators' as const, - })); - - const allYearReviewSessions = yearReviewSessions.map((s) => ({ - ...s, - workshopType: 'year-review' as const, - })); - - const allWeeklyCheckInSessions = weeklyCheckInSessions.map((s) => ({ - ...s, - workshopType: 'weekly-checkin' as const, - })); - - const allWeatherSessions = weatherSessions.map((s) => ({ - ...s, - workshopType: 'weather' as const, - })); + // Add workshopType to each session for unified display + const allSwotSessions = withWorkshopType(swotSessions, 'swot'); + const allMotivatorSessions = withWorkshopType(motivatorSessions, 'motivators'); + const allYearReviewSessions = withWorkshopType(yearReviewSessions, 'year-review'); + const allWeeklyCheckInSessions = withWorkshopType(weeklyCheckInSessions, 'weekly-checkin'); + const allWeatherSessions = withWorkshopType(weatherSessions, 'weather'); // Combine and sort by updatedAt const allSessions = [ diff --git a/src/app/weather/[id]/page.tsx b/src/app/weather/[id]/page.tsx index b18f89c..41ebe92 100644 --- a/src/app/weather/[id]/page.tsx +++ b/src/app/weather/[id]/page.tsx @@ -1,6 +1,7 @@ import { notFound } from 'next/navigation'; import Link from 'next/link'; import { auth } from '@/lib/auth'; +import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops'; import { getWeatherSessionById } from '@/services/weather'; import { getUserTeams } from '@/services/teams'; import { WeatherBoard, WeatherLiveWrapper, WeatherInfoPanel } from '@/components/weather'; @@ -33,8 +34,8 @@ export default async function WeatherSessionPage({ params }: WeatherSessionPageP {/* Header */}
- - Météo + + {getWorkshop('weather').labelShort} / {session.title} diff --git a/src/app/weekly-checkin/[id]/page.tsx b/src/app/weekly-checkin/[id]/page.tsx index 1970ee0..963137e 100644 --- a/src/app/weekly-checkin/[id]/page.tsx +++ b/src/app/weekly-checkin/[id]/page.tsx @@ -1,6 +1,7 @@ import { notFound } from 'next/navigation'; import Link from 'next/link'; import { auth } from '@/lib/auth'; +import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops'; import { getWeeklyCheckInSessionById } from '@/services/weekly-checkin'; import { getUserOKRsForPeriod } from '@/services/okrs'; import { getCurrentQuarterPeriod } from '@/lib/okr-utils'; @@ -44,8 +45,8 @@ export default async function WeeklyCheckInSessionPage({ params }: WeeklyCheckIn {/* Header */}
- - Weekly Check-in + + {getWorkshop('weekly-checkin').label} / {session.title} diff --git a/src/app/year-review/[id]/page.tsx b/src/app/year-review/[id]/page.tsx index 7e9bc70..3c55377 100644 --- a/src/app/year-review/[id]/page.tsx +++ b/src/app/year-review/[id]/page.tsx @@ -1,6 +1,7 @@ import { notFound } from 'next/navigation'; import Link from 'next/link'; import { auth } from '@/lib/auth'; +import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops'; import { getYearReviewSessionById } from '@/services/year-review'; import { YearReviewBoard, YearReviewLiveWrapper } from '@/components/year-review'; import { Badge, CollaboratorDisplay } from '@/components/ui'; @@ -29,8 +30,8 @@ export default async function YearReviewSessionPage({ params }: YearReviewSessio {/* Header */}
- - Year Review + + {getWorkshop('year-review').label} / {session.title} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 26f45f5..66822bc 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -6,6 +6,7 @@ import { useSession, signOut } from 'next-auth/react'; import { useTheme } from '@/contexts/ThemeContext'; import { useState } from 'react'; import { Avatar, RocketIcon } from '@/components/ui'; +import { WORKSHOPS } from '@/lib/workshops'; export function Header() { const { theme, toggleTheme } = useTheme(); @@ -65,11 +66,7 @@ export function Header() { onClick={() => setWorkshopsOpen(!workshopsOpen)} onBlur={() => setTimeout(() => setWorkshopsOpen(false), 150)} className={`flex items-center gap-1 text-sm font-medium transition-colors ${ - isActiveLink('/sessions/') || - isActiveLink('/motivators') || - isActiveLink('/year-review') || - isActiveLink('/weekly-checkin') || - isActiveLink('/weather') + WORKSHOPS.some((w) => isActiveLink(w.path)) ? 'text-primary' : 'text-muted hover:text-foreground' }`} @@ -92,61 +89,20 @@ export function Header() { {workshopsOpen && (
- setWorkshopsOpen(false)} - > - 📊 -
-
Analyse SWOT
-
Forces, faiblesses, opportunités
-
- - setWorkshopsOpen(false)} - > - 🎯 -
-
Moving Motivators
-
Motivations intrinsèques
-
- - setWorkshopsOpen(false)} - > - 📅 -
-
Year Review
-
Bilan de l'année
-
- - setWorkshopsOpen(false)} - > - 📝 -
-
Weekly Check-in
-
Suivi hebdomadaire
-
- - setWorkshopsOpen(false)} - > - 🌤️ -
-
Météo d'équipe
-
Humeur et énergie
-
- + {WORKSHOPS.map((w) => ( + setWorkshopsOpen(false)} + > + {w.icon} +
+
{w.label}
+
{w.description}
+
+ + ))}
)}
diff --git a/src/lib/workshops.ts b/src/lib/workshops.ts new file mode 100644 index 0000000..bae092a --- /dev/null +++ b/src/lib/workshops.ts @@ -0,0 +1,185 @@ +/** + * Single source of truth for workshop types and metadata. + * Used by: Header, NewWorkshopDropdown, WorkshopTabs, sessions page. + */ + +export const WORKSHOP_TYPE_IDS = [ + 'swot', + 'motivators', + 'year-review', + 'weekly-checkin', + 'weather', +] as const; + +export type WorkshopTypeId = (typeof WORKSHOP_TYPE_IDS)[number]; + +export type WorkshopTabType = WorkshopTypeId | 'all' | 'byPerson'; + +export const VALID_TAB_PARAMS: WorkshopTabType[] = [ + 'all', + ...WORKSHOP_TYPE_IDS, + 'byPerson', +]; + +export interface WorkshopConfig { + id: WorkshopTypeId; + icon: string; + label: string; + labelShort: string; + cardLabel: string; // For home page cards (e.g. "Météo" vs "Météo d'équipe") + description: string; + path: string; // e.g. /sessions, /motivators + newPath: string; // e.g. /sessions/new + accentColor: string; + hasParticipant: boolean; // false for weather + participantLabel: string; // 'Collaborateur' | 'Participant' | '' + /** Home page marketing content */ + home: { + tagline: string; + description: string; + features: string[]; + }; +} + +export const WORKSHOPS: WorkshopConfig[] = [ + { + id: 'swot', + icon: '📊', + label: 'Analyse SWOT', + labelShort: 'SWOT', + cardLabel: 'Analyse SWOT', + description: 'Forces, faiblesses, opportunités', + path: '/sessions', + newPath: '/sessions/new', + accentColor: '#06b6d4', + hasParticipant: true, + participantLabel: 'Collaborateur', + home: { + tagline: 'Analysez. Planifiez. Progressez.', + description: + "Cartographiez les forces et faiblesses de vos collaborateurs. Identifiez opportunités et menaces pour définir des actions concrètes.", + features: [ + 'Matrice interactive Forces/Faiblesses/Opportunités/Menaces', + 'Actions croisées et plan de développement', + 'Collaboration en temps réel', + ], + }, + }, + { + id: 'motivators', + icon: '🎯', + label: 'Moving Motivators', + labelShort: 'Motivators', + cardLabel: 'Moving Motivators', + description: 'Motivations intrinsèques', + path: '/motivators', + newPath: '/motivators/new', + accentColor: '#8b5cf6', + hasParticipant: true, + participantLabel: 'Participant', + home: { + tagline: 'Révélez ce qui motive vraiment', + description: + "Explorez les 10 motivations intrinsèques de vos collaborateurs. Comprenez leur impact et alignez aspirations et missions.", + features: [ + '10 cartes de motivation à classer', + "Évaluation de l'influence positive/négative", + 'Récapitulatif personnalisé des motivations', + ], + }, + }, + { + id: 'year-review', + icon: '📅', + label: 'Year Review', + labelShort: 'Year Review', + cardLabel: 'Year Review', + description: "Bilan de l'année", + path: '/year-review', + newPath: '/year-review/new', + accentColor: '#f59e0b', + hasParticipant: true, + participantLabel: 'Participant', + home: { + tagline: "Faites le bilan de l'année", + description: + "Réalisez un bilan complet de l'année écoulée. Identifiez réalisations, défis, apprentissages et définissez vos objectifs pour l'année à venir.", + features: [ + "5 catégories : Réalisations, Défis, Apprentissages, Objectifs, Moments", + 'Organisation par drag & drop', + "Vue d'ensemble de l'année", + ], + }, + }, + { + id: 'weekly-checkin', + icon: '📝', + label: 'Weekly Check-in', + labelShort: 'Check-in', + cardLabel: 'Weekly Check-in', + description: 'Suivi hebdomadaire', + path: '/weekly-checkin', + newPath: '/weekly-checkin/new', + accentColor: '#10b981', + hasParticipant: true, + participantLabel: 'Participant', + home: { + tagline: 'Le point hebdomadaire avec vos collaborateurs', + description: + "Chaque semaine, faites le point avec vos collaborateurs sur ce qui s'est bien passé, ce qui s'est mal passé, les enjeux du moment et les prochains enjeux.", + features: [ + "4 catégories : Bien passé, Mal passé, Enjeux du moment, Prochains enjeux", + "Ajout d'émotions à chaque item (fierté, joie, frustration, etc.)", + 'Suivi hebdomadaire régulier', + ], + }, + }, + { + id: 'weather', + icon: '🌤️', + label: "Météo d'équipe", + labelShort: 'Météo', + cardLabel: 'Météo', + description: 'Humeur et énergie', + path: '/weather', + newPath: '/weather/new', + accentColor: '#3b82f6', + hasParticipant: false, + participantLabel: '', + home: { + tagline: "Votre état en un coup d'œil", + description: + "Créez votre météo personnelle sur 4 axes clés (Performance, Moral, Flux, Création de valeur) et partagez-la avec votre équipe pour une meilleure visibilité de votre état.", + features: [ + '4 axes : Performance, Moral, Flux, Création de valeur', + 'Emojis météo pour exprimer votre état visuellement', + 'Notes globales pour détailler votre ressenti', + ], + }, + }, +]; + +export const WORKSHOP_BY_ID = Object.fromEntries(WORKSHOPS.map((w) => [w.id, w])) as Record< + WorkshopTypeId, + WorkshopConfig +>; + +export function getWorkshop(id: WorkshopTypeId): WorkshopConfig { + return WORKSHOP_BY_ID[id]; +} + +export function getSessionPath(id: WorkshopTypeId, sessionId: string): string { + return `${getWorkshop(id).path}/${sessionId}`; +} + +export function getSessionsTabUrl(id: WorkshopTypeId): string { + return `/sessions?tab=${id}`; +} + +/** Add workshopType to session objects for unified display. Preserves literal type for type safety. */ +export function withWorkshopType( + sessions: T[], + type: K +): (T & { workshopType: K })[] { + return sessions.map((s) => ({ ...s, workshopType: type })) as (T & { workshopType: K })[]; +}