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
TODO.md

View File

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

View File

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

View File

@@ -35,10 +35,7 @@ export async function updateProfileAction(data: { name?: string; email?: string
return result;
}
export async function updatePasswordAction(data: {
currentPassword: string;
newPassword: string;
}) {
export async function updatePasswordAction(data: { currentPassword: string; newPassword: string }) {
const session = await auth();
if (!session?.user?.id) {
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' };
}
const result = await updateUserPassword(
session.user.id,
data.currentPassword,
data.newPassword
);
const result = await updateUserPassword(session.user.id, data.currentPassword, data.newPassword);
return result;
}

View File

@@ -99,7 +99,12 @@ export async function updateSwotSession(
}
// 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');
@@ -130,4 +135,3 @@ export async function deleteSwotSession(sessionId: string) {
return { success: false, error: 'Erreur lors de la suppression' };
}
}

View File

@@ -2,11 +2,7 @@
import { revalidatePath } from 'next/cache';
import { auth } from '@/lib/auth';
import {
shareSession,
removeShare,
getSessionShares,
} from '@/services/sessions';
import { shareSession, removeShare, getSessionShares } from '@/services/sessions';
import type { ShareRole } from '@prisma/client';
export async function shareSessionAction(
@@ -26,10 +22,10 @@ export async function shareSessionAction(
} catch (error) {
const message = error instanceof Error ? error.message : 'Erreur inconnue';
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') {
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 };
}
@@ -65,5 +61,3 @@ export async function getSharesAction(sessionId: string) {
return { success: false, error: message, data: [] };
}
}

View File

@@ -228,4 +228,3 @@ export async function deleteAction(actionId: string, sessionId: string) {
return { success: false, error: 'Erreur lors de la suppression' };
}
}

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import { handlers } from '@/lib/auth';
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 });
}
}

View File

@@ -1,18 +1,12 @@
import { auth } from '@/lib/auth';
import {
canAccessMotivatorSession,
getMotivatorSessionEvents,
} from '@/services/moving-motivators';
import { canAccessMotivatorSession, getMotivatorSessionEvents } from '@/services/moving-motivators';
export const dynamic = 'force-dynamic';
// Store active connections per session
const connections = new Map<string, Set<ReadableStreamDefaultController>>();
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id: sessionId } = await params;
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
const connections = new Map<string, Set<ReadableStreamDefaultController>>();
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id: sessionId } = await params;
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;
if (!title || !collaborator) {
return NextResponse.json(
{ error: 'Titre et collaborateur requis' },
{ status: 400 }
);
return NextResponse.json({ error: 'Titre et collaborateur requis' }, { status: 400 });
}
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>
);
}

View File

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

View File

@@ -2,7 +2,15 @@
import { useState } from 'react';
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';
export default function NewMotivatorSessionPage() {
@@ -99,4 +107,3 @@ export default function NewMotivatorSessionPage() {
</main>
);
}

View File

@@ -10,8 +10,8 @@ export default function Home() {
Vos ateliers, <span className="text-primary">réinventés</span>
</h1>
<p className="mx-auto mb-8 max-w-2xl text-lg text-muted">
Des outils interactifs et collaboratifs pour accompagner vos équipes.
Analysez, comprenez et faites progresser vos collaborateurs avec des ateliers modernes.
Des outils interactifs et collaboratifs pour accompagner vos équipes. Analysez,
comprenez et faites progresser vos collaborateurs avec des ateliers modernes.
</p>
</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."
features={[
'10 cartes de motivation à classer',
'Évaluation de l\'influence positive/négative',
"Évaluation de l'influence positive/négative",
'Récapitulatif personnalisé des motivations',
]}
accentColor="#8b5cf6"
@@ -73,8 +73,9 @@ export default function Home() {
Pourquoi faire un SWOT ?
</h3>
<p className="text-muted mb-4">
L&apos;analyse SWOT est un outil puissant pour prendre du recul sur une situation professionnelle.
Elle permet de dresser un portrait objectif et structuré, base indispensable pour définir des actions pertinentes.
L&apos;analyse SWOT est un outil puissant pour prendre du recul sur une situation
professionnelle. Elle permet de dresser un portrait objectif et structuré, base
indispensable pour définir des actions pertinentes.
</p>
<ul className="space-y-2 text-sm text-muted">
<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="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="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 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="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 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="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 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>
@@ -129,10 +136,26 @@ export default function Home() {
Comment ça marche ?
</h3>
<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 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" />
<StepCard
number={1}
title="Remplir la matrice"
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>
@@ -156,8 +179,9 @@ export default function Home() {
Pourquoi explorer ses motivations ?
</h3>
<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.
Comprendre ce qui nous motive permet de mieux s&apos;épanouir et d&apos;aligner nos missions avec nos aspirations profondes.
Créé par Jurgen Appelo (Management 3.0), cet exercice révèle les motivations
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>
<ul className="space-y-2 text-sm text-muted">
<li className="flex items-start gap-2">
@@ -279,9 +303,7 @@ function WorkshopCard({
newHref: string;
}) {
return (
<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"
>
<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">
{/* Accent gradient */}
<div
className="absolute inset-x-0 top-0 h-1 opacity-80"
@@ -313,7 +335,12 @@ function WorkshopCard({
viewBox="0 0 24 24"
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>
{feature}
</li>
@@ -380,22 +407,16 @@ function StepCard({
);
}
function MotivatorPill({
icon,
name,
color,
}: {
icon: string;
name: string;
color: string;
}) {
function MotivatorPill({ icon, name, color }: { icon: string; name: string; color: string }) {
return (
<div
className="flex items-center gap-2 px-3 py-1.5 rounded-full"
style={{ backgroundColor: `${color}15`, border: `1px solid ${color}30` }}
>
<span>{icon}</span>
<span className="font-medium" style={{ color }}>{name}</span>
<span className="font-medium" style={{ color }}>
{name}
</span>
</div>
);
}

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,15 @@
import { useState, useTransition } from 'react';
import Link from 'next/link';
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 { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving-motivators';
@@ -130,9 +138,8 @@ export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsPr
// Get tab from URL or default to 'all'
const tabParam = searchParams.get('tab');
const activeTab: WorkshopType = tabParam && VALID_TABS.includes(tabParam as WorkshopType)
? (tabParam as WorkshopType)
: 'all';
const activeTab: WorkshopType =
tabParam && VALID_TABS.includes(tabParam as WorkshopType) ? (tabParam as WorkshopType) : 'all';
const setActiveTab = (tab: WorkshopType) => {
const params = new URLSearchParams(searchParams.toString());
@@ -205,9 +212,7 @@ export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsPr
{activeTab === 'byPerson' ? (
// By Person View
sortedPersons.length === 0 ? (
<div className="text-center py-12 text-muted">
Aucun atelier pour le moment
</div>
<div className="text-center py-12 text-muted">Aucun atelier pour le moment</div>
) : (
<div className="space-y-8">
{sortedPersons.map(([personKey, sessions]) => {
@@ -231,9 +236,7 @@ export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsPr
</div>
)
) : filteredSessions.length === 0 ? (
<div className="text-center py-12 text-muted">
Aucun atelier de ce type pour le moment
</div>
<div className="text-center py-12 text-muted">Aucun atelier de ce type pour le moment</div>
) : (
<div className="space-y-8">
{/* My Sessions */}
@@ -287,7 +290,8 @@ function TabButton({
onClick={onClick}
className={`
flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-colors
${active
${
active
? 'bg-primary text-primary-foreground'
: 'text-muted hover:bg-card-hover hover:text-foreground'
}
@@ -341,7 +345,10 @@ function SessionCard({ session }: { session: AnySession }) {
startTransition(async () => {
const result = isSwot
? 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) {
setShowEditModal(false);
@@ -372,14 +379,13 @@ function SessionCard({ session }: { session: AnySession }) {
{/* Header: Icon + Title + Role badge */}
<div className="flex items-center gap-2 mb-2">
<span className="text-xl">{icon}</span>
<h3 className="font-semibold text-foreground line-clamp-1 flex-1">
{session.title}
</h3>
<h3 className="font-semibold text-foreground line-clamp-1 flex-1">{session.title}</h3>
{!session.isOwner && (
<span
className="text-xs px-1.5 py-0.5 rounded"
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',
}}
>
@@ -390,12 +396,11 @@ function SessionCard({ session }: { session: AnySession }) {
{/* Participant + Owner info */}
<div className="mb-3 flex items-center gap-2">
<CollaboratorDisplay
collaborator={getResolvedCollaborator(session)}
size="sm"
/>
<CollaboratorDisplay collaborator={getResolvedCollaborator(session)} size="sm" />
{!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>
@@ -441,9 +446,7 @@ function SessionCard({ session }: { session: AnySession }) {
</div>
))}
{session.shares.length > 3 && (
<span className="text-[10px] text-muted">
+{session.shares.length - 3}
</span>
<span className="text-[10px] text-muted">+{session.shares.length - 3}</span>
)}
</div>
</div>
@@ -464,7 +467,12 @@ function SessionCard({ session }: { session: AnySession }) {
title="Modifier"
>
<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>
</button>
<button
@@ -477,7 +485,12 @@ function SessionCard({ session }: { session: AnySession }) {
title="Supprimer"
>
<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>
</button>
</div>
@@ -511,7 +524,10 @@ function SessionCard({ session }: { session: AnySession }) {
/>
</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'}
</label>
<Input
@@ -550,24 +566,17 @@ function SessionCard({ session }: { session: AnySession }) {
>
<div className="space-y-4">
<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 className="text-sm text-destructive">
Cette action est irréversible. Toutes les données seront perdues.
</p>
<ModalFooter>
<Button
variant="ghost"
onClick={() => setShowDeleteModal(false)}
disabled={isPending}
>
<Button variant="ghost" onClick={() => setShowDeleteModal(false)} disabled={isPending}>
Annuler
</Button>
<Button
variant="destructive"
onClick={handleDelete}
disabled={isPending}
>
<Button variant="destructive" onClick={handleDelete} disabled={isPending}>
{isPending ? 'Suppression...' : 'Supprimer'}
</Button>
</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}
canEdit={session.canEdit}
>
<SwotBoard
sessionId={session.id}
items={session.items}
actions={session.actions}
/>
<SwotBoard sessionId={session.id} items={session.items} actions={session.actions} />
</SessionLiveWrapper>
</main>
);
}

View File

@@ -2,7 +2,15 @@
import { useState } from 'react';
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() {
const router = useRouter();
@@ -100,4 +108,3 @@ export default function NewSessionPage() {
</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>
<h1 className="text-3xl font-bold text-foreground">Mes Ateliers</h1>
<p className="mt-1 text-muted">
Tous vos ateliers en un seul endroit
</p>
<p className="mt-1 text-muted">Tous vos ateliers en un seul endroit</p>
</div>
<div className="flex gap-2">
<Link href="/sessions/new">
@@ -90,7 +88,8 @@ export default async function SessionsPage() {
Commencez votre premier atelier
</h2>
<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>
<div className="flex gap-3 justify-center">
<Link href="/sessions/new">
@@ -109,10 +108,7 @@ export default async function SessionsPage() {
</Card>
) : (
<Suspense fallback={<WorkshopTabsSkeleton />}>
<WorkshopTabs
swotSessions={allSwotSessions}
motivatorSessions={allMotivatorSessions}
/>
<WorkshopTabs swotSessions={allSwotSessions} motivatorSessions={allMotivatorSessions} />
</Suspense>
)}
</main>

View File

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

View File

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

View File

@@ -101,11 +101,7 @@ export function SessionLiveWrapper({
</div>
)}
<Button
variant="outline"
size="sm"
onClick={() => setShareModalOpen(true)}
>
<Button variant="outline" size="sm" onClick={() => setShareModalOpen(true)}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
@@ -120,9 +116,7 @@ export function SessionLiveWrapper({
</div>
{/* Content */}
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>
{children}
</div>
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
{/* Share Modal */}
<ShareModal
@@ -136,5 +130,3 @@ export function SessionLiveWrapper({
</>
);
}

View File

@@ -105,14 +105,10 @@ export function ShareModal({
{/* Current shares */}
<div className="space-y-2">
<p className="text-sm font-medium text-foreground">
Collaborateurs ({shares.length})
</p>
<p className="text-sm font-medium text-foreground">Collaborateurs ({shares.length})</p>
{shares.length === 0 ? (
<p className="text-sm text-muted">
Aucun collaborateur pour le moment
</p>
<p className="text-sm text-muted">Aucun collaborateur pour le moment</p>
) : (
<ul className="space-y-2">
{shares.map((share) => (
@@ -126,9 +122,7 @@ export function ShareModal({
<p className="text-sm font-medium text-foreground">
{share.user.name || share.user.email}
</p>
{share.user.name && (
<p className="text-xs text-muted">{share.user.email}</p>
)}
{share.user.name && <p className="text-xs text-muted">{share.user.email}</p>}
</div>
</div>
@@ -176,5 +170,3 @@ export function ShareModal({
</Modal>
);
}

View File

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

View File

@@ -112,11 +112,7 @@ export function Header() {
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"
>
<Avatar
email={session.user.email!}
name={session.user.name}
size={24}
/>
<Avatar email={session.user.email!} name={session.user.name} size={24} />
<span className="text-sm font-medium text-foreground">
{session.user.name || session.user.email?.split('@')[0]}
</span>
@@ -137,10 +133,7 @@ export function Header() {
{menuOpen && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setMenuOpen(false)}
/>
<div 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="border-b border-border px-4 py-2">
<p className="text-xs text-muted">Connecté en tant que</p>

View File

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

View File

@@ -66,14 +66,15 @@ export function MotivatorBoard({ sessionId, cards: initialCards, canEdit }: Moti
// Persist to server
startTransition(async () => {
await reorderMotivatorCards(sessionId, newCards.map((c) => c.id));
await reorderMotivatorCards(
sessionId,
newCards.map((c) => c.id)
);
});
}
function handleInfluenceChange(cardId: string, influence: number) {
setCards((prev) =>
prev.map((c) => (c.id === cardId ? { ...c, influence } : c))
);
setCards((prev) => prev.map((c) => (c.id === cardId ? { ...c, influence } : c)));
startTransition(async () => {
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">
{sortedCards.map((card) => (
<MotivatorCard
key={card.id}
card={card}
disabled={!canEdit}
/>
<MotivatorCard key={card.id} card={card} disabled={!canEdit} />
))}
</div>
</SortableContext>
@@ -182,7 +179,8 @@ export function MotivatorBoard({ sessionId, cards: initialCards, canEdit }: Moti
Évaluez l&apos;influence de chaque motivation
</h2>
<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>
</div>
@@ -216,9 +214,7 @@ export function MotivatorBoard({ sessionId, cards: initialCards, canEdit }: Moti
<h2 className="text-xl font-semibold text-foreground mb-2">
Récapitulatif de vos Moving Motivators
</h2>
<p className="text-muted">
Voici l&apos;analyse de vos motivations et leur impact
</p>
<p className="text-muted">Voici l&apos;analyse de vos motivations et leur impact</p>
</div>
<MotivatorSummary cards={sortedCards} />
@@ -273,4 +269,3 @@ function StepIndicator({
</button>
);
}

View File

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

View File

@@ -101,11 +101,7 @@ export function MotivatorLiveWrapper({
</div>
)}
<Button
variant="outline"
size="sm"
onClick={() => setShareModalOpen(true)}
>
<Button variant="outline" size="sm" onClick={() => setShareModalOpen(true)}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
@@ -120,9 +116,7 @@ export function MotivatorLiveWrapper({
</div>
{/* Content */}
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>
{children}
</div>
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
{/* Share Modal */}
<MotivatorShareModal
@@ -136,4 +130,3 @@ export function MotivatorLiveWrapper({
</>
);
}

View File

@@ -105,14 +105,10 @@ export function MotivatorShareModal({
{/* Current shares */}
<div className="space-y-2">
<p className="text-sm font-medium text-foreground">
Collaborateurs ({shares.length})
</p>
<p className="text-sm font-medium text-foreground">Collaborateurs ({shares.length})</p>
{shares.length === 0 ? (
<p className="text-sm text-muted">
Aucun collaborateur pour le moment
</p>
<p className="text-sm text-muted">Aucun collaborateur pour le moment</p>
) : (
<ul className="space-y-2">
{shares.map((share) => (
@@ -126,9 +122,7 @@ export function MotivatorShareModal({
<p className="text-sm font-medium text-foreground">
{share.user.name || share.user.email}
</p>
{share.user.name && (
<p className="text-xs text-muted">{share.user.email}</p>
)}
{share.user.name && <p className="text-xs text-muted">{share.user.email}</p>}
</div>
</div>
@@ -176,4 +170,3 @@ export function MotivatorShareModal({
</Modal>
);
}

View File

@@ -18,10 +18,14 @@ export function MotivatorSummary({ cards }: MotivatorSummaryProps) {
const bottom3 = sortedByImportance.slice(0, 3);
// 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
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 (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
@@ -100,4 +104,3 @@ function SummarySection({
</div>
);
}

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,10 @@ interface ActionPanelProps {
onActionLeave: () => void;
}
const categoryBadgeVariant: Record<SwotCategory, 'strength' | 'weakness' | 'opportunity' | 'threat'> = {
const categoryBadgeVariant: Record<
SwotCategory,
'strength' | 'weakness' | 'opportunity' | 'threat'
> = {
STRENGTH: 'strength',
WEAKNESS: 'weakness',
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"
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
strokeLinecap="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"
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
strokeLinecap="round"
strokeLinejoin="round"
@@ -271,7 +284,7 @@ export function ActionPanel({
<Modal
isOpen={showModal}
onClose={closeModal}
title={editingAction ? 'Modifier l\'action' : 'Nouvelle action croisée'}
title={editingAction ? "Modifier l'action" : 'Nouvelle action croisée'}
size="lg"
>
<form onSubmit={handleSubmit}>
@@ -339,7 +352,7 @@ export function ActionPanel({
Annuler
</Button>
<Button type="submit" loading={isPending}>
{editingAction ? 'Enregistrer' : 'Créer l\'action'}
{editingAction ? 'Enregistrer' : "Créer l'action"}
</Button>
</ModalFooter>
</form>
@@ -347,4 +360,3 @@ export function ActionPanel({
</div>
);
}

View File

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

View File

@@ -1,12 +1,7 @@
'use client';
import { useState, useTransition } from 'react';
import {
DragDropContext,
Droppable,
Draggable,
DropResult,
} from '@hello-pangea/dnd';
import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd';
import type { SwotItem, Action, ActionLink, SwotCategory } from '@prisma/client';
import { SwotQuadrant } from './SwotQuadrant';
import { SwotCard } from './SwotCard';
@@ -67,9 +62,7 @@ export function SwotBoard({ sessionId, items, actions }: SwotBoardProps) {
if (!linkMode) return;
setSelectedItems((prev) =>
prev.includes(itemId)
? prev.filter((id) => id !== itemId)
: [...prev, itemId]
prev.includes(itemId) ? prev.filter((id) => id !== itemId) : [...prev, itemId]
);
}
@@ -167,4 +160,3 @@ export function SwotBoard({ sessionId, items, actions }: SwotBoardProps) {
</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"
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
strokeLinecap="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"
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
strokeLinecap="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"
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
strokeLinecap="round"
strokeLinejoin="round"
@@ -168,7 +183,9 @@ export const SwotCard = forwardRef<HTMLDivElement, SwotCardProps>(
{/* Selection indicator in link mode */}
{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">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
</svg>
@@ -182,4 +199,3 @@ export const SwotCard = forwardRef<HTMLDivElement, SwotCardProps>(
);
SwotCard.displayName = 'SwotCard';

View File

@@ -92,7 +92,8 @@ export const SwotQuadrant = forwardRef<HTMLDivElement, SwotQuadrantProps>(
className={`
flex h-5 w-5 items-center justify-center rounded-full
text-xs font-medium transition-all
${showHelp
${
showHelp
? 'bg-foreground/20 text-foreground'
: 'bg-foreground/5 text-muted hover:bg-foreground/10 hover:text-foreground'
}
@@ -112,7 +113,12 @@ export const SwotQuadrant = forwardRef<HTMLDivElement, SwotQuadrantProps>(
aria-label={`Ajouter un item ${title}`}
>
<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>
</button>
</div>
@@ -166,4 +172,3 @@ export const SwotQuadrant = forwardRef<HTMLDivElement, SwotQuadrantProps>(
);
SwotQuadrant.displayName = 'SwotQuadrant';

View File

@@ -3,4 +3,3 @@ export { SwotQuadrant } from './SwotQuadrant';
export { SwotCard } from './SwotCard';
export { ActionPanel } from './ActionPanel';
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';

View File

@@ -10,16 +10,11 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
}
const variantStyles: Record<ButtonVariant, string> = {
primary:
'bg-primary text-primary-foreground hover:bg-primary-hover border-transparent',
secondary:
'bg-card text-foreground hover:bg-card-hover border-border',
outline:
'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',
primary: 'bg-primary text-primary-foreground hover:bg-primary-hover border-transparent',
secondary: 'bg-card text-foreground hover:bg-card-hover border-border',
outline: '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> = {
@@ -29,7 +24,10 @@ const sizeStyles: Record<ButtonSize, string> = {
};
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 (
<button
ref={ref}
@@ -74,4 +72,3 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
);
Button.displayName = 'Button';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,4 +29,3 @@ export const authConfig: NextAuthConfig = {
},
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,
fallback: GravatarDefault = 'identicon'
): string {
const hash = createHash('md5')
.update(email.toLowerCase().trim())
.digest('hex');
const hash = createHash('md5').update(email.toLowerCase().trim()).digest('hex');
return `https://www.gravatar.com/avatar/${hash}?d=${fallback}&s=${size}`;
}

View File

@@ -230,7 +230,7 @@ export const MOTIVATORS_CONFIG: MotivatorConfig[] = [
type: 'POWER',
name: 'Pouvoir',
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
},
{
@@ -291,12 +291,10 @@ export const MOTIVATORS_CONFIG: MotivatorConfig[] = [
},
];
export const MOTIVATOR_BY_TYPE: Record<MotivatorType, MotivatorConfig> =
MOTIVATORS_CONFIG.reduce(
export const MOTIVATOR_BY_TYPE: Record<MotivatorType, MotivatorConfig> = MOTIVATORS_CONFIG.reduce(
(acc, config) => {
acc[config.type] = config;
return acc;
},
{} 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
matcher: ['/((?!api/auth|_next/static|_next/image|favicon.ico).*)'],
};

View File

@@ -100,9 +100,7 @@ export async function resolveCollaborator(collaborator: string): Promise<Resolve
});
const normalizedSearch = trimmed.toLowerCase();
const userByName = users.find(
(u) => u.name?.toLowerCase() === normalizedSearch
) || null;
const userByName = users.find((u) => u.name?.toLowerCase() === normalizedSearch) || null;
return { raw: collaborator, matchedUser: userByName };
}
@@ -248,4 +246,3 @@ export async function getAllUsersWithStats(): Promise<UserWithStats[]> {
return usersWithMotivators;
}

View File

@@ -49,7 +49,11 @@ export async function getMotivatorSessionsByUserId(userId: string) {
]);
// 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) => ({
...s.session,
isOwner: false as const,
@@ -200,10 +204,7 @@ export async function updateMotivatorCard(
});
}
export async function reorderMotivatorCards(
sessionId: string,
cardIds: string[]
) {
export async function reorderMotivatorCards(sessionId: string, cardIds: string[]) {
const updates = cardIds.map((id, index) =>
prisma.motivatorCard.update({
where: { id },
@@ -350,4 +351,3 @@ export async function getLatestMotivatorEventTimestamp(sessionId: string) {
});
return event?.createdAt;
}

View File

@@ -51,7 +51,11 @@ export async function getSessionsByUserId(userId: string) {
]);
// 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) => ({
...s.session,
isOwner: false as const,
@@ -248,11 +252,7 @@ export async function reorderSwotItems(
return prisma.$transaction(updates);
}
export async function moveSwotItem(
itemId: string,
newCategory: SwotCategory,
newOrder: number
) {
export async function moveSwotItem(itemId: string, newCategory: SwotCategory, newOrder: number) {
return prisma.swotItem.update({
where: { id: itemId },
data: {
@@ -464,4 +464,3 @@ export async function getLatestEventTimestamp(sessionId: string) {
});
return event?.createdAt;
}