hellth-hub/apps/admin/app/(authed)/aktivitaetsfeed/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

84 lines
3.9 KiB
TypeScript

"use client";
import Link from "next/link";
import { trpc } from "@/lib/trpc";
function statusLabel(status: string) {
if (status === "accepted") return "übernommen";
if (status === "rejected") return "abgelehnt";
if (status === "pending") return "in Prüfung";
return status;
}
export default function AktivitaetsfeedPage() {
const utils = trpc.useUtils();
const feedQuery = trpc.health.adminActivityFeed.useQuery();
const reviewStoreChange = trpc.health.reviewStoreChangeRequest.useMutation({
onSuccess: async () => {
await utils.health.adminActivityFeed.invalidate();
await utils.health.listStores.invalidate();
},
});
const items = feedQuery.data ?? [];
return (
<div className="mx-auto max-w-5xl space-y-5 pb-24">
<header className="rounded-[2rem] bg-white p-6 shadow-sm ring-1 ring-gray-100">
<p className="text-sm font-black uppercase text-[#0F766E]">Admin</p>
<h1 className="mt-2 text-4xl font-black tracking-normal text-gray-950">Activity Feed</h1>
<p className="mt-2 max-w-2xl text-sm font-semibold text-gray-400">
Eingereichte Rezeptvorschläge, Marktänderungen und weitere prüfpflichtige Aktionen an einem Ort.
</p>
</header>
<section className="rounded-[2rem] bg-white p-5 shadow-sm ring-1 ring-gray-100">
<div className="space-y-3">
{items.map((item) => (
<div key={`${item.type}-${item.id}`} className="rounded-3xl bg-gray-50 p-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<p className="text-xs font-black uppercase text-[#0F766E]">
{item.type === "recipe_submission" ? "Rezeptvorschlag" : "Marktänderung"}
</p>
<h2 className="mt-1 text-lg font-black text-gray-950">{item.title}</h2>
<p className="mt-1 text-sm font-bold text-gray-400">{item.actorName}{item.actorEmail ? ` · ${item.actorEmail}` : ""}</p>
<p className="mt-1 text-sm font-semibold text-gray-500">{item.detail}</p>
</div>
<div className="text-right">
<span className={`rounded-full px-3 py-1 text-xs font-black ${
item.status === "accepted"
? "bg-emerald-100 text-emerald-700"
: item.status === "rejected"
? "bg-red-100 text-red-700"
: "bg-amber-100 text-amber-700"
}`}>
{statusLabel(item.status)}
</span>
<p className="mt-2 text-xs font-bold text-gray-400">{new Date(item.createdAt).toLocaleString("de-DE")}</p>
</div>
</div>
<div className="mt-3 flex flex-wrap gap-2">
<Link href={item.href} className="rounded-full bg-white px-4 py-2 text-xs font-black text-gray-700">
Öffnen
</Link>
{item.type === "store_change" && item.status === "pending" ? (
<>
<button type="button" onClick={() => reviewStoreChange.mutate({ id: item.id, action: "accept" })} className="rounded-full bg-[#0F766E] px-4 py-2 text-xs font-black text-white">
Übernehmen
</button>
<button type="button" onClick={() => reviewStoreChange.mutate({ id: item.id, action: "reject" })} className="rounded-full bg-red-50 px-4 py-2 text-xs font-black text-red-700">
Ablehnen
</button>
</>
) : null}
</div>
</div>
))}
{!items.length && !feedQuery.isLoading ? (
<p className="rounded-3xl bg-gray-50 p-4 text-sm font-bold text-gray-400">Aktuell gibt es keine Einträge.</p>
) : null}
</div>
</section>
</div>
);
}