Files
workshop-manager/src/components/weekly-checkin/WeeklyCheckInSection.tsx
Julien Froidefond 53ee344ae7
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 6m24s
feat: add Weekly Check-in feature with models, UI components, and session management for enhanced team collaboration
2026-01-14 10:23:58 +01:00

174 lines
6.0 KiB
TypeScript

'use client';
import { forwardRef, useState, useTransition, useRef, ReactNode } from 'react';
import type { WeeklyCheckInCategory } from '@prisma/client';
import { createWeeklyCheckInItem } from '@/actions/weekly-checkin';
import { WEEKLY_CHECK_IN_BY_CATEGORY, EMOTION_BY_TYPE } from '@/lib/types';
import { Select } from '@/components/ui/Select';
interface WeeklyCheckInSectionProps {
category: WeeklyCheckInCategory;
sessionId: string;
isDraggingOver: boolean;
children: ReactNode;
}
export const WeeklyCheckInSection = forwardRef<HTMLDivElement, WeeklyCheckInSectionProps>(
({ category, sessionId, isDraggingOver, children, ...props }, ref) => {
const [isAdding, setIsAdding] = useState(false);
const [newContent, setNewContent] = useState('');
const [newEmotion, setNewEmotion] = useState<'NONE'>('NONE');
const [isPending, startTransition] = useTransition();
const isSubmittingRef = useRef(false);
const config = WEEKLY_CHECK_IN_BY_CATEGORY[category];
async function handleAdd() {
if (isSubmittingRef.current || !newContent.trim()) {
setIsAdding(false);
return;
}
isSubmittingRef.current = true;
startTransition(async () => {
await createWeeklyCheckInItem(sessionId, {
content: newContent.trim(),
category,
emotion: newEmotion,
});
setNewContent('');
setNewEmotion('NONE');
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('');
setNewEmotion('NONE');
}
}
return (
<div
ref={ref}
className={`
rounded-xl border-2 p-4 min-h-[200px] transition-colors
bg-card border-border
${isDraggingOver ? 'ring-2 ring-primary ring-offset-2' : ''}
`}
style={{
borderLeftColor: config.color,
borderLeftWidth: '4px',
}}
{...props}
>
{/* Header */}
<div className="mb-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-xl">{config.icon}</span>
<div>
<h3 className="font-semibold text-foreground">{config.title}</h3>
<p className="text-xs text-muted">{config.description}</p>
</div>
</div>
<button
onClick={() => setIsAdding(true)}
className="rounded-lg p-1.5 transition-colors hover:bg-card-hover text-muted hover:text-foreground"
aria-label={`Ajouter un item ${config.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"
onBlur={(e) => {
// Don't close if focus moves to another element in this container
const currentTarget = e.currentTarget;
const relatedTarget = e.relatedTarget as Node | null;
if (relatedTarget && currentTarget.contains(relatedTarget)) {
return;
}
// Only add on blur if content is not empty
if (newContent.trim()) {
handleAdd();
} else {
setIsAdding(false);
setNewContent('');
setNewEmotion('NONE');
}
}}
>
<textarea
autoFocus
value={newContent}
onChange={(e) => setNewContent(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={`Décrivez ${config.title.toLowerCase()}...`}
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-2 flex items-center justify-between gap-2">
<Select
value={newEmotion}
onChange={(e) => setNewEmotion(e.target.value as typeof newEmotion)}
className="text-xs flex-1"
options={Object.values(EMOTION_BY_TYPE).map((em) => ({
value: em.emotion,
label: `${em.icon} ${em.label}`,
}))}
/>
<div className="flex gap-1">
<button
onClick={() => {
setIsAdding(false);
setNewContent('');
setNewEmotion('NONE');
}}
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 text-primary hover:bg-primary/10 disabled:opacity-50"
>
{isPending ? '...' : 'Ajouter'}
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
}
);
WeeklyCheckInSection.displayName = 'WeeklyCheckInSection';