709 lines
26 KiB
TypeScript
709 lines
26 KiB
TypeScript
"use client";
|
||
|
||
import {
|
||
useCallback,
|
||
useEffect,
|
||
useRef,
|
||
useState,
|
||
type PointerEvent as ReactPointerEvent,
|
||
type TouchEvent as ReactTouchEvent,
|
||
type WheelEvent as ReactWheelEvent,
|
||
} from "react";
|
||
import Link from "next/link";
|
||
import { MainTabBar } from "@/features/predictions/components/main-tab-bar";
|
||
import styles from "@/features/predictions/styles/predictions.module.css";
|
||
import indicesStyles from "./page.module.css";
|
||
import { getProjectIndices } from "@/lib/indices-client";
|
||
import { getCurrentProjectId, getApiUrl, loadSession, PROJECT_CHANGED_EVENT } from "@/lib/auth";
|
||
import { getMyProjects } from "@/lib/projects-client";
|
||
import type { ParentType, ProjectIndicesResponse, Trimester } from "@/types/indices";
|
||
import type { ProjectSummary } from "@/types/projects";
|
||
|
||
const PARENT_LABELS: Record<ParentType, string> = {
|
||
PAPA: "Papa",
|
||
MAMAN: "Maman",
|
||
};
|
||
|
||
const TRIMESTER_LABELS: Record<Trimester, string> = {
|
||
DATATION: "Datation (1ère échographie)",
|
||
T1: "1er trimestre",
|
||
T2: "2ème trimestre",
|
||
T3: "3ème trimestre",
|
||
};
|
||
|
||
const TRIMESTER_ORDER: Trimester[] = ["DATATION", "T1", "T2", "T3"];
|
||
|
||
function hasTrimesterData(tri: {
|
||
poids: number | null;
|
||
taille: number | null;
|
||
perimCranien: number | null;
|
||
date: string | null;
|
||
note: string | null;
|
||
photos: Array<{ id: string; trimesterEntryId: string; url: string; sortOrder: number; createdAt: string }>;
|
||
}) {
|
||
return tri.poids != null || tri.taille != null || tri.perimCranien != null || Boolean(tri.date) || Boolean(tri.note) || tri.photos.length > 0;
|
||
}
|
||
|
||
function formatDate(value: string | null) {
|
||
if (!value) return "—";
|
||
return new Date(value).toLocaleDateString("fr-FR", {
|
||
day: "2-digit",
|
||
month: "long",
|
||
year: "numeric",
|
||
});
|
||
}
|
||
|
||
function StatRow({ label, value }: { label: string; value: string }) {
|
||
return (
|
||
<div className={indicesStyles.statRow}>
|
||
<span className={indicesStyles.statLabel}>{label}</span>
|
||
<span className={indicesStyles.statValue}>{value}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function PhotoGallery({
|
||
urls,
|
||
onOpen,
|
||
}: {
|
||
urls: string[];
|
||
onOpen: (images: string[], startIndex: number) => void;
|
||
}) {
|
||
if (urls.length === 0) return null;
|
||
const images = urls.map((url) => `${getApiUrl()}${url}`);
|
||
|
||
return (
|
||
<div className={indicesStyles.photoGrid}>
|
||
{images.map((image, index) => (
|
||
<button
|
||
key={`${image}-${index}`}
|
||
type="button"
|
||
className={indicesStyles.photoThumb}
|
||
onClick={() => onOpen(images, index)}
|
||
aria-label={`Ouvrir la photo ${index + 1}`}
|
||
>
|
||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||
<img src={image} alt="" />
|
||
</button>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function IndicesPage() {
|
||
const MIN_ZOOM = 1;
|
||
const MAX_ZOOM = 4;
|
||
|
||
const [indices, setIndices] = useState<ProjectIndicesResponse | null>(null);
|
||
const [currentProject, setCurrentProject] = useState<ProjectSummary | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [message, setMessage] = useState("");
|
||
const [activeParentTab, setActiveParentTab] = useState<ParentType>("MAMAN");
|
||
const [activeTrimesterByBaby, setActiveTrimesterByBaby] = useState<Record<string, Trimester>>({});
|
||
const [projectRefreshKey, setProjectRefreshKey] = useState(0);
|
||
const [isAdmin, setIsAdmin] = useState(false);
|
||
const [lightboxImages, setLightboxImages] = useState<string[]>([]);
|
||
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
|
||
const [lightboxZoom, setLightboxZoom] = useState(1);
|
||
const [lightboxOffset, setLightboxOffset] = useState({ x: 0, y: 0 });
|
||
const imageWrapRef = useRef<HTMLDivElement | null>(null);
|
||
const activePointerIdRef = useRef<number | null>(null);
|
||
const lastPointerRef = useRef<{ x: number; y: number } | null>(null);
|
||
const lastPinchDistanceRef = useRef<number | null>(null);
|
||
const lastPinchMidpointRef = useRef<{ x: number; y: number } | null>(null);
|
||
const lastTouchPointRef = useRef<{ x: number; y: number } | null>(null);
|
||
|
||
useEffect(() => {
|
||
const session = loadSession();
|
||
if (session?.user.role === "ADMIN") setIsAdmin(true);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
const handler = () => setProjectRefreshKey((k) => k + 1);
|
||
window.addEventListener(PROJECT_CHANGED_EVENT, handler);
|
||
return () => window.removeEventListener(PROJECT_CHANGED_EVENT, handler);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (lightboxIndex == null) return;
|
||
|
||
const handleKeyDown = (event: KeyboardEvent) => {
|
||
if (event.key === "Escape") {
|
||
setLightboxIndex(null);
|
||
setLightboxImages([]);
|
||
setLightboxZoom(1);
|
||
setLightboxOffset({ x: 0, y: 0 });
|
||
return;
|
||
}
|
||
|
||
if (event.key === "+" || event.key === "=") {
|
||
event.preventDefault();
|
||
setLightboxZoom((current) => Math.min(current + 0.25, MAX_ZOOM));
|
||
}
|
||
|
||
if (event.key === "-") {
|
||
event.preventDefault();
|
||
setLightboxZoom((current) => {
|
||
const next = Math.max(current - 0.25, MIN_ZOOM);
|
||
if (next === MIN_ZOOM) setLightboxOffset({ x: 0, y: 0 });
|
||
return next;
|
||
});
|
||
}
|
||
|
||
if (lightboxImages.length <= 1) return;
|
||
if (event.key === "ArrowLeft") {
|
||
setLightboxIndex((current) => {
|
||
if (current == null) return current;
|
||
return (current - 1 + lightboxImages.length) % lightboxImages.length;
|
||
});
|
||
}
|
||
if (event.key === "ArrowRight") {
|
||
setLightboxIndex((current) => {
|
||
if (current == null) return current;
|
||
return (current + 1) % lightboxImages.length;
|
||
});
|
||
}
|
||
};
|
||
|
||
const previousOverflow = document.body.style.overflow;
|
||
document.body.style.overflow = "hidden";
|
||
window.addEventListener("keydown", handleKeyDown);
|
||
|
||
return () => {
|
||
document.body.style.overflow = previousOverflow;
|
||
window.removeEventListener("keydown", handleKeyDown);
|
||
};
|
||
}, [lightboxImages.length, lightboxIndex, MAX_ZOOM, MIN_ZOOM]);
|
||
|
||
useEffect(() => {
|
||
if (lightboxZoom <= MIN_ZOOM) {
|
||
setLightboxOffset({ x: 0, y: 0 });
|
||
return;
|
||
}
|
||
|
||
const wrap = imageWrapRef.current;
|
||
if (!wrap) return;
|
||
const maxX = ((lightboxZoom - 1) * wrap.clientWidth) / 2;
|
||
const maxY = ((lightboxZoom - 1) * wrap.clientHeight) / 2;
|
||
setLightboxOffset((current) => ({
|
||
x: Math.max(-maxX, Math.min(maxX, current.x)),
|
||
y: Math.max(-maxY, Math.min(maxY, current.y)),
|
||
}));
|
||
}, [lightboxZoom, MIN_ZOOM]);
|
||
|
||
const loadData = useCallback(async () => {
|
||
setLoading(true);
|
||
setMessage("");
|
||
try {
|
||
const projects = await getMyProjects();
|
||
const currentProjectId = getCurrentProjectId();
|
||
const project = projects.find((p) => p.id === currentProjectId) ?? projects[0] ?? null;
|
||
setCurrentProject(project);
|
||
if (!project) {
|
||
setIndices(null);
|
||
return;
|
||
}
|
||
const data = await getProjectIndices(project.id);
|
||
setIndices(data);
|
||
} catch (err) {
|
||
setMessage(err instanceof Error ? err.message : "Impossible de charger les indices");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
void loadData();
|
||
}, [loadData, projectRefreshKey]);
|
||
|
||
const setActiveTrimester = (babyId: string, trimester: Trimester) => {
|
||
setActiveTrimesterByBaby((prev) => ({ ...prev, [babyId]: trimester }));
|
||
};
|
||
|
||
const papaIndices = indices?.parentIndices.find((p) => p.parentType === "PAPA") ?? null;
|
||
const mamanIndices = indices?.parentIndices.find((p) => p.parentType === "MAMAN") ?? null;
|
||
const activeParent = activeParentTab === "PAPA" ? papaIndices : mamanIndices;
|
||
const currentImage = lightboxIndex != null ? lightboxImages[lightboxIndex] : null;
|
||
|
||
const openLightbox = (images: string[], startIndex: number) => {
|
||
setLightboxImages(images);
|
||
setLightboxIndex(startIndex);
|
||
setLightboxZoom(1);
|
||
setLightboxOffset({ x: 0, y: 0 });
|
||
};
|
||
|
||
const closeLightbox = () => {
|
||
setLightboxIndex(null);
|
||
setLightboxImages([]);
|
||
setLightboxZoom(1);
|
||
setLightboxOffset({ x: 0, y: 0 });
|
||
};
|
||
|
||
const showPreviousImage = () => {
|
||
if (lightboxImages.length <= 1) return;
|
||
setLightboxIndex((current) => {
|
||
if (current == null) return current;
|
||
return (current - 1 + lightboxImages.length) % lightboxImages.length;
|
||
});
|
||
setLightboxZoom(1);
|
||
setLightboxOffset({ x: 0, y: 0 });
|
||
};
|
||
|
||
const showNextImage = () => {
|
||
if (lightboxImages.length <= 1) return;
|
||
setLightboxIndex((current) => {
|
||
if (current == null) return current;
|
||
return (current + 1) % lightboxImages.length;
|
||
});
|
||
setLightboxZoom(1);
|
||
setLightboxOffset({ x: 0, y: 0 });
|
||
};
|
||
|
||
const applyPan = (deltaX: number, deltaY: number) => {
|
||
const wrap = imageWrapRef.current;
|
||
if (!wrap || lightboxZoom <= MIN_ZOOM) return;
|
||
const maxX = ((lightboxZoom - 1) * wrap.clientWidth) / 2;
|
||
const maxY = ((lightboxZoom - 1) * wrap.clientHeight) / 2;
|
||
setLightboxOffset((current) => ({
|
||
x: Math.max(-maxX, Math.min(maxX, current.x + deltaX)),
|
||
y: Math.max(-maxY, Math.min(maxY, current.y + deltaY)),
|
||
}));
|
||
};
|
||
|
||
const updateZoom = (targetZoom: number | ((current: number) => number)) => {
|
||
setLightboxZoom((current) => {
|
||
const requestedZoom =
|
||
typeof targetZoom === "function" ? targetZoom(current) : targetZoom;
|
||
const nextZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, requestedZoom));
|
||
if (nextZoom <= MIN_ZOOM) {
|
||
setLightboxOffset({ x: 0, y: 0 });
|
||
}
|
||
return nextZoom;
|
||
});
|
||
};
|
||
|
||
const handleImageWheel = (event: ReactWheelEvent<HTMLDivElement>) => {
|
||
event.preventDefault();
|
||
const step = event.deltaY < 0 ? 0.15 : -0.15;
|
||
updateZoom((current) => current + step);
|
||
};
|
||
|
||
const handleImagePointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
|
||
if (event.pointerType === "touch") return;
|
||
if (event.pointerType === "mouse" && event.button !== 0) return;
|
||
if (event.target instanceof HTMLElement && event.target.closest("button")) return;
|
||
if (lightboxZoom <= MIN_ZOOM) return;
|
||
activePointerIdRef.current = event.pointerId;
|
||
lastPointerRef.current = { x: event.clientX, y: event.clientY };
|
||
event.currentTarget.setPointerCapture(event.pointerId);
|
||
};
|
||
|
||
const handleImagePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
|
||
if (activePointerIdRef.current !== event.pointerId || !lastPointerRef.current) return;
|
||
const deltaX = event.clientX - lastPointerRef.current.x;
|
||
const deltaY = event.clientY - lastPointerRef.current.y;
|
||
lastPointerRef.current = { x: event.clientX, y: event.clientY };
|
||
applyPan(deltaX, deltaY);
|
||
};
|
||
|
||
const handleImagePointerUp = (event: ReactPointerEvent<HTMLDivElement>) => {
|
||
if (activePointerIdRef.current === event.pointerId) {
|
||
activePointerIdRef.current = null;
|
||
lastPointerRef.current = null;
|
||
event.currentTarget.releasePointerCapture(event.pointerId);
|
||
}
|
||
};
|
||
|
||
const handleImageTouchStart = (event: ReactTouchEvent<HTMLDivElement>) => {
|
||
if (event.target instanceof HTMLElement && event.target.closest("button")) {
|
||
lastTouchPointRef.current = null;
|
||
return;
|
||
}
|
||
|
||
if (event.touches.length === 1) {
|
||
const touch = event.touches[0];
|
||
lastTouchPointRef.current = { x: touch.clientX, y: touch.clientY };
|
||
lastPinchDistanceRef.current = null;
|
||
lastPinchMidpointRef.current = null;
|
||
return;
|
||
}
|
||
|
||
if (event.touches.length === 2) {
|
||
const [a, b] = [event.touches[0], event.touches[1]];
|
||
lastPinchDistanceRef.current = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
||
lastPinchMidpointRef.current = {
|
||
x: (a.clientX + b.clientX) / 2,
|
||
y: (a.clientY + b.clientY) / 2,
|
||
};
|
||
lastTouchPointRef.current = null;
|
||
}
|
||
};
|
||
|
||
const handleImageTouchMove = (event: ReactTouchEvent<HTMLDivElement>) => {
|
||
if (event.target instanceof HTMLElement && event.target.closest("button")) return;
|
||
|
||
if (event.touches.length === 1) {
|
||
if (lightboxZoom <= MIN_ZOOM) return;
|
||
event.preventDefault();
|
||
const touch = event.touches[0];
|
||
if (!lastTouchPointRef.current) {
|
||
lastTouchPointRef.current = { x: touch.clientX, y: touch.clientY };
|
||
return;
|
||
}
|
||
const deltaX = touch.clientX - lastTouchPointRef.current.x;
|
||
const deltaY = touch.clientY - lastTouchPointRef.current.y;
|
||
lastTouchPointRef.current = { x: touch.clientX, y: touch.clientY };
|
||
applyPan(deltaX, deltaY);
|
||
return;
|
||
}
|
||
|
||
if (event.touches.length !== 2) {
|
||
lastPinchDistanceRef.current = null;
|
||
lastPinchMidpointRef.current = null;
|
||
return;
|
||
}
|
||
|
||
event.preventDefault();
|
||
const [a, b] = [event.touches[0], event.touches[1]];
|
||
const distance = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
||
const midpoint = { x: (a.clientX + b.clientX) / 2, y: (a.clientY + b.clientY) / 2 };
|
||
|
||
if (lastPinchDistanceRef.current != null && lastPinchDistanceRef.current > 0) {
|
||
const ratio = distance / lastPinchDistanceRef.current;
|
||
updateZoom((current) => current * ratio);
|
||
}
|
||
|
||
if (lastPinchMidpointRef.current && lightboxZoom > MIN_ZOOM) {
|
||
const moveX = midpoint.x - lastPinchMidpointRef.current.x;
|
||
const moveY = midpoint.y - lastPinchMidpointRef.current.y;
|
||
applyPan(moveX, moveY);
|
||
}
|
||
|
||
lastPinchDistanceRef.current = distance;
|
||
lastPinchMidpointRef.current = midpoint;
|
||
lastTouchPointRef.current = null;
|
||
};
|
||
|
||
const handleImageTouchEnd = () => {
|
||
lastPinchDistanceRef.current = null;
|
||
lastPinchMidpointRef.current = null;
|
||
lastTouchPointRef.current = null;
|
||
};
|
||
|
||
const toggleZoom = () => {
|
||
if (lightboxZoom > MIN_ZOOM) {
|
||
updateZoom(MIN_ZOOM);
|
||
return;
|
||
}
|
||
updateZoom(2);
|
||
};
|
||
|
||
const hasAnyParentData = (parent: typeof papaIndices) =>
|
||
parent && (parent.poids || parent.taille || parent.perimCranien || parent.dateNaissance || parent.photos.length > 0);
|
||
|
||
return (
|
||
<>
|
||
<section className={`${styles.recapHero} ${styles.indicesHero}`}>
|
||
<div className={styles.recapHeroContent}>
|
||
<span className={styles.recapHeroEyebrow}>Carnet d'indices</span>
|
||
<h1>Petites pistes de famille</h1>
|
||
<p>
|
||
Retrouvez les infos de maman et papa à leur naissance, puis l'évolution de bébé au fil des trimestres dans le ventre de maman.
|
||
</p>
|
||
<div className={styles.recapHeroStats} aria-hidden="true">
|
||
<div className={styles.recapHeroStatChip}>
|
||
<span className={styles.recapHeroStatValue}>Maman</span>
|
||
<span className={styles.recapHeroStatLabel}>Naissance</span>
|
||
</div>
|
||
<div className={styles.recapHeroStatChip}>
|
||
<span className={styles.recapHeroStatValue}>Papa</span>
|
||
<span className={styles.recapHeroStatLabel}>Naissance</span>
|
||
</div>
|
||
<div className={styles.recapHeroStatChip}>
|
||
<span className={styles.recapHeroStatValue}>T1-T3</span>
|
||
<span className={styles.recapHeroStatLabel}>Bébé</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className={styles.indicesHeroIllustration} aria-hidden="true">
|
||
<span className={styles.indicesHeroCardBack} />
|
||
<span className={styles.indicesHeroCardMiddle} />
|
||
<span className={styles.indicesHeroCardFront}>
|
||
<span className={styles.indicesHeroCardAccent} />
|
||
<span className={styles.indicesHeroCardLineStrong} />
|
||
<span className={styles.indicesHeroCardLineSoft} />
|
||
</span>
|
||
<span className={styles.indicesHeroCardBadge} />
|
||
</div>
|
||
</section>
|
||
|
||
<MainTabBar />
|
||
|
||
<section className={styles.sectionTitle}>
|
||
<h2>Indices de grossesse</h2>
|
||
</section>
|
||
|
||
{isAdmin && currentProject ? (
|
||
<section className={`panel ${indicesStyles.editBanner}`}>
|
||
<p>Vous êtes administrateur — vous pouvez modifier ces informations.</p>
|
||
<Link className="btn btn-soft" href={`/admin/projects/${currentProject.id}/indices`}>
|
||
Modifier les indices
|
||
</Link>
|
||
</section>
|
||
) : null}
|
||
|
||
{message ? (
|
||
<section className={`panel ${indicesStyles.messagePanel}`}>
|
||
<p className={styles.error}>{message}</p>
|
||
</section>
|
||
) : null}
|
||
|
||
{loading ? (
|
||
<section className={`panel ${indicesStyles.messagePanel}`}>
|
||
<p>Chargement des indices...</p>
|
||
</section>
|
||
) : null}
|
||
|
||
{!loading && !currentProject ? (
|
||
<section className={`panel ${indicesStyles.messagePanel}`}>
|
||
<p className={styles.empty}>Aucun projet sélectionné.</p>
|
||
</section>
|
||
) : null}
|
||
|
||
{!loading && currentProject && (
|
||
<>
|
||
{/* ===== PARENTS ===== */}
|
||
<section className={`panel ${indicesStyles.section}`}>
|
||
<h3 className={indicesStyles.sectionHeading}>Les parents</h3>
|
||
|
||
<div className={indicesStyles.parentTabs}>
|
||
{(["MAMAN", "PAPA"] as ParentType[]).map((type) => (
|
||
<button
|
||
key={type}
|
||
type="button"
|
||
className={`${indicesStyles.parentTab} ${activeParentTab === type ? indicesStyles.parentTabActive : ""}`}
|
||
onClick={() => setActiveParentTab(type)}
|
||
>
|
||
{PARENT_LABELS[type]}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{activeParent && hasAnyParentData(activeParent) ? (
|
||
<div className={indicesStyles.cardBlock}>
|
||
<div className={indicesStyles.statsGrid}>
|
||
{activeParent.poids != null && (
|
||
<StatRow label="Poids" value={`${activeParent.poids} kg`} />
|
||
)}
|
||
{activeParent.taille != null && (
|
||
<StatRow label="Taille" value={`${activeParent.taille} cm`} />
|
||
)}
|
||
{activeParent.perimCranien != null && (
|
||
<StatRow label="Périm. crânien" value={`${activeParent.perimCranien} cm`} />
|
||
)}
|
||
{activeParent.dateNaissance && (
|
||
<StatRow label="Date de naissance" value={formatDate(activeParent.dateNaissance)} />
|
||
)}
|
||
</div>
|
||
{activeParent.photos.length > 0 && (
|
||
<PhotoGallery
|
||
urls={activeParent.photos.map((p) => p.url)}
|
||
onOpen={openLightbox}
|
||
/>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<p className={indicesStyles.emptyHint}>
|
||
{`Aucune information renseignée pour ${PARENT_LABELS[activeParentTab].toLowerCase()} pour l'instant.`}
|
||
</p>
|
||
)}
|
||
</section>
|
||
|
||
{/* ===== BÉBÉS ===== */}
|
||
{(indices?.babyIndices ?? []).map((baby) => {
|
||
const babyLabel = currentProject.babies?.find((b) => b.babyIndex === baby.babyIndex)?.label ?? `Bébé ${baby.babyIndex}`;
|
||
const visibleTrimesters = baby.trimesters
|
||
.filter((tri) => hasTrimesterData(tri))
|
||
.sort(
|
||
(a, b) =>
|
||
TRIMESTER_ORDER.indexOf(a.trimester as Trimester) -
|
||
TRIMESTER_ORDER.indexOf(b.trimester as Trimester),
|
||
);
|
||
return (
|
||
<section key={baby.id} className={`panel ${indicesStyles.section}`}>
|
||
<h3 className={indicesStyles.sectionHeading}>{babyLabel}</h3>
|
||
|
||
{baby.dpa && (
|
||
<div className={indicesStyles.dpaBadge}>
|
||
<span className={indicesStyles.dpaLabel}>Date prévue d'accouchement</span>
|
||
<span className={indicesStyles.dpaValue}>{formatDate(baby.dpa)}</span>
|
||
</div>
|
||
)}
|
||
|
||
{visibleTrimesters.length === 0 && !baby.dpa ? (
|
||
<p className={indicesStyles.emptyHint}>Aucune information renseignée pour ce bébé.</p>
|
||
) : null}
|
||
|
||
{visibleTrimesters.length > 0 ? (() => {
|
||
const stored = activeTrimesterByBaby[baby.id];
|
||
const fallback = visibleTrimesters[0].trimester as Trimester;
|
||
const activeTri = visibleTrimesters.find((t) => t.trimester === stored)?.trimester as Trimester | undefined;
|
||
const currentTrimester = activeTri ?? fallback;
|
||
const tri = visibleTrimesters.find((t) => t.trimester === currentTrimester) ?? visibleTrimesters[0];
|
||
|
||
return (
|
||
<div className={indicesStyles.trimesterTabsWrapper}>
|
||
<div
|
||
className={indicesStyles.trimesterTabs}
|
||
role="tablist"
|
||
aria-label="Choisir un trimestre"
|
||
>
|
||
{visibleTrimesters.map((t) => {
|
||
const isActive = t.trimester === currentTrimester;
|
||
return (
|
||
<button
|
||
key={t.id}
|
||
type="button"
|
||
role="tab"
|
||
aria-selected={isActive}
|
||
className={`${indicesStyles.trimesterTab} ${isActive ? indicesStyles.trimesterTabActive : ""}`}
|
||
onClick={() => setActiveTrimester(baby.id, t.trimester as Trimester)}
|
||
>
|
||
{t.trimester === "DATATION" ? "Datation" : t.trimester}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<div className={indicesStyles.trimesterPanel} role="tabpanel">
|
||
<h4 className={indicesStyles.trimesterPanelTitle}>{TRIMESTER_LABELS[tri.trimester as Trimester]}</h4>
|
||
<div className={indicesStyles.statsGrid}>
|
||
{tri.date && <StatRow label="Date écho" value={formatDate(tri.date)} />}
|
||
{tri.poids != null && <StatRow label="Poids estimé" value={`${tri.poids} kg`} />}
|
||
{tri.taille != null && <StatRow label="Taille estimée" value={`${tri.taille} cm`} />}
|
||
{tri.perimCranien != null && <StatRow label="Périm. crânien" value={`${tri.perimCranien} cm`} />}
|
||
</div>
|
||
{tri.note && (
|
||
<p className={indicesStyles.triNote}>{tri.note}</p>
|
||
)}
|
||
{tri.photos.length > 0 && (
|
||
<PhotoGallery
|
||
urls={tri.photos.map((p) => p.url)}
|
||
onOpen={openLightbox}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
})() : null}
|
||
</section>
|
||
);
|
||
})}
|
||
|
||
{/* Si pas de données bébé du tout */}
|
||
{(indices?.babyIndices ?? []).length === 0 && (
|
||
<section className={`panel ${indicesStyles.messagePanel}`}>
|
||
<p className={indicesStyles.emptyHint}>
|
||
{`Les informations sur ${currentProject.babyCount > 1 ? "les bébés" : "le bébé"} n'ont pas encore été renseignées.`}
|
||
</p>
|
||
</section>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{currentImage && lightboxIndex != null && (
|
||
<div className={indicesStyles.lightboxOverlay} onClick={closeLightbox}>
|
||
<div
|
||
className={indicesStyles.lightboxDialog}
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-label="Aperçu photo"
|
||
onClick={(event) => event.stopPropagation()}
|
||
>
|
||
<button
|
||
type="button"
|
||
className={indicesStyles.lightboxClose}
|
||
onClick={closeLightbox}
|
||
aria-label="Fermer"
|
||
>
|
||
×
|
||
</button>
|
||
|
||
<div
|
||
ref={imageWrapRef}
|
||
className={indicesStyles.lightboxImageWrap}
|
||
onWheel={handleImageWheel}
|
||
onPointerDown={handleImagePointerDown}
|
||
onPointerMove={handleImagePointerMove}
|
||
onPointerUp={handleImagePointerUp}
|
||
onPointerCancel={handleImagePointerUp}
|
||
onTouchStart={handleImageTouchStart}
|
||
onTouchMove={handleImageTouchMove}
|
||
onTouchEnd={handleImageTouchEnd}
|
||
onTouchCancel={handleImageTouchEnd}
|
||
onDoubleClick={toggleZoom}
|
||
>
|
||
{lightboxImages.length > 1 && (
|
||
<button
|
||
type="button"
|
||
className={`${indicesStyles.lightboxNav} ${indicesStyles.lightboxNavPrev}`}
|
||
onClick={showPreviousImage}
|
||
aria-label="Photo précédente"
|
||
>
|
||
‹
|
||
</button>
|
||
)}
|
||
|
||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||
<img
|
||
className={indicesStyles.lightboxImage}
|
||
style={{ transform: `translate(${lightboxOffset.x}px, ${lightboxOffset.y}px) scale(${lightboxZoom})` }}
|
||
src={currentImage}
|
||
alt=""
|
||
draggable={false}
|
||
/>
|
||
|
||
{lightboxImages.length > 1 && (
|
||
<button
|
||
type="button"
|
||
className={`${indicesStyles.lightboxNav} ${indicesStyles.lightboxNavNext}`}
|
||
onClick={showNextImage}
|
||
aria-label="Photo suivante"
|
||
>
|
||
›
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<div className={indicesStyles.lightboxFooter}>
|
||
<span>{lightboxIndex + 1} / {lightboxImages.length}</span>
|
||
<div className={indicesStyles.lightboxActions}>
|
||
<button
|
||
type="button"
|
||
className={indicesStyles.lightboxZoomBtn}
|
||
onClick={() => updateZoom((current) => current - 0.25)}
|
||
aria-label="Dézoomer"
|
||
>
|
||
−
|
||
</button>
|
||
<span className={indicesStyles.lightboxZoomValue}>{Math.round(lightboxZoom * 100)}%</span>
|
||
<button
|
||
type="button"
|
||
className={indicesStyles.lightboxZoomBtn}
|
||
onClick={() => updateZoom((current) => current + 0.25)}
|
||
aria-label="Zoomer"
|
||
>
|
||
+
|
||
</button>
|
||
<button type="button" className="btn btn-soft" onClick={closeLightbox}>
|
||
Retour
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
);
|
||
}
|