diff --git a/components/dashboard-recent-gains-client.tsx b/components/dashboard-recent-gains-client.tsx new file mode 100644 index 0000000..3dd82ce --- /dev/null +++ b/components/dashboard-recent-gains-client.tsx @@ -0,0 +1,205 @@ +"use client"; + +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, +} from "@/components/ui/dialog"; +import { cn } from "@/lib/utils"; +import type { RouterOutputs } from "@/types/trpc"; + +const PREVIEW_COUNT = 4; + +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; +}) { + const hasChallenge = + activity.challengeSlug !== null && activity.challengeTitle !== null; + + return ( +
+
+
+ +
+
+
+ {hasChallenge ? ( + + {activity.challengeTitle} + + ) : ( + + {activity.description} + + )} + {activity.xpAmount > 0 && ( +
+ + +{activity.xpAmount} XP + +
+ )} +
+ {hasChallenge && ( +
+

+ {activity.description} +

+

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

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

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

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

Recent Activity

+
+ + {recentGains.length === 0 ? ( +

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

+ ) : ( + <> +
+ {preview.map((activity) => ( + + ))} +
+ + + + + + + Activity Log + + {/* Purple header with explicit close button */} +
+
+
+ + + Activity Log + +
+
+
+ + +{recentXp} XP recently + +
+ + {recentGains.length} recent{" "} + {recentGains.length === 1 ? "activity" : "activities"} + +
+
+ + + Close + +
+ + {/* 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 bc4942e..a8c873c 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/components/ui/dialog.tsx b/components/ui/dialog.tsx index 9dceac0..26bc3d5 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 + + )} )); diff --git a/server/api/routers/xpTransaction.ts b/server/api/routers/xpTransaction.ts index 058141c..d571e89 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; }),