merge(import): success token, CSV header single-source, Link nav, responsive grid, counters

This commit is contained in:
Marc-André Bombeck committed 2026-06-15 01:24:10 +02:00
commit 34875ce8c5
7 files changed
+41 -9

No files matched your search

+1
View File
@@ -4007,6 +4007,7 @@
"import": {
"heading": "Import",
"description": "Importiere Daten aus einem Apple-Health-Export oder einer JSON-Datei. Bereits vorhandene Einträge werden übersprungen, ein erneuter Import ist also unbedenklich.",
"charCount": "{used} / {max} Zeichen",
"appleHealth": {
"title": "Apple Health",
"description": "Lade die export.zip aus der Health-App auf dem iPhone hoch (Profil → Alle Gesundheitsdaten exportieren).",
+1
View File
@@ -4007,6 +4007,7 @@
"import": {
"heading": "Import",
"description": "Bring data into HealthLog from an Apple Health export, a JSON file, or a CSV. Imports skip rows that already exist, so re-running one is safe.",
"charCount": "{used} / {max} characters",
"appleHealth": {
"title": "Apple Health",
"description": "Upload the export.zip from the Health app on iPhone (Profile → Export All Health Data).",
+1
View File
@@ -4007,6 +4007,7 @@
"import": {
"heading": "Importar",
"description": "Importa datos a HealthLog desde una exportación de Apple Health o un archivo JSON. La importación omite las filas que ya existen, así que repetirla es seguro.",
"charCount": "{used} / {max} caracteres",
"appleHealth": {
"title": "Apple Health",
"description": "Sube el archivo export.zip de la app Salud del iPhone (Perfil → Exportar todos los datos de salud).",
+1
View File
@@ -4007,6 +4007,7 @@
"import": {
"heading": "Import",
"description": "Importez des données dans HealthLog depuis un export Apple Santé ou un fichier JSON. L'import ignore les lignes déjà présentes : le relancer est sans risque.",
"charCount": "{used} / {max} caractères",
"appleHealth": {
"title": "Apple Santé",
"description": "Importez le fichier export.zip de l'app Santé sur iPhone (Profil → Exporter toutes les données de santé).",
+1
View File
@@ -4007,6 +4007,7 @@
"import": {
"heading": "Importa",
"description": "Importa dati in HealthLog da un'esportazione di Apple Salute o da un file JSON. L'importazione salta le righe già presenti, quindi ripeterla è sicuro.",
"charCount": "{used} / {max} caratteri",
"appleHealth": {
"title": "Apple Salute",
"description": "Carica il file export.zip dall'app Salute su iPhone (Profilo → Esporta tutti i dati sanitari).",
+1
View File
@@ -4007,6 +4007,7 @@
"import": {
"heading": "Import",
"description": "Zaimportuj dane do HealthLog z eksportu Apple Health lub pliku JSON. Import pomija wiersze, które już istnieją, więc ponowne uruchomienie jest bezpieczne.",
"charCount": "{used} / {max} znaków",
"appleHealth": {
"title": "Apple Health",
"description": "Prześlij plik export.zip z aplikacji Zdrowie na iPhonie (Profil → Eksportuj wszystkie dane zdrowotne).",
+35 -9
View File
@@ -26,6 +26,7 @@
*/
import { useCallback, useId, useRef, useState } from "react";
import Link from "next/link";
import { useQuery } from "@tanstack/react-query";
import {
AlertCircle,
@@ -45,6 +46,14 @@ import { queryKeys } from "@/lib/query-keys";
import { useTranslations } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
import { apiFetchRaw, apiGet } from "@/lib/api/api-fetch";
import { CSV_EXAMPLE_COLUMNS } from "@/lib/import/csv-measurements";
/**
* Upper bound for the paste textareas, mirroring the 16 MB server-side body
* ceiling on `/api/import` and `/api/import/csv`. Caps an accidental over-paste
* before it ever reaches the route and feeds the live character counter.
*/
const MAX_PASTE_CHARS = 16 * 1024 * 1024;
// ─────────────────────────── Section wrapper ───────────────────────────
@@ -66,7 +75,7 @@ export function ImportPanel() {
{t("settings.sections.export.import.description")}
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<AppleHealthImportCard />
<JsonImportCard />
<CsvImportCard />
@@ -321,7 +330,7 @@ function AppleHealthImportCard() {
className="text-foreground flex items-start gap-2 text-xs"
>
<CheckCircle2
className="mt-0.5 h-3.5 w-3.5 shrink-0 text-emerald-500"
className="text-success mt-0.5 h-3.5 w-3.5 shrink-0"
aria-hidden="true"
/>
<span>
@@ -535,10 +544,17 @@ function JsonImportCard() {
value={text}
onChange={(e) => setText(e.target.value)}
rows={5}
maxLength={MAX_PASTE_CHARS}
spellCheck={false}
placeholder='{"measurements":[…],"moodEntries":[…]}'
className="font-mono text-xs"
/>
<p className="text-muted-foreground text-right text-xs tabular-nums">
{t("settings.sections.export.import.charCount", {
used: text.length,
max: MAX_PASTE_CHARS,
})}
</p>
</div>
<input
@@ -552,12 +568,12 @@ function JsonImportCard() {
<p className="text-muted-foreground text-xs">
{t("settings.sections.export.import.json.schemaHint")}{" "}
<a
<Link
href="/docs/integrations/data-import"
className="text-primary underline underline-offset-2"
>
{t("settings.sections.export.import.json.docsLink")}
</a>
</Link>
</p>
<div aria-live="polite" className="space-y-2">
@@ -567,7 +583,7 @@ function JsonImportCard() {
className="text-foreground flex items-start gap-2 text-xs"
>
<CheckCircle2
className="mt-0.5 h-3.5 w-3.5 shrink-0 text-emerald-500"
className="text-success mt-0.5 h-3.5 w-3.5 shrink-0"
aria-hidden="true"
/>
<span>
@@ -641,7 +657,7 @@ function JsonImportCard() {
* drift. Exported so a test can assert it stays a valid header.
*/
export const EXAMPLE_CSV = [
"type,value,unit,measuredAt,glucoseContext,notes,externalId",
CSV_EXAMPLE_COLUMNS.join(","),
"WEIGHT,80.5,kg,2026-05-01T08:00:00Z,,morning,",
"BLOOD_GLUCOSE,5.3,mmol/L,2026-05-01T08:05:00+02:00,FASTING,,meter-001",
"BLOOD_PRESSURE_SYS,120,mmHg,2026-05-01T08:05:00+02:00,,,",
@@ -760,10 +776,17 @@ function CsvImportCard() {
value={text}
onChange={(e) => setText(e.target.value)}
rows={5}
maxLength={MAX_PASTE_CHARS}
spellCheck={false}
placeholder="type,value,unit,measuredAt,…"
className="font-mono text-xs"
/>
<p className="text-muted-foreground text-right text-xs tabular-nums">
{t("settings.sections.export.import.charCount", {
used: text.length,
max: MAX_PASTE_CHARS,
})}
</p>
</div>
<input
@@ -777,12 +800,12 @@ function CsvImportCard() {
<p className="text-muted-foreground text-xs">
{t("settings.sections.export.import.csv.schemaHint")}{" "}
<a
<Link
href="/docs/integrations/data-import"
className="text-primary underline underline-offset-2"
>
{t("settings.sections.export.import.csv.docsLink")}
</a>
</Link>
</p>
<div aria-live="polite" className="space-y-2">
@@ -790,7 +813,7 @@ function CsvImportCard() {
<div data-testid="import-csv-result" className="space-y-1.5">
<p className="text-foreground flex items-start gap-2 text-xs">
<CheckCircle2
className="mt-0.5 h-3.5 w-3.5 shrink-0 text-emerald-500"
className="text-success mt-0.5 h-3.5 w-3.5 shrink-0"
aria-hidden="true"
/>
<span>
@@ -863,6 +886,9 @@ function CsvImportCard() {
onClick={() => void send(true)}
data-testid="import-csv-preview"
>
{busy && (
<Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
)}
{t("settings.sections.export.import.csv.preview")}
</Button>
<Button