feat: add PageHeader component and centralize page spacing
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m1s

- Create reusable PageHeader component (emoji + title + subtitle + actions)
- Use PageHeader in sessions, teams, users, objectives pages
- Centralize vertical padding in layout (py-6) and remove per-page py-* values

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 14:01:07 +01:00
parent 66ac190c15
commit 2e00522bfc
26 changed files with 97 additions and 84 deletions

View File

@@ -29,7 +29,7 @@ export default async function GifMoodSessionPage({ params }: GifMoodSessionPageP
const userTeams = await getUserTeams(authSession.user.id);
return (
<main className="mx-auto max-w-7xl px-4 py-8">
<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">

View File

@@ -20,7 +20,8 @@ export default function NewGifMoodPage() {
const [error, setError] = useState<string | null>(null);
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
const [title, setTitle] = useState(
() => `GIF Mood - ${new Date().toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })}`
() =>
`GIF Mood - ${new Date().toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })}`
);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
@@ -48,11 +49,11 @@ export default function NewGifMoodPage() {
}
return (
<main className="mx-auto max-w-2xl px-4 py-8">
<main className="mx-auto max-w-2xl px-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<span>🎭</span>
<span>🎞</span>
Nouveau GIF Mood Board
</CardTitle>
<CardDescription>

View File

@@ -46,7 +46,9 @@ export default function RootLayout({
<Providers>
<div className="min-h-screen bg-background">
<Header />
{children}
<div className="py-6">
{children}
</div>
</div>
</Providers>
</body>

View File

@@ -31,7 +31,7 @@ export default async function MotivatorSessionPage({ params }: MotivatorSessionP
}
return (
<main className="mx-auto max-w-7xl px-4 py-8">
<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">

View File

@@ -46,7 +46,7 @@ export default function NewMotivatorSessionPage() {
}
return (
<main className="mx-auto max-w-2xl px-4 py-8">
<main className="mx-auto max-w-2xl px-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">

View File

@@ -2,7 +2,7 @@ import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
import Link from 'next/link';
import { getUserOKRs } from '@/services/okrs';
import { Card } from '@/components/ui';
import { Card, PageHeader } from '@/components/ui';
import { ObjectivesList } from '@/components/okrs/ObjectivesList';
import { comparePeriods } from '@/lib/okr-utils';
@@ -31,17 +31,12 @@ export default async function ObjectivesPage() {
const periods = Object.keys(okrsByPeriod).sort(comparePeriods);
return (
<main className="mx-auto max-w-7xl px-4 py-8">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-foreground flex items-center gap-2">
<span className="text-3xl">🎯</span>
Mes Objectifs
</h1>
<p className="mt-2 text-muted">
Suivez la progression de vos OKRs à travers toutes vos équipes
</p>
</div>
<main className="mx-auto max-w-7xl px-4">
<PageHeader
emoji="🎯"
title="Mes Objectifs"
subtitle="Suivez la progression de vos OKRs à travers toutes vos équipes"
/>
{okrs.length === 0 ? (
<Card className="p-12 text-center">

View File

@@ -4,7 +4,7 @@ import { WORKSHOPS, getSessionsTabUrl } from '@/lib/workshops';
export default function Home() {
return (
<>
<main className="mx-auto max-w-7xl px-4 py-12">
<main className="mx-auto max-w-7xl px-4">
{/* Hero Section */}
<section className="mb-16 text-center">
<h1 className="mb-4 text-5xl font-bold text-foreground">

View File

@@ -19,18 +19,19 @@ export default async function ProfilePage() {
}
return (
<main className="mx-auto max-w-2xl px-4 py-8">
<div className="mb-8 flex items-center gap-6">
<main className="mx-auto max-w-2xl px-4">
<div className="mb-8 flex items-center gap-5">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={getGravatarUrl(user.email, 160)}
alt={user.name || user.email}
width={80}
height={80}
className="rounded-full border-2 border-border"
width={72}
height={72}
className="rounded-full border-2 border-border shrink-0"
/>
<div>
<h1 className="text-3xl font-bold text-foreground">Mon Profil</h1>
<p className="mt-1 text-muted">Gérez vos informations personnelles</p>
<h1 className="text-3xl font-bold tracking-tight text-foreground">Mon Profil</h1>
<p className="mt-1.5 text-sm text-muted">Gérez vos informations personnelles</p>
</div>
</div>

View File

@@ -31,7 +31,7 @@ export default async function SessionPage({ params }: SessionPageProps) {
}
return (
<main className="mx-auto max-w-7xl px-4 py-8">
<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">

View File

@@ -56,7 +56,7 @@ export default function NewSessionPage() {
}
return (
<main className="mx-auto max-w-2xl px-4 py-8">
<main className="mx-auto max-w-2xl px-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">

View File

@@ -24,7 +24,7 @@ import {
getGifMoodSessionsByUserId,
getTeamCollaboratorSessionsForAdmin as getTeamGifMoodSessions,
} from '@/services/gif-mood';
import { Card } from '@/components/ui';
import { Card, PageHeader } from '@/components/ui';
import { withWorkshopType } from '@/lib/workshops';
import { WorkshopTabs } from './WorkshopTabs';
import { NewWorkshopDropdown } from './NewWorkshopDropdown';
@@ -113,22 +113,17 @@ export default async function SessionsPage() {
const totalCount = allSessions.length;
return (
<main className="mx-auto max-w-7xl px-4 py-8 sm:py-12">
{/* Header */}
<div className="mb-10 flex flex-col sm:flex-row sm:items-end justify-between gap-6">
<div>
<div className="flex items-center gap-3 mb-1.5">
<h1 className="text-3xl font-bold tracking-tight text-foreground">Mes Ateliers</h1>
{totalCount > 0 && (
<span className="inline-flex items-center justify-center px-2.5 h-6 rounded-full bg-primary/10 text-primary text-sm font-semibold">
{totalCount}
</span>
)}
</div>
<p className="text-sm text-muted">Tous vos ateliers en un seul endroit</p>
</div>
<NewWorkshopDropdown />
</div>
<main className="mx-auto max-w-7xl px-4">
<PageHeader
emoji="🗂️"
title="Mes Ateliers"
subtitle={
totalCount > 0
? `${totalCount} atelier${totalCount > 1 ? 's' : ''} · Tous vos ateliers en un seul endroit`
: 'Tous vos ateliers en un seul endroit'
}
actions={<NewWorkshopDropdown />}
/>
{/* Content */}
{hasNoSessions ? (

View File

@@ -121,7 +121,7 @@ export default function EditOKRPage() {
if (loading) {
return (
<main className="mx-auto max-w-4xl px-4 py-8">
<main className="mx-auto max-w-4xl px-4">
<div className="text-center">Chargement...</div>
</main>
);
@@ -129,7 +129,7 @@ export default function EditOKRPage() {
if (!okr) {
return (
<main className="mx-auto max-w-4xl px-4 py-8">
<main className="mx-auto max-w-4xl px-4">
<div className="text-center">OKR non trouvé</div>
</main>
);
@@ -147,7 +147,7 @@ export default function EditOKRPage() {
};
return (
<main className="mx-auto max-w-4xl px-4 py-8">
<main className="mx-auto max-w-4xl px-4">
<div className="mb-6">
<Link href={`/teams/${teamId}/okrs/${okrId}`} className="text-muted hover:text-foreground">
Retour à l&apos;OKR

View File

@@ -124,7 +124,7 @@ export default function OKRDetailPage() {
if (loading) {
return (
<main className="mx-auto max-w-4xl px-4 py-8">
<main className="mx-auto max-w-4xl px-4">
<div className="text-center">Chargement...</div>
</main>
);
@@ -132,7 +132,7 @@ export default function OKRDetailPage() {
if (!okr) {
return (
<main className="mx-auto max-w-4xl px-4 py-8">
<main className="mx-auto max-w-4xl px-4">
<div className="text-center">OKR non trouvé</div>
</main>
);
@@ -145,7 +145,7 @@ export default function OKRDetailPage() {
const canDelete = okr.permissions?.canDelete ?? false;
return (
<main className="mx-auto max-w-4xl px-4 py-8">
<main className="mx-auto max-w-4xl px-4">
<div className="mb-6">
<Link href={`/teams/${teamId}`} className="text-muted hover:text-foreground">
Retour à l&apos;équipe

View File

@@ -54,14 +54,14 @@ export default function NewOKRPage() {
if (loading) {
return (
<main className="mx-auto max-w-4xl px-4 py-8">
<main className="mx-auto max-w-4xl px-4">
<div className="text-center">Chargement...</div>
</main>
);
}
return (
<main className="mx-auto max-w-4xl px-4 py-8">
<main className="mx-auto max-w-4xl px-4">
<div className="mb-6">
<Link href={`/teams/${teamId}`} className="text-muted hover:text-foreground">
Retour à l&apos;équipe

View File

@@ -39,7 +39,7 @@ export default async function TeamDetailPage({ params }: TeamDetailPageProps) {
const okrsData = await getTeamOKRs(id);
return (
<main className="mx-auto max-w-7xl px-4 py-8">
<main className="mx-auto max-w-7xl px-4">
{/* Header */}
<div className="mb-8">
<div className="mb-4 flex items-center gap-2">

View File

@@ -48,7 +48,7 @@ export default function NewTeamPage() {
};
return (
<main className="mx-auto max-w-2xl px-4 py-8">
<main className="mx-auto max-w-2xl px-4">
<div className="mb-6">
<Link href="/teams" className="text-muted hover:text-foreground">
Retour aux équipes

View File

@@ -2,7 +2,7 @@ import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
import Link from 'next/link';
import { TeamCard } from '@/components/teams';
import { Button } from '@/components/ui';
import { Button, PageHeader } from '@/components/ui';
import { getUserTeams } from '@/services/teams';
export default async function TeamsPage() {
@@ -15,23 +15,19 @@ export default async function TeamsPage() {
const teams = await getUserTeams(session.user.id);
return (
<main className="mx-auto max-w-7xl px-4 py-8">
{/* Header */}
<div className="mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-foreground">Équipes</h1>
<p className="mt-1 text-muted">
{teams.length} équipe{teams.length !== 1 ? 's' : ''}
</p>
</div>
<div>
<main className="mx-auto max-w-7xl px-4">
<PageHeader
emoji="👥"
title="Équipes"
subtitle={`${teams.length} équipe${teams.length !== 1 ? 's' : ''} · Collaborez et définissez vos OKRs`}
actions={
<Link href="/teams/new">
<Button className="bg-[var(--purple)] text-white hover:opacity-90 border-transparent">
Créer une équipe
</Button>
</Link>
</div>
</div>
}
/>
{/* Teams Grid */}
{teams.length > 0 ? (

View File

@@ -2,6 +2,7 @@ import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
import { getAllUsersWithStats } from '@/services/auth';
import { getGravatarUrl } from '@/lib/gravatar';
import { PageHeader } from '@/components/ui';
function formatRelativeTime(date: Date): string {
const now = new Date();
@@ -33,15 +34,12 @@ export default async function UsersPage() {
const avgSessionsPerUser = users.length > 0 ? totalSessions / users.length : 0;
return (
<main className="mx-auto max-w-6xl px-4 py-8">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-foreground">Utilisateurs</h1>
<p className="mt-1 text-muted">
{users.length} utilisateur{users.length > 1 ? 's' : ''} inscrit
{users.length > 1 ? 's' : ''}
</p>
</div>
<main className="mx-auto max-w-6xl px-4">
<PageHeader
emoji="🧑‍💻"
title="Utilisateurs"
subtitle={`${users.length} utilisateur${users.length > 1 ? 's' : ''} inscrit${users.length > 1 ? 's' : ''} · Vue d'ensemble de la communauté`}
/>
{/* Global Stats */}
<div className="mb-8 grid grid-cols-2 gap-4 sm:grid-cols-4">

View File

@@ -45,7 +45,7 @@ export default async function WeatherSessionPage({ params }: WeatherSessionPageP
]);
return (
<main className="mx-auto max-w-7xl px-4 py-8">
<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">

View File

@@ -63,7 +63,7 @@ export default function NewWeatherPage() {
}
return (
<main className="mx-auto max-w-2xl px-4 py-8">
<main className="mx-auto max-w-2xl px-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">

View File

@@ -47,7 +47,7 @@ export default async function WeeklyCheckInSessionPage({ params }: WeeklyCheckIn
}
return (
<main className="mx-auto max-w-7xl px-4 py-8">
<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">

View File

@@ -66,7 +66,7 @@ export default function NewWeeklyCheckInPage() {
}
return (
<main className="mx-auto max-w-2xl px-4 py-8">
<main className="mx-auto max-w-2xl px-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">

View File

@@ -31,7 +31,7 @@ export default async function YearReviewSessionPage({ params }: YearReviewSessio
}
return (
<main className="mx-auto max-w-7xl px-4 py-8">
<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">

View File

@@ -49,7 +49,7 @@ export default function NewYearReviewPage() {
}
return (
<main className="mx-auto max-w-2xl px-4 py-8">
<main className="mx-auto max-w-2xl px-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">

View File

@@ -0,0 +1,24 @@
import { ReactNode } from 'react';
interface PageHeaderProps {
emoji: string;
title: string;
subtitle?: ReactNode;
actions?: ReactNode;
className?: string;
}
export function PageHeader({ emoji, title, subtitle, actions, className }: PageHeaderProps) {
return (
<div className={`mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-4 ${className ?? ''}`}>
<div>
<h1 className="flex items-center gap-3 text-3xl font-bold tracking-tight text-foreground">
<span className="text-3xl leading-none">{emoji}</span>
{title}
</h1>
{subtitle && <p className="mt-1.5 text-sm text-muted">{subtitle}</p>}
</div>
{actions && <div className="shrink-0">{actions}</div>}
</div>
);
}

View File

@@ -9,6 +9,7 @@ export { EditableMotivatorTitle } from './EditableMotivatorTitle';
export { EditableYearReviewTitle } from './EditableYearReviewTitle';
export { EditableWeeklyCheckInTitle } from './EditableWeeklyCheckInTitle';
export { EditableWeatherTitle } from './EditableWeatherTitle';
export { PageHeader } from './PageHeader';
export { Input } from './Input';
export { ParticipantInput } from './ParticipantInput';
export { Modal, ModalFooter } from './Modal';