Files
workshop-manager/src/components/moving-motivators/MotivatorBoard.tsx

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&apos;influence de chaque motivation
</h2>
<p className="text-muted">
Pour chaque carte, indiquez si cette motivation a une influence positive ou négative
sur votre situation actuelle
</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&apos;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>
);
}