chore: mark completion of sessions and SWOT components in devbook.md; add @hello-pangea/dnd dependency for drag & drop functionality
This commit is contained in:
170
src/components/swot/SwotBoard.tsx
Normal file
170
src/components/swot/SwotBoard.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
import {
|
||||
DragDropContext,
|
||||
Droppable,
|
||||
Draggable,
|
||||
DropResult,
|
||||
} from '@hello-pangea/dnd';
|
||||
import type { SwotItem, Action, ActionLink, SwotCategory } from '@prisma/client';
|
||||
import { SwotQuadrant } from './SwotQuadrant';
|
||||
import { SwotCard } from './SwotCard';
|
||||
import { ActionPanel } from './ActionPanel';
|
||||
import { moveSwotItem } from '@/actions/swot';
|
||||
|
||||
type ActionWithLinks = Action & {
|
||||
links: (ActionLink & { swotItem: SwotItem })[];
|
||||
};
|
||||
|
||||
interface SwotBoardProps {
|
||||
sessionId: string;
|
||||
items: SwotItem[];
|
||||
actions: ActionWithLinks[];
|
||||
}
|
||||
|
||||
const QUADRANTS: { category: SwotCategory; title: string; icon: string }[] = [
|
||||
{ category: 'STRENGTH', title: 'Forces', icon: '💪' },
|
||||
{ category: 'WEAKNESS', title: 'Faiblesses', icon: '⚠️' },
|
||||
{ category: 'OPPORTUNITY', title: 'Opportunités', icon: '🚀' },
|
||||
{ category: 'THREAT', title: 'Menaces', icon: '🛡️' },
|
||||
];
|
||||
|
||||
export function SwotBoard({ sessionId, items, actions }: SwotBoardProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [linkMode, setLinkMode] = useState(false);
|
||||
const [selectedItems, setSelectedItems] = useState<string[]>([]);
|
||||
const [highlightedItems, setHighlightedItems] = useState<string[]>([]);
|
||||
|
||||
const itemsByCategory = QUADRANTS.reduce(
|
||||
(acc, q) => {
|
||||
acc[q.category] = items
|
||||
.filter((item) => item.category === q.category)
|
||||
.sort((a, b) => a.order - b.order);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<SwotCategory, SwotItem[]>
|
||||
);
|
||||
|
||||
function handleDragEnd(result: DropResult) {
|
||||
if (!result.destination) return;
|
||||
|
||||
const { source, destination, draggableId } = result;
|
||||
const sourceCategory = source.droppableId as SwotCategory;
|
||||
const destCategory = destination.droppableId as SwotCategory;
|
||||
|
||||
// If same position, do nothing
|
||||
if (sourceCategory === destCategory && source.index === destination.index) {
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
await moveSwotItem(draggableId, sessionId, destCategory, destination.index);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleItemSelection(itemId: string) {
|
||||
if (!linkMode) return;
|
||||
|
||||
setSelectedItems((prev) =>
|
||||
prev.includes(itemId)
|
||||
? prev.filter((id) => id !== itemId)
|
||||
: [...prev, itemId]
|
||||
);
|
||||
}
|
||||
|
||||
function handleActionHover(linkedItemIds: string[]) {
|
||||
setHighlightedItems(linkedItemIds);
|
||||
}
|
||||
|
||||
function handleActionLeave() {
|
||||
setHighlightedItems([]);
|
||||
}
|
||||
|
||||
function exitLinkMode() {
|
||||
setLinkMode(false);
|
||||
setSelectedItems([]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`space-y-6 ${isPending ? 'opacity-70 pointer-events-none' : ''}`}>
|
||||
{/* Link Mode Banner */}
|
||||
{linkMode && (
|
||||
<div className="flex items-center justify-between rounded-lg border border-primary/30 bg-primary/10 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl">🔗</span>
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Mode Liaison</p>
|
||||
<p className="text-sm text-muted">
|
||||
Sélectionnez les items à lier ({selectedItems.length} sélectionné
|
||||
{selectedItems.length > 1 ? 's' : ''})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={exitLinkMode}
|
||||
className="rounded-lg border border-border bg-card px-4 py-2 text-sm font-medium hover:bg-card-hover"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SWOT Matrix */}
|
||||
<DragDropContext onDragEnd={handleDragEnd}>
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
{QUADRANTS.map((quadrant) => (
|
||||
<Droppable key={quadrant.category} droppableId={quadrant.category}>
|
||||
{(provided, snapshot) => (
|
||||
<SwotQuadrant
|
||||
category={quadrant.category}
|
||||
title={quadrant.title}
|
||||
icon={quadrant.icon}
|
||||
sessionId={sessionId}
|
||||
isDraggingOver={snapshot.isDraggingOver}
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{itemsByCategory[quadrant.category].map((item, index) => (
|
||||
<Draggable key={item.id} draggableId={item.id} index={index}>
|
||||
{(dragProvided, dragSnapshot) => (
|
||||
<SwotCard
|
||||
item={item}
|
||||
sessionId={sessionId}
|
||||
isSelected={selectedItems.includes(item.id)}
|
||||
isHighlighted={highlightedItems.includes(item.id)}
|
||||
isDragging={dragSnapshot.isDragging}
|
||||
linkMode={linkMode}
|
||||
onSelect={() => toggleItemSelection(item.id)}
|
||||
ref={dragProvided.innerRef}
|
||||
{...dragProvided.draggableProps}
|
||||
{...dragProvided.dragHandleProps}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</SwotQuadrant>
|
||||
)}
|
||||
</Droppable>
|
||||
))}
|
||||
</div>
|
||||
</DragDropContext>
|
||||
|
||||
{/* Actions Panel */}
|
||||
<ActionPanel
|
||||
sessionId={sessionId}
|
||||
actions={actions}
|
||||
allItems={items}
|
||||
linkMode={linkMode}
|
||||
selectedItems={selectedItems}
|
||||
onEnterLinkMode={() => setLinkMode(true)}
|
||||
onExitLinkMode={exitLinkMode}
|
||||
onClearSelection={() => setSelectedItems([])}
|
||||
onActionHover={handleActionHover}
|
||||
onActionLeave={handleActionLeave}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user