From bc4534871829c9c305c59fc44b0914c1d33b5135 Mon Sep 17 00:00:00 2001 From: Paul Brissaud Date: Mon, 9 Mar 2026 15:17:12 +0100 Subject: [PATCH 1/4] feat: replace recent activity list with modal showing full history - Limit dashboard preview to 3 items (was 5) - Increase backend query limit to 20 for modal use - Extract client component with "View All Activity" button that opens a dialog - Dialog features a purple header with total XP + count, and timeline grouped by month Co-Authored-By: Claude Sonnet 4.6 --- components/dashboard-recent-gains-client.tsx | 171 +++++++++++++++++++ components/dashboard-recent-gains.tsx | 60 +------ server/api/routers/xpTransaction.ts | 2 +- 3 files changed, 174 insertions(+), 59 deletions(-) create mode 100644 components/dashboard-recent-gains-client.tsx diff --git a/components/dashboard-recent-gains-client.tsx b/components/dashboard-recent-gains-client.tsx new file mode 100644 index 00000000..10621e2f --- /dev/null +++ b/components/dashboard-recent-gains-client.tsx @@ -0,0 +1,171 @@ +"use client"; + +import { Clock, History, Trophy } from "lucide-react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { cn } from "@/lib/utils"; +import type { RouterOutputs } from "@/types/trpc"; + +type RecentGains = RouterOutputs["xpTransaction"]["getRecentGains"]; +type Gain = RecentGains[number]; + +function groupByMonth(gains: RecentGains) { + const map = new Map(); + for (const gain of gains) { + const key = `${gain.createdAt.getFullYear()}-${gain.createdAt.getMonth()}`; + const label = gain.createdAt.toLocaleDateString("en-US", { + month: "long", + year: "numeric", + }); + const group = map.get(key); + if (group) { + group.items.push(gain); + } else { + map.set(key, { label, items: [gain] }); + } + } + return Array.from(map.entries()).map(([key, value]) => ({ key, ...value })); +} + +function ActivityItem({ + activity, + compact = false, +}: { + activity: Gain; + compact?: boolean; +}) { + return ( +
+
+
+ +
+
+
+ + {activity.challengeTitle} + + {activity.xpAmount > 0 && ( +
+ + +{activity.xpAmount} XP + +
+ )} +
+
+

+ {activity.description} +

+

+ {activity.createdAt.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + })} +

+
+
+
+
+ ); +} + +export function DashboardRecentGainsClient({ + recentGains, +}: { + recentGains: RecentGains; +}) { + const preview = recentGains.slice(0, 3); + const totalXp = recentGains.reduce((sum, g) => sum + g.xpAmount, 0); + const grouped = groupByMonth(recentGains); + + return ( +
+
+
+ +
+

Recent Activity

+
+
+ {preview.map((activity) => ( + + ))} +
+ + + + + + button:last-child]:text-primary-foreground [&>button:last-child]:opacity-80 [&>button:last-child]:hover:opacity-100", + )} + > + Activity Log + + {/* Purple header */} +
+
+ + + Activity Log + +
+
+
+ + +{totalXp} XP + +
+ + {recentGains.length}{" "} + {recentGains.length === 1 ? "activity" : "activities"} + +
+
+ + {/* Grouped list */} +
+ {grouped.map(({ label, key, items }) => ( +
+
+ + {label} + +
+
+
+ {items.map((gain) => ( + + ))} +
+
+ ))} +
+ +
+
+ ); +} diff --git a/components/dashboard-recent-gains.tsx b/components/dashboard-recent-gains.tsx index bc4942e2..a8c873c3 100644 --- a/components/dashboard-recent-gains.tsx +++ b/components/dashboard-recent-gains.tsx @@ -1,66 +1,10 @@ -import { ArrowRight, Clock, Trophy } from "lucide-react"; -import Link from "next/link"; -import { Button } from "@/components/ui/button"; import { getQueryClient, trpc } from "@/trpc/server"; +import { DashboardRecentGainsClient } from "./dashboard-recent-gains-client"; export async function DashboardRecentGains() { const queryClient = getQueryClient(); - const recentGains = await queryClient.fetchQuery( trpc.xpTransaction.getRecentGains.queryOptions(), ); - - return ( -
-
-
- -
-

Recent Activity

-
-
- {recentGains.map((activity) => ( -
-
-
- -
-
- - {activity.challengeTitle} - -

- {activity.description} -

- {activity.xpAmount > 0 && ( -
-
- - +{activity.xpAmount} XP - -
-
- )} -

- {activity.createdAt.toLocaleDateString()} -

-
-
-
- ))} -
- -
- ); + return ; } diff --git a/server/api/routers/xpTransaction.ts b/server/api/routers/xpTransaction.ts index 058141c7..d571e899 100644 --- a/server/api/routers/xpTransaction.ts +++ b/server/api/routers/xpTransaction.ts @@ -24,7 +24,7 @@ export const xpTransactionRouter = createTRPCRouter({ .leftJoin(challenge, eq(userXpTransaction.challengeId, challenge.id)) .where(eq(userXpTransaction.userId, userId)) .orderBy(desc(userXpTransaction.createdAt)) - .limit(5); + .limit(20); return recentGains; }), From 16e834c3dce971680db079a804ee213baaf98040 Mon Sep 17 00:00:00 2001 From: Paul Brissaud Date: Mon, 9 Mar 2026 15:25:39 +0100 Subject: [PATCH 2/4] fix: address null challenge fields, misleading XP total, and fragile dialog close button - Guard against null challengeSlug/challengeTitle from leftJoin - Rename totalXp to recentXp and label it "+X XP recently" in modal header - Add hideCloseButton prop to DialogContent; use explicit DialogClose in purple header - Hide "View All Activity" button and modal when recentGains is empty - Extract PREVIEW_COUNT constant (was magic number 3) Co-Authored-By: Claude Sonnet 4.6 --- components/dashboard-recent-gains-client.tsx | 180 +++++++++++-------- components/ui/dialog.tsx | 16 +- 2 files changed, 117 insertions(+), 79 deletions(-) diff --git a/components/dashboard-recent-gains-client.tsx b/components/dashboard-recent-gains-client.tsx index 10621e2f..28b91982 100644 --- a/components/dashboard-recent-gains-client.tsx +++ b/components/dashboard-recent-gains-client.tsx @@ -1,10 +1,11 @@ "use client"; -import { Clock, History, Trophy } from "lucide-react"; +import { Clock, History, Trophy, X } from "lucide-react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Dialog, + DialogClose, DialogContent, DialogTitle, DialogTrigger, @@ -12,6 +13,8 @@ import { import { cn } from "@/lib/utils"; import type { RouterOutputs } from "@/types/trpc"; +const PREVIEW_COUNT = 3; + type RecentGains = RouterOutputs["xpTransaction"]["getRecentGains"]; type Gain = RecentGains[number]; @@ -40,6 +43,9 @@ function ActivityItem({ activity: Gain; compact?: boolean; }) { + const hasChallenge = + activity.challengeSlug !== null && activity.challengeTitle !== null; + return (
- - {activity.challengeTitle} - + {hasChallenge ? ( + + {activity.challengeTitle} + + ) : ( + + {activity.description} + + )} {activity.xpAmount > 0 && (
@@ -69,17 +81,27 @@ function ActivityItem({
)}
-
-

- {activity.description} -

-

+ {hasChallenge && ( +

+

+ {activity.description} +

+

+ {activity.createdAt.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + })} +

+
+ )} + {!hasChallenge && ( +

{activity.createdAt.toLocaleDateString("en-US", { month: "short", day: "numeric", })}

-
+ )}
@@ -91,8 +113,8 @@ export function DashboardRecentGainsClient({ }: { recentGains: RecentGains; }) { - const preview = recentGains.slice(0, 3); - const totalXp = recentGains.reduce((sum, g) => sum + g.xpAmount, 0); + const preview = recentGains.slice(0, PREVIEW_COUNT); + const recentXp = recentGains.reduce((sum, g) => sum + g.xpAmount, 0); const grouped = groupByMonth(recentGains); return ( @@ -103,69 +125,81 @@ export function DashboardRecentGainsClient({

Recent Activity

-
- {preview.map((activity) => ( - - ))} -
- - - - - button:last-child]:text-primary-foreground [&>button:last-child]:opacity-80 [&>button:last-child]:hover:opacity-100", - )} - > - Activity Log - - {/* Purple header */} -
-
- - - Activity Log - -
-
-
- - +{totalXp} XP - -
- - {recentGains.length}{" "} - {recentGains.length === 1 ? "activity" : "activities"} - -
+ {recentGains.length === 0 ? ( +

+ No activity yet. Complete a challenge to get started! +

+ ) : ( + <> +
+ {preview.map((activity) => ( + + ))}
- {/* Grouped list */} -
- {grouped.map(({ label, key, items }) => ( -
-
- - {label} - -
-
-
- {items.map((gain) => ( - - ))} + + + + + + Activity Log + + {/* Purple header with explicit close button */} +
+
+
+ + + Activity Log + +
+
+
+ + +{recentXp} XP recently + +
+ + {recentGains.length}{" "} + {recentGains.length === 1 ? "activity" : "activities"} + +
+ + + Close +
- ))} -
- -
+ + {/* Grouped list */} +
+ {grouped.map(({ label, key, items }) => ( +
+
+ + {label} + +
+
+
+ {items.map((gain) => ( + + ))} +
+
+ ))} +
+ + + + )}
); } diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx index 9dceac09..26bc3d5c 100644 --- a/components/ui/dialog.tsx +++ b/components/ui/dialog.tsx @@ -31,8 +31,10 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { + hideCloseButton?: boolean; + } +>(({ className, children, hideCloseButton = false, ...props }, ref) => ( {children} - - - Close - + {!hideCloseButton && ( + + + Close + + )} )); From 34a615818de0fc084f668c50af3f243841513924 Mon Sep 17 00:00:00 2001 From: Paul Brissaud Date: Mon, 9 Mar 2026 15:45:36 +0100 Subject: [PATCH 3/4] test with 4 Signed-off-by: Paul Brissaud --- components/dashboard-recent-gains-client.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/dashboard-recent-gains-client.tsx b/components/dashboard-recent-gains-client.tsx index 28b91982..d29cf4f1 100644 --- a/components/dashboard-recent-gains-client.tsx +++ b/components/dashboard-recent-gains-client.tsx @@ -13,7 +13,7 @@ import { import { cn } from "@/lib/utils"; import type { RouterOutputs } from "@/types/trpc"; -const PREVIEW_COUNT = 3; +const PREVIEW_COUNT = 4; type RecentGains = RouterOutputs["xpTransaction"]["getRecentGains"]; type Gain = RecentGains[number]; From 93e0b0bc8f76fb2af170524cdf0ce428afa9e5c1 Mon Sep 17 00:00:00 2001 From: Paul Brissaud Date: Mon, 9 Mar 2026 15:51:23 +0100 Subject: [PATCH 4/4] nit: qualify activity count as "recent" in modal header Co-Authored-By: Claude Sonnet 4.6 --- components/dashboard-recent-gains-client.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/dashboard-recent-gains-client.tsx b/components/dashboard-recent-gains-client.tsx index d29cf4f1..3dd82ce6 100644 --- a/components/dashboard-recent-gains-client.tsx +++ b/components/dashboard-recent-gains-client.tsx @@ -167,7 +167,7 @@ export function DashboardRecentGainsClient({ - {recentGains.length}{" "} + {recentGains.length} recent{" "} {recentGains.length === 1 ? "activity" : "activities"}