hellth-hub/apps/admin/app/(authed)/wearables/page.tsx
Sebastian Mayer 2436dd9cac Initial commit: Hellth Hub monorepo
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>
2026-05-27 22:15:16 +02:00

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>
);
}