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:
Julien Froidefond
2025-11-27 13:15:56 +01:00
parent 27e409fb76
commit 628d64a5c6
12 changed files with 1398 additions and 45 deletions

View File

@@ -0,0 +1,149 @@
'use client';
import { forwardRef, useState, useTransition, ReactNode } from 'react';
import type { SwotCategory } from '@prisma/client';
import { createSwotItem } from '@/actions/swot';
interface SwotQuadrantProps {
category: SwotCategory;
title: string;
icon: string;
sessionId: string;
isDraggingOver: boolean;
children: ReactNode;
}
const categoryStyles: Record<SwotCategory, { bg: string; border: string; text: string }> = {
STRENGTH: {
bg: 'bg-strength-bg',
border: 'border-strength-border',
text: 'text-strength',
},
WEAKNESS: {
bg: 'bg-weakness-bg',
border: 'border-weakness-border',
text: 'text-weakness',
},
OPPORTUNITY: {
bg: 'bg-opportunity-bg',
border: 'border-opportunity-border',
text: 'text-opportunity',
},
THREAT: {
bg: 'bg-threat-bg',
border: 'border-threat-border',
text: 'text-threat',
},
};
export const SwotQuadrant = forwardRef<HTMLDivElement, SwotQuadrantProps>(
({ category, title, icon, sessionId, isDraggingOver, children, ...props }, ref) => {
const [isAdding, setIsAdding] = useState(false);
const [newContent, setNewContent] = useState('');
const [isPending, startTransition] = useTransition();
const styles = categoryStyles[category];
async function handleAdd() {
if (!newContent.trim()) {
setIsAdding(false);
return;
}
startTransition(async () => {
await createSwotItem(sessionId, {
content: newContent.trim(),
category,
});
setNewContent('');
setIsAdding(false);
});
}
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleAdd();
} else if (e.key === 'Escape') {
setIsAdding(false);
setNewContent('');
}
}
return (
<div
ref={ref}
className={`
rounded-xl border-2 p-4 min-h-[250px] transition-colors
${styles.bg} ${styles.border}
${isDraggingOver ? 'ring-2 ring-primary ring-offset-2' : ''}
`}
{...props}
>
{/* Header */}
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-xl">{icon}</span>
<h3 className={`font-semibold ${styles.text}`}>{title}</h3>
</div>
<button
onClick={() => setIsAdding(true)}
className={`
rounded-lg p-1.5 transition-colors
hover:bg-white/50 ${styles.text}
`}
aria-label={`Ajouter un item ${title}`}
>
<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" />
</svg>
</button>
</div>
{/* Items */}
<div className="space-y-2">
{children}
{/* Add Form */}
{isAdding && (
<div className="rounded-lg border border-border bg-card p-2 shadow-sm">
<textarea
autoFocus
value={newContent}
onChange={(e) => setNewContent(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleAdd}
placeholder="Décrivez cet élément..."
className="w-full resize-none rounded border-0 bg-transparent p-1 text-sm text-foreground placeholder:text-muted focus:outline-none focus:ring-0"
rows={2}
disabled={isPending}
/>
<div className="mt-1 flex justify-end gap-1">
<button
onClick={() => {
setIsAdding(false);
setNewContent('');
}}
className="rounded px-2 py-1 text-xs text-muted hover:bg-card-hover"
disabled={isPending}
>
Annuler
</button>
<button
onClick={handleAdd}
disabled={isPending || !newContent.trim()}
className={`rounded px-2 py-1 text-xs font-medium ${styles.text} hover:bg-white/50 disabled:opacity-50`}
>
{isPending ? '...' : 'Ajouter'}
</button>
</div>
</div>
)}
</div>
</div>
);
}
);
SwotQuadrant.displayName = 'SwotQuadrant';