Skip to content

Commit db0aba1

Browse files
authored
Merge pull request #6 from prazgaitis/claude/reorganize-admin-console-zJXtJ
Reorganize admin console with grouped navigation and merged integrations
2 parents 811f989 + 6aa58b5 commit db0aba1

23 files changed

Lines changed: 733 additions & 566 deletions

File tree

.claude/napkin.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
## Patterns That Work
1111
- Convex queries can join related data inline (e.g., activity types + categories in one query)
1212
- `conditional-header.tsx` DASHBOARD_LAYOUT_PATTERNS array controls navbar visibility per route
13+
- Admin console sidebar approach was scrapped — revisit admin nav design in the future
1314

1415
## Patterns That Don't Work
1516

apps/web/app/challenges/[id]/activities/[activityId]/activity-detail-content.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,6 @@ export function ActivityDetailContent({
313313
preload="metadata"
314314
/>
315315
) : (
316-
// eslint-disable-next-line @next/next/no-img-element
317316
<img
318317
src={url}
319318
alt={`Activity media ${index + 1}`}

apps/web/app/challenges/[id]/admin/emails/[emailId]/page.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,6 @@ export default function EmailDetailPage() {
470470
<div className="flex items-center gap-2">
471471
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-zinc-800">
472472
{user.avatarUrl ? (
473-
// eslint-disable-next-line @next/next/no-img-element
474473
<img
475474
src={user.avatarUrl}
476475
alt=""
@@ -537,7 +536,6 @@ export default function EmailDetailPage() {
537536
<div className="flex items-center gap-2">
538537
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-zinc-800">
539538
{send.user?.avatarUrl ? (
540-
// eslint-disable-next-line @next/next/no-img-element
541539
<img
542540
src={send.user.avatarUrl}
543541
alt=""

apps/web/app/challenges/[id]/admin/emails/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -819,7 +819,6 @@ export default function EmailsAdminPage() {
819819
>
820820
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-zinc-800">
821821
{user.avatarUrl ? (
822-
// eslint-disable-next-line @next/next/no-img-element
823822
<img
824823
src={user.avatarUrl}
825824
alt=""
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"use client";
2+
3+
import React, { useState } from "react";
4+
import { cn } from "@/lib/utils";
5+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
6+
import { AdminIntegrationsTable } from "@/components/admin/admin-integrations-table";
7+
import { StravaPreviewClient } from "./strava-preview-client";
8+
9+
type IntegrationsTabsProps = {
10+
challengeId: string;
11+
activityTypes: React.ComponentProps<typeof AdminIntegrationsTable>["activityTypes"];
12+
participantsWithStrava: React.ComponentProps<typeof StravaPreviewClient>["participantsWithStrava"];
13+
};
14+
15+
const tabs = [
16+
{ id: "mappings", label: "Mappings" },
17+
{ id: "test", label: "Test with Participants" },
18+
] as const;
19+
20+
type TabId = (typeof tabs)[number]["id"];
21+
22+
export function IntegrationsTabs({
23+
challengeId,
24+
activityTypes,
25+
participantsWithStrava,
26+
}: IntegrationsTabsProps) {
27+
const [activeTab, setActiveTab] = useState<TabId>("mappings");
28+
29+
return (
30+
<div className="space-y-4">
31+
{/* Tab Bar */}
32+
<div className="flex items-center gap-1 border-b border-zinc-800">
33+
{tabs.map((tab) => (
34+
<button
35+
key={tab.id}
36+
type="button"
37+
onClick={() => setActiveTab(tab.id)}
38+
className={cn(
39+
"relative px-3 py-2 text-sm font-medium transition-colors",
40+
activeTab === tab.id
41+
? "text-amber-400"
42+
: "text-zinc-500 hover:text-zinc-300"
43+
)}
44+
>
45+
{tab.label}
46+
{activeTab === tab.id && (
47+
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-amber-400" />
48+
)}
49+
</button>
50+
))}
51+
</div>
52+
53+
{/* Tab Content */}
54+
{activeTab === "mappings" && (
55+
<Card>
56+
<CardHeader>
57+
<CardTitle>Integration Mappings</CardTitle>
58+
<CardDescription>
59+
Configure how activities from external services like Strava are mapped to your challenge
60+
activity types. When users sync activities from connected services, they will
61+
automatically be logged as the mapped activity type.
62+
</CardDescription>
63+
</CardHeader>
64+
<CardContent>
65+
<AdminIntegrationsTable
66+
challengeId={challengeId}
67+
activityTypes={activityTypes}
68+
/>
69+
</CardContent>
70+
</Card>
71+
)}
72+
73+
{activeTab === "test" && (
74+
<Card>
75+
<CardHeader>
76+
<CardTitle>Strava Activity Preview</CardTitle>
77+
<CardDescription>
78+
Select a participant with Strava connected to preview their recent activities
79+
and see how they would be scored according to your activity type configuration.
80+
</CardDescription>
81+
</CardHeader>
82+
<CardContent>
83+
<StravaPreviewClient
84+
challengeId={challengeId}
85+
participantsWithStrava={participantsWithStrava}
86+
/>
87+
</CardContent>
88+
</Card>
89+
)}
90+
</div>
91+
);
92+
}

apps/web/app/challenges/[id]/admin/integrations/page.tsx

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import type { Id } from "@repo/backend/_generated/dataModel";
44

55
import { requireAuth } from "@/lib/auth";
66
import { getChallengeOrThrow } from "@/lib/challenge-helpers";
7-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
8-
import { AdminIntegrationsTable } from "@/components/admin/admin-integrations-table";
7+
import { IntegrationsTabs } from "./integrations-tabs";
98

109
interface IntegrationsAdminPageProps {
1110
params: Promise<{ id: string }>;
@@ -24,27 +23,21 @@ export default async function IntegrationsAdminPage({
2423
return null;
2524
}
2625

27-
// Fetch activity types for this challenge
28-
const activityTypes = await convex.query(api.queries.activityTypes.getByChallengeId, {
29-
challengeId: challenge.id as Id<"challenges">,
30-
});
26+
// Fetch activity types and Strava participants in parallel
27+
const [activityTypes, participantsWithStrava] = await Promise.all([
28+
convex.query(api.queries.activityTypes.getByChallengeId, {
29+
challengeId: challenge.id as Id<"challenges">,
30+
}),
31+
convex.query(api.queries.integrations.getChallengeParticipantsWithStrava, {
32+
challengeId: challenge.id as Id<"challenges">,
33+
}),
34+
]);
3135

3236
return (
33-
<Card>
34-
<CardHeader>
35-
<CardTitle>Integration Mappings</CardTitle>
36-
<CardDescription>
37-
Configure how activities from external services like Strava are mapped to your challenge
38-
activity types. When users sync activities from connected services, they will
39-
automatically be logged as the mapped activity type.
40-
</CardDescription>
41-
</CardHeader>
42-
<CardContent>
43-
<AdminIntegrationsTable
44-
challengeId={challenge.id}
45-
activityTypes={activityTypes}
46-
/>
47-
</CardContent>
48-
</Card>
37+
<IntegrationsTabs
38+
challengeId={challenge.id}
39+
activityTypes={activityTypes}
40+
participantsWithStrava={participantsWithStrava}
41+
/>
4942
);
5043
}

apps/web/app/challenges/[id]/admin/strava-preview/strava-preview-client.tsx renamed to apps/web/app/challenges/[id]/admin/integrations/strava-preview-client.tsx

File renamed without changes.

apps/web/app/challenges/[id]/admin/layout.tsx

Lines changed: 30 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { dateOnlyToUtcMs, formatDateShortFromDateOnly } from "@/lib/date-only";
88
import { ArrowLeft } from "lucide-react";
99

1010
import { requireAuth } from "@/lib/auth";
11-
import { AdminNavigation } from "@/components/admin/admin-navigation";
11+
import { AdminNavigation, type AdminNavigationGroup } from "@/components/admin/admin-navigation";
1212

1313
const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
1414

@@ -44,61 +44,43 @@ export default async function ChallengeAdminLayout({
4444
challengeId: id as Id<"challenges">,
4545
});
4646

47-
const navItems: { href: string; label: string; segment: string }[] = [
48-
{
49-
href: `/challenges/${challenge._id}/admin`,
50-
label: "Overview",
51-
segment: "(overview)",
52-
},
53-
{
54-
href: `/challenges/${challenge._id}/admin/settings`,
55-
label: "Settings",
56-
segment: "settings",
57-
},
58-
{
59-
href: `/challenges/${challenge._id}/admin/flagged-activities`,
60-
label: "Flagged",
61-
segment: "flagged-activities",
62-
},
63-
{
64-
href: `/challenges/${challenge._id}/admin/activity-types`,
65-
label: "Activity Types",
66-
segment: "activity-types",
67-
},
68-
{
69-
href: `/challenges/${challenge._id}/admin/integrations`,
70-
label: "Integrations",
71-
segment: "integrations",
72-
},
73-
{
74-
href: `/challenges/${challenge._id}/admin/strava-preview`,
75-
label: "Strava Preview",
76-
segment: "strava-preview",
77-
},
47+
const base = `/challenges/${challenge._id}/admin`;
48+
49+
const navGroups: AdminNavigationGroup[] = [
7850
{
79-
href: `/challenges/${challenge._id}/admin/achievements`,
80-
label: "Achievements",
81-
segment: "achievements",
51+
label: "Monitor",
52+
items: [
53+
{ href: base, label: "Overview", segment: "(overview)" },
54+
{ href: `${base}/flagged-activities`, label: "Flagged", segment: "flagged-activities" },
55+
],
8256
},
8357
{
84-
href: `/challenges/${challenge._id}/admin/mini-games`,
85-
label: "Mini Games",
86-
segment: "mini-games",
58+
label: "Scoring",
59+
items: [
60+
{ href: `${base}/activity-types`, label: "Activity Types", segment: "activity-types" },
61+
{ href: `${base}/integrations`, label: "Integrations", segment: "integrations" },
62+
{ href: `${base}/achievements`, label: "Achievements", segment: "achievements" },
63+
],
8764
},
8865
{
89-
href: `/challenges/${challenge._id}/admin/emails`,
90-
label: "Emails",
91-
segment: "emails",
66+
label: "Engage",
67+
items: [
68+
{ href: `${base}/mini-games`, label: "Mini Games", segment: "mini-games" },
69+
{ href: `${base}/emails`, label: "Emails", segment: "emails" },
70+
],
9271
},
9372
{
94-
href: `/challenges/${challenge._id}/admin/participants`,
95-
label: "Participants",
96-
segment: "participants",
73+
label: "People",
74+
items: [
75+
{ href: `${base}/participants`, label: "Participants", segment: "participants" },
76+
{ href: `${base}/payments`, label: "Payments", segment: "payments" },
77+
],
9778
},
9879
{
99-
href: `/challenges/${challenge._id}/admin/payments`,
100-
label: "Payments",
101-
segment: "payments",
80+
label: "Configure",
81+
items: [
82+
{ href: `${base}/settings`, label: "Settings", segment: "settings" },
83+
],
10284
},
10385
];
10486

@@ -156,7 +138,7 @@ export default async function ChallengeAdminLayout({
156138

157139
{/* Navigation Tabs */}
158140
<div className="border-t border-zinc-800/50 px-3">
159-
<AdminNavigation items={navItems} />
141+
<AdminNavigation groups={navGroups} />
160142
</div>
161143
</header>
162144

apps/web/app/challenges/[id]/admin/mini-games/[gameId]/page.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,6 @@ export default function MiniGameDetailPage() {
410410
<div className="col-span-3 flex items-center gap-2">
411411
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-zinc-800">
412412
{participant.user?.avatarUrl ? (
413-
// eslint-disable-next-line @next/next/no-img-element
414413
<img
415414
src={participant.user.avatarUrl}
416415
alt=""
@@ -433,7 +432,6 @@ export default function MiniGameDetailPage() {
433432
<>
434433
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-zinc-800">
435434
{participant.partnerUser.avatarUrl ? (
436-
// eslint-disable-next-line @next/next/no-img-element
437435
<img
438436
src={participant.partnerUser.avatarUrl}
439437
alt=""
@@ -472,7 +470,6 @@ export default function MiniGameDetailPage() {
472470
<>
473471
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-zinc-800">
474472
{participant.preyUser.avatarUrl ? (
475-
// eslint-disable-next-line @next/next/no-img-element
476473
<img
477474
src={participant.preyUser.avatarUrl}
478475
alt=""
@@ -496,7 +493,6 @@ export default function MiniGameDetailPage() {
496493
<>
497494
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-zinc-800">
498495
{participant.hunterUser.avatarUrl ? (
499-
// eslint-disable-next-line @next/next/no-img-element
500496
<img
501497
src={participant.hunterUser.avatarUrl}
502498
alt=""

apps/web/app/challenges/[id]/admin/participants/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,6 @@ export default function AdminParticipantsPage() {
181181
<div className="col-span-4 flex items-center gap-2">
182182
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-zinc-800">
183183
{participant.user.avatarUrl ? (
184-
// eslint-disable-next-line @next/next/no-img-element
185184
<img
186185
src={participant.user.avatarUrl}
187186
alt=""

0 commit comments

Comments
 (0)