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>
84 lines
3.9 KiB
TypeScript
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>
|
|
);
|
|
}
|