All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m39s
277 lines
8.2 KiB
TypeScript
277 lines
8.2 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useTransition, useEffect } from 'react';
|
|
import {
|
|
DndContext,
|
|
closestCenter,
|
|
KeyboardSensor,
|
|
PointerSensor,
|
|
useSensor,
|
|
useSensors,
|
|
DragEndEvent,
|
|
} from '@dnd-kit/core';
|
|
import {
|
|
SortableContext,
|
|
sortableKeyboardCoordinates,
|
|
horizontalListSortingStrategy,
|
|
arrayMove,
|
|
} from '@dnd-kit/sortable';
|
|
import type { MotivatorCard as MotivatorCardType } from '@/lib/types';
|
|
import { MotivatorCard } from './MotivatorCard';
|
|
import { MotivatorSummary } from './MotivatorSummary';
|
|
import { InfluenceZone } from './InfluenceZone';
|
|
import { reorderMotivatorCards, updateCardInfluence } from '@/actions/moving-motivators';
|
|
|
|
interface MotivatorBoardProps {
|
|
sessionId: string;
|
|
cards: MotivatorCardType[];
|
|
canEdit: boolean;
|
|
}
|
|
|
|
type Step = 'ranking' | 'influence' | 'summary';
|
|
|
|
export function MotivatorBoard({ sessionId, cards: initialCards, canEdit }: MotivatorBoardProps) {
|
|
const [cards, setCards] = useState(initialCards);
|
|
const [step, setStep] = useState<Step>('ranking');
|
|
const [isPending, startTransition] = useTransition();
|
|
|
|
// Sync local state with props when they change (e.g., from SSE refresh)
|
|
useEffect(() => {
|
|
setCards(initialCards);
|
|
}, [initialCards]);
|
|
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor, {
|
|
activationConstraint: {
|
|
distance: 8,
|
|
},
|
|
}),
|
|
useSensor(KeyboardSensor, {
|
|
coordinateGetter: sortableKeyboardCoordinates,
|
|
})
|
|
);
|
|
|
|
// Sort cards by orderIndex
|
|
const sortedCards = [...cards].sort((a, b) => a.orderIndex - b.orderIndex);
|
|
|
|
function handleDragEnd(event: DragEndEvent) {
|
|
const { active, over } = event;
|
|
|
|
if (!over || active.id === over.id) return;
|
|
|
|
const oldIndex = sortedCards.findIndex((c) => c.id === active.id);
|
|
const newIndex = sortedCards.findIndex((c) => c.id === over.id);
|
|
|
|
const newCards = arrayMove(sortedCards, oldIndex, newIndex).map((card, index) => ({
|
|
...card,
|
|
orderIndex: index + 1,
|
|
}));
|
|
|
|
setCards(newCards);
|
|
|
|
// Persist to server
|
|
startTransition(async () => {
|
|
await reorderMotivatorCards(
|
|
sessionId,
|
|
newCards.map((c) => c.id)
|
|
);
|
|
});
|
|
}
|
|
|
|
function handleInfluenceChange(cardId: string, influence: number) {
|
|
setCards((prev) => prev.map((c) => (c.id === cardId ? { ...c, influence } : c)));
|
|
|
|
startTransition(async () => {
|
|
await updateCardInfluence(cardId, sessionId, influence);
|
|
});
|
|
}
|
|
|
|
function nextStep() {
|
|
if (step === 'ranking') setStep('influence');
|
|
else if (step === 'influence') setStep('summary');
|
|
}
|
|
|
|
function prevStep() {
|
|
if (step === 'influence') setStep('ranking');
|
|
else if (step === 'summary') setStep('influence');
|
|
}
|
|
|
|
return (
|
|
<div className={`space-y-6 ${isPending ? 'opacity-70' : ''}`}>
|
|
{/* Progress Steps */}
|
|
<div className="flex items-center justify-center gap-4">
|
|
<StepIndicator
|
|
number={1}
|
|
label="Classement"
|
|
active={step === 'ranking'}
|
|
completed={step !== 'ranking'}
|
|
onClick={() => setStep('ranking')}
|
|
/>
|
|
<div className="h-px w-12 bg-border" />
|
|
<StepIndicator
|
|
number={2}
|
|
label="Influence"
|
|
active={step === 'influence'}
|
|
completed={step === 'summary'}
|
|
onClick={() => setStep('influence')}
|
|
/>
|
|
<div className="h-px w-12 bg-border" />
|
|
<StepIndicator
|
|
number={3}
|
|
label="Récapitulatif"
|
|
active={step === 'summary'}
|
|
completed={false}
|
|
onClick={() => setStep('summary')}
|
|
/>
|
|
</div>
|
|
|
|
{/* Step Content */}
|
|
{step === 'ranking' && (
|
|
<div className="space-y-6">
|
|
<div className="text-center">
|
|
<h2 className="text-xl font-semibold text-foreground mb-2">
|
|
Classez vos motivations par importance
|
|
</h2>
|
|
<p className="text-muted">
|
|
Glissez les cartes de gauche (moins important) à droite (plus important)
|
|
</p>
|
|
</div>
|
|
|
|
{/* Importance axis */}
|
|
<div className="relative">
|
|
<div className="flex justify-between text-sm text-muted mb-4 px-4">
|
|
<span>← Moins important</span>
|
|
<span>Plus important →</span>
|
|
</div>
|
|
|
|
{/* Cards container */}
|
|
<div className="bg-gradient-to-r from-red-500/10 via-yellow-500/10 to-green-500/10 rounded-2xl p-4 border border-border overflow-x-auto">
|
|
<DndContext
|
|
sensors={sensors}
|
|
collisionDetection={closestCenter}
|
|
onDragEnd={handleDragEnd}
|
|
>
|
|
<SortableContext
|
|
items={sortedCards.map((c) => c.id)}
|
|
strategy={horizontalListSortingStrategy}
|
|
>
|
|
<div className="flex gap-2 min-w-max px-2">
|
|
{sortedCards.map((card) => (
|
|
<MotivatorCard key={card.id} card={card} disabled={!canEdit} />
|
|
))}
|
|
</div>
|
|
</SortableContext>
|
|
</DndContext>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Next button */}
|
|
<div className="flex justify-end">
|
|
<button
|
|
onClick={nextStep}
|
|
className="px-6 py-2 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors"
|
|
>
|
|
Suivant →
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{step === 'influence' && (
|
|
<div className="space-y-6">
|
|
<div className="text-center">
|
|
<h2 className="text-xl font-semibold text-foreground mb-2">
|
|
Évaluez l'influence de chaque motivation
|
|
</h2>
|
|
<p className="text-muted">
|
|
Pour chaque carte, indiquez si cette motivation a une influence positive ou négative
|
|
sur votre situation actuelle
|
|
</p>
|
|
</div>
|
|
|
|
<InfluenceZone
|
|
cards={sortedCards}
|
|
onInfluenceChange={handleInfluenceChange}
|
|
canEdit={canEdit}
|
|
/>
|
|
|
|
{/* Navigation buttons */}
|
|
<div className="flex justify-between">
|
|
<button
|
|
onClick={prevStep}
|
|
className="px-6 py-2 border border-border rounded-lg font-medium hover:bg-card transition-colors"
|
|
>
|
|
← Retour
|
|
</button>
|
|
<button
|
|
onClick={nextStep}
|
|
className="px-6 py-2 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors"
|
|
>
|
|
Voir le récapitulatif →
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{step === 'summary' && (
|
|
<div className="space-y-6">
|
|
<div className="text-center">
|
|
<h2 className="text-xl font-semibold text-foreground mb-2">
|
|
Récapitulatif de vos Moving Motivators
|
|
</h2>
|
|
<p className="text-muted">Voici l'analyse de vos motivations et leur impact</p>
|
|
</div>
|
|
|
|
<MotivatorSummary cards={sortedCards} />
|
|
|
|
{/* Navigation buttons */}
|
|
<div className="flex justify-start">
|
|
<button
|
|
onClick={prevStep}
|
|
className="px-6 py-2 border border-border rounded-lg font-medium hover:bg-card transition-colors"
|
|
>
|
|
← Modifier
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StepIndicator({
|
|
number,
|
|
label,
|
|
active,
|
|
completed,
|
|
onClick,
|
|
}: {
|
|
number: number;
|
|
label: string;
|
|
active: boolean;
|
|
completed: boolean;
|
|
onClick: () => void;
|
|
}) {
|
|
return (
|
|
<button
|
|
onClick={onClick}
|
|
className={`
|
|
flex flex-col items-center gap-1 transition-colors
|
|
${active ? 'text-primary' : completed ? 'text-green-500' : 'text-muted'}
|
|
`}
|
|
>
|
|
<div
|
|
className={`
|
|
w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm
|
|
${active ? 'bg-primary text-primary-foreground' : ''}
|
|
${completed ? 'bg-green-500 text-white' : ''}
|
|
${!active && !completed ? 'bg-muted/20 text-muted' : ''}
|
|
`}
|
|
>
|
|
{completed ? '✓' : number}
|
|
</div>
|
|
<span className="text-xs font-medium">{label}</span>
|
|
</button>
|
|
);
|
|
}
|