feat(me): show actionable personal tasks with unavailable states

This commit is contained in:
Simo committed 2026-09-11 00:15:46 +02:00
1 parent d9ea93a2e7
commit a537c55793
9 files changed
+435 -12

No files matched your search

File diff suppressed because it is too large. Load diff
+72
View File
@@ -0,0 +1,72 @@
import { getFormatter, getTranslations } from "next-intl/server";
import Link from "@/components/link";
import { SurfaceCard } from "@/components/surface-card";
import { slugify } from "@/lib/format";
import { loadDashboardTasks } from "@/lib/services/dashboard-tasks";
import styles from "./dashboard.module.css";
export default async function DashboardTasks({
userId,
rewardAvailable,
}: {
userId: number;
rewardAvailable: boolean | null;
}) {
const [data, t, format] = await Promise.all([
loadDashboardTasks(userId),
getTranslations("pages.myDashboard.tasks"),
getFormatter(),
]);
const items: { href: string; label: string }[] = [];
if (data.support)
items.push({
href: "/help/tickets",
label: t("support", { count: data.support }),
});
if (data.friendRequests)
items.push({
href: "/friends",
label: t("friends", { count: data.friendRequests }),
});
if (data.registeredEvent)
items.push({
href: `/events/${slugify(data.registeredEvent.title)}-${data.registeredEvent.id}`,
label: t("event", {
title: data.registeredEvent.title,
date: format.dateTime(data.registeredEvent.startsAt, {
dateStyle: "medium",
timeStyle: "short",
}),
}),
});
if (rewardAvailable)
items.push({ href: "#referral-reward", label: t("reward") });
const unavailable =
data.support === null ||
data.friendRequests === null ||
data.registeredEvent === null ||
rewardAvailable === null;
return (
<SurfaceCard title={t("title")} bodyClassName="p-5">
{items.length > 0 ? (
<ul className={styles.tasks}>
{items.map((item) => (
<li key={item.href}>
<Link href={item.href}>
{item.label}
<span aria-hidden="true"> →</span>
</Link>
</li>
))}
</ul>
) : !unavailable ? (
<p className="muted">{t("empty")}</p>
) : null}
{unavailable && (
<p role="status" className="muted">
{t("unavailable")} <Link href="/me">{t("retry")}</Link>
</p>
)}
</SurfaceCard>
);
}
+17
View File
@@ -325,3 +325,20 @@
text-align: center;
}
}
.tasks {
display: grid;
gap: 10px;
list-style: none;
padding: 0;
margin: 0;
}
.tasks a {
display: block;
padding: 12px;
border: 1px solid var(--color-border);
border-radius: 10px;
overflow-wrap: anywhere;
}
.tasks a:hover {
background: color-mix(in srgb, var(--color-primary) 8%, var(--color-surface));
}
+17 -3
View File
@@ -13,6 +13,7 @@ import { auth } from "@/lib/auth";
import { avatarImageUrl, slugify } from "@/lib/format";
import { loadUserDashboard } from "@/lib/services/user-dashboard";
import CopyReferralButton from "./CopyReferralButton";
import DashboardTasks from "./DashboardTasks";
import styles from "./dashboard.module.css";
export async function generateMetadata() {
@@ -96,9 +97,13 @@ export default async function MePage({
: null;
const referralTotal = Math.max(
0,
Number(referralsRows[0]?.referralsTotal ?? 0),
Number(referralsRows?.[0]?.referralsTotal ?? 0),
);
const canClaim = referralTotal >= needed;
const canClaim =
referralsRows !== null &&
referralTotal >= needed &&
rewardAmount > 0 &&
rewardKind !== null;
const remaining = Math.max(0, needed - referralTotal);
const onlineFriends = friends.filter((friend) => friend.online === "1");
const date = (value: number) =>
@@ -195,6 +200,10 @@ export default async function MePage({
))}
</nav>
{alertRaw && <p className={styles.feedback}>{alertRaw}</p>}
<DashboardTasks
userId={userId}
rewardAvailable={referralsRows === null ? null : canClaim}
/>
<section aria-label={t("wallet")} className={styles.wallet}>
{wallet.map((currency) => (
<SurfaceCard key={currency.kind} className={styles.currency}>
@@ -334,6 +343,7 @@ export default async function MePage({
</div>
<aside className={styles.stack}>
<SurfaceCard title={t("referrals")} bodyClassName="p-5">
<div id="referral-reward" />
<p>{t("referralHint")}</p>
<div className={styles.progressLabel}>
<strong>
@@ -369,7 +379,11 @@ export default async function MePage({
className={styles.referralInput}
/>
<CopyReferralButton value={referralLink} />
{canClaim ? (
{referralsRows === null ? (
<p role="status" className="muted">
{t("tasks.rewardUnavailable")}
</p>
) : canClaim ? (
<form action={claimReferral} className={styles.claim}>
<button type="submit" className="btn btn-primary">
{t("claim")}