import { ChangeEvent, FormEvent, useEffect, useMemo, useRef, useState } from "react"; import styled from "styled-components"; import { ChildProfile } from "@family-planner/types"; import { CreateChildPayload, UpdateChildPayload, useChildren } from "../state/ChildrenContext"; import { uploadAvatar, listAvatars } from "../services/api-client"; type ChildProfilePanelProps = { mode?: "create" | "edit"; child?: ChildProfile | null; onCancel?: () => void; }; type AvatarSelection = | { source: "upload"; file: File; previewUrl: string; name: string } | { source: "gallery"; url: string; name?: string }; type GalleryAvatar = { filename: string; url: string; }; const Panel = styled.aside` flex: 1; padding: 24px; border-radius: 18px; background: rgba(29, 36, 66, 0.92); border: 1px solid rgba(126, 136, 180, 0.22); display: flex; flex-direction: column; gap: 20px; `; const Title = styled.h2` margin: 0; `; const Description = styled.p` margin: 0; color: var(--text-muted); `; const Form = styled.form` display: flex; flex-direction: column; gap: 18px; `; const Label = styled.label` display: flex; flex-direction: column; gap: 8px; font-weight: 600; `; const Row = styled.div` display: flex; gap: 12px; flex-wrap: wrap; `; const BaseInput = styled.input` padding: 12px 14px; border-radius: 12px; border: 1px solid rgba(126, 136, 180, 0.28); background: rgba(16, 22, 52, 0.9); color: #ffffff; `; const TextArea = styled.textarea` padding: 12px 14px; border-radius: 12px; border: 1px solid rgba(126, 136, 180, 0.28); min-height: 96px; background: rgba(16, 22, 52, 0.9); color: #ffffff; `; const SubmitButton = styled.button<{ $loading?: boolean }>` padding: 12px 16px; border-radius: 12px; border: none; background: ${({ $loading }) => $loading ? "rgba(85, 98, 255, 0.25)" : "linear-gradient(135deg, #5562ff, #7d6cff)"}; color: #ffffff; font-weight: 600; cursor: ${({ $loading }) => ($loading ? "progress" : "pointer")}; display: inline-flex; align-items: center; justify-content: center; gap: 8px; transition: opacity 0.2s ease; opacity: ${({ $loading }) => ($loading ? 0.7 : 1)}; `; const SecondaryButton = styled.button` padding: 12px 16px; border-radius: 12px; border: 1px solid rgba(126, 136, 180, 0.4); background: rgba(16, 22, 52, 0.9); color: #d9dcff; font-weight: 600; cursor: pointer; transition: transform 0.2s ease; &:hover { transform: translateY(-1px); } `; const AvatarSection = styled.section` display: flex; flex-direction: column; gap: 16px; `; const AvatarHeader = styled.div` display: flex; align-items: center; justify-content: space-between; gap: 12px; `; const AvatarPreview = styled.div` display: flex; align-items: center; gap: 14px; padding: 12px 14px; border-radius: 14px; background: rgba(12, 18, 42, 0.8); border: 1px solid rgba(126, 136, 180, 0.24); `; const AvatarImage = styled.img` width: 64px; height: 64px; border-radius: 18px; object-fit: cover; border: 2px solid rgba(126, 136, 180, 0.35); `; const AvatarFallback = styled.div<{ $color: string }>` width: 64px; height: 64px; border-radius: 18px; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 1.4rem; background: ${({ $color }) => $color}; color: #040411; `; const AvatarInfo = styled.div` display: flex; flex-direction: column; gap: 4px; color: var(--text-muted); `; const AvatarPicker = styled.div` border-radius: 16px; background: rgba(10, 14, 34, 0.6); border: 1px solid rgba(148, 156, 210, 0.24); padding: 16px; display: flex; flex-direction: column; gap: 16px; `; const GalleryGrid = styled.div` display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 12px; `; const GalleryItem = styled.button<{ $selected?: boolean }>` border: ${({ $selected }) => ($selected ? "2px solid #7d6cff" : "1px solid rgba(126,136,180,0.3)")}; border-radius: 14px; background: rgba(16, 22, 52, 0.85); padding: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: transform 0.2s ease, border 0.2s ease; &:hover { transform: translateY(-2px); } `; const GalleryThumbnail = styled.img` width: 100%; height: 100%; border-radius: 10px; object-fit: cover; `; const Helper = styled.span` font-size: 0.85rem; color: var(--text-muted); `; const ErrorText = styled.span` font-size: 0.85rem; color: #ff7b8a; `; const StatusMessage = styled.div` padding: 12px; border-radius: 12px; background: rgba(12, 18, 42, 0.7); border: 1px solid rgba(126, 136, 180, 0.24); color: var(--text-muted); font-size: 0.9rem; `; const DEFAULT_COLOR = "#5562ff"; export const ChildProfilePanel = ({ mode = "create", child, onCancel }: ChildProfilePanelProps) => { const isEdit = mode === "edit" && !!child; const { createChild, updateChild } = useChildren(); const [fullName, setFullName] = useState(child?.fullName ?? ""); const [colorHex, setColorHex] = useState(child?.colorHex ?? DEFAULT_COLOR); const [email, setEmail] = useState(child?.email ?? ""); const [notes, setNotes] = useState(child?.notes ?? ""); const [avatarSelection, setAvatarSelection] = useState(null); const [removeExistingAvatar, setRemoveExistingAvatar] = useState(false); const [avatarPickerOpen, setAvatarPickerOpen] = useState(false); const [gallery, setGallery] = useState([]); const [galleryLoading, setGalleryLoading] = useState(false); const [galleryError, setGalleryError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const fileInputRef = useRef(null); useEffect(() => { if (avatarSelection?.source === "upload") { return () => { URL.revokeObjectURL(avatarSelection.previewUrl); }; } return undefined; }, [avatarSelection]); useEffect(() => { if (!gallery.length) { setGalleryLoading(true); listAvatars() .then((items) => { setGallery(items); setGalleryError(null); }) .catch(() => { setGalleryError("Impossible de charger la galerie locale."); }) .finally(() => setGalleryLoading(false)); } }, [gallery.length]); useEffect(() => { if (isEdit && child) { setFullName(child.fullName); setColorHex(child.colorHex); setEmail(child.email ?? ""); setNotes(child.notes ?? ""); setAvatarSelection(null); setRemoveExistingAvatar(false); if (fileInputRef.current) { fileInputRef.current.value = ""; } setError(null); } else if (!isEdit) { setFullName(""); setColorHex(DEFAULT_COLOR); setEmail(""); setNotes(""); setAvatarSelection(null); setRemoveExistingAvatar(false); if (fileInputRef.current) { fileInputRef.current.value = ""; } setError(null); } }, [isEdit, child]); const initials = useMemo(() => { return fullName .split(" ") .filter(Boolean) .map((part) => part.charAt(0)) .join("") .slice(0, 2) .toUpperCase(); }, [fullName]); const currentAvatarUrl = useMemo(() => { if (avatarSelection) { return avatarSelection.source === "upload" ? avatarSelection.previewUrl : avatarSelection.url; } if (removeExistingAvatar) { return null; } return child?.avatar?.url ?? null; }, [avatarSelection, child?.avatar, removeExistingAvatar]); const currentAvatarLabel = useMemo(() => { if (avatarSelection) { return avatarSelection.source === "upload" ? avatarSelection.name : avatarSelection.name ?? "Avatar local"; } if (removeExistingAvatar) { return "Aucun avatar selectionne"; } return child?.avatar?.name ?? "Aucun avatar selectionne"; }, [avatarSelection, child?.avatar, removeExistingAvatar]); const handleFileChange = (event: ChangeEvent) => { const file = event.target.files?.[0]; if (!file) { return; } if (!file.type.startsWith("image/")) { setError("Le fichier doit etre une image (png, jpg, svg...)."); return; } setAvatarSelection({ source: "upload", file, previewUrl: URL.createObjectURL(file), name: file.name }); setRemoveExistingAvatar(false); setError(null); }; const handleSelectGallery = (item: GalleryAvatar) => { setAvatarSelection({ source: "gallery", url: item.url, name: item.filename }); setRemoveExistingAvatar(false); if (fileInputRef.current) { fileInputRef.current.value = ""; } }; const handleClearAvatar = () => { if (avatarSelection?.source === "upload") { URL.revokeObjectURL(avatarSelection.previewUrl); } setAvatarSelection(null); if (fileInputRef.current) { fileInputRef.current.value = ""; } setRemoveExistingAvatar(isEdit); }; const handleSubmit = async (event: FormEvent) => { event.preventDefault(); if (isSubmitting) { return; } if (!fullName.trim()) { setError("Merci de saisir le nom complet de l enfant."); return; } const payload: CreateChildPayload | UpdateChildPayload = { fullName: fullName.trim(), colorHex, email: email.trim() ? email.trim() : undefined, notes: notes.trim() ? notes.trim() : undefined }; setIsSubmitting(true); setError(null); try { let avatarPayload: ChildProfile["avatar"] | null | undefined = undefined; if (avatarSelection?.source === "upload") { const uploaded = await uploadAvatar(avatarSelection.file); avatarPayload = { kind: "custom", url: uploaded.url, name: avatarSelection.name }; URL.revokeObjectURL(avatarSelection.previewUrl); setAvatarSelection({ source: "gallery", url: uploaded.url, name: avatarSelection.name }); } else if (avatarSelection?.source === "gallery") { avatarPayload = { kind: "custom", url: avatarSelection.url, name: avatarSelection.name }; } else if (isEdit && child?.avatar && !removeExistingAvatar) { avatarPayload = child.avatar; } else if (removeExistingAvatar) { avatarPayload = null; } if (avatarPayload !== undefined) { payload.avatar = avatarPayload ?? undefined; } if (isEdit && child) { await updateChild(child.id, payload as UpdateChildPayload); onCancel?.(); } else { await createChild(payload as CreateChildPayload); setFullName(""); setColorHex(DEFAULT_COLOR); setEmail(""); setNotes(""); setAvatarSelection(null); setRemoveExistingAvatar(false); if (fileInputRef.current) { fileInputRef.current.value = ""; } } } catch (err) { setError("Impossible d enregistrer pour le moment. Merci de reessayer."); } finally { setIsSubmitting(false); } }; return ( {isEdit ? "Modifier l enfant" : "Ajouter un enfant"} {isEdit ? "Ajuste le profil, la couleur ou l avatar. Les modifications sont visibles partout." : "Cree rapidement un nouveau profil en renseignant email et avatar pour automatiser les agendas."}