chore: clean up code formatting and remove unnecessary whitespace across multiple files for improved readability

This commit is contained in:
Julien Froidefond
2025-12-05 11:05:14 +01:00
parent b3157fffbd
commit 71d850c985
65 changed files with 347 additions and 505 deletions

View File

@@ -44,3 +44,4 @@ README.md
devbook.md devbook.md
TODO.md TODO.md

View File

@@ -4,7 +4,7 @@ services:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
ports: ports:
- "3011:3000" - '3011:3000'
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- DATABASE_URL=file:/app/data/dev.db - DATABASE_URL=file:/app/data/dev.db
@@ -14,4 +14,3 @@ services:
volumes: volumes:
- ./data:/app/data - ./data:/app/data
restart: unless-stopped restart: unless-stopped

View File

@@ -15,10 +15,7 @@ export async function createMotivatorSession(data: { title: string; participant:
} }
try { try {
const motivatorSession = await motivatorsService.createMotivatorSession( const motivatorSession = await motivatorsService.createMotivatorSession(session.user.id, data);
session.user.id,
data
);
revalidatePath('/motivators'); revalidatePath('/motivators');
return { success: true, data: motivatorSession }; return { success: true, data: motivatorSession };
} catch (error) { } catch (error) {
@@ -89,10 +86,7 @@ export async function updateMotivatorCard(
} }
// Check edit permission // Check edit permission
const canEdit = await motivatorsService.canEditMotivatorSession( const canEdit = await motivatorsService.canEditMotivatorSession(sessionId, authSession.user.id);
sessionId,
authSession.user.id
);
if (!canEdit) { if (!canEdit) {
return { success: false, error: 'Permission refusée' }; return { success: false, error: 'Permission refusée' };
} }
@@ -132,10 +126,7 @@ export async function reorderMotivatorCards(sessionId: string, cardIds: string[]
} }
// Check edit permission // Check edit permission
const canEdit = await motivatorsService.canEditMotivatorSession( const canEdit = await motivatorsService.canEditMotivatorSession(sessionId, authSession.user.id);
sessionId,
authSession.user.id
);
if (!canEdit) { if (!canEdit) {
return { success: false, error: 'Permission refusée' }; return { success: false, error: 'Permission refusée' };
} }
@@ -159,11 +150,7 @@ export async function reorderMotivatorCards(sessionId: string, cardIds: string[]
} }
} }
export async function updateCardInfluence( export async function updateCardInfluence(cardId: string, sessionId: string, influence: number) {
cardId: string,
sessionId: string,
influence: number
) {
return updateMotivatorCard(cardId, sessionId, { influence }); return updateMotivatorCard(cardId, sessionId, { influence });
} }
@@ -192,8 +179,7 @@ export async function shareMotivatorSession(
return { success: true, data: share }; return { success: true, data: share };
} catch (error) { } catch (error) {
console.error('Error sharing motivator session:', error); console.error('Error sharing motivator session:', error);
const message = const message = error instanceof Error ? error.message : 'Erreur lors du partage';
error instanceof Error ? error.message : 'Erreur lors du partage';
return { success: false, error: message }; return { success: false, error: message };
} }
} }
@@ -205,11 +191,7 @@ export async function removeMotivatorShare(sessionId: string, shareUserId: strin
} }
try { try {
await motivatorsService.removeMotivatorShare( await motivatorsService.removeMotivatorShare(sessionId, authSession.user.id, shareUserId);
sessionId,
authSession.user.id,
shareUserId
);
revalidatePath(`/motivators/${sessionId}`); revalidatePath(`/motivators/${sessionId}`);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
@@ -217,4 +199,3 @@ export async function removeMotivatorShare(sessionId: string, shareUserId: strin
return { success: false, error: 'Erreur lors de la suppression du partage' }; return { success: false, error: 'Erreur lors de la suppression du partage' };
} }
} }

View File

@@ -35,10 +35,7 @@ export async function updateProfileAction(data: { name?: string; email?: string
return result; return result;
} }
export async function updatePasswordAction(data: { export async function updatePasswordAction(data: { currentPassword: string; newPassword: string }) {
currentPassword: string;
newPassword: string;
}) {
const session = await auth(); const session = await auth();
if (!session?.user?.id) { if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' }; return { success: false, error: 'Non authentifié' };
@@ -48,12 +45,6 @@ export async function updatePasswordAction(data: {
return { success: false, error: 'Le nouveau mot de passe doit faire au moins 6 caractères' }; return { success: false, error: 'Le nouveau mot de passe doit faire au moins 6 caractères' };
} }
const result = await updateUserPassword( const result = await updateUserPassword(session.user.id, data.currentPassword, data.newPassword);
session.user.id,
data.currentPassword,
data.newPassword
);
return result; return result;
} }

View File

@@ -99,7 +99,12 @@ export async function updateSwotSession(
} }
// Emit event for real-time sync // Emit event for real-time sync
await sessionsService.createSessionEvent(sessionId, session.user.id, 'SESSION_UPDATED', updateData); await sessionsService.createSessionEvent(
sessionId,
session.user.id,
'SESSION_UPDATED',
updateData
);
revalidatePath(`/sessions/${sessionId}`); revalidatePath(`/sessions/${sessionId}`);
revalidatePath('/sessions'); revalidatePath('/sessions');
@@ -130,4 +135,3 @@ export async function deleteSwotSession(sessionId: string) {
return { success: false, error: 'Erreur lors de la suppression' }; return { success: false, error: 'Erreur lors de la suppression' };
} }
} }

View File

@@ -2,11 +2,7 @@
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { import { shareSession, removeShare, getSessionShares } from '@/services/sessions';
shareSession,
removeShare,
getSessionShares,
} from '@/services/sessions';
import type { ShareRole } from '@prisma/client'; import type { ShareRole } from '@prisma/client';
export async function shareSessionAction( export async function shareSessionAction(
@@ -26,10 +22,10 @@ export async function shareSessionAction(
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : 'Erreur inconnue'; const message = error instanceof Error ? error.message : 'Erreur inconnue';
if (message === 'User not found') { if (message === 'User not found') {
return { success: false, error: "Aucun utilisateur trouvé avec cet email" }; return { success: false, error: 'Aucun utilisateur trouvé avec cet email' };
} }
if (message === 'Cannot share session with yourself') { if (message === 'Cannot share session with yourself') {
return { success: false, error: "Vous ne pouvez pas partager avec vous-même" }; return { success: false, error: 'Vous ne pouvez pas partager avec vous-même' };
} }
return { success: false, error: message }; return { success: false, error: message };
} }
@@ -65,5 +61,3 @@ export async function getSharesAction(sessionId: string) {
return { success: false, error: message, data: [] }; return { success: false, error: message, data: [] };
} }
} }

View File

@@ -20,14 +20,14 @@ export async function createSwotItem(
try { try {
const item = await sessionsService.createSwotItem(sessionId, data); const item = await sessionsService.createSwotItem(sessionId, data);
// Emit event for real-time sync // Emit event for real-time sync
await sessionsService.createSessionEvent(sessionId, session.user.id, 'ITEM_CREATED', { await sessionsService.createSessionEvent(sessionId, session.user.id, 'ITEM_CREATED', {
itemId: item.id, itemId: item.id,
content: item.content, content: item.content,
category: item.category, category: item.category,
}); });
revalidatePath(`/sessions/${sessionId}`); revalidatePath(`/sessions/${sessionId}`);
return { success: true, data: item }; return { success: true, data: item };
} catch (error) { } catch (error) {
@@ -48,13 +48,13 @@ export async function updateSwotItem(
try { try {
const item = await sessionsService.updateSwotItem(itemId, data); const item = await sessionsService.updateSwotItem(itemId, data);
// Emit event for real-time sync // Emit event for real-time sync
await sessionsService.createSessionEvent(sessionId, session.user.id, 'ITEM_UPDATED', { await sessionsService.createSessionEvent(sessionId, session.user.id, 'ITEM_UPDATED', {
itemId: item.id, itemId: item.id,
...data, ...data,
}); });
revalidatePath(`/sessions/${sessionId}`); revalidatePath(`/sessions/${sessionId}`);
return { success: true, data: item }; return { success: true, data: item };
} catch (error) { } catch (error) {
@@ -71,12 +71,12 @@ export async function deleteSwotItem(itemId: string, sessionId: string) {
try { try {
await sessionsService.deleteSwotItem(itemId); await sessionsService.deleteSwotItem(itemId);
// Emit event for real-time sync // Emit event for real-time sync
await sessionsService.createSessionEvent(sessionId, session.user.id, 'ITEM_DELETED', { await sessionsService.createSessionEvent(sessionId, session.user.id, 'ITEM_DELETED', {
itemId, itemId,
}); });
revalidatePath(`/sessions/${sessionId}`); revalidatePath(`/sessions/${sessionId}`);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
@@ -93,7 +93,7 @@ export async function duplicateSwotItem(itemId: string, sessionId: string) {
try { try {
const item = await sessionsService.duplicateSwotItem(itemId); const item = await sessionsService.duplicateSwotItem(itemId);
// Emit event for real-time sync // Emit event for real-time sync
await sessionsService.createSessionEvent(sessionId, session.user.id, 'ITEM_CREATED', { await sessionsService.createSessionEvent(sessionId, session.user.id, 'ITEM_CREATED', {
itemId: item.id, itemId: item.id,
@@ -101,7 +101,7 @@ export async function duplicateSwotItem(itemId: string, sessionId: string) {
category: item.category, category: item.category,
duplicatedFrom: itemId, duplicatedFrom: itemId,
}); });
revalidatePath(`/sessions/${sessionId}`); revalidatePath(`/sessions/${sessionId}`);
return { success: true, data: item }; return { success: true, data: item };
} catch (error) { } catch (error) {
@@ -123,14 +123,14 @@ export async function moveSwotItem(
try { try {
const item = await sessionsService.moveSwotItem(itemId, newCategory, newOrder); const item = await sessionsService.moveSwotItem(itemId, newCategory, newOrder);
// Emit event for real-time sync // Emit event for real-time sync
await sessionsService.createSessionEvent(sessionId, session.user.id, 'ITEM_MOVED', { await sessionsService.createSessionEvent(sessionId, session.user.id, 'ITEM_MOVED', {
itemId: item.id, itemId: item.id,
newCategory, newCategory,
newOrder, newOrder,
}); });
revalidatePath(`/sessions/${sessionId}`); revalidatePath(`/sessions/${sessionId}`);
return { success: true, data: item }; return { success: true, data: item };
} catch (error) { } catch (error) {
@@ -159,14 +159,14 @@ export async function createAction(
try { try {
const action = await sessionsService.createAction(sessionId, data); const action = await sessionsService.createAction(sessionId, data);
// Emit event for real-time sync // Emit event for real-time sync
await sessionsService.createSessionEvent(sessionId, session.user.id, 'ACTION_CREATED', { await sessionsService.createSessionEvent(sessionId, session.user.id, 'ACTION_CREATED', {
actionId: action.id, actionId: action.id,
title: action.title, title: action.title,
linkedItemIds: data.linkedItemIds, linkedItemIds: data.linkedItemIds,
}); });
revalidatePath(`/sessions/${sessionId}`); revalidatePath(`/sessions/${sessionId}`);
return { success: true, data: action }; return { success: true, data: action };
} catch (error) { } catch (error) {
@@ -192,13 +192,13 @@ export async function updateAction(
try { try {
const action = await sessionsService.updateAction(actionId, data); const action = await sessionsService.updateAction(actionId, data);
// Emit event for real-time sync // Emit event for real-time sync
await sessionsService.createSessionEvent(sessionId, session.user.id, 'ACTION_UPDATED', { await sessionsService.createSessionEvent(sessionId, session.user.id, 'ACTION_UPDATED', {
actionId: action.id, actionId: action.id,
...data, ...data,
}); });
revalidatePath(`/sessions/${sessionId}`); revalidatePath(`/sessions/${sessionId}`);
return { success: true, data: action }; return { success: true, data: action };
} catch (error) { } catch (error) {
@@ -215,12 +215,12 @@ export async function deleteAction(actionId: string, sessionId: string) {
try { try {
await sessionsService.deleteAction(actionId); await sessionsService.deleteAction(actionId);
// Emit event for real-time sync // Emit event for real-time sync
await sessionsService.createSessionEvent(sessionId, session.user.id, 'ACTION_DELETED', { await sessionsService.createSessionEvent(sessionId, session.user.id, 'ACTION_DELETED', {
actionId, actionId,
}); });
revalidatePath(`/sessions/${sessionId}`); revalidatePath(`/sessions/${sessionId}`);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
@@ -228,4 +228,3 @@ export async function deleteAction(actionId: string, sessionId: string) {
return { success: false, error: 'Erreur lors de la suppression' }; return { success: false, error: 'Erreur lors de la suppression' };
} }
} }

View File

@@ -109,4 +109,3 @@ export default function LoginPage() {
</div> </div>
); );
} }

View File

@@ -170,4 +170,3 @@ export default function RegisterPage() {
</div> </div>
); );
} }

View File

@@ -1,4 +1,3 @@
import { handlers } from '@/lib/auth'; import { handlers } from '@/lib/auth';
export const { GET, POST } = handlers; export const { GET, POST } = handlers;

View File

@@ -22,4 +22,3 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Erreur lors de la création du compte' }, { status: 500 }); return NextResponse.json({ error: 'Erreur lors de la création du compte' }, { status: 500 });
} }
} }

View File

@@ -1,18 +1,12 @@
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { import { canAccessMotivatorSession, getMotivatorSessionEvents } from '@/services/moving-motivators';
canAccessMotivatorSession,
getMotivatorSessionEvents,
} from '@/services/moving-motivators';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
// Store active connections per session // Store active connections per session
const connections = new Map<string, Set<ReadableStreamDefaultController>>(); const connections = new Map<string, Set<ReadableStreamDefaultController>>();
export async function GET( export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: sessionId } = await params; const { id: sessionId } = await params;
const session = await auth(); const session = await auth();
@@ -115,4 +109,3 @@ export function broadcastToMotivatorSession(sessionId: string, event: object) {
} }
} }
} }

View File

@@ -6,10 +6,7 @@ export const dynamic = 'force-dynamic';
// Store active connections per session // Store active connections per session
const connections = new Map<string, Set<ReadableStreamDefaultController>>(); const connections = new Map<string, Set<ReadableStreamDefaultController>>();
export async function GET( export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: sessionId } = await params; const { id: sessionId } = await params;
const session = await auth(); const session = await auth();
@@ -112,4 +109,3 @@ export function broadcastToSession(sessionId: string, event: object) {
} }
} }
} }

View File

@@ -45,10 +45,7 @@ export async function POST(request: Request) {
const { title, collaborator } = body; const { title, collaborator } = body;
if (!title || !collaborator) { if (!title || !collaborator) {
return NextResponse.json( return NextResponse.json({ error: 'Titre et collaborateur requis' }, { status: 400 });
{ error: 'Titre et collaborateur requis' },
{ status: 400 }
);
} }
const newSession = await prisma.session.create({ const newSession = await prisma.session.create({
@@ -68,4 +65,3 @@ export async function POST(request: Request) {
); );
} }
} }

View File

@@ -107,4 +107,3 @@ export function EditableMotivatorTitle({
</button> </button>
); );
} }

View File

@@ -49,11 +49,7 @@ export default async function MotivatorSessionPage({ params }: MotivatorSessionP
isOwner={session.isOwner} isOwner={session.isOwner}
/> />
<div className="mt-2"> <div className="mt-2">
<CollaboratorDisplay <CollaboratorDisplay collaborator={session.resolvedParticipant} size="lg" showEmail />
collaborator={session.resolvedParticipant}
size="lg"
showEmail
/>
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -80,13 +76,8 @@ export default async function MotivatorSessionPage({ params }: MotivatorSessionP
isOwner={session.isOwner} isOwner={session.isOwner}
canEdit={session.canEdit} canEdit={session.canEdit}
> >
<MotivatorBoard <MotivatorBoard sessionId={session.id} cards={session.cards} canEdit={session.canEdit} />
sessionId={session.id}
cards={session.cards}
canEdit={session.canEdit}
/>
</MotivatorLiveWrapper> </MotivatorLiveWrapper>
</main> </main>
); );
} }

View File

@@ -2,7 +2,15 @@
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Card, CardHeader, CardTitle, CardDescription, CardContent, Button, Input } from '@/components/ui'; import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
Button,
Input,
} from '@/components/ui';
import { createMotivatorSession } from '@/actions/moving-motivators'; import { createMotivatorSession } from '@/actions/moving-motivators';
export default function NewMotivatorSessionPage() { export default function NewMotivatorSessionPage() {
@@ -99,4 +107,3 @@ export default function NewMotivatorSessionPage() {
</main> </main>
); );
} }

View File

@@ -10,8 +10,8 @@ export default function Home() {
Vos ateliers, <span className="text-primary">réinventés</span> Vos ateliers, <span className="text-primary">réinventés</span>
</h1> </h1>
<p className="mx-auto mb-8 max-w-2xl text-lg text-muted"> <p className="mx-auto mb-8 max-w-2xl text-lg text-muted">
Des outils interactifs et collaboratifs pour accompagner vos équipes. Des outils interactifs et collaboratifs pour accompagner vos équipes. Analysez,
Analysez, comprenez et faites progresser vos collaborateurs avec des ateliers modernes. comprenez et faites progresser vos collaborateurs avec des ateliers modernes.
</p> </p>
</section> </section>
@@ -46,7 +46,7 @@ export default function Home() {
description="Explorez les 10 motivations intrinsèques de vos collaborateurs. Comprenez leur impact et alignez aspirations et missions." description="Explorez les 10 motivations intrinsèques de vos collaborateurs. Comprenez leur impact et alignez aspirations et missions."
features={[ features={[
'10 cartes de motivation à classer', '10 cartes de motivation à classer',
'Évaluation de l\'influence positive/négative', "Évaluation de l'influence positive/négative",
'Récapitulatif personnalisé des motivations', 'Récapitulatif personnalisé des motivations',
]} ]}
accentColor="#8b5cf6" accentColor="#8b5cf6"
@@ -73,8 +73,9 @@ export default function Home() {
Pourquoi faire un SWOT ? Pourquoi faire un SWOT ?
</h3> </h3>
<p className="text-muted mb-4"> <p className="text-muted mb-4">
L&apos;analyse SWOT est un outil puissant pour prendre du recul sur une situation professionnelle. L&apos;analyse SWOT est un outil puissant pour prendre du recul sur une situation
Elle permet de dresser un portrait objectif et structuré, base indispensable pour définir des actions pertinentes. professionnelle. Elle permet de dresser un portrait objectif et structuré, base
indispensable pour définir des actions pertinentes.
</p> </p>
<ul className="space-y-2 text-sm text-muted"> <ul className="space-y-2 text-sm text-muted">
<li className="flex items-start gap-2"> <li className="flex items-start gap-2">
@@ -105,15 +106,21 @@ export default function Home() {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="rounded-lg bg-green-500/10 p-3 border border-green-500/20"> <div className="rounded-lg bg-green-500/10 p-3 border border-green-500/20">
<p className="font-semibold text-green-600 text-sm mb-1">💪 Forces</p> <p className="font-semibold text-green-600 text-sm mb-1">💪 Forces</p>
<p className="text-xs text-muted">Compétences, talents, réussites, qualités distinctives</p> <p className="text-xs text-muted">
Compétences, talents, réussites, qualités distinctives
</p>
</div> </div>
<div className="rounded-lg bg-orange-500/10 p-3 border border-orange-500/20"> <div className="rounded-lg bg-orange-500/10 p-3 border border-orange-500/20">
<p className="font-semibold text-orange-600 text-sm mb-1"> Faiblesses</p> <p className="font-semibold text-orange-600 text-sm mb-1"> Faiblesses</p>
<p className="text-xs text-muted">Lacunes, difficultés récurrentes, axes de progression</p> <p className="text-xs text-muted">
Lacunes, difficultés récurrentes, axes de progression
</p>
</div> </div>
<div className="rounded-lg bg-blue-500/10 p-3 border border-blue-500/20"> <div className="rounded-lg bg-blue-500/10 p-3 border border-blue-500/20">
<p className="font-semibold text-blue-600 text-sm mb-1">🚀 Opportunités</p> <p className="font-semibold text-blue-600 text-sm mb-1">🚀 Opportunités</p>
<p className="text-xs text-muted">Projets, formations, évolutions, nouveaux défis</p> <p className="text-xs text-muted">
Projets, formations, évolutions, nouveaux défis
</p>
</div> </div>
<div className="rounded-lg bg-red-500/10 p-3 border border-red-500/20"> <div className="rounded-lg bg-red-500/10 p-3 border border-red-500/20">
<p className="font-semibold text-red-600 text-sm mb-1">🛡 Menaces</p> <p className="font-semibold text-red-600 text-sm mb-1">🛡 Menaces</p>
@@ -129,10 +136,26 @@ export default function Home() {
Comment ça marche ? Comment ça marche ?
</h3> </h3>
<div className="grid md:grid-cols-4 gap-4"> <div className="grid md:grid-cols-4 gap-4">
<StepCard number={1} title="Remplir la matrice" description="Identifiez ensemble les éléments de chaque quadrant lors d'un échange constructif" /> <StepCard
<StepCard number={2} title="Prioriser" description="Classez les éléments par importance et impact pour concentrer les efforts" /> number={1}
<StepCard number={3} title="Croiser" description="Reliez les forces aux opportunités, anticipez les menaces avec les atouts" /> title="Remplir la matrice"
<StepCard number={4} title="Agir" description="Définissez des actions concrètes avec des échéances et des responsables" /> description="Identifiez ensemble les éléments de chaque quadrant lors d'un échange constructif"
/>
<StepCard
number={2}
title="Prioriser"
description="Classez les éléments par importance et impact pour concentrer les efforts"
/>
<StepCard
number={3}
title="Croiser"
description="Reliez les forces aux opportunités, anticipez les menaces avec les atouts"
/>
<StepCard
number={4}
title="Agir"
description="Définissez des actions concrètes avec des échéances et des responsables"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -156,8 +179,9 @@ export default function Home() {
Pourquoi explorer ses motivations ? Pourquoi explorer ses motivations ?
</h3> </h3>
<p className="text-muted mb-4"> <p className="text-muted mb-4">
Créé par Jurgen Appelo (Management 3.0), cet exercice révèle les motivations intrinsèques qui nous animent. Créé par Jurgen Appelo (Management 3.0), cet exercice révèle les motivations
Comprendre ce qui nous motive permet de mieux s&apos;épanouir et d&apos;aligner nos missions avec nos aspirations profondes. intrinsèques qui nous animent. Comprendre ce qui nous motive permet de mieux
s&apos;épanouir et d&apos;aligner nos missions avec nos aspirations profondes.
</p> </p>
<ul className="space-y-2 text-sm text-muted"> <ul className="space-y-2 text-sm text-muted">
<li className="flex items-start gap-2"> <li className="flex items-start gap-2">
@@ -206,20 +230,20 @@ export default function Home() {
Comment ça marche ? Comment ça marche ?
</h3> </h3>
<div className="grid md:grid-cols-3 gap-4"> <div className="grid md:grid-cols-3 gap-4">
<StepCard <StepCard
number={1} number={1}
title="Classer par importance" title="Classer par importance"
description="Ordonnez les 10 cartes de la moins importante (gauche) à la plus importante (droite) pour vous" description="Ordonnez les 10 cartes de la moins importante (gauche) à la plus importante (droite) pour vous"
/> />
<StepCard <StepCard
number={2} number={2}
title="Évaluer l'influence" title="Évaluer l'influence"
description="Pour chaque motivation, indiquez si votre situation actuelle l'impacte positivement ou négativement" description="Pour chaque motivation, indiquez si votre situation actuelle l'impacte positivement ou négativement"
/> />
<StepCard <StepCard
number={3} number={3}
title="Analyser et discuter" title="Analyser et discuter"
description="Le récapitulatif révèle les motivations clés et les points de vigilance pour un échange constructif" description="Le récapitulatif révèle les motivations clés et les points de vigilance pour un échange constructif"
/> />
</div> </div>
</div> </div>
@@ -279,9 +303,7 @@ function WorkshopCard({
newHref: string; newHref: string;
}) { }) {
return ( return (
<div <div className="group relative overflow-hidden rounded-2xl border-2 border-border bg-card p-8 transition-all hover:border-primary/50 hover:shadow-xl">
className="group relative overflow-hidden rounded-2xl border-2 border-border bg-card p-8 transition-all hover:border-primary/50 hover:shadow-xl"
>
{/* Accent gradient */} {/* Accent gradient */}
<div <div
className="absolute inset-x-0 top-0 h-1 opacity-80" className="absolute inset-x-0 top-0 h-1 opacity-80"
@@ -313,7 +335,12 @@ function WorkshopCard({
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke="currentColor" stroke="currentColor"
> >
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> <path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg> </svg>
{feature} {feature}
</li> </li>
@@ -380,22 +407,16 @@ function StepCard({
); );
} }
function MotivatorPill({ function MotivatorPill({ icon, name, color }: { icon: string; name: string; color: string }) {
icon,
name,
color,
}: {
icon: string;
name: string;
color: string;
}) {
return ( return (
<div <div
className="flex items-center gap-2 px-3 py-1.5 rounded-full" className="flex items-center gap-2 px-3 py-1.5 rounded-full"
style={{ backgroundColor: `${color}15`, border: `1px solid ${color}30` }} style={{ backgroundColor: `${color}15`, border: `1px solid ${color}30` }}
> >
<span>{icon}</span> <span>{icon}</span>
<span className="font-medium" style={{ color }}>{name}</span> <span className="font-medium" style={{ color }}>
{name}
</span>
</div> </div>
); );
} }

View File

@@ -12,9 +12,7 @@ export function PasswordForm() {
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const canSubmit = const canSubmit =
currentPassword.length > 0 && currentPassword.length > 0 && newPassword.length >= 6 && newPassword === confirmPassword;
newPassword.length >= 6 &&
newPassword === confirmPassword;
async function handleSubmit(e: React.FormEvent) { async function handleSubmit(e: React.FormEvent) {
e.preventDefault(); e.preventDefault();
@@ -58,10 +56,7 @@ export function PasswordForm() {
</div> </div>
<div> <div>
<label <label htmlFor="newPassword" className="mb-1.5 block text-sm font-medium text-foreground">
htmlFor="newPassword"
className="mb-1.5 block text-sm font-medium text-foreground"
>
Nouveau mot de passe Nouveau mot de passe
</label> </label>
<Input <Input
@@ -90,17 +85,13 @@ export function PasswordForm() {
required required
/> />
{confirmPassword && newPassword !== confirmPassword && ( {confirmPassword && newPassword !== confirmPassword && (
<p className="mt-1 text-xs text-destructive"> <p className="mt-1 text-xs text-destructive">Les mots de passe ne correspondent pas</p>
Les mots de passe ne correspondent pas
</p>
)} )}
</div> </div>
{message && ( {message && (
<p <p
className={`text-sm ${ className={`text-sm ${message.type === 'success' ? 'text-success' : 'text-destructive'}`}
message.type === 'success' ? 'text-success' : 'text-destructive'
}`}
> >
{message.text} {message.text}
</p> </p>
@@ -112,5 +103,3 @@ export function PasswordForm() {
</form> </form>
); );
} }

View File

@@ -67,9 +67,7 @@ export function ProfileForm({ initialData }: ProfileFormProps) {
{message && ( {message && (
<p <p
className={`text-sm ${ className={`text-sm ${message.type === 'success' ? 'text-success' : 'text-destructive'}`}
message.type === 'success' ? 'text-success' : 'text-destructive'
}`}
> >
{message.text} {message.text}
</p> </p>
@@ -81,5 +79,3 @@ export function ProfileForm({ initialData }: ProfileFormProps) {
</form> </form>
); );
} }

View File

@@ -38,9 +38,7 @@ export default async function ProfilePage() {
{/* Profile Info */} {/* Profile Info */}
<section className="rounded-xl border border-border bg-card p-6"> <section className="rounded-xl border border-border bg-card p-6">
<div className="mb-6 flex items-start justify-between"> <div className="mb-6 flex items-start justify-between">
<h2 className="text-xl font-semibold text-foreground"> <h2 className="text-xl font-semibold text-foreground">Informations personnelles</h2>
Informations personnelles
</h2>
<a <a
href="https://gravatar.com" href="https://gravatar.com"
target="_blank" target="_blank"
@@ -60,17 +58,13 @@ export default async function ProfilePage() {
{/* Password */} {/* Password */}
<section className="rounded-xl border border-border bg-card p-6"> <section className="rounded-xl border border-border bg-card p-6">
<h2 className="mb-6 text-xl font-semibold text-foreground"> <h2 className="mb-6 text-xl font-semibold text-foreground">Changer le mot de passe</h2>
Changer le mot de passe
</h2>
<PasswordForm /> <PasswordForm />
</section> </section>
{/* Account Info */} {/* Account Info */}
<section className="rounded-xl border border-border bg-card p-6"> <section className="rounded-xl border border-border bg-card p-6">
<h2 className="mb-4 text-xl font-semibold text-foreground"> <h2 className="mb-4 text-xl font-semibold text-foreground">Informations du compte</h2>
Informations du compte
</h2>
<div className="space-y-3 text-sm"> <div className="space-y-3 text-sm">
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-muted">ID du compte</span> <span className="text-muted">ID du compte</span>
@@ -92,4 +86,3 @@ export default async function ProfilePage() {
</main> </main>
); );
} }

View File

@@ -3,7 +3,15 @@
import { useState, useTransition } from 'react'; import { useState, useTransition } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { useSearchParams, useRouter } from 'next/navigation'; import { useSearchParams, useRouter } from 'next/navigation';
import { Card, Badge, Button, Modal, ModalFooter, Input, CollaboratorDisplay } from '@/components/ui'; import {
Card,
Badge,
Button,
Modal,
ModalFooter,
Input,
CollaboratorDisplay,
} from '@/components/ui';
import { deleteSwotSession, updateSwotSession } from '@/actions/session'; import { deleteSwotSession, updateSwotSession } from '@/actions/session';
import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving-motivators'; import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving-motivators';
@@ -104,10 +112,10 @@ function getGroupKey(session: AnySession): string {
// Group sessions by participant (using matched user ID when available) // Group sessions by participant (using matched user ID when available)
function groupByPerson(sessions: AnySession[]): Map<string, AnySession[]> { function groupByPerson(sessions: AnySession[]): Map<string, AnySession[]> {
const grouped = new Map<string, AnySession[]>(); const grouped = new Map<string, AnySession[]>();
sessions.forEach((session) => { sessions.forEach((session) => {
const key = getGroupKey(session); const key = getGroupKey(session);
const existing = grouped.get(key); const existing = grouped.get(key);
if (existing) { if (existing) {
existing.push(session); existing.push(session);
@@ -115,24 +123,23 @@ function groupByPerson(sessions: AnySession[]): Map<string, AnySession[]> {
grouped.set(key, [session]); grouped.set(key, [session]);
} }
}); });
// Sort sessions within each group by date // Sort sessions within each group by date
grouped.forEach((sessions) => { grouped.forEach((sessions) => {
sessions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); sessions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
}); });
return grouped; return grouped;
} }
export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsProps) { export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsProps) {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const router = useRouter(); const router = useRouter();
// Get tab from URL or default to 'all' // Get tab from URL or default to 'all'
const tabParam = searchParams.get('tab'); const tabParam = searchParams.get('tab');
const activeTab: WorkshopType = tabParam && VALID_TABS.includes(tabParam as WorkshopType) const activeTab: WorkshopType =
? (tabParam as WorkshopType) tabParam && VALID_TABS.includes(tabParam as WorkshopType) ? (tabParam as WorkshopType) : 'all';
: 'all';
const setActiveTab = (tab: WorkshopType) => { const setActiveTab = (tab: WorkshopType) => {
const params = new URLSearchParams(searchParams.toString()); const params = new URLSearchParams(searchParams.toString());
@@ -205,9 +212,7 @@ export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsPr
{activeTab === 'byPerson' ? ( {activeTab === 'byPerson' ? (
// By Person View // By Person View
sortedPersons.length === 0 ? ( sortedPersons.length === 0 ? (
<div className="text-center py-12 text-muted"> <div className="text-center py-12 text-muted">Aucun atelier pour le moment</div>
Aucun atelier pour le moment
</div>
) : ( ) : (
<div className="space-y-8"> <div className="space-y-8">
{sortedPersons.map(([personKey, sessions]) => { {sortedPersons.map(([personKey, sessions]) => {
@@ -231,9 +236,7 @@ export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsPr
</div> </div>
) )
) : filteredSessions.length === 0 ? ( ) : filteredSessions.length === 0 ? (
<div className="text-center py-12 text-muted"> <div className="text-center py-12 text-muted">Aucun atelier de ce type pour le moment</div>
Aucun atelier de ce type pour le moment
</div>
) : ( ) : (
<div className="space-y-8"> <div className="space-y-8">
{/* My Sessions */} {/* My Sessions */}
@@ -287,9 +290,10 @@ function TabButton({
onClick={onClick} onClick={onClick}
className={` className={`
flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-colors flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-colors
${active ${
? 'bg-primary text-primary-foreground' active
: 'text-muted hover:bg-card-hover hover:text-foreground' ? 'bg-primary text-primary-foreground'
: 'text-muted hover:bg-card-hover hover:text-foreground'
} }
`} `}
> >
@@ -306,7 +310,7 @@ function SessionCard({ session }: { session: AnySession }) {
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false);
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
// Edit form state // Edit form state
const [editTitle, setEditTitle] = useState(session.title); const [editTitle, setEditTitle] = useState(session.title);
const [editParticipant, setEditParticipant] = useState( const [editParticipant, setEditParticipant] = useState(
@@ -328,7 +332,7 @@ function SessionCard({ session }: { session: AnySession }) {
const result = isSwot const result = isSwot
? await deleteSwotSession(session.id) ? await deleteSwotSession(session.id)
: await deleteMotivatorSession(session.id); : await deleteMotivatorSession(session.id);
if (result.success) { if (result.success) {
setShowDeleteModal(false); setShowDeleteModal(false);
} else { } else {
@@ -341,8 +345,11 @@ function SessionCard({ session }: { session: AnySession }) {
startTransition(async () => { startTransition(async () => {
const result = isSwot const result = isSwot
? await updateSwotSession(session.id, { title: editTitle, collaborator: editParticipant }) ? await updateSwotSession(session.id, { title: editTitle, collaborator: editParticipant })
: await updateMotivatorSession(session.id, { title: editTitle, participant: editParticipant }); : await updateMotivatorSession(session.id, {
title: editTitle,
participant: editParticipant,
});
if (result.success) { if (result.success) {
setShowEditModal(false); setShowEditModal(false);
} else { } else {
@@ -372,14 +379,13 @@ function SessionCard({ session }: { session: AnySession }) {
{/* Header: Icon + Title + Role badge */} {/* Header: Icon + Title + Role badge */}
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<span className="text-xl">{icon}</span> <span className="text-xl">{icon}</span>
<h3 className="font-semibold text-foreground line-clamp-1 flex-1"> <h3 className="font-semibold text-foreground line-clamp-1 flex-1">{session.title}</h3>
{session.title}
</h3>
{!session.isOwner && ( {!session.isOwner && (
<span <span
className="text-xs px-1.5 py-0.5 rounded" className="text-xs px-1.5 py-0.5 rounded"
style={{ style={{
backgroundColor: session.role === 'EDITOR' ? 'rgba(6,182,212,0.1)' : 'rgba(234,179,8,0.1)', backgroundColor:
session.role === 'EDITOR' ? 'rgba(6,182,212,0.1)' : 'rgba(234,179,8,0.1)',
color: session.role === 'EDITOR' ? '#06b6d4' : '#eab308', color: session.role === 'EDITOR' ? '#06b6d4' : '#eab308',
}} }}
> >
@@ -390,12 +396,11 @@ function SessionCard({ session }: { session: AnySession }) {
{/* Participant + Owner info */} {/* Participant + Owner info */}
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<CollaboratorDisplay <CollaboratorDisplay collaborator={getResolvedCollaborator(session)} size="sm" />
collaborator={getResolvedCollaborator(session)}
size="sm"
/>
{!session.isOwner && ( {!session.isOwner && (
<span className="text-xs text-muted">· par {session.user.name || session.user.email}</span> <span className="text-xs text-muted">
· par {session.user.name || session.user.email}
</span>
)} )}
</div> </div>
@@ -441,9 +446,7 @@ function SessionCard({ session }: { session: AnySession }) {
</div> </div>
))} ))}
{session.shares.length > 3 && ( {session.shares.length > 3 && (
<span className="text-[10px] text-muted"> <span className="text-[10px] text-muted">+{session.shares.length - 3}</span>
+{session.shares.length - 3}
</span>
)} )}
</div> </div>
</div> </div>
@@ -464,7 +467,12 @@ function SessionCard({ session }: { session: AnySession }) {
title="Modifier" title="Modifier"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /> <path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg> </svg>
</button> </button>
<button <button
@@ -477,7 +485,12 @@ function SessionCard({ session }: { session: AnySession }) {
title="Supprimer" title="Supprimer"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg className="w-4 h-4" 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" /> <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> </svg>
</button> </button>
</div> </div>
@@ -511,7 +524,10 @@ function SessionCard({ session }: { session: AnySession }) {
/> />
</div> </div>
<div> <div>
<label htmlFor="edit-participant" className="block text-sm font-medium text-foreground mb-1"> <label
htmlFor="edit-participant"
className="block text-sm font-medium text-foreground mb-1"
>
{isSwot ? 'Collaborateur' : 'Participant'} {isSwot ? 'Collaborateur' : 'Participant'}
</label> </label>
<Input <Input
@@ -550,24 +566,17 @@ function SessionCard({ session }: { session: AnySession }) {
> >
<div className="space-y-4"> <div className="space-y-4">
<p className="text-muted"> <p className="text-muted">
Êtes-vous sûr de vouloir supprimer l&apos;atelier <strong className="text-foreground">&quot;{session.title}&quot;</strong> ? Êtes-vous sûr de vouloir supprimer l&apos;atelier{' '}
<strong className="text-foreground">&quot;{session.title}&quot;</strong> ?
</p> </p>
<p className="text-sm text-destructive"> <p className="text-sm text-destructive">
Cette action est irréversible. Toutes les données seront perdues. Cette action est irréversible. Toutes les données seront perdues.
</p> </p>
<ModalFooter> <ModalFooter>
<Button <Button variant="ghost" onClick={() => setShowDeleteModal(false)} disabled={isPending}>
variant="ghost"
onClick={() => setShowDeleteModal(false)}
disabled={isPending}
>
Annuler Annuler
</Button> </Button>
<Button <Button variant="destructive" onClick={handleDelete} disabled={isPending}>
variant="destructive"
onClick={handleDelete}
disabled={isPending}
>
{isPending ? 'Suppression...' : 'Supprimer'} {isPending ? 'Suppression...' : 'Supprimer'}
</Button> </Button>
</ModalFooter> </ModalFooter>
@@ -576,4 +585,3 @@ function SessionCard({ session }: { session: AnySession }) {
</> </>
); );
} }

View File

@@ -80,13 +80,8 @@ export default async function SessionPage({ params }: SessionPageProps) {
isOwner={session.isOwner} isOwner={session.isOwner}
canEdit={session.canEdit} canEdit={session.canEdit}
> >
<SwotBoard <SwotBoard sessionId={session.id} items={session.items} actions={session.actions} />
sessionId={session.id}
items={session.items}
actions={session.actions}
/>
</SessionLiveWrapper> </SessionLiveWrapper>
</main> </main>
); );
} }

View File

@@ -2,7 +2,15 @@
import { useState } from 'react'; import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Card, CardHeader, CardTitle, CardDescription, CardContent, Button, Input } from '@/components/ui'; import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
Button,
Input,
} from '@/components/ui';
export default function NewSessionPage() { export default function NewSessionPage() {
const router = useRouter(); const router = useRouter();
@@ -100,4 +108,3 @@ export default function NewSessionPage() {
</main> </main>
); );
} }

View File

@@ -62,9 +62,7 @@ export default async function SessionsPage() {
<div className="mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-4"> <div className="mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div> <div>
<h1 className="text-3xl font-bold text-foreground">Mes Ateliers</h1> <h1 className="text-3xl font-bold text-foreground">Mes Ateliers</h1>
<p className="mt-1 text-muted"> <p className="mt-1 text-muted">Tous vos ateliers en un seul endroit</p>
Tous vos ateliers en un seul endroit
</p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Link href="/sessions/new"> <Link href="/sessions/new">
@@ -90,7 +88,8 @@ export default async function SessionsPage() {
Commencez votre premier atelier Commencez votre premier atelier
</h2> </h2>
<p className="text-muted mb-6 max-w-md mx-auto"> <p className="text-muted mb-6 max-w-md mx-auto">
Créez un atelier SWOT pour analyser les forces et faiblesses, ou un Moving Motivators pour découvrir les motivations de vos collaborateurs. Créez un atelier SWOT pour analyser les forces et faiblesses, ou un Moving Motivators
pour découvrir les motivations de vos collaborateurs.
</p> </p>
<div className="flex gap-3 justify-center"> <div className="flex gap-3 justify-center">
<Link href="/sessions/new"> <Link href="/sessions/new">
@@ -109,10 +108,7 @@ export default async function SessionsPage() {
</Card> </Card>
) : ( ) : (
<Suspense fallback={<WorkshopTabsSkeleton />}> <Suspense fallback={<WorkshopTabsSkeleton />}>
<WorkshopTabs <WorkshopTabs swotSessions={allSwotSessions} motivatorSessions={allMotivatorSessions} />
swotSessions={allSwotSessions}
motivatorSessions={allMotivatorSessions}
/>
</Suspense> </Suspense>
)} )}
</main> </main>

View File

@@ -54,16 +54,13 @@ export default async function UsersPage() {
<div className="text-sm text-muted">Sessions totales</div> <div className="text-sm text-muted">Sessions totales</div>
</div> </div>
<div className="rounded-xl border border-border bg-card p-4"> <div className="rounded-xl border border-border bg-card p-4">
<div className="text-2xl font-bold text-opportunity"> <div className="text-2xl font-bold text-opportunity">{avgSessionsPerUser.toFixed(1)}</div>
{avgSessionsPerUser.toFixed(1)}
</div>
<div className="text-sm text-muted">Moy. par user</div> <div className="text-sm text-muted">Moy. par user</div>
</div> </div>
<div className="rounded-xl border border-border bg-card p-4"> <div className="rounded-xl border border-border bg-card p-4">
<div className="text-2xl font-bold text-accent"> <div className="text-2xl font-bold text-accent">
{users.reduce( {users.reduce(
(acc, u) => (acc, u) => acc + u._count.sharedSessions + u._count.sharedMotivatorSessions,
acc + u._count.sharedSessions + u._count.sharedMotivatorSessions,
0 0
)} )}
</div> </div>
@@ -74,10 +71,8 @@ export default async function UsersPage() {
{/* Users List */} {/* Users List */}
<div className="space-y-3"> <div className="space-y-3">
{users.map((user) => { {users.map((user) => {
const totalUserSessions = const totalUserSessions = user._count.sessions + user._count.motivatorSessions;
user._count.sessions + user._count.motivatorSessions; const totalShares = user._count.sharedSessions + user._count.sharedMotivatorSessions;
const totalShares =
user._count.sharedSessions + user._count.sharedMotivatorSessions;
const isCurrentUser = user.id === session.user?.id; const isCurrentUser = user.id === session.user?.id;
return ( return (
@@ -194,16 +189,12 @@ export default async function UsersPage() {
<div className="text-sm font-medium text-foreground"> <div className="text-sm font-medium text-foreground">
{totalUserSessions} session{totalUserSessions !== 1 ? 's' : ''} {totalUserSessions} session{totalUserSessions !== 1 ? 's' : ''}
</div> </div>
<div className="text-xs text-muted"> <div className="text-xs text-muted">{formatRelativeTime(user.createdAt)}</div>
{formatRelativeTime(user.createdAt)}
</div>
</div> </div>
{/* Date Info */} {/* Date Info */}
<div className="hidden flex-col items-end sm:flex"> <div className="hidden flex-col items-end sm:flex">
<div className="text-sm text-foreground"> <div className="text-sm text-foreground">{formatRelativeTime(user.createdAt)}</div>
{formatRelativeTime(user.createdAt)}
</div>
<div className="text-xs text-muted"> <div className="text-xs text-muted">
{new Date(user.createdAt).toLocaleDateString('fr-FR')} {new Date(user.createdAt).toLocaleDateString('fr-FR')}
</div> </div>
@@ -217,9 +208,7 @@ export default async function UsersPage() {
{users.length === 0 && ( {users.length === 0 && (
<div className="flex flex-col items-center justify-center rounded-xl border border-border bg-card py-16"> <div className="flex flex-col items-center justify-center rounded-xl border border-border bg-card py-16">
<div className="text-4xl">👥</div> <div className="text-4xl">👥</div>
<div className="mt-4 text-lg font-medium text-foreground"> <div className="mt-4 text-lg font-medium text-foreground">Aucun utilisateur</div>
Aucun utilisateur
</div>
<div className="mt-1 text-sm text-muted"> <div className="mt-1 text-sm text-muted">
Les utilisateurs apparaîtront ici une fois inscrits Les utilisateurs apparaîtront ici une fois inscrits
</div> </div>
@@ -228,4 +217,3 @@ export default async function UsersPage() {
</main> </main>
); );
} }

View File

@@ -18,19 +18,13 @@ export function LiveIndicator({ isConnected, error }: LiveIndicatorProps) {
return ( return (
<div <div
className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-sm transition-colors ${ className={`flex items-center gap-2 rounded-full px-3 py-1.5 text-sm transition-colors ${
isConnected isConnected ? 'bg-success/10 text-success' : 'bg-yellow/10 text-yellow'
? 'bg-success/10 text-success'
: 'bg-yellow/10 text-yellow'
}`} }`}
> >
<span <span
className={`h-2 w-2 rounded-full ${ className={`h-2 w-2 rounded-full ${isConnected ? 'bg-success animate-pulse' : 'bg-yellow'}`}
isConnected ? 'bg-success animate-pulse' : 'bg-yellow'
}`}
/> />
<span>{isConnected ? 'Live' : 'Connexion...'}</span> <span>{isConnected ? 'Live' : 'Connexion...'}</span>
</div> </div>
); );
} }

View File

@@ -64,7 +64,7 @@ export function SessionLiveWrapper({
<div className="mb-4 flex items-center justify-between rounded-lg border border-border bg-card p-3"> <div className="mb-4 flex items-center justify-between rounded-lg border border-border bg-card p-3">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<LiveIndicator isConnected={isConnected} error={error} /> <LiveIndicator isConnected={isConnected} error={error} />
{lastEventUser && ( {lastEventUser && (
<div className="flex items-center gap-2 text-sm text-muted animate-pulse"> <div className="flex items-center gap-2 text-sm text-muted animate-pulse">
<span></span> <span></span>
@@ -101,11 +101,7 @@ export function SessionLiveWrapper({
</div> </div>
)} )}
<Button <Button variant="outline" size="sm" onClick={() => setShareModalOpen(true)}>
variant="outline"
size="sm"
onClick={() => setShareModalOpen(true)}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -120,9 +116,7 @@ export function SessionLiveWrapper({
</div> </div>
{/* Content */} {/* Content */}
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}> <div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
{children}
</div>
{/* Share Modal */} {/* Share Modal */}
<ShareModal <ShareModal
@@ -136,5 +130,3 @@ export function SessionLiveWrapper({
</> </>
); );
} }

View File

@@ -105,14 +105,10 @@ export function ShareModal({
{/* Current shares */} {/* Current shares */}
<div className="space-y-2"> <div className="space-y-2">
<p className="text-sm font-medium text-foreground"> <p className="text-sm font-medium text-foreground">Collaborateurs ({shares.length})</p>
Collaborateurs ({shares.length})
</p>
{shares.length === 0 ? ( {shares.length === 0 ? (
<p className="text-sm text-muted"> <p className="text-sm text-muted">Aucun collaborateur pour le moment</p>
Aucun collaborateur pour le moment
</p>
) : ( ) : (
<ul className="space-y-2"> <ul className="space-y-2">
{shares.map((share) => ( {shares.map((share) => (
@@ -126,12 +122,10 @@ export function ShareModal({
<p className="text-sm font-medium text-foreground"> <p className="text-sm font-medium text-foreground">
{share.user.name || share.user.email} {share.user.name || share.user.email}
</p> </p>
{share.user.name && ( {share.user.name && <p className="text-xs text-muted">{share.user.email}</p>}
<p className="text-xs text-muted">{share.user.email}</p>
)}
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant={share.role === 'EDITOR' ? 'primary' : 'default'}> <Badge variant={share.role === 'EDITOR' ? 'primary' : 'default'}>
{share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'} {share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'}
@@ -176,5 +170,3 @@ export function ShareModal({
</Modal> </Modal>
); );
} }

View File

@@ -1,4 +1,3 @@
export { LiveIndicator } from './LiveIndicator'; export { LiveIndicator } from './LiveIndicator';
export { ShareModal } from './ShareModal'; export { ShareModal } from './ShareModal';
export { SessionLiveWrapper } from './SessionLiveWrapper'; export { SessionLiveWrapper } from './SessionLiveWrapper';

View File

@@ -112,11 +112,7 @@ export function Header() {
onClick={() => setMenuOpen(!menuOpen)} onClick={() => setMenuOpen(!menuOpen)}
className="flex h-9 items-center gap-2 rounded-lg border border-border bg-card pl-1.5 pr-3 transition-colors hover:bg-card-hover" className="flex h-9 items-center gap-2 rounded-lg border border-border bg-card pl-1.5 pr-3 transition-colors hover:bg-card-hover"
> >
<Avatar <Avatar email={session.user.email!} name={session.user.name} size={24} />
email={session.user.email!}
name={session.user.name}
size={24}
/>
<span className="text-sm font-medium text-foreground"> <span className="text-sm font-medium text-foreground">
{session.user.name || session.user.email?.split('@')[0]} {session.user.name || session.user.email?.split('@')[0]}
</span> </span>
@@ -137,10 +133,7 @@ export function Header() {
{menuOpen && ( {menuOpen && (
<> <>
<div <div className="fixed inset-0 z-10" onClick={() => setMenuOpen(false)} />
className="fixed inset-0 z-10"
onClick={() => setMenuOpen(false)}
/>
<div className="absolute right-0 z-20 mt-2 w-48 rounded-lg border border-border bg-card py-1 shadow-lg"> <div className="absolute right-0 z-20 mt-2 w-48 rounded-lg border border-border bg-card py-1 shadow-lg">
<div className="border-b border-border px-4 py-2"> <div className="border-b border-border px-4 py-2">
<p className="text-xs text-muted">Connecté en tant que</p> <p className="text-xs text-muted">Connecté en tant que</p>

View File

@@ -140,4 +140,3 @@ function InfluenceSlider({
</div> </div>
); );
} }

View File

@@ -66,14 +66,15 @@ export function MotivatorBoard({ sessionId, cards: initialCards, canEdit }: Moti
// Persist to server // Persist to server
startTransition(async () => { startTransition(async () => {
await reorderMotivatorCards(sessionId, newCards.map((c) => c.id)); await reorderMotivatorCards(
sessionId,
newCards.map((c) => c.id)
);
}); });
} }
function handleInfluenceChange(cardId: string, influence: number) { function handleInfluenceChange(cardId: string, influence: number) {
setCards((prev) => setCards((prev) => prev.map((c) => (c.id === cardId ? { ...c, influence } : c)));
prev.map((c) => (c.id === cardId ? { ...c, influence } : c))
);
startTransition(async () => { startTransition(async () => {
await updateCardInfluence(cardId, sessionId, influence); await updateCardInfluence(cardId, sessionId, influence);
@@ -151,11 +152,7 @@ export function MotivatorBoard({ sessionId, cards: initialCards, canEdit }: Moti
> >
<div className="flex gap-2 min-w-max px-2"> <div className="flex gap-2 min-w-max px-2">
{sortedCards.map((card) => ( {sortedCards.map((card) => (
<MotivatorCard <MotivatorCard key={card.id} card={card} disabled={!canEdit} />
key={card.id}
card={card}
disabled={!canEdit}
/>
))} ))}
</div> </div>
</SortableContext> </SortableContext>
@@ -182,7 +179,8 @@ export function MotivatorBoard({ sessionId, cards: initialCards, canEdit }: Moti
Évaluez l&apos;influence de chaque motivation Évaluez l&apos;influence de chaque motivation
</h2> </h2>
<p className="text-muted"> <p className="text-muted">
Pour chaque carte, indiquez si cette motivation a une influence positive ou négative sur votre situation actuelle Pour chaque carte, indiquez si cette motivation a une influence positive ou négative
sur votre situation actuelle
</p> </p>
</div> </div>
@@ -216,9 +214,7 @@ export function MotivatorBoard({ sessionId, cards: initialCards, canEdit }: Moti
<h2 className="text-xl font-semibold text-foreground mb-2"> <h2 className="text-xl font-semibold text-foreground mb-2">
Récapitulatif de vos Moving Motivators Récapitulatif de vos Moving Motivators
</h2> </h2>
<p className="text-muted"> <p className="text-muted">Voici l&apos;analyse de vos motivations et leur impact</p>
Voici l&apos;analyse de vos motivations et leur impact
</p>
</div> </div>
<MotivatorSummary cards={sortedCards} /> <MotivatorSummary cards={sortedCards} />
@@ -273,4 +269,3 @@ function StepIndicator({
</button> </button>
); );
} }

View File

@@ -19,14 +19,7 @@ export function MotivatorCard({
}: MotivatorCardProps) { }: MotivatorCardProps) {
const config = MOTIVATOR_BY_TYPE[card.type]; const config = MOTIVATOR_BY_TYPE[card.type];
const { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id: card.id, id: card.id,
disabled, disabled,
}); });
@@ -62,10 +55,7 @@ export function MotivatorCard({
<div className="text-3xl mb-1 mt-2">{config.icon}</div> <div className="text-3xl mb-1 mt-2">{config.icon}</div>
{/* Name */} {/* Name */}
<div <div className="font-semibold text-sm text-center px-2" style={{ color: config.color }}>
className="font-semibold text-sm text-center px-2"
style={{ color: config.color }}
>
{config.name} {config.name}
</div> </div>
@@ -129,9 +119,7 @@ export function MotivatorCardStatic({
/> />
{/* Icon */} {/* Icon */}
<div className={`mb-1 mt-2 ${size === 'small' ? 'text-xl' : 'text-3xl'}`}> <div className={`mb-1 mt-2 ${size === 'small' ? 'text-xl' : 'text-3xl'}`}>{config.icon}</div>
{config.icon}
</div>
{/* Name */} {/* Name */}
<div <div
@@ -169,4 +157,3 @@ export function MotivatorCardStatic({
</div> </div>
); );
} }

View File

@@ -64,7 +64,7 @@ export function MotivatorLiveWrapper({
<div className="mb-4 flex items-center justify-between rounded-lg border border-border bg-card p-3"> <div className="mb-4 flex items-center justify-between rounded-lg border border-border bg-card p-3">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<LiveIndicator isConnected={isConnected} error={error} /> <LiveIndicator isConnected={isConnected} error={error} />
{lastEventUser && ( {lastEventUser && (
<div className="flex items-center gap-2 text-sm text-muted animate-pulse"> <div className="flex items-center gap-2 text-sm text-muted animate-pulse">
<span></span> <span></span>
@@ -101,11 +101,7 @@ export function MotivatorLiveWrapper({
</div> </div>
)} )}
<Button <Button variant="outline" size="sm" onClick={() => setShareModalOpen(true)}>
variant="outline"
size="sm"
onClick={() => setShareModalOpen(true)}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -120,9 +116,7 @@ export function MotivatorLiveWrapper({
</div> </div>
{/* Content */} {/* Content */}
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}> <div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
{children}
</div>
{/* Share Modal */} {/* Share Modal */}
<MotivatorShareModal <MotivatorShareModal
@@ -136,4 +130,3 @@ export function MotivatorLiveWrapper({
</> </>
); );
} }

View File

@@ -105,14 +105,10 @@ export function MotivatorShareModal({
{/* Current shares */} {/* Current shares */}
<div className="space-y-2"> <div className="space-y-2">
<p className="text-sm font-medium text-foreground"> <p className="text-sm font-medium text-foreground">Collaborateurs ({shares.length})</p>
Collaborateurs ({shares.length})
</p>
{shares.length === 0 ? ( {shares.length === 0 ? (
<p className="text-sm text-muted"> <p className="text-sm text-muted">Aucun collaborateur pour le moment</p>
Aucun collaborateur pour le moment
</p>
) : ( ) : (
<ul className="space-y-2"> <ul className="space-y-2">
{shares.map((share) => ( {shares.map((share) => (
@@ -126,12 +122,10 @@ export function MotivatorShareModal({
<p className="text-sm font-medium text-foreground"> <p className="text-sm font-medium text-foreground">
{share.user.name || share.user.email} {share.user.name || share.user.email}
</p> </p>
{share.user.name && ( {share.user.name && <p className="text-xs text-muted">{share.user.email}</p>}
<p className="text-xs text-muted">{share.user.email}</p>
)}
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant={share.role === 'EDITOR' ? 'primary' : 'default'}> <Badge variant={share.role === 'EDITOR' ? 'primary' : 'default'}>
{share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'} {share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'}
@@ -176,4 +170,3 @@ export function MotivatorShareModal({
</Modal> </Modal>
); );
} }

View File

@@ -18,10 +18,14 @@ export function MotivatorSummary({ cards }: MotivatorSummaryProps) {
const bottom3 = sortedByImportance.slice(0, 3); const bottom3 = sortedByImportance.slice(0, 3);
// Cards with positive influence // Cards with positive influence
const positiveInfluence = cards.filter((c) => c.influence > 0).sort((a, b) => b.influence - a.influence); const positiveInfluence = cards
.filter((c) => c.influence > 0)
.sort((a, b) => b.influence - a.influence);
// Cards with negative influence // Cards with negative influence
const negativeInfluence = cards.filter((c) => c.influence < 0).sort((a, b) => a.influence - b.influence); const negativeInfluence = cards
.filter((c) => c.influence < 0)
.sort((a, b) => a.influence - b.influence);
return ( return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
@@ -100,4 +104,3 @@ function SummarySection({
</div> </div>
); );
} }

View File

@@ -4,4 +4,3 @@ export { MotivatorSummary } from './MotivatorSummary';
export { InfluenceZone } from './InfluenceZone'; export { InfluenceZone } from './InfluenceZone';
export { MotivatorLiveWrapper } from './MotivatorLiveWrapper'; export { MotivatorLiveWrapper } from './MotivatorLiveWrapper';
export { MotivatorShareModal } from './MotivatorShareModal'; export { MotivatorShareModal } from './MotivatorShareModal';

View File

@@ -103,4 +103,3 @@ export function EditableTitle({ sessionId, initialTitle, isOwner }: EditableTitl
</button> </button>
); );
} }

View File

@@ -1,2 +1 @@
export { EditableTitle } from './EditableTitle'; export { EditableTitle } from './EditableTitle';

View File

@@ -22,7 +22,10 @@ interface ActionPanelProps {
onActionLeave: () => void; onActionLeave: () => void;
} }
const categoryBadgeVariant: Record<SwotCategory, 'strength' | 'weakness' | 'opportunity' | 'threat'> = { const categoryBadgeVariant: Record<
SwotCategory,
'strength' | 'weakness' | 'opportunity' | 'threat'
> = {
STRENGTH: 'strength', STRENGTH: 'strength',
WEAKNESS: 'weakness', WEAKNESS: 'weakness',
OPPORTUNITY: 'opportunity', OPPORTUNITY: 'opportunity',
@@ -189,7 +192,12 @@ export function ActionPanel({
className="rounded p-1 text-muted opacity-0 transition-opacity hover:bg-card-hover hover:text-foreground group-hover:opacity-100" className="rounded p-1 text-muted opacity-0 transition-opacity hover:bg-card-hover hover:text-foreground group-hover:opacity-100"
aria-label="Modifier" aria-label="Modifier"
> >
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path <path
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
@@ -203,7 +211,12 @@ export function ActionPanel({
className="rounded p-1 text-muted opacity-0 transition-opacity hover:bg-destructive/10 hover:text-destructive group-hover:opacity-100" className="rounded p-1 text-muted opacity-0 transition-opacity hover:bg-destructive/10 hover:text-destructive group-hover:opacity-100"
aria-label="Supprimer" aria-label="Supprimer"
> >
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path <path
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
@@ -271,7 +284,7 @@ export function ActionPanel({
<Modal <Modal
isOpen={showModal} isOpen={showModal}
onClose={closeModal} onClose={closeModal}
title={editingAction ? 'Modifier l\'action' : 'Nouvelle action croisée'} title={editingAction ? "Modifier l'action" : 'Nouvelle action croisée'}
size="lg" size="lg"
> >
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
@@ -339,7 +352,7 @@ export function ActionPanel({
Annuler Annuler
</Button> </Button>
<Button type="submit" loading={isPending}> <Button type="submit" loading={isPending}>
{editingAction ? 'Enregistrer' : 'Créer l\'action'} {editingAction ? 'Enregistrer' : "Créer l'action"}
</Button> </Button>
</ModalFooter> </ModalFooter>
</form> </form>
@@ -347,4 +360,3 @@ export function ActionPanel({
</div> </div>
); );
} }

View File

@@ -11,22 +11,17 @@ interface HelpContent {
const HELP_CONTENT: Record<SwotCategory, HelpContent> = { const HELP_CONTENT: Record<SwotCategory, HelpContent> = {
STRENGTH: { STRENGTH: {
description: description: 'Les atouts internes et qualités qui distinguent positivement.',
'Les atouts internes et qualités qui distinguent positivement.',
examples: [ examples: [
'Expertise technique solide', 'Expertise technique solide',
'Excellentes capacités de communication', 'Excellentes capacités de communication',
'Leadership naturel', 'Leadership naturel',
'Rigueur et organisation', 'Rigueur et organisation',
], ],
questions: [ questions: ["Qu'est-ce qui le/la distingue ?", 'Quels retours positifs reçoit-il/elle ?'],
'Qu\'est-ce qui le/la distingue ?',
'Quels retours positifs reçoit-il/elle ?',
],
}, },
WEAKNESS: { WEAKNESS: {
description: description: "Les axes d'amélioration et points à travailler.",
'Les axes d\'amélioration et points à travailler.',
examples: [ examples: [
'Difficulté à déléguer', 'Difficulté à déléguer',
'Gestion du stress à améliorer', 'Gestion du stress à améliorer',
@@ -39,22 +34,17 @@ const HELP_CONTENT: Record<SwotCategory, HelpContent> = {
], ],
}, },
OPPORTUNITY: { OPPORTUNITY: {
description: description: 'Les facteurs externes favorables à saisir.',
'Les facteurs externes favorables à saisir.',
examples: [ examples: [
'Nouveau projet stratégique', 'Nouveau projet stratégique',
'Formation disponible', 'Formation disponible',
'Poste ouvert en interne', 'Poste ouvert en interne',
'Mentor potentiel identifié', 'Mentor potentiel identifié',
], ],
questions: [ questions: ["Quelles évolutions pourraient l'aider ?", 'Quelles ressources sont disponibles ?'],
'Quelles évolutions pourraient l\'aider ?',
'Quelles ressources sont disponibles ?',
],
}, },
THREAT: { THREAT: {
description: description: 'Les risques externes à anticiper.',
'Les risques externes à anticiper.',
examples: [ examples: [
'Réorganisation menaçant le poste', 'Réorganisation menaçant le poste',
'Compétences devenant obsolètes', 'Compétences devenant obsolètes',
@@ -82,9 +72,10 @@ export function QuadrantHelp({ category }: QuadrantHelpProps) {
className={` className={`
flex h-5 w-5 items-center justify-center rounded-full flex h-5 w-5 items-center justify-center rounded-full
text-xs font-medium transition-all text-xs font-medium transition-all
${isOpen ${
? 'bg-foreground/20 text-foreground rotate-45' isOpen
: 'bg-foreground/5 text-muted hover:bg-foreground/10 hover:text-foreground' ? 'bg-foreground/20 text-foreground rotate-45'
: 'bg-foreground/5 text-muted hover:bg-foreground/10 hover:text-foreground'
} }
`} `}
aria-label="Aide" aria-label="Aide"
@@ -113,9 +104,7 @@ export function QuadrantHelpPanel({ category, isOpen }: QuadrantHelpPanelProps)
<div className="overflow-hidden"> <div className="overflow-hidden">
<div className="rounded-lg bg-white/40 dark:bg-black/20 p-3 mb-3"> <div className="rounded-lg bg-white/40 dark:bg-black/20 p-3 mb-3">
{/* Description */} {/* Description */}
<p className="text-xs text-foreground/80 leading-relaxed"> <p className="text-xs text-foreground/80 leading-relaxed">{content.description}</p>
{content.description}
</p>
<div className="mt-3 flex gap-4"> <div className="mt-3 flex gap-4">
{/* Examples */} {/* Examples */}
@@ -125,10 +114,7 @@ export function QuadrantHelpPanel({ category, isOpen }: QuadrantHelpPanelProps)
</h4> </h4>
<ul className="space-y-0.5"> <ul className="space-y-0.5">
{content.examples.map((example, i) => ( {content.examples.map((example, i) => (
<li <li key={i} className="flex items-start gap-1.5 text-xs text-foreground/70">
key={i}
className="flex items-start gap-1.5 text-xs text-foreground/70"
>
<span className="mt-1.5 h-1 w-1 flex-shrink-0 rounded-full bg-current opacity-50" /> <span className="mt-1.5 h-1 w-1 flex-shrink-0 rounded-full bg-current opacity-50" />
{example} {example}
</li> </li>
@@ -143,10 +129,7 @@ export function QuadrantHelpPanel({ category, isOpen }: QuadrantHelpPanelProps)
</h4> </h4>
<ul className="space-y-1"> <ul className="space-y-1">
{content.questions.map((question, i) => ( {content.questions.map((question, i) => (
<li <li key={i} className="text-xs italic text-foreground/60">
key={i}
className="text-xs italic text-foreground/60"
>
{question} {question}
</li> </li>
))} ))}
@@ -158,4 +141,3 @@ export function QuadrantHelpPanel({ category, isOpen }: QuadrantHelpPanelProps)
</div> </div>
); );
} }

View File

@@ -1,12 +1,7 @@
'use client'; 'use client';
import { useState, useTransition } from 'react'; import { useState, useTransition } from 'react';
import { import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd';
DragDropContext,
Droppable,
Draggable,
DropResult,
} from '@hello-pangea/dnd';
import type { SwotItem, Action, ActionLink, SwotCategory } from '@prisma/client'; import type { SwotItem, Action, ActionLink, SwotCategory } from '@prisma/client';
import { SwotQuadrant } from './SwotQuadrant'; import { SwotQuadrant } from './SwotQuadrant';
import { SwotCard } from './SwotCard'; import { SwotCard } from './SwotCard';
@@ -67,9 +62,7 @@ export function SwotBoard({ sessionId, items, actions }: SwotBoardProps) {
if (!linkMode) return; if (!linkMode) return;
setSelectedItems((prev) => setSelectedItems((prev) =>
prev.includes(itemId) prev.includes(itemId) ? prev.filter((id) => id !== itemId) : [...prev, itemId]
? prev.filter((id) => id !== itemId)
: [...prev, itemId]
); );
} }
@@ -167,4 +160,3 @@ export function SwotBoard({ sessionId, items, actions }: SwotBoardProps) {
</div> </div>
); );
} }

View File

@@ -120,7 +120,12 @@ export const SwotCard = forwardRef<HTMLDivElement, SwotCardProps>(
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 className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path <path
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
@@ -137,7 +142,12 @@ export const SwotCard = forwardRef<HTMLDivElement, SwotCardProps>(
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 className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path <path
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
@@ -154,7 +164,12 @@ export const SwotCard = forwardRef<HTMLDivElement, SwotCardProps>(
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 className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path <path
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
@@ -168,7 +183,9 @@ export const SwotCard = forwardRef<HTMLDivElement, SwotCardProps>(
{/* Selection indicator in link mode */} {/* Selection indicator in link mode */}
{linkMode && isSelected && ( {linkMode && isSelected && (
<div className={`absolute -right-1 -top-1 rounded-full bg-card p-0.5 shadow ${styles.text}`}> <div
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"> <svg className="h-4 w-4" 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" /> <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
</svg> </svg>
@@ -182,4 +199,3 @@ export const SwotCard = forwardRef<HTMLDivElement, SwotCardProps>(
); );
SwotCard.displayName = 'SwotCard'; SwotCard.displayName = 'SwotCard';

View File

@@ -92,9 +92,10 @@ export const SwotQuadrant = forwardRef<HTMLDivElement, SwotQuadrantProps>(
className={` className={`
flex h-5 w-5 items-center justify-center rounded-full flex h-5 w-5 items-center justify-center rounded-full
text-xs font-medium transition-all text-xs font-medium transition-all
${showHelp ${
? 'bg-foreground/20 text-foreground' showHelp
: 'bg-foreground/5 text-muted hover:bg-foreground/10 hover:text-foreground' ? 'bg-foreground/20 text-foreground'
: 'bg-foreground/5 text-muted hover:bg-foreground/10 hover:text-foreground'
} }
`} `}
aria-label="Aide" aria-label="Aide"
@@ -112,7 +113,12 @@ 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"> <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> <path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg> </svg>
</button> </button>
</div> </div>
@@ -166,4 +172,3 @@ export const SwotQuadrant = forwardRef<HTMLDivElement, SwotQuadrantProps>(
); );
SwotQuadrant.displayName = 'SwotQuadrant'; SwotQuadrant.displayName = 'SwotQuadrant';

View File

@@ -3,4 +3,3 @@ export { SwotQuadrant } from './SwotQuadrant';
export { SwotCard } from './SwotCard'; export { SwotCard } from './SwotCard';
export { ActionPanel } from './ActionPanel'; export { ActionPanel } from './ActionPanel';
export { QuadrantHelp } from './QuadrantHelp'; export { QuadrantHelp } from './QuadrantHelp';

View File

@@ -28,5 +28,3 @@ export function Avatar({
/> />
); );
} }

View File

@@ -49,4 +49,3 @@ export const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
); );
Badge.displayName = 'Badge'; Badge.displayName = 'Badge';

View File

@@ -10,16 +10,11 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
} }
const variantStyles: Record<ButtonVariant, string> = { const variantStyles: Record<ButtonVariant, string> = {
primary: primary: 'bg-primary text-primary-foreground hover:bg-primary-hover border-transparent',
'bg-primary text-primary-foreground hover:bg-primary-hover border-transparent', secondary: 'bg-card text-foreground hover:bg-card-hover border-border',
secondary: outline: 'bg-transparent text-foreground hover:bg-card-hover border-border',
'bg-card text-foreground hover:bg-card-hover border-border', ghost: 'bg-transparent text-foreground hover:bg-card-hover border-transparent',
outline: destructive: 'bg-destructive text-white hover:bg-destructive/90 border-transparent',
'bg-transparent text-foreground hover:bg-card-hover border-border',
ghost:
'bg-transparent text-foreground hover:bg-card-hover border-transparent',
destructive:
'bg-destructive text-white hover:bg-destructive/90 border-transparent',
}; };
const sizeStyles: Record<ButtonSize, string> = { const sizeStyles: Record<ButtonSize, string> = {
@@ -29,7 +24,10 @@ const sizeStyles: Record<ButtonSize, string> = {
}; };
export const Button = forwardRef<HTMLButtonElement, ButtonProps>( export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className = '', variant = 'primary', size = 'md', loading, disabled, children, ...props }, ref) => { (
{ className = '', variant = 'primary', size = 'md', loading, disabled, children, ...props },
ref
) => {
return ( return (
<button <button
ref={ref} ref={ref}
@@ -74,4 +72,3 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
); );
Button.displayName = 'Button'; Button.displayName = 'Button';

View File

@@ -40,11 +40,12 @@ export const CardTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadi
CardTitle.displayName = 'CardTitle'; CardTitle.displayName = 'CardTitle';
export const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>( export const CardDescription = forwardRef<
({ className = '', ...props }, ref) => ( HTMLParagraphElement,
<p ref={ref} className={`mt-1 text-sm text-muted ${className}`} {...props} /> HTMLAttributes<HTMLParagraphElement>
) >(({ className = '', ...props }, ref) => (
); <p ref={ref} className={`mt-1 text-sm text-muted ${className}`} {...props} />
));
CardDescription.displayName = 'CardDescription'; CardDescription.displayName = 'CardDescription';
@@ -63,4 +64,3 @@ export const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivEleme
); );
CardFooter.displayName = 'CardFooter'; CardFooter.displayName = 'CardFooter';

View File

@@ -79,5 +79,3 @@ export function CollaboratorDisplay({
</div> </div>
); );
} }

View File

@@ -12,10 +12,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
return ( return (
<div className="w-full"> <div className="w-full">
{label && ( {label && (
<label <label htmlFor={inputId} className="mb-2 block text-sm font-medium text-foreground">
htmlFor={inputId}
className="mb-2 block text-sm font-medium text-foreground"
>
{label} {label}
</label> </label>
)} )}
@@ -32,13 +29,10 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
`} `}
{...props} {...props}
/> />
{error && ( {error && <p className="mt-1.5 text-sm text-destructive">{error}</p>}
<p className="mt-1.5 text-sm text-destructive">{error}</p>
)}
</div> </div>
); );
} }
); );
Input.displayName = 'Input'; Input.displayName = 'Input';

View File

@@ -84,12 +84,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 <svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path <path
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"

View File

@@ -12,10 +12,7 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
return ( return (
<div className="w-full"> <div className="w-full">
{label && ( {label && (
<label <label htmlFor={textareaId} className="mb-2 block text-sm font-medium text-foreground">
htmlFor={textareaId}
className="mb-2 block text-sm font-medium text-foreground"
>
{label} {label}
</label> </label>
)} )}
@@ -33,13 +30,10 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
`} `}
{...props} {...props}
/> />
{error && ( {error && <p className="mt-1.5 text-sm text-destructive">{error}</p>}
<p className="mt-1.5 text-sm text-destructive">{error}</p>
)}
</div> </div>
); );
} }
); );
Textarea.displayName = 'Textarea'; Textarea.displayName = 'Textarea';

View File

@@ -6,4 +6,3 @@ export { CollaboratorDisplay } from './CollaboratorDisplay';
export { Input } from './Input'; export { Input } from './Input';
export { Modal, ModalFooter } from './Modal'; export { Modal, ModalFooter } from './Modal';
export { Textarea } from './Textarea'; export { Textarea } from './Textarea';

View File

@@ -128,4 +128,3 @@ export function useMotivatorLive({
return { isConnected, lastEvent, error }; return { isConnected, lastEvent, error };
} }

View File

@@ -71,7 +71,7 @@ export function useSessionLive({
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
try { try {
const data = JSON.parse(event.data) as LiveEvent; const data = JSON.parse(event.data) as LiveEvent;
// Handle connection event // Handle connection event
if (data.type === 'connected') { if (data.type === 'connected') {
return; return;
@@ -129,4 +129,3 @@ export function useSessionLive({
return { isConnected, lastEvent, error }; return { isConnected, lastEvent, error };
} }

View File

@@ -29,4 +29,3 @@ export const authConfig: NextAuthConfig = {
}, },
providers: [], // Configured in auth.ts providers: [], // Configured in auth.ts
}; };

View File

@@ -59,4 +59,3 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
}, },
}, },
}); });

View File

@@ -14,11 +14,7 @@ export function getGravatarUrl(
size: number = 40, size: number = 40,
fallback: GravatarDefault = 'identicon' fallback: GravatarDefault = 'identicon'
): string { ): string {
const hash = createHash('md5') const hash = createHash('md5').update(email.toLowerCase().trim()).digest('hex');
.update(email.toLowerCase().trim())
.digest('hex');
return `https://www.gravatar.com/avatar/${hash}?d=${fallback}&s=${size}`; return `https://www.gravatar.com/avatar/${hash}?d=${fallback}&s=${size}`;
} }

View File

@@ -230,7 +230,7 @@ export const MOTIVATORS_CONFIG: MotivatorConfig[] = [
type: 'POWER', type: 'POWER',
name: 'Pouvoir', name: 'Pouvoir',
icon: '⚡', icon: '⚡',
description: 'Avoir de l\'influence et du contrôle sur les décisions', description: "Avoir de l'influence et du contrôle sur les décisions",
color: '#ef4444', // red color: '#ef4444', // red
}, },
{ {
@@ -291,12 +291,10 @@ export const MOTIVATORS_CONFIG: MotivatorConfig[] = [
}, },
]; ];
export const MOTIVATOR_BY_TYPE: Record<MotivatorType, MotivatorConfig> = export const MOTIVATOR_BY_TYPE: Record<MotivatorType, MotivatorConfig> = MOTIVATORS_CONFIG.reduce(
MOTIVATORS_CONFIG.reduce( (acc, config) => {
(acc, config) => { acc[config.type] = config;
acc[config.type] = config; return acc;
return acc; },
}, {} as Record<MotivatorType, MotivatorConfig>
{} as Record<MotivatorType, MotivatorConfig> );
);

View File

@@ -7,4 +7,3 @@ export const config = {
// Match all paths except static files and api routes that don't need auth // Match all paths except static files and api routes that don't need auth
matcher: ['/((?!api/auth|_next/static|_next/image|favicon.ico).*)'], matcher: ['/((?!api/auth|_next/static|_next/image|favicon.ico).*)'],
}; };

View File

@@ -77,19 +77,19 @@ export interface ResolvedCollaborator {
// Resolve collaborator string to user - try email first, then name // Resolve collaborator string to user - try email first, then name
export async function resolveCollaborator(collaborator: string): Promise<ResolvedCollaborator> { export async function resolveCollaborator(collaborator: string): Promise<ResolvedCollaborator> {
const trimmed = collaborator.trim(); const trimmed = collaborator.trim();
// 1. Try email match first // 1. Try email match first
if (isEmail(trimmed)) { if (isEmail(trimmed)) {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { email: trimmed.toLowerCase() }, where: { email: trimmed.toLowerCase() },
select: { id: true, email: true, name: true }, select: { id: true, email: true, name: true },
}); });
if (user) { if (user) {
return { raw: collaborator, matchedUser: user }; return { raw: collaborator, matchedUser: user };
} }
} }
// 2. Fallback: try matching by name (case-insensitive via raw query for SQLite) // 2. Fallback: try matching by name (case-insensitive via raw query for SQLite)
// SQLite LIKE is case-insensitive by default for ASCII // SQLite LIKE is case-insensitive by default for ASCII
const users = await prisma.user.findMany({ const users = await prisma.user.findMany({
@@ -98,12 +98,10 @@ export async function resolveCollaborator(collaborator: string): Promise<Resolve
}, },
select: { id: true, email: true, name: true }, select: { id: true, email: true, name: true },
}); });
const normalizedSearch = trimmed.toLowerCase(); const normalizedSearch = trimmed.toLowerCase();
const userByName = users.find( const userByName = users.find((u) => u.name?.toLowerCase() === normalizedSearch) || null;
(u) => u.name?.toLowerCase() === normalizedSearch
) || null;
return { raw: collaborator, matchedUser: userByName }; return { raw: collaborator, matchedUser: userByName };
} }
@@ -248,4 +246,3 @@ export async function getAllUsersWithStats(): Promise<UserWithStats[]> {
return usersWithMotivators; return usersWithMotivators;
} }

View File

@@ -49,7 +49,11 @@ export async function getMotivatorSessionsByUserId(userId: string) {
]); ]);
// Mark owned sessions and merge with shared // Mark owned sessions and merge with shared
const ownedWithRole = owned.map((s) => ({ ...s, isOwner: true as const, role: 'OWNER' as const })); const ownedWithRole = owned.map((s) => ({
...s,
isOwner: true as const,
role: 'OWNER' as const,
}));
const sharedWithRole = shared.map((s) => ({ const sharedWithRole = shared.map((s) => ({
...s.session, ...s.session,
isOwner: false as const, isOwner: false as const,
@@ -200,10 +204,7 @@ export async function updateMotivatorCard(
}); });
} }
export async function reorderMotivatorCards( export async function reorderMotivatorCards(sessionId: string, cardIds: string[]) {
sessionId: string,
cardIds: string[]
) {
const updates = cardIds.map((id, index) => const updates = cardIds.map((id, index) =>
prisma.motivatorCard.update({ prisma.motivatorCard.update({
where: { id }, where: { id },
@@ -350,4 +351,3 @@ export async function getLatestMotivatorEventTimestamp(sessionId: string) {
}); });
return event?.createdAt; return event?.createdAt;
} }

View File

@@ -51,7 +51,11 @@ export async function getSessionsByUserId(userId: string) {
]); ]);
// Mark owned sessions and merge with shared // Mark owned sessions and merge with shared
const ownedWithRole = owned.map((s) => ({ ...s, isOwner: true as const, role: 'OWNER' as const })); const ownedWithRole = owned.map((s) => ({
...s,
isOwner: true as const,
role: 'OWNER' as const,
}));
const sharedWithRole = shared.map((s) => ({ const sharedWithRole = shared.map((s) => ({
...s.session, ...s.session,
isOwner: false as const, isOwner: false as const,
@@ -248,11 +252,7 @@ export async function reorderSwotItems(
return prisma.$transaction(updates); return prisma.$transaction(updates);
} }
export async function moveSwotItem( export async function moveSwotItem(itemId: string, newCategory: SwotCategory, newOrder: number) {
itemId: string,
newCategory: SwotCategory,
newOrder: number
) {
return prisma.swotItem.update({ return prisma.swotItem.update({
where: { id: itemId }, where: { id: itemId },
data: { data: {
@@ -464,4 +464,3 @@ export async function getLatestEventTimestamp(sessionId: string) {
}); });
return event?.createdAt; return event?.createdAt;
} }