Compare commits

...

3 Commits

Author SHA1 Message Date
9a43980412 refactor: extract Icons and InlineFormActions UI components
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m28s
- Add Icons.tsx: IconEdit, IconTrash, IconDuplicate, IconPlus, IconCheck, IconClose
- Add InlineFormActions.tsx: unified Annuler/Ajouter-Enregistrer button pair
- Replace inline SVGs in SwotCard, YearReviewCard, WeeklyCheckInCard, SwotQuadrant,
  YearReviewSection, WeeklyCheckInSection, EditableTitle, Modal, GifMoodCard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 14:25:35 +01:00
09a849279b 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>
2026-03-03 14:15:43 +01:00
b1ba43fd30 refactor: merge 6 EditableTitle wrappers into one file
Replace EditableSessionTitle, EditableMotivatorTitle, EditableYearReviewTitle,
EditableWeatherTitle, EditableWeeklyCheckInTitle, EditableGifMoodTitle individual
files with a single EditableTitles.tsx using spread props. Same public API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 14:06:45 +01:00
27 changed files with 388 additions and 701 deletions

View File

@@ -1,12 +1,9 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import Link from 'next/link';
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
import { getGifMoodSessionById } from '@/services/gif-mood'; import { getGifMoodSessionById } from '@/services/gif-mood';
import { getUserTeams } from '@/services/teams'; import { getUserTeams } from '@/services/teams';
import { GifMoodBoard, GifMoodLiveWrapper } from '@/components/gif-mood'; import { GifMoodBoard, GifMoodLiveWrapper } from '@/components/gif-mood';
import { Badge } from '@/components/ui'; import { Badge, SessionPageHeader } from '@/components/ui';
import { EditableGifMoodTitle } from '@/components/ui/EditableGifMoodTitle';
interface GifMoodSessionPageProps { interface GifMoodSessionPageProps {
params: Promise<{ id: string }>; params: Promise<{ id: string }>;
@@ -30,41 +27,16 @@ export default async function GifMoodSessionPage({ params }: GifMoodSessionPageP
return ( return (
<main className="mx-auto max-w-7xl px-4"> <main className="mx-auto max-w-7xl px-4">
{/* Header */} <SessionPageHeader
<div className="mb-8"> workshopType="gif-mood"
<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} sessionId={session.id}
initialTitle={session.title} sessionTitle={session.title}
isOwner={session.isOwner}
canEdit={session.canEdit} canEdit={session.canEdit}
ownerUser={session.user}
date={session.date}
badges={<Badge variant="primary">{session.items.length} GIFs</Badge>}
/> />
</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>
{/* Live Wrapper + Board */} {/* Live Wrapper + Board */}
<GifMoodLiveWrapper <GifMoodLiveWrapper

View File

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

View File

@@ -1,13 +1,10 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import Link from 'next/link';
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
import { getSessionById } from '@/services/sessions'; import { getSessionById } from '@/services/sessions';
import { getUserTeams } from '@/services/teams'; import { getUserTeams } from '@/services/teams';
import { SwotBoard } from '@/components/swot/SwotBoard'; import { SwotBoard } from '@/components/swot/SwotBoard';
import { SessionLiveWrapper } from '@/components/collaboration'; import { SessionLiveWrapper } from '@/components/collaboration';
import { EditableSessionTitle } from '@/components/ui'; import { Badge, SessionPageHeader } from '@/components/ui';
import { Badge, CollaboratorDisplay } from '@/components/ui';
interface SessionPageProps { interface SessionPageProps {
params: Promise<{ id: string }>; params: Promise<{ id: string }>;
@@ -32,49 +29,20 @@ export default async function SessionPage({ params }: SessionPageProps) {
return ( return (
<main className="mx-auto max-w-7xl px-4"> <main className="mx-auto max-w-7xl px-4">
{/* Header */} <SessionPageHeader
<div className="mb-8"> workshopType="swot"
<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} sessionId={session.id}
initialTitle={session.title} sessionTitle={session.title}
isOwner={session.isOwner}
canEdit={session.canEdit} canEdit={session.canEdit}
/> ownerUser={session.user}
<div className="mt-2"> date={session.date}
<CollaboratorDisplay
collaborator={session.resolvedCollaborator} collaborator={session.resolvedCollaborator}
size="lg" badges={<>
showEmail
/>
</div>
</div>
<div className="flex items-center gap-3">
<Badge variant="primary">{session.items.length} items</Badge> <Badge variant="primary">{session.items.length} items</Badge>
<Badge variant="success">{session.actions.length} actions</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>
{/* Live Session Wrapper */} {/* Live Session Wrapper */}
<SessionLiveWrapper <SessionLiveWrapper

View File

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

View File

@@ -1,7 +1,5 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import Link from 'next/link';
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
import { import {
getWeatherSessionById, getWeatherSessionById,
getPreviousWeatherEntriesForUsers, getPreviousWeatherEntriesForUsers,
@@ -15,8 +13,7 @@ import {
WeatherAverageBar, WeatherAverageBar,
WeatherTrendChart, WeatherTrendChart,
} from '@/components/weather'; } from '@/components/weather';
import { Badge } from '@/components/ui'; import { Badge, SessionPageHeader } from '@/components/ui';
import { EditableWeatherTitle } from '@/components/ui/EditableWeatherTitle';
interface WeatherSessionPageProps { interface WeatherSessionPageProps {
params: Promise<{ id: string }>; params: Promise<{ id: string }>;
@@ -46,41 +43,16 @@ export default async function WeatherSessionPage({ params }: WeatherSessionPageP
return ( return (
<main className="mx-auto max-w-7xl px-4"> <main className="mx-auto max-w-7xl px-4">
{/* Header */} <SessionPageHeader
<div className="mb-8"> workshopType="weather"
<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} sessionId={session.id}
initialTitle={session.title} sessionTitle={session.title}
isOwner={session.isOwner}
canEdit={session.canEdit} canEdit={session.canEdit}
ownerUser={session.user}
date={session.date}
badges={<Badge variant="primary">{session.entries.length} membres</Badge>}
/> />
</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>
{/* Info sur les catégories */} {/* Info sur les catégories */}
<WeatherInfoPanel /> <WeatherInfoPanel />

View File

@@ -1,7 +1,5 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import Link from 'next/link';
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
import { getWeeklyCheckInSessionById } from '@/services/weekly-checkin'; import { getWeeklyCheckInSessionById } from '@/services/weekly-checkin';
import { getUserTeams } from '@/services/teams'; import { getUserTeams } from '@/services/teams';
import type { ResolvedCollaborator } from '@/services/auth'; import type { ResolvedCollaborator } from '@/services/auth';
@@ -9,8 +7,7 @@ import { getUserOKRsForPeriod } from '@/services/okrs';
import { getCurrentQuarterPeriod } from '@/lib/okr-utils'; import { getCurrentQuarterPeriod } from '@/lib/okr-utils';
import { WeeklyCheckInBoard, WeeklyCheckInLiveWrapper } from '@/components/weekly-checkin'; import { WeeklyCheckInBoard, WeeklyCheckInLiveWrapper } from '@/components/weekly-checkin';
import { CurrentQuarterOKRs } from '@/components/weekly-checkin/CurrentQuarterOKRs'; import { CurrentQuarterOKRs } from '@/components/weekly-checkin/CurrentQuarterOKRs';
import { Badge, CollaboratorDisplay } from '@/components/ui'; import { Badge, SessionPageHeader } from '@/components/ui';
import { EditableWeeklyCheckInTitle } from '@/components/ui';
interface WeeklyCheckInSessionPageProps { interface WeeklyCheckInSessionPageProps {
params: Promise<{ id: string }>; params: Promise<{ id: string }>;
@@ -48,44 +45,17 @@ export default async function WeeklyCheckInSessionPage({ params }: WeeklyCheckIn
return ( return (
<main className="mx-auto max-w-7xl px-4"> <main className="mx-auto max-w-7xl px-4">
{/* Header */} <SessionPageHeader
<div className="mb-8"> workshopType="weekly-checkin"
<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} sessionId={session.id}
initialTitle={session.title} sessionTitle={session.title}
isOwner={session.isOwner}
canEdit={session.canEdit} canEdit={session.canEdit}
ownerUser={session.user}
date={session.date}
collaborator={resolvedParticipant}
badges={<Badge variant="primary">{session.items.length} items</Badge>}
/> />
<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>
{/* Current Quarter OKRs - editable by participant or team admin */} {/* Current Quarter OKRs - editable by participant or team admin */}
{currentQuarterOKRs.length > 0 && ( {currentQuarterOKRs.length > 0 && (

View File

@@ -1,13 +1,10 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import Link from 'next/link';
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
import { getYearReviewSessionById } from '@/services/year-review'; import { getYearReviewSessionById } from '@/services/year-review';
import { getUserTeams } from '@/services/teams'; import { getUserTeams } from '@/services/teams';
import type { ResolvedCollaborator } from '@/services/auth'; import type { ResolvedCollaborator } from '@/services/auth';
import { YearReviewBoard, YearReviewLiveWrapper } from '@/components/year-review'; import { YearReviewBoard, YearReviewLiveWrapper } from '@/components/year-review';
import { Badge, CollaboratorDisplay } from '@/components/ui'; import { Badge, SessionPageHeader } from '@/components/ui';
import { EditableYearReviewTitle } from '@/components/ui';
interface YearReviewSessionPageProps { interface YearReviewSessionPageProps {
params: Promise<{ id: string }>; params: Promise<{ id: string }>;
@@ -32,49 +29,20 @@ export default async function YearReviewSessionPage({ params }: YearReviewSessio
return ( return (
<main className="mx-auto max-w-7xl px-4"> <main className="mx-auto max-w-7xl px-4">
{/* Header */} <SessionPageHeader
<div className="mb-8"> workshopType="year-review"
<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} sessionId={session.id}
initialTitle={session.title} sessionTitle={session.title}
isOwner={session.isOwner}
canEdit={session.canEdit} canEdit={session.canEdit}
/> ownerUser={session.user}
<div className="mt-2"> date={session.updatedAt}
<CollaboratorDisplay
collaborator={session.resolvedParticipant as ResolvedCollaborator} collaborator={session.resolvedParticipant as ResolvedCollaborator}
size="lg" badges={<>
showEmail
/>
</div>
</div>
<div className="flex items-center gap-3">
<Badge variant="primary">{session.items.length} items</Badge> <Badge variant="primary">{session.items.length} items</Badge>
<Badge variant="default">Année {session.year}</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>
{/* Live Wrapper + Board */} {/* Live Wrapper + Board */}
<YearReviewLiveWrapper <YearReviewLiveWrapper

View File

@@ -2,6 +2,7 @@
import { memo, useState, useTransition } from 'react'; import { memo, useState, useTransition } from 'react';
import { updateGifMoodItem, deleteGifMoodItem } from '@/actions/gif-mood'; import { updateGifMoodItem, deleteGifMoodItem } from '@/actions/gif-mood';
import { IconClose } from '@/components/ui';
interface GifMoodCardProps { interface GifMoodCardProps {
sessionId: string; sessionId: string;
@@ -83,9 +84,7 @@ export const GifMoodCard = memo(function GifMoodCard({
className="absolute top-2 right-2 p-1.5 rounded-full bg-black/50 text-white opacity-0 group-hover:opacity-100 hover:bg-black/70 transition-all backdrop-blur-sm" className="absolute top-2 right-2 p-1.5 rounded-full bg-black/50 text-white opacity-0 group-hover:opacity-100 hover:bg-black/70 transition-all backdrop-blur-sm"
title="Supprimer ce GIF" title="Supprimer ce GIF"
> >
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <IconClose className="w-3 h-3" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
)} )}

View File

@@ -3,6 +3,7 @@
import { forwardRef, memo, useState, useTransition } from 'react'; import { forwardRef, memo, useState, useTransition } from 'react';
import type { SwotItem, SwotCategory } from '@prisma/client'; import type { SwotItem, SwotCategory } from '@prisma/client';
import { updateSwotItem, deleteSwotItem, duplicateSwotItem } from '@/actions/swot'; import { updateSwotItem, deleteSwotItem, duplicateSwotItem } from '@/actions/swot';
import { IconEdit, IconTrash, IconDuplicate, IconCheck } from '@/components/ui';
interface SwotCardProps { interface SwotCardProps {
item: SwotItem; item: SwotItem;
@@ -121,63 +122,21 @@ export const SwotCard = memo(
className="rounded p-1 text-muted hover:bg-card-hover hover:text-foreground" className="rounded p-1 text-muted hover:bg-card-hover hover:text-foreground"
aria-label="Modifier" aria-label="Modifier"
> >
<svg <IconEdit />
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
/>
</svg>
</button> </button>
<button <button
onClick={(e) => { onClick={(e) => { e.stopPropagation(); handleDuplicate(); }}
e.stopPropagation();
handleDuplicate();
}}
className="rounded p-1 text-muted hover:bg-primary/10 hover:text-primary" className="rounded p-1 text-muted hover:bg-primary/10 hover:text-primary"
aria-label="Dupliquer" aria-label="Dupliquer"
> >
<svg <IconDuplicate />
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button> </button>
<button <button
onClick={(e) => { onClick={(e) => { e.stopPropagation(); handleDelete(); }}
e.stopPropagation();
handleDelete();
}}
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive" className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive"
aria-label="Supprimer" aria-label="Supprimer"
> >
<svg <IconTrash />
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button> </button>
</div> </div>
)} )}
@@ -187,9 +146,7 @@ export const SwotCard = memo(
<div <div
className={`absolute -right-1 -top-1 rounded-full bg-card p-0.5 shadow ${styles.text}`} className={`absolute -right-1 -top-1 rounded-full bg-card p-0.5 shadow ${styles.text}`}
> >
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 24 24"> <IconCheck />
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
</svg>
</div> </div>
)} )}
</> </>

View File

@@ -4,6 +4,7 @@ import { forwardRef, useState, useTransition, useRef, ReactNode } from 'react';
import type { SwotCategory } from '@prisma/client'; import type { SwotCategory } from '@prisma/client';
import { createSwotItem } from '@/actions/swot'; import { createSwotItem } from '@/actions/swot';
import { QuadrantHelpPanel } from './QuadrantHelp'; import { QuadrantHelpPanel } from './QuadrantHelp';
import { IconPlus, InlineFormActions } from '@/components/ui';
interface SwotQuadrantProps { interface SwotQuadrantProps {
category: SwotCategory; category: SwotCategory;
@@ -115,14 +116,7 @@ export const SwotQuadrant = forwardRef<HTMLDivElement, SwotQuadrantProps>(
`} `}
aria-label={`Ajouter un item ${title}`} aria-label={`Ajouter un item ${title}`}
> >
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <IconPlus />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
</button> </button>
</div> </div>
@@ -152,28 +146,14 @@ export const SwotQuadrant = forwardRef<HTMLDivElement, SwotQuadrantProps>(
rows={2} rows={2}
disabled={isPending} disabled={isPending}
/> />
<div className="mt-1 flex justify-end gap-1"> <InlineFormActions
<button onCancel={() => { setIsAdding(false); setNewContent(''); }}
onClick={() => { onSubmit={handleAdd}
setIsAdding(false); isPending={isPending}
setNewContent(''); disabled={!newContent.trim()}
}} submitColorClass={`${styles.text} hover:bg-white/50`}
className="rounded px-2 py-1 text-xs text-muted hover:bg-card-hover" className="mt-1"
disabled={isPending} />
>
Annuler
</button>
<button
onMouseDown={(e) => {
e.preventDefault(); // Prevent blur from textarea
}}
onClick={handleAdd}
disabled={isPending || !newContent.trim()}
className={`rounded px-2 py-1 text-xs font-medium ${styles.text} hover:bg-white/50 disabled:opacity-50`}
>
{isPending ? '...' : 'Ajouter'}
</button>
</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -1,28 +0,0 @@
'use client';
import { EditableTitle } from './EditableTitle';
import { updateGifMoodSession } from '@/actions/gif-mood';
interface EditableGifMoodTitleProps {
sessionId: string;
initialTitle: string;
canEdit: boolean;
}
export function EditableGifMoodTitle({
sessionId,
initialTitle,
canEdit,
}: EditableGifMoodTitleProps) {
return (
<EditableTitle
sessionId={sessionId}
initialTitle={initialTitle}
canEdit={canEdit}
onUpdate={async (id, title) => {
const result = await updateGifMoodSession(id, { title });
return result;
}}
/>
);
}

View File

@@ -1,28 +0,0 @@
'use client';
import { EditableTitle } from './EditableTitle';
import { updateMotivatorSession } from '@/actions/moving-motivators';
interface EditableMotivatorTitleProps {
sessionId: string;
initialTitle: string;
canEdit: boolean;
}
export function EditableMotivatorTitle({
sessionId,
initialTitle,
canEdit,
}: EditableMotivatorTitleProps) {
return (
<EditableTitle
sessionId={sessionId}
initialTitle={initialTitle}
canEdit={canEdit}
onUpdate={async (id, title) => {
const result = await updateMotivatorSession(id, { title });
return result;
}}
/>
);
}

View File

@@ -1,28 +0,0 @@
'use client';
import { EditableTitle } from './EditableTitle';
import { updateSessionTitle } from '@/actions/session';
interface EditableSessionTitleProps {
sessionId: string;
initialTitle: string;
canEdit: boolean;
}
export function EditableSessionTitle({
sessionId,
initialTitle,
canEdit,
}: EditableSessionTitleProps) {
return (
<EditableTitle
sessionId={sessionId}
initialTitle={initialTitle}
canEdit={canEdit}
onUpdate={async (id, title) => {
const result = await updateSessionTitle(id, title);
return result;
}}
/>
);
}

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import { useState, useTransition, useRef, useEffect, useMemo } from 'react'; import { useState, useTransition, useRef, useEffect, useMemo } from 'react';
import { IconEdit } from './Icons';
interface EditableTitleProps { interface EditableTitleProps {
sessionId: string; sessionId: string;
@@ -92,19 +93,7 @@ export function EditableTitle({ sessionId, initialTitle, canEdit, onUpdate }: Ed
title="Cliquez pour modifier" title="Cliquez pour modifier"
> >
<h1 className="text-3xl font-bold text-foreground">{title}</h1> <h1 className="text-3xl font-bold text-foreground">{title}</h1>
<svg <IconEdit className="h-5 w-5 text-muted opacity-0 transition-opacity group-hover:opacity-100" />
className="h-5 w-5 text-muted opacity-0 transition-opacity group-hover:opacity-100"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
/>
</svg>
</button> </button>
); );
} }

View File

@@ -0,0 +1,39 @@
'use client';
import { EditableTitle } from './EditableTitle';
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';
interface TitleProps {
sessionId: string;
initialTitle: string;
canEdit: boolean;
}
export function EditableSessionTitle(props: TitleProps) {
return <EditableTitle {...props} onUpdate={updateSessionTitle} />;
}
export function EditableMotivatorTitle(props: TitleProps) {
return <EditableTitle {...props} onUpdate={(id, title) => updateMotivatorSession(id, { title })} />;
}
export function EditableYearReviewTitle(props: TitleProps) {
return <EditableTitle {...props} onUpdate={(id, title) => updateYearReviewSession(id, { title })} />;
}
export function EditableWeatherTitle(props: TitleProps) {
return <EditableTitle {...props} onUpdate={(id, title) => updateWeatherSession(id, { title })} />;
}
export function EditableWeeklyCheckInTitle(props: TitleProps) {
return <EditableTitle {...props} onUpdate={(id, title) => updateWeeklyCheckInSession(id, { title })} />;
}
export function EditableGifMoodTitle(props: TitleProps) {
return <EditableTitle {...props} onUpdate={(id, title) => updateGifMoodSession(id, { title })} />;
}

View File

@@ -1,28 +0,0 @@
'use client';
import { EditableTitle } from './EditableTitle';
import { updateWeatherSession } from '@/actions/weather';
interface EditableWeatherTitleProps {
sessionId: string;
initialTitle: string;
canEdit: boolean;
}
export function EditableWeatherTitle({
sessionId,
initialTitle,
canEdit,
}: EditableWeatherTitleProps) {
return (
<EditableTitle
sessionId={sessionId}
initialTitle={initialTitle}
canEdit={canEdit}
onUpdate={async (id, title) => {
const result = await updateWeatherSession(id, { title });
return result;
}}
/>
);
}

View File

@@ -1,28 +0,0 @@
'use client';
import { EditableTitle } from './EditableTitle';
import { updateWeeklyCheckInSession } from '@/actions/weekly-checkin';
interface EditableWeeklyCheckInTitleProps {
sessionId: string;
initialTitle: string;
canEdit: boolean;
}
export function EditableWeeklyCheckInTitle({
sessionId,
initialTitle,
canEdit,
}: EditableWeeklyCheckInTitleProps) {
return (
<EditableTitle
sessionId={sessionId}
initialTitle={initialTitle}
canEdit={canEdit}
onUpdate={async (id, title) => {
const result = await updateWeeklyCheckInSession(id, { title });
return result;
}}
/>
);
}

View File

@@ -1,28 +0,0 @@
'use client';
import { EditableTitle } from './EditableTitle';
import { updateYearReviewSession } from '@/actions/year-review';
interface EditableYearReviewTitleProps {
sessionId: string;
initialTitle: string;
canEdit: boolean;
}
export function EditableYearReviewTitle({
sessionId,
initialTitle,
canEdit,
}: EditableYearReviewTitleProps) {
return (
<EditableTitle
sessionId={sessionId}
initialTitle={initialTitle}
canEdit={canEdit}
onUpdate={async (id, title) => {
const result = await updateYearReviewSession(id, { title });
return result;
}}
/>
);
}

View File

@@ -0,0 +1,54 @@
interface IconProps {
className?: string;
}
const base = { fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor' } as const;
const path = { strokeLinecap: 'round', strokeLinejoin: 'round', strokeWidth: 2 } as const;
export function IconEdit({ className = 'h-3.5 w-3.5' }: IconProps) {
return (
<svg className={className} {...base}>
<path {...path} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
);
}
export function IconTrash({ className = 'h-3.5 w-3.5' }: IconProps) {
return (
<svg className={className} {...base}>
<path {...path} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
);
}
export function IconDuplicate({ className = 'h-3.5 w-3.5' }: IconProps) {
return (
<svg className={className} {...base}>
<path {...path} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
);
}
export function IconPlus({ className = 'h-5 w-5' }: IconProps) {
return (
<svg className={className} {...base}>
<path {...path} d="M12 4v16m8-8H4" />
</svg>
);
}
export function IconCheck({ className = 'h-4 w-4' }: IconProps) {
return (
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
</svg>
);
}
export function IconClose({ className = 'h-5 w-5' }: IconProps) {
return (
<svg className={className} {...base}>
<path {...path} d="M6 18L18 6M6 6l12 12" />
</svg>
);
}

View File

@@ -0,0 +1,39 @@
interface InlineFormActionsProps {
onCancel: () => void;
onSubmit: () => void;
isPending: boolean;
disabled?: boolean;
submitLabel?: string;
submitColorClass?: string;
className?: string;
}
export function InlineFormActions({
onCancel,
onSubmit,
isPending,
disabled = false,
submitLabel = 'Ajouter',
submitColorClass = 'text-primary hover:bg-primary/10',
className = '',
}: InlineFormActionsProps) {
return (
<div className={`flex justify-end gap-1 ${className}`}>
<button
onClick={onCancel}
className="rounded px-2 py-1 text-xs text-muted hover:bg-card-hover"
disabled={isPending}
>
Annuler
</button>
<button
onMouseDown={(e) => e.preventDefault()}
onClick={onSubmit}
disabled={isPending || disabled}
className={`rounded px-2 py-1 text-xs font-medium disabled:opacity-50 ${submitColorClass}`}
>
{isPending ? '...' : submitLabel}
</button>
</div>
);
}

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import { Fragment, ReactNode, useEffect, useSyncExternalStore } from 'react'; import { Fragment, ReactNode, useEffect, useSyncExternalStore } from 'react';
import { IconClose } from './Icons';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
interface ModalProps { interface ModalProps {
@@ -84,14 +85,7 @@ export function Modal({ isOpen, onClose, title, children, size = 'md' }: ModalPr
className="rounded-lg p-1 text-muted hover:bg-card-hover hover:text-foreground transition-colors" className="rounded-lg p-1 text-muted hover:bg-card-hover hover:text-foreground transition-colors"
aria-label="Fermer" aria-label="Fermer"
> >
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <IconClose />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button> </button>
</div> </div>
)} )}

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

@@ -4,12 +4,18 @@ export { Button } from './Button';
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card'; export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card';
export { CollaboratorDisplay } from './CollaboratorDisplay'; export { CollaboratorDisplay } from './CollaboratorDisplay';
export { EditableTitle } from './EditableTitle'; export { EditableTitle } from './EditableTitle';
export { EditableSessionTitle } from './EditableSessionTitle'; export {
export { EditableMotivatorTitle } from './EditableMotivatorTitle'; EditableSessionTitle,
export { EditableYearReviewTitle } from './EditableYearReviewTitle'; EditableMotivatorTitle,
export { EditableWeeklyCheckInTitle } from './EditableWeeklyCheckInTitle'; EditableYearReviewTitle,
export { EditableWeatherTitle } from './EditableWeatherTitle'; EditableWeeklyCheckInTitle,
EditableWeatherTitle,
EditableGifMoodTitle,
} from './EditableTitles';
export { IconEdit, IconTrash, IconDuplicate, IconPlus, IconCheck, IconClose } from './Icons';
export { InlineFormActions } from './InlineFormActions';
export { PageHeader } from './PageHeader'; export { PageHeader } from './PageHeader';
export { SessionPageHeader } from './SessionPageHeader';
export { Input } from './Input'; export { Input } from './Input';
export { ParticipantInput } from './ParticipantInput'; export { ParticipantInput } from './ParticipantInput';
export { Modal, ModalFooter } from './Modal'; export { Modal, ModalFooter } from './Modal';

View File

@@ -4,6 +4,7 @@ import { forwardRef, memo, useState, useTransition } from 'react';
import type { WeeklyCheckInItem } from '@prisma/client'; import type { WeeklyCheckInItem } from '@prisma/client';
import { updateWeeklyCheckInItem, deleteWeeklyCheckInItem } from '@/actions/weekly-checkin'; import { updateWeeklyCheckInItem, deleteWeeklyCheckInItem } from '@/actions/weekly-checkin';
import { WEEKLY_CHECK_IN_BY_CATEGORY, EMOTION_BY_TYPE } from '@/lib/types'; import { WEEKLY_CHECK_IN_BY_CATEGORY, EMOTION_BY_TYPE } from '@/lib/types';
import { IconEdit, IconTrash, InlineFormActions } from '@/components/ui';
import { Select } from '@/components/ui/Select'; import { Select } from '@/components/ui/Select';
interface WeeklyCheckInCardProps { interface WeeklyCheckInCardProps {
@@ -113,26 +114,13 @@ export const WeeklyCheckInCard = memo(
label: `${em.icon} ${em.label}`, label: `${em.icon} ${em.label}`,
}))} }))}
/> />
<div className="flex justify-end gap-2"> <InlineFormActions
<button onCancel={() => { setContent(item.content); setEmotion(item.emotion); setIsEditing(false); }}
onClick={() => { onSubmit={handleSave}
setContent(item.content); isPending={isPending}
setEmotion(item.emotion); disabled={!content.trim()}
setIsEditing(false); submitLabel="Enregistrer"
}} />
className="rounded px-2 py-1 text-xs text-muted hover:bg-card-hover"
disabled={isPending}
>
Annuler
</button>
<button
onClick={handleSave}
disabled={isPending || !content.trim()}
className="rounded px-2 py-1 text-xs font-medium text-primary hover:bg-primary/10 disabled:opacity-50"
>
{isPending ? '...' : 'Enregistrer'}
</button>
</div>
</div> </div>
) : ( ) : (
<> <>
@@ -164,41 +152,14 @@ export const WeeklyCheckInCard = memo(
className="rounded p-1 text-muted hover:bg-card-hover hover:text-foreground" className="rounded p-1 text-muted hover:bg-card-hover hover:text-foreground"
aria-label="Modifier" aria-label="Modifier"
> >
<svg <IconEdit />
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
/>
</svg>
</button> </button>
<button <button
onClick={(e) => { onClick={(e) => { e.stopPropagation(); handleDelete(); }}
e.stopPropagation();
handleDelete();
}}
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive" className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive"
aria-label="Supprimer" aria-label="Supprimer"
> >
<svg <IconTrash />
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button> </button>
</div> </div>
</> </>

View File

@@ -4,6 +4,7 @@ import { forwardRef, useState, useTransition, useRef, ReactNode } from 'react';
import type { WeeklyCheckInCategory } from '@prisma/client'; import type { WeeklyCheckInCategory } from '@prisma/client';
import { createWeeklyCheckInItem } from '@/actions/weekly-checkin'; import { createWeeklyCheckInItem } from '@/actions/weekly-checkin';
import { WEEKLY_CHECK_IN_BY_CATEGORY, EMOTION_BY_TYPE } from '@/lib/types'; import { WEEKLY_CHECK_IN_BY_CATEGORY, EMOTION_BY_TYPE } from '@/lib/types';
import { IconPlus, InlineFormActions } from '@/components/ui';
import { Select } from '@/components/ui/Select'; import { Select } from '@/components/ui/Select';
interface WeeklyCheckInSectionProps { interface WeeklyCheckInSectionProps {
@@ -82,14 +83,7 @@ export const WeeklyCheckInSection = forwardRef<HTMLDivElement, WeeklyCheckInSect
className="rounded-lg p-1.5 transition-colors hover:bg-card-hover text-muted hover:text-foreground" className="rounded-lg p-1.5 transition-colors hover:bg-card-hover text-muted hover:text-foreground"
aria-label={`Ajouter un item ${config.title}`} aria-label={`Ajouter un item ${config.title}`}
> >
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <IconPlus />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
</button> </button>
</div> </div>
@@ -138,29 +132,12 @@ export const WeeklyCheckInSection = forwardRef<HTMLDivElement, WeeklyCheckInSect
label: `${em.icon} ${em.label}`, label: `${em.icon} ${em.label}`,
}))} }))}
/> />
<div className="flex gap-1"> <InlineFormActions
<button onCancel={() => { setIsAdding(false); setNewContent(''); setNewEmotion('NONE'); }}
onClick={() => { onSubmit={handleAdd}
setIsAdding(false); isPending={isPending}
setNewContent(''); disabled={!newContent.trim()}
setNewEmotion('NONE'); />
}}
className="rounded px-2 py-1 text-xs text-muted hover:bg-card-hover"
disabled={isPending}
>
Annuler
</button>
<button
onMouseDown={(e) => {
e.preventDefault(); // Prevent blur from textarea
}}
onClick={handleAdd}
disabled={isPending || !newContent.trim()}
className="rounded px-2 py-1 text-xs font-medium text-primary hover:bg-primary/10 disabled:opacity-50"
>
{isPending ? '...' : 'Ajouter'}
</button>
</div>
</div> </div>
</div> </div>
)} )}

View File

@@ -4,6 +4,7 @@ import { forwardRef, memo, useState, useTransition } from 'react';
import type { YearReviewItem } from '@prisma/client'; import type { YearReviewItem } from '@prisma/client';
import { updateYearReviewItem, deleteYearReviewItem } from '@/actions/year-review'; import { updateYearReviewItem, deleteYearReviewItem } from '@/actions/year-review';
import { YEAR_REVIEW_BY_CATEGORY } from '@/lib/types'; import { YEAR_REVIEW_BY_CATEGORY } from '@/lib/types';
import { IconEdit, IconTrash } from '@/components/ui';
interface YearReviewCardProps { interface YearReviewCardProps {
item: YearReviewItem; item: YearReviewItem;
@@ -95,41 +96,14 @@ export const YearReviewCard = memo(
className="rounded p-1 text-muted hover:bg-card-hover hover:text-foreground" className="rounded p-1 text-muted hover:bg-card-hover hover:text-foreground"
aria-label="Modifier" aria-label="Modifier"
> >
<svg <IconEdit />
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
/>
</svg>
</button> </button>
<button <button
onClick={(e) => { onClick={(e) => { e.stopPropagation(); handleDelete(); }}
e.stopPropagation();
handleDelete();
}}
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive" className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive"
aria-label="Supprimer" aria-label="Supprimer"
> >
<svg <IconTrash />
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button> </button>
</div> </div>
</> </>

View File

@@ -4,6 +4,7 @@ import { forwardRef, useState, useTransition, useRef, ReactNode } from 'react';
import type { YearReviewCategory } from '@prisma/client'; import type { YearReviewCategory } from '@prisma/client';
import { createYearReviewItem } from '@/actions/year-review'; import { createYearReviewItem } from '@/actions/year-review';
import { YEAR_REVIEW_BY_CATEGORY } from '@/lib/types'; import { YEAR_REVIEW_BY_CATEGORY } from '@/lib/types';
import { IconPlus, InlineFormActions } from '@/components/ui';
interface YearReviewSectionProps { interface YearReviewSectionProps {
category: YearReviewCategory; category: YearReviewCategory;
@@ -77,14 +78,7 @@ export const YearReviewSection = forwardRef<HTMLDivElement, YearReviewSectionPro
className="rounded-lg p-1.5 transition-colors hover:bg-card-hover text-muted hover:text-foreground" className="rounded-lg p-1.5 transition-colors hover:bg-card-hover text-muted hover:text-foreground"
aria-label={`Ajouter un item ${config.title}`} aria-label={`Ajouter un item ${config.title}`}
> >
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <IconPlus />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
</button> </button>
</div> </div>
@@ -111,28 +105,13 @@ export const YearReviewSection = forwardRef<HTMLDivElement, YearReviewSectionPro
rows={2} rows={2}
disabled={isPending} disabled={isPending}
/> />
<div className="mt-1 flex justify-end gap-1"> <InlineFormActions
<button onCancel={() => { setIsAdding(false); setNewContent(''); }}
onClick={() => { onSubmit={handleAdd}
setIsAdding(false); isPending={isPending}
setNewContent(''); disabled={!newContent.trim()}
}} className="mt-1"
className="rounded px-2 py-1 text-xs text-muted hover:bg-card-hover" />
disabled={isPending}
>
Annuler
</button>
<button
onMouseDown={(e) => {
e.preventDefault(); // Prevent blur from textarea
}}
onClick={handleAdd}
disabled={isPending || !newContent.trim()}
className="rounded px-2 py-1 text-xs font-medium text-primary hover:bg-primary/10 disabled:opacity-50"
>
{isPending ? '...' : 'Ajouter'}
</button>
</div>
</div> </div>
)} )}
</div> </div>