diff --git a/apps/pwa/src/components/health/MedsSection.tsx b/apps/pwa/src/components/health/MedsSection.tsx new file mode 100644 index 0000000..dd73581 --- /dev/null +++ b/apps/pwa/src/components/health/MedsSection.tsx @@ -0,0 +1,39 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import type { DailyCheckin } from "@ordinarthur-os/shared"; +import { api } from "@/api/client"; +import { Label } from "@/design"; +import { MedsSlider } from "./MedsSlider"; + +export function MedsSection() { + const qc = useQueryClient(); + + const { data, isLoading } = useQuery({ + queryKey: ["health-tab", "today"], + queryFn: () => api("/health-tab/today"), + }); + + const toggle = useMutation({ + mutationFn: () => + api("/health-tab/today/toggle", { method: "POST", body: "{}" }), + onSuccess: (updated) => qc.setQueryData(["health-tab", "today"], updated), + }); + + const medsTaken = data?.meds_taken ?? false; + + return ( +
+
+ + + {isLoading ? "…" : medsTaken ? "Pris" : "Non pris"} + +
+ + toggle.mutate()} + disabled={toggle.isPending || isLoading} + /> +
+ ); +} diff --git a/apps/pwa/src/components/health/MedsSlider.tsx b/apps/pwa/src/components/health/MedsSlider.tsx new file mode 100644 index 0000000..fef2654 --- /dev/null +++ b/apps/pwa/src/components/health/MedsSlider.tsx @@ -0,0 +1,108 @@ +import { useRef, useState, useEffect } from "react"; + +const THUMB_W = 64; + +interface Props { + medsTaken: boolean; + onToggle: () => void; + disabled?: boolean; +} + +export function MedsSlider({ medsTaken, onToggle, disabled = false }: Props) { + const trackRef = useRef(null); + const [dragX, setDragX] = useState(0); + const dragging = useRef(false); + const startX = useRef(0); + + useEffect(() => { + if (!medsTaken) setDragX(0); + }, [medsTaken]); + + const maxX = () => (trackRef.current?.clientWidth ?? 300) - THUMB_W; + + const onPointerDown = (e: React.PointerEvent) => { + if (disabled || medsTaken) return; + e.currentTarget.setPointerCapture(e.pointerId); + dragging.current = true; + startX.current = e.clientX - dragX; + }; + + const onPointerMove = (e: React.PointerEvent) => { + if (!dragging.current) return; + setDragX(Math.max(0, Math.min(e.clientX - startX.current, maxX()))); + }; + + const onPointerUp = () => { + if (!dragging.current) return; + dragging.current = false; + if (dragX >= maxX() * 0.65) { + onToggle(); + } else { + setDragX(0); + } + }; + + const progress = medsTaken ? 1 : dragX / (maxX() || 1); + const isActive = dragging.current; + + return ( +
medsTaken && !disabled && onToggle()} + className="relative h-16 border border-ink overflow-hidden select-none" + style={{ cursor: medsTaken ? "pointer" : "default" }} + > + {/* Fill orange qui suit le thumb */} +
+ + {/* Labels */} +
+ {!medsTaken && ( + + Glisse → médicaments pris + + )} + {!medsTaken && progress > 0.3 && ( + + Lâche pour confirmer + + )} + {medsTaken && ( + + ✓ Médicaments pris — tap pour annuler + + )} +
+ + {/* Thumb */} +
+ {medsTaken ? "✓" : "→"} +
+
+ ); +} diff --git a/apps/pwa/src/routes/index.tsx b/apps/pwa/src/routes/index.tsx index c3c0d45..5a3ffa9 100644 --- a/apps/pwa/src/routes/index.tsx +++ b/apps/pwa/src/routes/index.tsx @@ -1,168 +1,23 @@ import { createFileRoute, Link } from "@tanstack/react-router"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { useRef, useState, useEffect } from "react"; -import type { Todo, DailyCheckin } from "@ordinarthur-os/shared"; +import { useQuery } from "@tanstack/react-query"; +import type { Todo } from "@ordinarthur-os/shared"; import { api } from "@/api/client"; import { BigHeading, Label } from "@/design"; import { MagicButton } from "@/components/ai/MagicButton"; +import { MedsSection } from "@/components/health/MedsSection"; export const Route = createFileRoute("/")({ component: Dashboard }); -// --------------------------------------------------------------------------- -// Slide-to-confirm — inspiré du "slide to unlock" iOS, style Swiss brutalist -// --------------------------------------------------------------------------- - -const THUMB_W = 64; // px - -function MedsSlider({ - medsTaken, - onToggle, - disabled, -}: { - medsTaken: boolean; - onToggle: () => void; - disabled: boolean; -}) { - const trackRef = useRef(null); - const [dragX, setDragX] = useState(0); - const dragging = useRef(false); - const startX = useRef(0); - - // Reset thumb when state flips back to false - useEffect(() => { - if (!medsTaken) setDragX(0); - }, [medsTaken]); - - function maxX() { - return (trackRef.current?.clientWidth ?? 300) - THUMB_W; - } - - function onPointerDown(e: React.PointerEvent) { - if (disabled || medsTaken) return; - e.currentTarget.setPointerCapture(e.pointerId); - dragging.current = true; - startX.current = e.clientX - dragX; - } - - function onPointerMove(e: React.PointerEvent) { - if (!dragging.current) return; - const x = Math.max(0, Math.min(e.clientX - startX.current, maxX())); - setDragX(x); - } - - function onPointerUp() { - if (!dragging.current) return; - dragging.current = false; - if (dragX >= maxX() * 0.65) { - onToggle(); - } else { - setDragX(0); - } - } - - // Quand c'est pris : clic sur la piste entière pour annuler - function onTrackClick() { - if (medsTaken && !disabled) onToggle(); - } - - const progress = medsTaken ? 1 : dragX / (maxX() || 1); - const isDragging = dragX > 0; - - return ( -
- {/* Fill de couleur qui suit la progression */} -
- - {/* Texte centré dans la piste */} -
- {!medsTaken && ( - - Glisse → médicaments pris - - )} - {medsTaken && ( - - ✓ Médicaments pris — tap pour annuler - - )} - {!medsTaken && progress > 0.3 && ( - - Lâche pour confirmer - - )} -
- - {/* Thumb */} -
- - {medsTaken ? "✓" : "→"} - -
-
- ); -} - -// --------------------------------------------------------------------------- -// Dashboard -// --------------------------------------------------------------------------- - function Dashboard() { - const qc = useQueryClient(); - - const todos = useQuery({ + const { data, isLoading, isError } = useQuery({ queryKey: ["todos", false], queryFn: () => api("/todos"), }); - const checkin = useQuery({ - queryKey: ["health-tab", "today"], - queryFn: () => api("/health-tab/today"), - }); - - const toggleMeds = useMutation({ - mutationFn: () => - api("/health-tab/today/toggle", { method: "POST", body: "{}" }), - onSuccess: (data) => { - qc.setQueryData(["health-tab", "today"], data); - }, - }); - - const active = (todos.data ?? []).filter( + const active = (data ?? []).filter( (t) => t.status !== "done" && t.status !== "archived", ); - const medsTaken = checkin.data?.meds_taken ?? false; - return (
@@ -180,29 +35,8 @@ function Dashboard() {
- {/* ── Médocs ──────────────────────────────────────────────────── */} -
-
- - - {checkin.isLoading ? "…" : medsTaken ? "Pris" : "Non pris"} - -
+ -
- toggleMeds.mutate()} - disabled={toggleMeds.isPending || checkin.isLoading} - /> -
-
- - {/* ── En cours ──────────────────────────────────────────────── */}
@@ -214,9 +48,9 @@ function Dashboard() {
- {todos.isLoading ? ( + {isLoading ? ( - ) : todos.isError ? ( + ) : isError ? ( ) : active.length === 0 ? (