feat: implement Moving Motivators feature with session management, real-time event handling, and UI components for enhanced user experience
This commit is contained in:
276
src/components/moving-motivators/MotivatorBoard.tsx
Normal file
276
src/components/moving-motivators/MotivatorBoard.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } 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();
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user