Skip to content

Commit bd3817b

Browse files
prazgaitisclaude
andauthored
Improve onboarding card UX and dashboard layout (#26)
Move onboarding card to sidebar once challenge starts, show countdown title before start date, widen right sidebar visibility to lg breakpoint, and forward scroll events from sidebars to main content area. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c543c32 commit bd3817b

5 files changed

Lines changed: 37 additions & 21 deletions

File tree

apps/web/app/challenges/[id]/dashboard/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,9 @@ async function DashboardContent({
183183
initialSummary={initialSummary}
184184
>
185185
<div className="mx-auto max-w-2xl px-4 py-6 space-y-4">
186-
<OnboardingCard challengeId={challenge.id} userId={user._id} />
186+
{dateOnlyToUtcMs(challenge.startDate) > Date.now() && (
187+
<OnboardingCard challengeId={challenge.id} userId={user._id} challengeStartDate={challenge.startDate} />
188+
)}
187189
<ActivityFeed
188190
challengeId={challenge.id}
189191
initialItems={initialFeed.page}

apps/web/components/dashboard/challenge-sidebar.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ import { useChallengeRealtime } from './challenge-realtime-context';
66
import { UserAvatar } from '@/components/user-avatar';
77
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
88
import { ActiveMiniGames } from '@/components/mini-games';
9+
import { OnboardingCard } from './onboarding-card';
10+
import { dateOnlyToUtcMs } from '@/lib/date-only';
911
import { cn } from '@/lib/utils';
1012

1113
interface ChallengeSidebarProps {
1214
challengeId: string;
1315
currentUserId: string;
16+
challengeStartDate: string;
1417
}
1518

16-
export function ChallengeSidebar({ challengeId, currentUserId }: ChallengeSidebarProps) {
19+
export function ChallengeSidebar({ challengeId, currentUserId, challengeStartDate }: ChallengeSidebarProps) {
1720
const { summary } = useChallengeRealtime();
1821
const { stats, leaderboard } = summary;
1922

@@ -112,6 +115,10 @@ export function ChallengeSidebar({ challengeId, currentUserId }: ChallengeSideba
112115
})}
113116
</CardContent>
114117
</Card>
118+
119+
{dateOnlyToUtcMs(challengeStartDate) <= Date.now() && (
120+
<OnboardingCard challengeId={challengeId} userId={currentUserId} challengeStartDate={challengeStartDate} />
121+
)}
115122
</div>
116123
);
117124
}

apps/web/components/dashboard/dashboard-layout.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { ReactNode } from "react";
3+
import { ReactNode, useCallback, useRef, WheelEvent } from "react";
44
import { Plus } from "lucide-react";
55
import type { Doc } from "@repo/backend/_generated/dataModel";
66
import { formatDateShortFromDateOnly } from "@/lib/date-only";
@@ -34,10 +34,15 @@ export function DashboardLayout({
3434
children,
3535
hideRightSidebar = false,
3636
}: DashboardLayoutProps) {
37+
const mainRef = useRef<HTMLElement>(null);
38+
const forwardScroll = useCallback((e: WheelEvent) => {
39+
mainRef.current?.scrollBy({ top: e.deltaY, left: e.deltaX });
40+
}, []);
41+
3742
return (
3843
<div className="flex h-screen bg-black text-white">
3944
{/* Left Sidebar - Collapsed (lg) */}
40-
<aside className="hidden w-[72px] flex-shrink-0 flex-col border-r border-zinc-800 lg:flex xl:hidden">
45+
<aside onWheel={forwardScroll} className="hidden w-[72px] flex-shrink-0 flex-col border-r border-zinc-800 lg:flex xl:hidden">
4146
<div className="flex h-full flex-col items-center py-4">
4247
{/* Logo/Icon */}
4348
<div className="mb-4 h-10 w-10 rounded-full bg-gradient-to-r from-indigo-500 to-fuchsia-500" />
@@ -80,7 +85,7 @@ export function DashboardLayout({
8085
</aside>
8186

8287
{/* Left Sidebar - Full (xl) */}
83-
<aside className="hidden w-72 flex-shrink-0 flex-col border-r border-zinc-800 xl:flex">
88+
<aside onWheel={forwardScroll} className="hidden w-72 flex-shrink-0 flex-col border-r border-zinc-800 xl:flex">
8489
<div className="flex h-full flex-col">
8590
{/* Challenge Header */}
8691
<div className="p-4">
@@ -125,20 +130,20 @@ export function DashboardLayout({
125130
</aside>
126131

127132
{/* Main Content - Scrollable */}
128-
<main className="flex-1 overflow-y-auto scrollbar-hide pb-20 lg:pb-0">
133+
<main ref={mainRef} className="flex-1 overflow-y-auto overscroll-contain scrollbar-hide pb-20 lg:pb-0">
129134
<PaymentRequiredBanner challengeId={challenge.id} />
130135
<AnnouncementBanner challengeId={challenge.id} />
131136
{children}
132137
</main>
133138

134139
{/* Right Sidebar - Fixed (xl only) */}
135140
{!hideRightSidebar && (
136-
<aside className="hidden w-96 flex-shrink-0 flex-col border-l border-zinc-800 xl:flex">
141+
<aside onWheel={forwardScroll} className="hidden w-96 flex-shrink-0 flex-col border-l border-zinc-800 lg:flex">
137142
<div className="p-4">
138143
<UserSearch challengeId={challenge.id} />
139144
</div>
140145
<div className="flex-1 overflow-y-auto scrollbar-hide p-4">
141-
<ChallengeSidebar challengeId={challenge.id} currentUserId={currentUserId} />
146+
<ChallengeSidebar challengeId={challenge.id} currentUserId={currentUserId} challengeStartDate={challenge.startDate} />
142147
</div>
143148
</aside>
144149
)}

apps/web/components/dashboard/onboarding-card.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { Input } from "@/components/ui/input";
99
import {
1010
Card,
1111
CardContent,
12-
CardDescription,
1312
CardHeader,
1413
CardTitle,
1514
} from "@/components/ui/card";
@@ -21,6 +20,7 @@ import {
2120
SelectValue,
2221
} from "@/components/ui/select";
2322
import { StravaConnectButton } from "@/components/integrations/strava-connect-button";
23+
import { parseDateOnlyToUtcMs } from "@/lib/date-only";
2424
import {
2525
CheckCircle2,
2626
UserPlus,
@@ -35,12 +35,22 @@ import {
3535
ChevronUp,
3636
} from "lucide-react";
3737

38+
function getOnboardingTitle(startDate: string): string {
39+
const startMs = parseDateOnlyToUtcMs(startDate);
40+
const now = Date.now();
41+
const daysUntilStart = Math.ceil((startMs - now) / (1000 * 60 * 60 * 24));
42+
if (daysUntilStart > 1) return `${daysUntilStart} days until the challenge`;
43+
if (daysUntilStart === 1) return "Challenge starts tomorrow";
44+
return "Getting started";
45+
}
46+
3847
interface OnboardingCardProps {
3948
challengeId: string;
4049
userId: string;
50+
challengeStartDate: string;
4151
}
4252

43-
export function OnboardingCard({ challengeId, userId }: OnboardingCardProps) {
53+
export function OnboardingCard({ challengeId, userId, challengeStartDate }: OnboardingCardProps) {
4454
const [dismissed, setDismissed] = useState(false);
4555
const [expandedStep, setExpandedStep] = useState<number | null>(null);
4656

@@ -127,13 +137,6 @@ export function OnboardingCard({ challengeId, userId }: OnboardingCardProps) {
127137
},
128138
];
129139

130-
const completedCount = steps.filter((s) => s.complete).length;
131-
// Don't count the invite step in total since it's never "complete"
132-
const completableCount = steps.filter((s) => s.key !== "invite").length;
133-
const completedCompletable = steps.filter(
134-
(s) => s.key !== "invite" && s.complete
135-
).length;
136-
137140
const toggleStep = (index: number) => {
138141
setExpandedStep(expandedStep === index ? null : index);
139142
};
@@ -143,10 +146,7 @@ export function OnboardingCard({ challengeId, userId }: OnboardingCardProps) {
143146
<CardHeader className="pb-3">
144147
<div className="flex items-center justify-between">
145148
<div>
146-
<CardTitle className="text-base">Get ready for the challenge</CardTitle>
147-
<CardDescription>
148-
{completedCompletable}/{completableCount} steps completed
149-
</CardDescription>
149+
<CardTitle className="text-base">{getOnboardingTitle(challengeStartDate)}</CardTitle>
150150
</div>
151151
{allCompletableStepsDone && (
152152
<Button

packages/backend/_generated/api.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import type * as mutations_payments from "../mutations/payments.js";
5656
import type * as mutations_stravaWebhook from "../mutations/stravaWebhook.js";
5757
import type * as mutations_templates from "../mutations/templates.js";
5858
import type * as mutations_users from "../mutations/users.js";
59+
import type * as mutations_webhookPayloads from "../mutations/webhookPayloads.js";
5960
import type * as queries_achievements from "../queries/achievements.js";
6061
import type * as queries_activities from "../queries/activities.js";
6162
import type * as queries_activityTypes from "../queries/activityTypes.js";
@@ -141,6 +142,7 @@ declare const fullApi: ApiFromModules<{
141142
"mutations/stravaWebhook": typeof mutations_stravaWebhook;
142143
"mutations/templates": typeof mutations_templates;
143144
"mutations/users": typeof mutations_users;
145+
"mutations/webhookPayloads": typeof mutations_webhookPayloads;
144146
"queries/achievements": typeof queries_achievements;
145147
"queries/activities": typeof queries_activities;
146148
"queries/activityTypes": typeof queries_activityTypes;

0 commit comments

Comments
 (0)