Files
workshop-manager/src/components/gif-mood/GifMoodAddForm.tsx
Froidefond Julien 766f3d5a59
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m5s
feat: add GIF Mood Board workshop
- New workshop where each team member shares up to 5 GIFs with notes to express their weekly mood
- Per-user week rating (1-5 stars) visible next to each member's section
- Masonry-style grid with adjustable column count (3/4/5) toggle
- Handwriting font (Caveat) for GIF notes
- Full real-time collaboration via SSE
- Clean migration (add_gif_mood_workshop) safe for production deploy
- DB backup via cp before each migration in docker-entrypoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 10:04:56 +01:00

127 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useTransition } from 'react';
import { Button, Input } from '@/components/ui';
import { addGifMoodItem } from '@/actions/gif-mood';
import { GIF_MOOD_MAX_ITEMS } from '@/lib/types';
interface GifMoodAddFormProps {
sessionId: string;
currentCount: number;
}
export function GifMoodAddForm({ sessionId, currentCount }: GifMoodAddFormProps) {
const [open, setOpen] = useState(false);
const [gifUrl, setGifUrl] = useState('');
const [note, setNote] = useState('');
const [previewUrl, setPreviewUrl] = useState('');
const [error, setError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
const remaining = GIF_MOOD_MAX_ITEMS - currentCount;
function handleUrlBlur() {
const trimmed = gifUrl.trim();
if (!trimmed) { setPreviewUrl(''); return; }
try { new URL(trimmed); setPreviewUrl(trimmed); setError(null); }
catch { setPreviewUrl(''); }
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
const trimmed = gifUrl.trim();
if (!trimmed) { setError("L'URL est requise"); return; }
try { new URL(trimmed); } catch { setError('URL invalide'); return; }
startTransition(async () => {
const result = await addGifMoodItem(sessionId, {
gifUrl: trimmed,
note: note.trim() || undefined,
});
if (result.success) {
setGifUrl(''); setNote(''); setPreviewUrl(''); setOpen(false);
} else {
setError(result.error || "Erreur lors de l'ajout");
}
});
}
// Collapsed state — placeholder card
if (!open) {
return (
<button
onClick={() => setOpen(true)}
className="group flex flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-border/50 hover:border-primary/40 hover:bg-primary/5 min-h-[120px] transition-all duration-200 text-muted hover:text-primary w-full"
>
<span className="text-2xl opacity-40 group-hover:opacity-70 transition-opacity"></span>
<span className="text-xs font-medium">{remaining} slot{remaining !== 1 ? 's' : ''} restant{remaining !== 1 ? 's' : ''}</span>
</button>
);
}
// Expanded form
return (
<form
onSubmit={handleSubmit}
className="rounded-2xl border border-border bg-card shadow-sm p-4 space-y-3"
>
<div className="flex items-center justify-between">
<span className="text-sm font-semibold text-foreground">Ajouter un GIF</span>
<button
type="button"
onClick={() => { setOpen(false); setError(null); setPreviewUrl(''); }}
className="text-muted hover:text-foreground transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{error && (
<p className="text-xs text-destructive">{error}</p>
)}
<Input
label="URL du GIF"
value={gifUrl}
onChange={(e) => setGifUrl(e.target.value)}
onBlur={handleUrlBlur}
placeholder="https://media.giphy.com/…"
disabled={isPending}
/>
{previewUrl && (
<div className="rounded-xl overflow-hidden border border-border/50">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={previewUrl}
alt="Aperçu"
className="w-full object-contain max-h-40"
onError={() => setPreviewUrl('')}
/>
</div>
)}
<div>
<label className="block text-xs font-medium text-muted mb-1">
Note <span className="font-normal opacity-60">(optionnelle)</span>
</label>
<textarea
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="Ce que ce GIF exprime…"
rows={2}
disabled={isPending}
className="w-full rounded-lg border border-border bg-input px-3 py-2 text-sm text-foreground outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 resize-none"
/>
</div>
<Button type="submit" loading={isPending} className="w-full" size="sm">
Ajouter
</Button>
</form>
);
}