Skip to content

Commit a790652

Browse files
committed
Reorganize admin console with grouped navigation and merge Strava Preview
- Group navigation tabs into labeled sections (Monitor, Scoring, Engage, People, Configure) with visual dividers for better discoverability - Merge standalone Strava Preview page into Integrations as a "Test with Participants" tab, eliminating duplication - Reduce top-level nav from 11 flat items to 10 items in 5 logical groups https://claude.ai/code/session_015K3ffEwmJ6UwTYTHfBEu1r
1 parent c4493ac commit a790652

7 files changed

Lines changed: 222 additions & 145 deletions

File tree

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/strava-preview/page.tsx

Lines changed: 0 additions & 50 deletions
This file was deleted.

apps/web/components/admin/admin-navigation.tsx

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,38 +11,58 @@ export interface AdminNavigationItem {
1111
segment: string;
1212
}
1313

14-
interface AdminNavigationProps {
14+
export interface AdminNavigationGroup {
15+
label: string;
1516
items: AdminNavigationItem[];
1617
}
1718

18-
export function AdminNavigation({ items }: AdminNavigationProps) {
19+
interface AdminNavigationProps {
20+
groups: AdminNavigationGroup[];
21+
}
22+
23+
export function AdminNavigation({ groups }: AdminNavigationProps) {
1924
const segment = useSelectedLayoutSegment();
2025

2126
return (
2227
<nav className="flex items-center gap-0.5 -mb-px">
23-
{items.map((item) => {
24-
const isActive =
25-
segment === item.segment ||
26-
(!segment && item.segment === "(overview)");
27-
28-
return (
29-
<Link
30-
key={item.href}
31-
href={item.href}
32-
className={cn(
33-
"relative px-3 py-2 text-xs font-medium transition-colors",
34-
isActive
35-
? "text-amber-400"
36-
: "text-zinc-500 hover:text-zinc-300"
37-
)}
38-
>
39-
{item.label}
40-
{isActive && (
41-
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-amber-400" />
42-
)}
43-
</Link>
44-
);
45-
})}
28+
{groups.map((group, groupIdx) => (
29+
<div key={group.label} className="flex items-center">
30+
{/* Divider between groups */}
31+
{groupIdx > 0 && (
32+
<div className="mx-1.5 h-4 w-px bg-zinc-800" />
33+
)}
34+
35+
{/* Group label */}
36+
<span className="mr-1 px-1 text-[10px] font-medium uppercase tracking-wider text-zinc-600 select-none">
37+
{group.label}
38+
</span>
39+
40+
{/* Group items */}
41+
{group.items.map((item) => {
42+
const isActive =
43+
segment === item.segment ||
44+
(!segment && item.segment === "(overview)");
45+
46+
return (
47+
<Link
48+
key={item.href}
49+
href={item.href}
50+
className={cn(
51+
"relative px-2.5 py-2 text-xs font-medium transition-colors",
52+
isActive
53+
? "text-amber-400"
54+
: "text-zinc-500 hover:text-zinc-300"
55+
)}
56+
>
57+
{item.label}
58+
{isActive && (
59+
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-amber-400" />
60+
)}
61+
</Link>
62+
);
63+
})}
64+
</div>
65+
))}
4666
</nav>
4767
);
4868
}

0 commit comments

Comments
 (0)