Skip to content

Commit b599139

Browse files
committed
trying to SSR the data
1 parent 55eb679 commit b599139

8 files changed

Lines changed: 201 additions & 86 deletions

File tree

.claude/napkin.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,20 @@
66
| 2026-02-11 | self | Ran `ls` before reading napkin (again) | Always read `.claude/napkin.md` before any other command |
77
| 2026-02-10 | self | Ran `ls` before reading napkin | Always read `.claude/napkin.md` before any other command |
88
| 2026-02-10 | self | Used backticks in a shell-quoted PR body so the shell tried to execute `turbo` | Use a heredoc or escape backticks when passing PR bodies to shell commands |
9+
| 2026-02-13 | self | Assumed mobile perf issue was mostly JS parse cost before checking runtime endpoints | Verify browser-facing service URLs (`NEXT_PUBLIC_*`) first; loopback hosts break phone/LAN query paths |
10+
| 2026-02-13 | self | Broke a TS function signature during a broad apply_patch edit | Re-open edited file immediately after structural patches before running full checks |
11+
| 2026-02-13 | self | Ran `sed` on a bracketed path without quoting (`[id]`) and zsh globbed it | Quote paths containing `[]` (e.g., `'apps/web/app/challenges/[id]/dashboard/page.tsx'`) |
912

1013
## User Preferences
1114
- Hide navbar on full-screen flow pages (invite, dashboard, admin) via `ConditionalHeader` patterns + remove `page-with-header` class
15+
- Avoid LAN-specific runtime rewrites in product code unless explicitly requested
1216

1317
## Patterns That Work
1418
- Convex queries can join related data inline (e.g., activity types + categories in one query)
1519
- `conditional-header.tsx` DASHBOARD_LAYOUT_PATTERNS array controls navbar visibility per route
1620
- Admin console sidebar approach was scrapped — revisit admin nav design in the future
21+
- Mobile feed performance improves by skipping non-critical per-item work (engagement count scans and media URL generation) on initial query
22+
- For mobile perceived performance, SSR the first feed page from server auth and then let client `usePaginatedQuery` take over for realtime/pagination
1723

1824
## Patterns That Don't Work
1925
- Deriving env vars inside `convex deploy --cmd` shell strings — escaping hell, fragile, hard to debug. Instead, derive them in `next.config.ts` which runs at build time and can set `process.env` before Next.js compiles.
@@ -23,4 +29,4 @@
2329
- `page-with-header` CSS class = `pt-16` to offset fixed navbar
2430
- Seed data lives in `packages/backend/actions/seed.ts`
2531
- Schema changes auto-deploy locally via `pnpm dev`
26-
| 2026-02-11 | self | Spent 10 commits trying to derive env vars inside `convex deploy --cmd` shell strings | Don't fight shell escaping — derive env vars in `next.config.ts` instead (runs at build time, sets `process.env`) |
32+
- Dev-only third-party scripts should be opt-in; avoid `beforeInteractive` for non-critical tooling (e.g., `react-grab`)

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

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { notFound, redirect } from "next/navigation";
2+
import { headers } from "next/headers";
23
import { getConvexClient } from "@/lib/convex-server";
34
import { api } from "@repo/backend";
45
import type { Id } from "@repo/backend/_generated/dataModel";
@@ -7,7 +8,7 @@ import { ActivityFeed } from "@/components/dashboard/activity-feed";
78
import { OnboardingCard } from "@/components/dashboard/onboarding-card";
89
import { type ChallengeSummary } from "@/components/dashboard/challenge-realtime-context";
910
import { getCurrentUser } from "@/lib/auth";
10-
import { getToken } from "@/lib/server-auth";
11+
import { fetchAuthQuery, getToken } from "@/lib/server-auth";
1112
import { DashboardLayoutWrapper } from "../notifications/dashboard-layout-wrapper";
1213
import { dateOnlyToUtcMs } from "@/lib/date-only";
1314

@@ -17,6 +18,41 @@ interface ChallengeDashboardPageProps {
1718
params: Promise<{ id: string }>;
1819
}
1920

21+
interface InitialFeedResponse {
22+
page: Array<{
23+
activity: {
24+
_id: string;
25+
notes: string | null;
26+
pointsEarned: number;
27+
loggedDate: number;
28+
createdAt: number;
29+
metrics?: Record<string, unknown>;
30+
triggeredBonuses?: Array<{
31+
metric: string;
32+
threshold: number;
33+
bonusPoints: number;
34+
description: string;
35+
}>;
36+
};
37+
user: {
38+
id: string;
39+
name: string | null;
40+
username: string;
41+
avatarUrl: string | null;
42+
} | null;
43+
activityType: {
44+
id: string | null;
45+
name: string | null;
46+
categoryId: string | null;
47+
scoringConfig?: Record<string, unknown>;
48+
} | null;
49+
likes: number;
50+
comments: number;
51+
likedByUser: boolean;
52+
mediaUrls: string[];
53+
}>;
54+
}
55+
2056
export default async function ChallengeDashboardPage({
2157
params,
2258
}: ChallengeDashboardPageProps) {
@@ -39,11 +75,30 @@ export default async function ChallengeDashboardPage({
3975
}
4076

4177
const challengeId = id as Id<"challenges">;
78+
const userAgent = (await headers()).get("user-agent") ?? "";
79+
const isMobileRequest = /Android|iPhone|iPad|iPod|Mobile|CriOS|FxiOS/i.test(
80+
userAgent,
81+
);
4282

43-
const dashboardData = await convex.query(api.queries.challenges.getDashboardData, {
44-
challengeId,
45-
userId: user._id,
46-
});
83+
const [dashboardData, initialFeed] = await Promise.all([
84+
convex.query(api.queries.challenges.getDashboardData, {
85+
challengeId,
86+
userId: user._id,
87+
}),
88+
fetchAuthQuery<InitialFeedResponse>(api.queries.activities.getChallengeFeed, {
89+
challengeId,
90+
followingOnly: false,
91+
includeEngagementCounts: !isMobileRequest,
92+
includeMediaUrls: !isMobileRequest,
93+
paginationOpts: {
94+
numItems: 10,
95+
cursor: null,
96+
},
97+
}).catch((error) => {
98+
console.error("[perf] dashboard initial feed preload failed", error);
99+
return { page: [] };
100+
}),
101+
]);
47102

48103
console.log(
49104
`[perf] dashboard page total: ${Math.round(performance.now() - dashStart)}ms`,
@@ -99,7 +154,11 @@ export default async function ChallengeDashboardPage({
99154
>
100155
<div className="mx-auto max-w-2xl px-4 py-6 space-y-4">
101156
<OnboardingCard challengeId={challenge.id} userId={user._id} />
102-
<ActivityFeed challengeId={challenge.id} />
157+
<ActivityFeed
158+
challengeId={challenge.id}
159+
initialItems={initialFeed.page}
160+
initialLightweightMode={isMobileRequest}
161+
/>
103162
</div>
104163
</DashboardLayoutWrapper>
105164
);

apps/web/app/challenges/page.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { redirect } from "next/navigation";
22
import { ChallengesGrid } from "@/components/challenges/challenges-grid";
33
import { getCurrentUser } from "@/lib/auth";
4+
import { getConvexClient } from "@/lib/convex-server";
5+
import { api } from "@repo/backend";
46

57
export const dynamic = "force-dynamic";
68

@@ -11,6 +13,20 @@ export default async function ChallengesPage() {
1113
redirect("/sign-in");
1214
}
1315

16+
const challengesStart = performance.now();
17+
const challenges = await getConvexClient()
18+
.query(api.queries.challenges.listPublic, {
19+
limit: 20,
20+
offset: 0,
21+
})
22+
.catch((error) => {
23+
console.error("[perf] challenges listPublic failed", error);
24+
return null;
25+
});
26+
console.log(
27+
`[perf] challenges listPublic: ${Math.round(performance.now() - challengesStart)}ms`,
28+
);
29+
1430
return (
1531
<main className="min-h-screen bg-background text-foreground page-with-header">
1632
<div className="container mx-auto px-6 py-12">
@@ -26,7 +42,7 @@ export default async function ChallengesPage() {
2642
</p>
2743
</div>
2844

29-
<ChallengesGrid />
45+
<ChallengesGrid challenges={challenges} />
3046
</div>
3147
</main>
3248
);

apps/web/app/layout.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ export default async function RootLayout({
3636
}: Readonly<{
3737
children: React.ReactNode;
3838
}>) {
39+
const enableDevCursorScripts =
40+
process.env.NODE_ENV === "development" &&
41+
process.env.NEXT_PUBLIC_ENABLE_REACT_GRAB === "1";
42+
3943
const layoutStart = performance.now();
4044
const [token, preloadedUser] = await Promise.all([
4145
getToken(),
@@ -47,14 +51,14 @@ export default async function RootLayout({
4751
<ConvexProviderWrapper initialToken={token ?? null}>
4852
<html lang="en">
4953
<head>
50-
{process.env.NODE_ENV === "development" && (
54+
{enableDevCursorScripts && (
5155
<Script
5256
src="//unpkg.com/react-grab/dist/index.global.js"
5357
crossOrigin="anonymous"
54-
strategy="beforeInteractive"
58+
strategy="lazyOnload"
5559
/>
5660
)}
57-
{process.env.NODE_ENV === "development" && (
61+
{enableDevCursorScripts && (
5862
<Script
5963
src="//unpkg.com/@react-grab/cursor/dist/client.global.js"
6064
strategy="lazyOnload"

apps/web/components/challenges/challenges-grid.tsx

Lines changed: 7 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
"use client";
2-
3-
import { useQuery } from "convex/react";
4-
import { api } from "@repo/backend";
51
import Link from "next/link";
62
import { dateOnlyToUtcMs, formatDateShortFromDateOnly } from "@/lib/date-only";
73

@@ -71,45 +67,16 @@ function ChallengeCard({ challenge }: { challenge: Challenge }) {
7167
);
7268
}
7369

74-
export function ChallengesGrid() {
75-
const challenges = useQuery(api.queries.challenges.listPublic, {
76-
limit: 20,
77-
offset: 0,
78-
});
79-
80-
const loading = challenges === undefined;
81-
const error = challenges === null ? "Failed to load challenges" : null;
82-
83-
if (loading) {
84-
return (
85-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
86-
{Array.from({ length: 6 }).map((_, i) => (
87-
<div
88-
key={i}
89-
className="rounded-2xl bg-zinc-900/50 border border-zinc-800 p-6 animate-pulse"
90-
>
91-
<div className="mb-4 flex items-center justify-between">
92-
<div className="h-4 bg-zinc-800 rounded w-16" />
93-
<div className="h-4 bg-zinc-800 rounded w-20" />
94-
</div>
95-
<div className="h-6 bg-zinc-800 rounded mb-2" />
96-
<div className="h-4 bg-zinc-800 rounded mb-4 w-3/4" />
97-
<div className="flex items-center justify-between">
98-
<div className="h-3 bg-zinc-800 rounded w-20" />
99-
<div className="h-3 bg-zinc-800 rounded w-3" />
100-
<div className="h-3 bg-zinc-800 rounded w-20" />
101-
</div>
102-
</div>
103-
))}
104-
</div>
105-
);
106-
}
107-
108-
if (error) {
70+
export function ChallengesGrid({
71+
challenges,
72+
}: {
73+
challenges: Challenge[] | null;
74+
}) {
75+
if (challenges === null) {
10976
return (
11077
<div className="text-center py-12">
11178
<div className="text-red-400 text-lg mb-2">Failed to load challenges</div>
112-
<p className="text-zinc-500">{error}</p>
79+
<p className="text-zinc-500">Try refreshing the page.</p>
11380
</div>
11481
);
11582
}

0 commit comments

Comments
 (0)