All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m39s
186 lines
5.8 KiB
TypeScript
186 lines
5.8 KiB
TypeScript
'use client';
|
||
|
||
import { forwardRef, useState, useTransition, useRef, ReactNode } from 'react';
|
||
import type { SwotCategory } from '@prisma/client';
|
||
import { createSwotItem } from '@/actions/swot';
|
||
import { QuadrantHelpPanel } from './QuadrantHelp';
|
||
|
||
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 [showHelp, setShowHelp] = useState(false);
|
||
const isSubmittingRef = useRef(false);
|
||
|
||
const styles = categoryStyles[category];
|
||
|
||
async function handleAdd() {
|
||
if (isSubmittingRef.current || !newContent.trim()) {
|
||
setIsAdding(false);
|
||
return;
|
||
}
|
||
|
||
isSubmittingRef.current = true;
|
||
startTransition(async () => {
|
||
await createSwotItem(sessionId, {
|
||
content: newContent.trim(),
|
||
category,
|
||
});
|
||
setNewContent('');
|
||
setIsAdding(false);
|
||
isSubmittingRef.current = 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-3 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>
|
||
<button
|
||
onClick={() => setShowHelp(!showHelp)}
|
||
className={`
|
||
flex h-5 w-5 items-center justify-center rounded-full
|
||
text-xs font-medium transition-all
|
||
${
|
||
showHelp
|
||
? 'bg-foreground/20 text-foreground'
|
||
: 'bg-foreground/5 text-muted hover:bg-foreground/10 hover:text-foreground'
|
||
}
|
||
`}
|
||
aria-label="Aide"
|
||
aria-expanded={showHelp}
|
||
>
|
||
{showHelp ? '×' : '?'}
|
||
</button>
|
||
</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>
|
||
|
||
{/* Help Panel */}
|
||
<QuadrantHelpPanel category={category} isOpen={showHelp} />
|
||
|
||
{/* 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={(e) => {
|
||
// Don't trigger on blur if clicking on a button
|
||
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
||
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
|
||
onMouseDown={(e) => {
|
||
e.preventDefault(); // Prevent blur from textarea
|
||
}}
|
||
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';
|