Feature: Fremdprofil-Seite /profil/[handle] + klickbare Usernames im Leaderboard
UI-Teil zum Fremdprofil-Backend (2dfc770).
apps/admin/app/(authed)/profil/[handle]/page.tsx:
- Oeffentliche Profilseite: Avatar/Initiale, Handle, Bio, Punkte (gesamt +
30 Tage) und aktive Tage (7-Tage-Streak).
- Sauberes NOT_FOUND-Handling fuer private/unbekannte Profile.
- Hinweis, dass Gesundheitsdaten immer privat bleiben.
dashboard.ts (clubLeaderboard):
- Liefert pro Eintrag einen handleSlug, aber NUR fuer profileVisible=true
(sonst null -> kein toter Link).
hellfireclub/page.tsx:
- Leaderboard-Namen werden zu Links auf /profil/[handleSlug], wenn das
Profil oeffentlich ist; sonst unveraenderter Text.
Verifiziert: Typecheck 3/3, 75 API-Tests + 10 Admin-Tests gruen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2dfc770950
commit
af66d49f2c
3 changed files with 124 additions and 1 deletions
|
|
@ -436,7 +436,16 @@ export default function HellfireclubPage() {
|
|||
<div key={entry.userKey} className="flex items-center gap-3 rounded-2xl bg-gray-50 p-3">
|
||||
<div className="grid h-9 w-9 place-items-center rounded-full bg-gray-950 text-sm font-black text-white">{index + 1}</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-black text-gray-950">{entry.name}</p>
|
||||
{entry.handleSlug ? (
|
||||
<Link
|
||||
href={`/profil/${entry.handleSlug}`}
|
||||
className="truncate text-sm font-black text-[#0F766E] underline-offset-2 hover:underline"
|
||||
>
|
||||
{entry.name}
|
||||
</Link>
|
||||
) : (
|
||||
<p className="truncate text-sm font-black text-gray-950">{entry.name}</p>
|
||||
)}
|
||||
<p className="text-xs font-bold text-gray-400">{entry.completed} Punkte-Einträge</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
|
|
|
|||
113
apps/admin/app/(authed)/profil/[handle]/page.tsx
Normal file
113
apps/admin/app/(authed)/profil/[handle]/page.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
"use client";
|
||||
|
||||
import { FireIcon, SparklesIcon } from "@heroicons/react/24/outline";
|
||||
import { useParams } from "next/navigation";
|
||||
import { DataPageLayout } from "@/app/_components/data-page-layout";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
|
||||
export default function PublicProfilePage() {
|
||||
const params = useParams<{ handle: string }>();
|
||||
const handleSlug = decodeURIComponent(params.handle ?? "");
|
||||
|
||||
const profileQuery = trpc.users.publicProfileByHandle.useQuery(
|
||||
{ handle: handleSlug },
|
||||
{ retry: false },
|
||||
);
|
||||
|
||||
if (profileQuery.isLoading) {
|
||||
return (
|
||||
<DataPageLayout title="Profil">
|
||||
<p className="text-sm text-gray-500">Profil wird geladen …</p>
|
||||
</DataPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (profileQuery.isError || !profileQuery.data) {
|
||||
return (
|
||||
<DataPageLayout title="Profil nicht gefunden">
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-6 text-sm text-gray-600">
|
||||
Dieses Profil gibt es nicht oder es ist privat. Nur Mitglieder, die ihr
|
||||
Profil öffentlich geschaltet haben, sind hier sichtbar.
|
||||
</div>
|
||||
</DataPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const profile = profileQuery.data;
|
||||
const initial = (profile.username ?? "?").charAt(0).toUpperCase();
|
||||
|
||||
return (
|
||||
<DataPageLayout title={profile.handle ?? profile.username ?? "Profil"}>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4 rounded-xl border border-gray-200 bg-white p-5">
|
||||
{profile.image ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={profile.image}
|
||||
alt={profile.username ?? "Avatar"}
|
||||
className="h-16 w-16 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-[#0F766E] text-xl font-bold text-white">
|
||||
{initial}
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-lg font-semibold text-gray-900">
|
||||
{profile.username}
|
||||
<span className="font-normal text-gray-400">#{profile.discriminator}</span>
|
||||
</p>
|
||||
{profile.bio ? (
|
||||
<p className="mt-1 whitespace-pre-line text-sm text-gray-600">{profile.bio}</p>
|
||||
) : (
|
||||
<p className="mt-1 text-sm text-gray-400">Keine Bio hinterlegt.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
||||
<StatCard
|
||||
icon={<FireIcon className="h-5 w-5 text-[#0F766E]" />}
|
||||
label="Punkte gesamt"
|
||||
value={profile.points.total.toLocaleString("de-DE")}
|
||||
/>
|
||||
<StatCard
|
||||
icon={<FireIcon className="h-5 w-5 text-[#0F766E]" />}
|
||||
label="Punkte (30 Tage)"
|
||||
value={profile.points.last30Days.toLocaleString("de-DE")}
|
||||
/>
|
||||
<StatCard
|
||||
icon={<SparklesIcon className="h-5 w-5 text-[#0F766E]" />}
|
||||
label="Aktive Tage (7 Tage)"
|
||||
value={`${profile.activeDaysLast7}/7`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="px-1 text-xs text-gray-400">
|
||||
Gesundheitsdaten wie Gewicht, Kalorien und Ziele sind immer privat und
|
||||
werden hier nie angezeigt.
|
||||
</p>
|
||||
</div>
|
||||
</DataPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4">
|
||||
<div className="flex items-center gap-2 text-xs font-medium text-gray-500">
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
<p className="mt-2 text-2xl font-bold text-gray-900">{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { schema } from "@hellth/db";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { handleToSlug } from "../../../lib/handle";
|
||||
import { platformProcedure, protectedProcedure, router } from "../../init";
|
||||
import {
|
||||
auditHealthMutation,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue