Compare commits
3 Commits
2e00522bfc
...
9a43980412
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a43980412 | |||
| 09a849279b | |||
| b1ba43fd30 |
@@ -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">
|
sessionId={session.id}
|
||||||
<Link href={getSessionsTabUrl('gif-mood')} className="hover:text-foreground">
|
sessionTitle={session.title}
|
||||||
{getWorkshop('gif-mood').labelShort}
|
isOwner={session.isOwner}
|
||||||
</Link>
|
canEdit={session.canEdit}
|
||||||
<span>/</span>
|
ownerUser={session.user}
|
||||||
<span className="text-foreground">{session.title}</span>
|
date={session.date}
|
||||||
{!session.isOwner && (
|
badges={<Badge variant="primary">{session.items.length} GIFs</Badge>}
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Live Wrapper + Board */}
|
{/* Live Wrapper + Board */}
|
||||||
<GifMoodLiveWrapper
|
<GifMoodLiveWrapper
|
||||||
|
|||||||
@@ -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">
|
sessionId={session.id}
|
||||||
<Link href={getSessionsTabUrl('motivators')} className="hover:text-foreground">
|
sessionTitle={session.title}
|
||||||
{getWorkshop('motivators').label}
|
isOwner={session.isOwner}
|
||||||
</Link>
|
canEdit={session.canEdit}
|
||||||
<span>/</span>
|
ownerUser={session.user}
|
||||||
<span className="text-foreground">{session.title}</span>
|
date={session.date}
|
||||||
{!session.isOwner && (
|
collaborator={session.resolvedParticipant as ResolvedCollaborator}
|
||||||
<Badge variant="accent" className="ml-2">
|
badges={
|
||||||
Partagé par {session.user.name || session.user.email}
|
<Badge variant="primary">
|
||||||
</Badge>
|
{session.cards.filter((c) => c.influence !== 0).length} / 10 évalués
|
||||||
)}
|
</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>
|
|
||||||
|
|
||||||
{/* Live Wrapper + Board */}
|
{/* Live Wrapper + Board */}
|
||||||
<MotivatorLiveWrapper
|
<MotivatorLiveWrapper
|
||||||
|
|||||||
@@ -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">
|
sessionId={session.id}
|
||||||
<Link href={getSessionsTabUrl('swot')} className="hover:text-foreground">
|
sessionTitle={session.title}
|
||||||
{getWorkshop('swot').labelShort}
|
isOwner={session.isOwner}
|
||||||
</Link>
|
canEdit={session.canEdit}
|
||||||
<span>/</span>
|
ownerUser={session.user}
|
||||||
<span className="text-foreground">{session.title}</span>
|
date={session.date}
|
||||||
{!session.isOwner && (
|
collaborator={session.resolvedCollaborator}
|
||||||
<Badge variant="accent" className="ml-2">
|
badges={<>
|
||||||
Partagé par {session.user.name || session.user.email}
|
<Badge variant="primary">{session.items.length} items</Badge>
|
||||||
</Badge>
|
<Badge variant="success">{session.actions.length} actions</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>
|
|
||||||
|
|
||||||
{/* Live Session Wrapper */}
|
{/* Live Session Wrapper */}
|
||||||
<SessionLiveWrapper
|
<SessionLiveWrapper
|
||||||
|
|||||||
@@ -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">
|
← Retour aux équipes
|
||||||
<Link href="/teams" className="text-muted hover:text-foreground">
|
</Link>
|
||||||
← Retour aux équipes
|
</div>
|
||||||
</Link>
|
<PageHeader
|
||||||
</div>
|
emoji="👥"
|
||||||
<div className="flex items-start justify-between">
|
title={team.name}
|
||||||
<div>
|
subtitle={team.description ?? undefined}
|
||||||
<h1 className="text-3xl font-bold text-foreground flex items-center gap-2">
|
actions={
|
||||||
<span className="text-3xl">👥</span>
|
isAdmin ? (
|
||||||
{team.name}
|
|
||||||
</h1>
|
|
||||||
{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">
|
||||||
|
|||||||
@@ -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">
|
sessionId={session.id}
|
||||||
<Link href={getSessionsTabUrl('weather')} className="hover:text-foreground">
|
sessionTitle={session.title}
|
||||||
{getWorkshop('weather').labelShort}
|
isOwner={session.isOwner}
|
||||||
</Link>
|
canEdit={session.canEdit}
|
||||||
<span>/</span>
|
ownerUser={session.user}
|
||||||
<span className="text-foreground">{session.title}</span>
|
date={session.date}
|
||||||
{!session.isOwner && (
|
badges={<Badge variant="primary">{session.entries.length} membres</Badge>}
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Info sur les catégories */}
|
{/* Info sur les catégories */}
|
||||||
<WeatherInfoPanel />
|
<WeatherInfoPanel />
|
||||||
|
|||||||
@@ -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">
|
sessionId={session.id}
|
||||||
<Link href={getSessionsTabUrl('weekly-checkin')} className="hover:text-foreground">
|
sessionTitle={session.title}
|
||||||
{getWorkshop('weekly-checkin').label}
|
isOwner={session.isOwner}
|
||||||
</Link>
|
canEdit={session.canEdit}
|
||||||
<span>/</span>
|
ownerUser={session.user}
|
||||||
<span className="text-foreground">{session.title}</span>
|
date={session.date}
|
||||||
{!session.isOwner && (
|
collaborator={resolvedParticipant}
|
||||||
<Badge variant="accent" className="ml-2">
|
badges={<Badge variant="primary">{session.items.length} items</Badge>}
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Current Quarter OKRs - editable by participant or team admin */}
|
{/* Current Quarter OKRs - editable by participant or team admin */}
|
||||||
{currentQuarterOKRs.length > 0 && (
|
{currentQuarterOKRs.length > 0 && (
|
||||||
|
|||||||
@@ -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">
|
sessionId={session.id}
|
||||||
<Link href={getSessionsTabUrl('year-review')} className="hover:text-foreground">
|
sessionTitle={session.title}
|
||||||
{getWorkshop('year-review').label}
|
isOwner={session.isOwner}
|
||||||
</Link>
|
canEdit={session.canEdit}
|
||||||
<span>/</span>
|
ownerUser={session.user}
|
||||||
<span className="text-foreground">{session.title}</span>
|
date={session.updatedAt}
|
||||||
{!session.isOwner && (
|
collaborator={session.resolvedParticipant as ResolvedCollaborator}
|
||||||
<Badge variant="accent" className="ml-2">
|
badges={<>
|
||||||
Partagé par {session.user.name || session.user.email}
|
<Badge variant="primary">{session.items.length} items</Badge>
|
||||||
</Badge>
|
<Badge variant="default">Année {session.year}</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>
|
|
||||||
|
|
||||||
{/* Live Wrapper + Board */}
|
{/* Live Wrapper + Board */}
|
||||||
<YearReviewLiveWrapper
|
<YearReviewLiveWrapper
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/components/ui/EditableTitles.tsx
Normal file
39
src/components/ui/EditableTitles.tsx
Normal 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 })} />;
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
54
src/components/ui/Icons.tsx
Normal file
54
src/components/ui/Icons.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
src/components/ui/InlineFormActions.tsx
Normal file
39
src/components/ui/InlineFormActions.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
95
src/components/ui/SessionPageHeader.tsx
Normal file
95
src/components/ui/SessionPageHeader.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user