Mobile-first Web-App fuer persoenliches Health-, Ernaehrungs-, Aktivitaets- und Einkaufs-Tracking im Home-Lab-Betrieb. Stack: pnpm Monorepo - Next.js 16 + React 19 + Tailwind (apps/admin) - Hono + tRPC + Better Auth mit 2FA und Passkeys (packages/api) - PostgreSQL + Drizzle ORM (packages/db) - Docker fuer Postgres und pgAdmin. Module: - Auth mit Login, 2FA (TOTP + Backup-Codes), Passkeys und Admin-Recovery - Tagesansicht mit Essen, Trinken, Gewicht, Aktivitaeten und Coach-Hinweisen - Lebensmittelkatalog mit Naehrwerten, Portionen, OCR/Barcode/Open Food Facts - Rezepte mit Live-Naehrwertberechnung und Community-Vorschlaegen - Essensplanung und Wochenbudget mit Sportbonus - Einkaufslisten mit Maerkten und Laufweg-Sortierung - Hellfireclub mit Aktivitaetspunkten und gemeinsamer Aktivitaetsplanung - Wearables-Vorbereitung (Fitbit / Google Fit / Health Connect) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
109 lines
5 KiB
TypeScript
109 lines
5 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
ArrowPathIcon,
|
|
BoltIcon,
|
|
CheckCircleIcon,
|
|
DevicePhoneMobileIcon,
|
|
ExclamationTriangleIcon,
|
|
} from "@heroicons/react/24/outline";
|
|
import { trpc } from "@/lib/trpc";
|
|
|
|
const plannedImports = [
|
|
"Schritte und aktive Minuten",
|
|
"Trainingseinheiten von der Pixel Watch",
|
|
"Aktivitätskalorien als optionaler Sportbonus",
|
|
"Dublettenprüfung über externe Aktivitäts-IDs",
|
|
"Punktevergabe für importierte Aktivitäten",
|
|
];
|
|
|
|
export default function WearablesPage() {
|
|
const connectionsQuery = trpc.health.wearableConnections.useQuery();
|
|
const connections = connectionsQuery.data ?? [];
|
|
const fitbit = connections.find((connection) => connection.provider === "fitbit");
|
|
|
|
return (
|
|
<div className="mx-auto max-w-5xl space-y-5 pb-24">
|
|
<header className="overflow-hidden rounded-[2rem] bg-gray-950 p-6 text-white shadow-sm">
|
|
<div className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
|
|
<div>
|
|
<p className="text-sm font-black uppercase text-emerald-300">Pixel Watch & Fitbit</p>
|
|
<h1 className="mt-2 text-4xl font-black tracking-normal">Wearables</h1>
|
|
<p className="mt-3 max-w-2xl text-sm font-semibold text-white/60">
|
|
Importiere Aktivitäten aus deiner Uhr und verwandle sie in Sportbonus, Aktivitätspunkte und bessere Wochenprognosen.
|
|
</p>
|
|
</div>
|
|
<div className="grid h-28 w-28 place-items-center rounded-full bg-white text-gray-950 ring-[12px] ring-white/10">
|
|
<DevicePhoneMobileIcon className="h-14 w-14" />
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<section className="grid gap-4 lg:grid-cols-[1fr_0.8fr]">
|
|
<div className="rounded-[2rem] bg-white p-5 shadow-sm ring-1 ring-gray-100">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div>
|
|
<h2 className="text-2xl font-black text-gray-950">Fitbit</h2>
|
|
<p className="mt-2 text-sm font-semibold text-gray-400">
|
|
Empfohlener erster Weg für die Pixel Watch. Google Fit wird nicht mehr als neue Basis aufgebaut; Health Connect bleibt als spätere Android-Brücke vorgesehen.
|
|
</p>
|
|
</div>
|
|
<span className={`rounded-full px-3 py-1 text-xs font-black ${fitbit ? "bg-emerald-50 text-emerald-700" : "bg-amber-50 text-amber-700"}`}>
|
|
{fitbit ? "verbunden" : "vorbereitet"}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="mt-5 rounded-3xl bg-gray-50 p-4">
|
|
{fitbit ? (
|
|
<div className="flex items-center gap-3">
|
|
<CheckCircleIcon className="h-7 w-7 text-emerald-600" />
|
|
<div>
|
|
<p className="text-sm font-black text-gray-950">Fitbit-Verbindung aktiv</p>
|
|
<p className="text-xs font-bold text-gray-400">
|
|
Letzter Sync: {fitbit.lastSyncAt ? new Date(fitbit.lastSyncAt).toLocaleString("de-DE") : "noch nicht synchronisiert"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-start gap-3">
|
|
<ExclamationTriangleIcon className="h-7 w-7 text-amber-600" />
|
|
<div>
|
|
<p className="text-sm font-black text-gray-950">OAuth-Verbindung ist der nächste technische Schritt</p>
|
|
<p className="mt-1 text-xs font-bold text-gray-400">
|
|
Die Tabellen und API-Struktur sind vorbereitet. Für den echten Login fehlen noch Fitbit Client-ID, Secret und Callback-Route.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-5 grid gap-3 sm:grid-cols-2">
|
|
<button type="button" disabled className="inline-flex items-center justify-center gap-2 rounded-full bg-gray-950 px-4 py-4 text-sm font-black text-white disabled:opacity-40">
|
|
<DevicePhoneMobileIcon className="h-5 w-5" />
|
|
Fitbit verbinden
|
|
</button>
|
|
<button type="button" disabled className="inline-flex items-center justify-center gap-2 rounded-full bg-gray-100 px-4 py-4 text-sm font-black text-gray-950 disabled:opacity-50">
|
|
<ArrowPathIcon className="h-5 w-5" />
|
|
Jetzt synchronisieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<aside className="rounded-[2rem] bg-white p-5 shadow-sm ring-1 ring-gray-100">
|
|
<div className="flex items-center gap-3">
|
|
<BoltIcon className="h-8 w-8 text-emerald-500" />
|
|
<h2 className="text-xl font-black text-gray-950">Geplanter Import</h2>
|
|
</div>
|
|
<div className="mt-5 space-y-3">
|
|
{plannedImports.map((item) => (
|
|
<div key={item} className="flex items-center gap-3 rounded-2xl bg-gray-50 p-3">
|
|
<CheckCircleIcon className="h-5 w-5 text-emerald-600" />
|
|
<span className="text-sm font-bold text-gray-600">{item}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</aside>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|