refactor: add SessionPageHeader and apply to all 6 session detail pages

- Create SessionPageHeader component (breadcrumb + editable title + collaborator + badges + date)
- Embed UPDATE_FN map internally, keyed by workshopType — no prop drilling
- Replace duplicated header blocks in sessions, motivators, year-review, weather, weekly-checkin, gif-mood

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 14:15:43 +01:00
parent b1ba43fd30
commit 09a849279b
9 changed files with 191 additions and 281 deletions

View File

@@ -1,11 +1,9 @@
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { auth } from '@/lib/auth';
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
import { getGifMoodSessionById } from '@/services/gif-mood';
import { getUserTeams } from '@/services/teams';
import { GifMoodBoard, GifMoodLiveWrapper } from '@/components/gif-mood';
import { Badge, EditableGifMoodTitle } from '@/components/ui';
import { Badge, SessionPageHeader } from '@/components/ui';
interface GifMoodSessionPageProps {
params: Promise<{ id: string }>;
@@ -29,41 +27,16 @@ export default async function GifMoodSessionPage({ params }: GifMoodSessionPageP
return (
<main className="mx-auto max-w-7xl px-4">
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-2 text-sm text-muted mb-2">
<Link href={getSessionsTabUrl('gif-mood')} className="hover:text-foreground">
{getWorkshop('gif-mood').labelShort}
</Link>
<span>/</span>
<span className="text-foreground">{session.title}</span>
{!session.isOwner && (
<Badge variant="accent" className="ml-2">
Partagé par {session.user.name || session.user.email}
</Badge>
)}
</div>
<div className="flex items-start justify-between">
<div>
<EditableGifMoodTitle
sessionId={session.id}
initialTitle={session.title}
canEdit={session.canEdit}
/>
</div>
<div className="flex items-center gap-3">
<Badge variant="primary">{session.items.length} GIFs</Badge>
<span className="text-sm text-muted">
{new Date(session.date).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</span>
</div>
</div>
</div>
<SessionPageHeader
workshopType="gif-mood"
sessionId={session.id}
sessionTitle={session.title}
isOwner={session.isOwner}
canEdit={session.canEdit}
ownerUser={session.user}
date={session.date}
badges={<Badge variant="primary">{session.items.length} GIFs</Badge>}
/>
{/* Live Wrapper + Board */}
<GifMoodLiveWrapper

View File

@@ -1,13 +1,10 @@
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 { getUserTeams } from '@/services/teams';
import type { ResolvedCollaborator } from '@/services/auth';
import { MotivatorBoard, MotivatorLiveWrapper } from '@/components/moving-motivators';
import { Badge, CollaboratorDisplay } from '@/components/ui';
import { EditableMotivatorTitle } from '@/components/ui';
import { Badge, SessionPageHeader } from '@/components/ui';
interface MotivatorSessionPageProps {
params: Promise<{ id: string }>;
@@ -32,50 +29,21 @@ export default async function MotivatorSessionPage({ params }: MotivatorSessionP
return (
<main className="mx-auto max-w-7xl px-4">
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-2 text-sm text-muted mb-2">
<Link href={getSessionsTabUrl('motivators')} className="hover:text-foreground">
{getWorkshop('motivators').label}
</Link>
<span>/</span>
<span className="text-foreground">{session.title}</span>
{!session.isOwner && (
<Badge variant="accent" className="ml-2">
Partagé par {session.user.name || session.user.email}
</Badge>
)}
</div>
<div className="flex items-start justify-between">
<div>
<EditableMotivatorTitle
sessionId={session.id}
initialTitle={session.title}
canEdit={session.canEdit}
/>
<div className="mt-2">
<CollaboratorDisplay
collaborator={session.resolvedParticipant as ResolvedCollaborator}
size="lg"
showEmail
/>
</div>
</div>
<div className="flex items-center gap-3">
<Badge variant="primary">
{session.cards.filter((c) => c.influence !== 0).length} / 10 évalués
</Badge>
<span className="text-sm text-muted">
{new Date(session.date).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</span>
</div>
</div>
</div>
<SessionPageHeader
workshopType="motivators"
sessionId={session.id}
sessionTitle={session.title}
isOwner={session.isOwner}
canEdit={session.canEdit}
ownerUser={session.user}
date={session.date}
collaborator={session.resolvedParticipant as ResolvedCollaborator}
badges={
<Badge variant="primary">
{session.cards.filter((c) => c.influence !== 0).length} / 10 évalués
</Badge>
}
/>
{/* Live Wrapper + Board */}
<MotivatorLiveWrapper

View File

@@ -1,13 +1,10 @@
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { auth } from '@/lib/auth';
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
import { getSessionById } from '@/services/sessions';
import { getUserTeams } from '@/services/teams';
import { SwotBoard } from '@/components/swot/SwotBoard';
import { SessionLiveWrapper } from '@/components/collaboration';
import { EditableSessionTitle } from '@/components/ui';
import { Badge, CollaboratorDisplay } from '@/components/ui';
import { Badge, SessionPageHeader } from '@/components/ui';
interface SessionPageProps {
params: Promise<{ id: string }>;
@@ -32,49 +29,20 @@ export default async function SessionPage({ params }: SessionPageProps) {
return (
<main className="mx-auto max-w-7xl px-4">
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-2 text-sm text-muted mb-2">
<Link href={getSessionsTabUrl('swot')} className="hover:text-foreground">
{getWorkshop('swot').labelShort}
</Link>
<span>/</span>
<span className="text-foreground">{session.title}</span>
{!session.isOwner && (
<Badge variant="accent" className="ml-2">
Partagé par {session.user.name || session.user.email}
</Badge>
)}
</div>
<div className="flex items-start justify-between">
<div>
<EditableSessionTitle
sessionId={session.id}
initialTitle={session.title}
canEdit={session.canEdit}
/>
<div className="mt-2">
<CollaboratorDisplay
collaborator={session.resolvedCollaborator}
size="lg"
showEmail
/>
</div>
</div>
<div className="flex items-center gap-3">
<Badge variant="primary">{session.items.length} items</Badge>
<Badge variant="success">{session.actions.length} actions</Badge>
<span className="text-sm text-muted">
{new Date(session.date).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</span>
</div>
</div>
</div>
<SessionPageHeader
workshopType="swot"
sessionId={session.id}
sessionTitle={session.title}
isOwner={session.isOwner}
canEdit={session.canEdit}
ownerUser={session.user}
date={session.date}
collaborator={session.resolvedCollaborator}
badges={<>
<Badge variant="primary">{session.items.length} items</Badge>
<Badge variant="success">{session.actions.length} actions</Badge>
</>}
/>
{/* Live Session Wrapper */}
<SessionLiveWrapper

View File

@@ -6,8 +6,7 @@ import { getTeamOKRs } from '@/services/okrs';
import { TeamDetailClient } from '@/components/teams/TeamDetailClient';
import { DeleteTeamButton } from '@/components/teams/DeleteTeamButton';
import { OKRsList } from '@/components/okrs';
import { Button } from '@/components/ui';
import { Card } from '@/components/ui';
import { Button, Card, PageHeader } from '@/components/ui';
import { notFound } from 'next/navigation';
import type { TeamMember } from '@/lib/types';
@@ -40,22 +39,17 @@ export default async function TeamDetailPage({ params }: TeamDetailPageProps) {
return (
<main className="mx-auto max-w-7xl px-4">
{/* Header */}
<div className="mb-8">
<div className="mb-4 flex items-center gap-2">
<Link href="/teams" className="text-muted hover:text-foreground">
Retour aux équipes
</Link>
</div>
<div className="flex items-start justify-between">
<div>
<h1 className="text-3xl font-bold text-foreground flex items-center gap-2">
<span className="text-3xl">👥</span>
{team.name}
</h1>
{team.description && <p className="mt-2 text-muted">{team.description}</p>}
</div>
{isAdmin && (
<div className="mb-2">
<Link href="/teams" className="text-sm text-muted hover:text-foreground">
Retour aux équipes
</Link>
</div>
<PageHeader
emoji="👥"
title={team.name}
subtitle={team.description ?? undefined}
actions={
isAdmin ? (
<div className="flex items-center gap-3">
<Link href={`/teams/${id}/okrs/new`}>
<Button className="bg-[var(--purple)] text-white hover:opacity-90 border-transparent">
@@ -64,9 +58,9 @@ export default async function TeamDetailPage({ params }: TeamDetailPageProps) {
</Link>
<DeleteTeamButton teamId={id} teamName={team.name} />
</div>
)}
</div>
</div>
) : undefined
}
/>
{/* Members Section */}
<Card className="mb-8 p-6">

View File

@@ -1,7 +1,5 @@
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { auth } from '@/lib/auth';
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
import {
getWeatherSessionById,
getPreviousWeatherEntriesForUsers,
@@ -15,7 +13,7 @@ import {
WeatherAverageBar,
WeatherTrendChart,
} from '@/components/weather';
import { Badge, EditableWeatherTitle } from '@/components/ui';
import { Badge, SessionPageHeader } from '@/components/ui';
interface WeatherSessionPageProps {
params: Promise<{ id: string }>;
@@ -45,41 +43,16 @@ export default async function WeatherSessionPage({ params }: WeatherSessionPageP
return (
<main className="mx-auto max-w-7xl px-4">
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-2 text-sm text-muted mb-2">
<Link href={getSessionsTabUrl('weather')} className="hover:text-foreground">
{getWorkshop('weather').labelShort}
</Link>
<span>/</span>
<span className="text-foreground">{session.title}</span>
{!session.isOwner && (
<Badge variant="accent" className="ml-2">
Partagé par {session.user.name || session.user.email}
</Badge>
)}
</div>
<div className="flex items-start justify-between">
<div>
<EditableWeatherTitle
sessionId={session.id}
initialTitle={session.title}
canEdit={session.canEdit}
/>
</div>
<div className="flex items-center gap-3">
<Badge variant="primary">{session.entries.length} membres</Badge>
<span className="text-sm text-muted">
{new Date(session.date).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</span>
</div>
</div>
</div>
<SessionPageHeader
workshopType="weather"
sessionId={session.id}
sessionTitle={session.title}
isOwner={session.isOwner}
canEdit={session.canEdit}
ownerUser={session.user}
date={session.date}
badges={<Badge variant="primary">{session.entries.length} membres</Badge>}
/>
{/* Info sur les catégories */}
<WeatherInfoPanel />

View File

@@ -1,7 +1,5 @@
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 { getUserTeams } from '@/services/teams';
import type { ResolvedCollaborator } from '@/services/auth';
@@ -9,8 +7,7 @@ import { getUserOKRsForPeriod } from '@/services/okrs';
import { getCurrentQuarterPeriod } from '@/lib/okr-utils';
import { WeeklyCheckInBoard, WeeklyCheckInLiveWrapper } from '@/components/weekly-checkin';
import { CurrentQuarterOKRs } from '@/components/weekly-checkin/CurrentQuarterOKRs';
import { Badge, CollaboratorDisplay } from '@/components/ui';
import { EditableWeeklyCheckInTitle } from '@/components/ui';
import { Badge, SessionPageHeader } from '@/components/ui';
interface WeeklyCheckInSessionPageProps {
params: Promise<{ id: string }>;
@@ -48,44 +45,17 @@ export default async function WeeklyCheckInSessionPage({ params }: WeeklyCheckIn
return (
<main className="mx-auto max-w-7xl px-4">
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-2 text-sm text-muted mb-2">
<Link href={getSessionsTabUrl('weekly-checkin')} className="hover:text-foreground">
{getWorkshop('weekly-checkin').label}
</Link>
<span>/</span>
<span className="text-foreground">{session.title}</span>
{!session.isOwner && (
<Badge variant="accent" className="ml-2">
Partagé par {session.user.name || session.user.email}
</Badge>
)}
</div>
<div className="flex items-start justify-between">
<div>
<EditableWeeklyCheckInTitle
sessionId={session.id}
initialTitle={session.title}
canEdit={session.canEdit}
/>
<div className="mt-2">
<CollaboratorDisplay collaborator={resolvedParticipant} size="lg" showEmail />
</div>
</div>
<div className="flex items-center gap-3">
<Badge variant="primary">{session.items.length} items</Badge>
<span className="text-sm text-muted">
{new Date(session.date).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</span>
</div>
</div>
</div>
<SessionPageHeader
workshopType="weekly-checkin"
sessionId={session.id}
sessionTitle={session.title}
isOwner={session.isOwner}
canEdit={session.canEdit}
ownerUser={session.user}
date={session.date}
collaborator={resolvedParticipant}
badges={<Badge variant="primary">{session.items.length} items</Badge>}
/>
{/* Current Quarter OKRs - editable by participant or team admin */}
{currentQuarterOKRs.length > 0 && (

View File

@@ -1,13 +1,10 @@
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 { getUserTeams } from '@/services/teams';
import type { ResolvedCollaborator } from '@/services/auth';
import { YearReviewBoard, YearReviewLiveWrapper } from '@/components/year-review';
import { Badge, CollaboratorDisplay } from '@/components/ui';
import { EditableYearReviewTitle } from '@/components/ui';
import { Badge, SessionPageHeader } from '@/components/ui';
interface YearReviewSessionPageProps {
params: Promise<{ id: string }>;
@@ -32,49 +29,20 @@ export default async function YearReviewSessionPage({ params }: YearReviewSessio
return (
<main className="mx-auto max-w-7xl px-4">
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-2 text-sm text-muted mb-2">
<Link href={getSessionsTabUrl('year-review')} className="hover:text-foreground">
{getWorkshop('year-review').label}
</Link>
<span>/</span>
<span className="text-foreground">{session.title}</span>
{!session.isOwner && (
<Badge variant="accent" className="ml-2">
Partagé par {session.user.name || session.user.email}
</Badge>
)}
</div>
<div className="flex items-start justify-between">
<div>
<EditableYearReviewTitle
sessionId={session.id}
initialTitle={session.title}
canEdit={session.canEdit}
/>
<div className="mt-2">
<CollaboratorDisplay
collaborator={session.resolvedParticipant as ResolvedCollaborator}
size="lg"
showEmail
/>
</div>
</div>
<div className="flex items-center gap-3">
<Badge variant="primary">{session.items.length} items</Badge>
<Badge variant="default">Année {session.year}</Badge>
<span className="text-sm text-muted">
{new Date(session.updatedAt).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</span>
</div>
</div>
</div>
<SessionPageHeader
workshopType="year-review"
sessionId={session.id}
sessionTitle={session.title}
isOwner={session.isOwner}
canEdit={session.canEdit}
ownerUser={session.user}
date={session.updatedAt}
collaborator={session.resolvedParticipant as ResolvedCollaborator}
badges={<>
<Badge variant="primary">{session.items.length} items</Badge>
<Badge variant="default">Année {session.year}</Badge>
</>}
/>
{/* Live Wrapper + Board */}
<YearReviewLiveWrapper

View File

@@ -0,0 +1,95 @@
'use client';
import Link from 'next/link';
import { ReactNode } from 'react';
import { getWorkshop, getSessionsTabUrl, WorkshopTypeId } from '@/lib/workshops';
import { Badge } from './Badge';
import { EditableTitle } from './EditableTitle';
import { CollaboratorDisplay } from './CollaboratorDisplay';
import type { ResolvedCollaborator } from '@/services/auth';
import { updateSessionTitle } from '@/actions/session';
import { updateMotivatorSession } from '@/actions/moving-motivators';
import { updateYearReviewSession } from '@/actions/year-review';
import { updateWeatherSession } from '@/actions/weather';
import { updateWeeklyCheckInSession } from '@/actions/weekly-checkin';
import { updateGifMoodSession } from '@/actions/gif-mood';
type UpdateFn = (id: string, title: string) => Promise<{ success: boolean; error?: string }>;
const UPDATE_FN: Record<WorkshopTypeId, UpdateFn> = {
'swot': updateSessionTitle,
'motivators': (id, title) => updateMotivatorSession(id, { title }),
'year-review': (id, title) => updateYearReviewSession(id, { title }),
'weekly-checkin': (id, title) => updateWeeklyCheckInSession(id, { title }),
'weather': (id, title) => updateWeatherSession(id, { title }),
'gif-mood': (id, title) => updateGifMoodSession(id, { title }),
};
interface SessionPageHeaderProps {
workshopType: WorkshopTypeId;
sessionId: string;
sessionTitle: string;
isOwner: boolean;
canEdit: boolean;
ownerUser: { name: string | null; email: string };
date: Date | string;
collaborator?: ResolvedCollaborator | null;
badges?: ReactNode;
}
export function SessionPageHeader({
workshopType,
sessionId,
sessionTitle,
isOwner,
canEdit,
ownerUser,
date,
collaborator,
badges,
}: SessionPageHeaderProps) {
const workshop = getWorkshop(workshopType);
return (
<div className="mb-8">
<div className="flex items-center gap-2 text-sm text-muted mb-2">
<Link href={getSessionsTabUrl(workshopType)} className="hover:text-foreground">
{workshop.labelShort}
</Link>
<span>/</span>
<span className="text-foreground">{sessionTitle}</span>
{!isOwner && (
<Badge variant="accent" className="ml-2">
Partagé par {ownerUser.name || ownerUser.email}
</Badge>
)}
</div>
<div className="flex items-start justify-between">
<div>
<EditableTitle
sessionId={sessionId}
initialTitle={sessionTitle}
canEdit={canEdit}
onUpdate={UPDATE_FN[workshopType]}
/>
{collaborator && (
<div className="mt-2">
<CollaboratorDisplay collaborator={collaborator} size="lg" showEmail />
</div>
)}
</div>
<div className="flex items-center gap-3">
{badges}
<span className="text-sm text-muted">
{new Date(date).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</span>
</div>
</div>
</div>
);
}

View File

@@ -13,6 +13,7 @@ export {
EditableGifMoodTitle,
} from './EditableTitles';
export { PageHeader } from './PageHeader';
export { SessionPageHeader } from './SessionPageHeader';
export { Input } from './Input';
export { ParticipantInput } from './ParticipantInput';
export { Modal, ModalFooter } from './Modal';