chore: clean up code formatting and remove unnecessary whitespace across multiple files for improved readability
This commit is contained in:
@@ -44,3 +44,4 @@ README.md
|
|||||||
devbook.md
|
devbook.md
|
||||||
TODO.md
|
TODO.md
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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: [] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,4 +109,3 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -170,4 +170,3 @@ export default function RegisterPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { handlers } from '@/lib/auth';
|
import { handlers } from '@/lib/auth';
|
||||||
|
|
||||||
export const { GET, POST } = handlers;
|
export const { GET, POST } = handlers;
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,4 +107,3 @@ export function EditableMotivatorTitle({
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
101
src/app/page.tsx
101
src/app/page.tsx
@@ -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'analyse SWOT est un outil puissant pour prendre du recul sur une situation professionnelle.
|
L'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'épanouir et d'aligner nos missions avec nos aspirations profondes.
|
intrinsèques qui nous animent. Comprendre ce qui nous motive permet de mieux
|
||||||
|
s'épanouir et d'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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'atelier <strong className="text-foreground">"{session.title}"</strong> ?
|
Êtes-vous sûr de vouloir supprimer l'atelier{' '}
|
||||||
|
<strong className="text-foreground">"{session.title}"</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 }) {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -140,4 +140,3 @@ function InfluenceSlider({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'influence de chaque motivation
|
Évaluez l'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'analyse de vos motivations et leur impact</p>
|
||||||
Voici l'analyse de vos motivations et leur impact
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MotivatorSummary cards={sortedCards} />
|
<MotivatorSummary cards={sortedCards} />
|
||||||
@@ -273,4 +269,3 @@ function StepIndicator({
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -103,4 +103,3 @@ export function EditableTitle({ sessionId, initialTitle, isOwner }: EditableTitl
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
export { EditableTitle } from './EditableTitle';
|
export { EditableTitle } from './EditableTitle';
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -28,5 +28,3 @@ export function Avatar({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,4 +49,3 @@ export const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
Badge.displayName = 'Badge';
|
Badge.displayName = 'Badge';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -79,5 +79,3 @@ export function CollaboratorDisplay({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -128,4 +128,3 @@ export function useMotivatorLive({
|
|||||||
|
|
||||||
return { isConnected, lastEvent, error };
|
return { isConnected, lastEvent, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,4 +29,3 @@ export const authConfig: NextAuthConfig = {
|
|||||||
},
|
},
|
||||||
providers: [], // Configured in auth.ts
|
providers: [], // Configured in auth.ts
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -59,4 +59,3 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
);
|
||||||
);
|
|
||||||
|
|
||||||
|
|||||||
@@ -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).*)'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user