Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2df7d4f
Adopt RSC-first patterns: provider split, privacy SSR, playground pre…
cursoragent Sep 5, 2026
4fe379e
Phase 2: RSC refactor plan, cached playground SEO, transition navigat…
cursoragent Sep 5, 2026
e92270b
Fix playground 500 when case slug is invalid
cursoragent Sep 6, 2026
466a70a
Address review follow-ups: SEO cache invalidation, private metadata, …
cursoragent Sep 6, 2026
5cc443e
Revert dev-only next-env.d.ts path change
cursoragent Sep 6, 2026
87a72a2
Fix playground loading shell: include app bar and reduce skeleton fli…
cursoragent Sep 6, 2026
5a4f4bd
Fix playground double flicker on instant navigation
cursoragent Sep 6, 2026
8edc898
Stabilize playground initial loading without layout jumps
cursoragent Sep 6, 2026
b31dcbd
Fix Prettier formatting in usePlaygroundPanelsReady test
cursoragent Sep 6, 2026
e7d12e5
Address deep review: slug loading sync, prefetch dedup, prefetch para…
cursoragent Sep 6, 2026
b519099
Collapse playground bootstrap into one server canonical redirect
cursoragent Sep 6, 2026
663f2ff
Scope Apollo to daily/profile with server GraphQL prefetch
cursoragent Sep 6, 2026
1fc5271
Tighten playground architecture: scoped browser, cached brief, gate sync
cursoragent Sep 6, 2026
99e8869
Add marketing loading.tsx fallbacks and update RSC refactor plan
cursoragent Sep 6, 2026
76d9334
Fix playground CI: serialize RSC props, client canonical redirect, Ap…
cursoragent Sep 6, 2026
6984a2e
Fix E2E: preserve bare playground landing URL, harden home nav waits
cursoragent Sep 6, 2026
ed9a368
Fix lint: add routeProjectSlug to canonical redirect effect deps
cursoragent Sep 6, 2026
cfd3026
Fix app bar scroll state on privacy via persistent marketing layout
cursoragent Sep 6, 2026
2316847
Address code review: bare playground browse, cache invalidation, dail…
cursoragent Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions e2e/helpers/playgroundMonacoEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export async function clickAppBarHomeLink(page: Page): Promise<void> {
.getByTestId("app-bar-home-link")
.locator("visible=true");

await homeLinks.first().waitFor({ state: "visible", timeout: 30_000 });

const linkCount = await homeLinks.count();
if (linkCount === 0) {
throw new Error("No visible app bar home link found");
Expand Down
24 changes: 24 additions & 0 deletions e2e/helpers/playgroundRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/** Canonical invert-binary-tree path segments (project / case / solution). */
export const INVERT_BINARY_TREE_CANONICAL_PATH =
"/playground/invert-binary-tree/case-1/solution-1";

export function isCanonicalPlaygroundProjectPath(
pathname: string,
projectSlug: string,
): boolean {
const segments = pathname.split("/").filter(Boolean);
const playgroundIndex = segments.indexOf("playground");
if (playgroundIndex === -1) {
return false;
}

const projectIndex = playgroundIndex + 1;
return (
segments[projectIndex] === projectSlug &&
segments.length >= projectIndex + 3
);
}

export function isInvertBinaryTreeCanonicalPath(pathname: string): boolean {
return isCanonicalPlaygroundProjectPath(pathname, "invert-binary-tree");
}
11 changes: 8 additions & 3 deletions e2e/home-webgl-nav.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,26 @@ test.describe("home landing WebGL canvases", () => {
test("keep active WebGL contexts after client navigation away and back", async ({
page,
}) => {
const contextLostMessages = collectWebGlContextLostMessages(page);

await page.goto("/");
await dismissCookieBannerIfVisible(page);
await waitForActiveLandingWebGLCanvases(page);

await clickFooterPrivacyPolicyLink(page);
await page.waitForURL((url) => url.pathname === "/privacy");
await page
.getByTestId("app-bar-home-link")
.locator("visible=true")
.first()
.waitFor({ state: "visible", timeout: 30_000 });

const contextLostAfterReturn = collectWebGlContextLostMessages(page);

await clickAppBarHomeLink(page);
await page.waitForURL((url) => url.pathname === "/");

const activeCanvases = await waitForActiveLandingWebGLCanvases(page);
expect(activeCanvases.length).toBeGreaterThanOrEqual(2);
expect(contextLostMessages).toEqual([]);
expect(contextLostAfterReturn).toEqual([]);
});

test("remounts landing canvases after forced WEBGL_lose_context", async ({
Expand Down
10 changes: 6 additions & 4 deletions e2e/instant-playground-nav.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
visiblePlaygroundMonacoEditor,
waitForPlaygroundMonacoEditor,
} from "./helpers/playgroundMonacoEditor";
import { isInvertBinaryTreeCanonicalPath } from "./helpers/playgroundRoute";

/**
* L5: playground opts into `instant = true` with Suspense skeleton fallback.
Expand All @@ -24,12 +25,13 @@ test.describe("instant playground navigation (L5)", () => {

await instant(page, async () => {
await page.getByTestId("cta-to-playground").click();
await page.waitForURL(
(url) => url.pathname === "/playground/invert-binary-tree",
{ timeout: 30_000 },
);
});

await page.waitForURL(
(url) => isInvertBinaryTreeCanonicalPath(url.pathname),
{ timeout: 30_000 },
);

await waitForPlaygroundMonacoEditor(page);
await expect(visiblePlaygroundMonacoEditor(page)).toBeVisible();
});
Expand Down
11 changes: 7 additions & 4 deletions e2e/playground-monaco-nav.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import {
visiblePlaygroundMonacoEditor,
waitForPlaygroundMonacoEditor,
} from "./helpers/playgroundMonacoEditor";
import {
INVERT_BINARY_TREE_CANONICAL_PATH,
isInvertBinaryTreeCanonicalPath,
} from "./helpers/playgroundRoute";

const PLAYGROUND_PYTHON_URL =
"/playground/invert-binary-tree?view=code&language=python";
const PLAYGROUND_PYTHON_URL = `${INVERT_BINARY_TREE_CANONICAL_PATH}?view=code&language=python`;

test.describe("playground runtime navigation", () => {
test.describe.configure({ mode: "serial" });
Expand All @@ -31,7 +34,7 @@ test.describe("playground runtime navigation", () => {
for (let roundIndex = 0; roundIndex < 3; roundIndex += 1) {
await page.getByTestId("cta-to-playground").click();
await page.waitForURL(
(url) => url.pathname === "/playground/invert-binary-tree",
(url) => isInvertBinaryTreeCanonicalPath(url.pathname),
{ timeout: 30_000 },
);
await waitForPlaygroundMonacoEditor(page);
Expand All @@ -43,7 +46,7 @@ test.describe("playground runtime navigation", () => {
await dismissCookieBannerIfVisible(page);
}

await page.goto("/playground/invert-binary-tree?view=code");
await page.goto(`${INVERT_BINARY_TREE_CANONICAL_PATH}?view=code`);
await dismissCookieBannerIfVisible(page);
await waitForPlaygroundMonacoEditor(page);

Expand Down
15 changes: 8 additions & 7 deletions src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { vi } from "vitest";
import { LANDING_PRIMARY_PLAYGROUND_HREF } from "#/features/homePage/lib/landingPlaygroundDemos";
import { MarketingHomeView } from "#/features/homePage/ui/MarketingHomeView";
import { mockUseSearchParam } from "#/features/project/ui/ProjectBrowser/__tests__/testUtils";
import { ProjectBrowserProvider } from "#/features/project/ui/ProjectBrowser/ProjectBrowserContext";
import { QuestionOfTodayDocument } from "#/graphql/generated";
import en from "#/i18n/en/index";
import type { Translation } from "#/i18n/i18n-types";
Expand All @@ -15,6 +14,8 @@ import { I18nProvider } from "#/shared/ui/providers/I18nProvider";
import { StateThemeProvider } from "#/shared/ui/providers/StateThemeProvider";
import { makeStore } from "#/store/makeStore";

import { RuntimeDeviceHintProvider } from "#/app/locale-app/RuntimeDeviceHintContext";

const store = makeStore();

vi.mock("next-auth/react", () => {
Expand Down Expand Up @@ -68,13 +69,13 @@ describe("MarketingHomeView", () => {
render(
<ReduxProvider store={store}>
<MockedProvider mocks={mocks} addTypename={false}>
<StateThemeProvider>
<I18nProvider locale="en" i18n={i18n}>
<ProjectBrowserProvider>
<RuntimeDeviceHintProvider initialSsrDeviceType="desktop">
<StateThemeProvider>
<I18nProvider locale="en" i18n={i18n}>
<MarketingHomeView />
</ProjectBrowserProvider>
</I18nProvider>
</StateThemeProvider>
</I18nProvider>
</StateThemeProvider>
</RuntimeDeviceHintProvider>
</MockedProvider>
</ReduxProvider>,
{ wrapper: withNextTRPC },
Expand Down
12 changes: 12 additions & 0 deletions src/app/(default-locale)/(app)/daily/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ReactNode } from "react";

import { AppChromeLayoutClient } from "#/features/appChrome/ui/AppChromeLayoutClient";

/** Persistent chrome for daily — Apollo prefetch stays in {@link DailyPage}. */
export default function DefaultLocaleDailyLayout({
children,
}: {
children: ReactNode;
}) {
return <AppChromeLayoutClient>{children}</AppChromeLayoutClient>;
}
24 changes: 24 additions & 0 deletions src/app/(default-locale)/(app)/daily/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Box, Container, Skeleton } from "@mui/material";

/** Instant-nav fallback for `/daily` while the client view hydrates. */
export default function DailyLoading() {
return (
<Box component="main" sx={{ minHeight: "85vh", py: 8 }}>
<Container maxWidth="lg">
<Skeleton
variant="text"
width="60%"
height={48}
animation="wave"
sx={{ mx: "auto", mb: 4 }}
/>
<Skeleton
variant="rounded"
height={320}
animation="wave"
sx={{ borderRadius: 2 }}
/>
</Container>
</Box>
);
}
8 changes: 8 additions & 0 deletions src/app/(default-locale)/(app)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** App routes under `(app)` — Apollo mounts on daily/profile segment layouts only. */
export default function DefaultLocaleAppLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { PlaygroundPageLoading } from "#/features/playground/ui/PlaygroundPageLoading";

export default PlaygroundPageLoading;
9 changes: 9 additions & 0 deletions src/app/(default-locale)/(app)/playground/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { PlaygroundLayoutClient } from "#/features/playground/ui/PlaygroundLayoutClient";

export default function DefaultLocalePlaygroundLayout({
children,
}: {
children: React.ReactNode;
}) {
return <PlaygroundLayoutClient>{children}</PlaygroundLayoutClient>;
}
5 changes: 5 additions & 0 deletions src/app/(default-locale)/(app)/profile/[userId]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ProfilePageSkeleton } from "#/features/profile/ui/ProfilePageSkeleton";

export default function ProfileLoading() {
return <ProfilePageSkeleton />;
}
8 changes: 8 additions & 0 deletions src/app/(default-locale)/(app)/profile/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** Profile route — Apollo client is created in profile page with server prefetch. */
export default function DefaultLocaleProfileLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}
12 changes: 12 additions & 0 deletions src/app/(default-locale)/(marketing)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ReactNode } from "react";

import { MarketingLayoutClient } from "#/features/marketing/ui/MarketingLayoutClient";

/** Shared marketing shell (home, privacy) — instant navigations keep scroll + app bar. */
export default function DefaultLocaleMarketingLayout({
children,
}: {
children: ReactNode;
}) {
return <MarketingLayoutClient>{children}</MarketingLayoutClient>;
}
41 changes: 41 additions & 0 deletions src/app/(default-locale)/(marketing)/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Box, Container, Skeleton } from "@mui/material";

/** Instant-nav fallback for marketing home while client islands hydrate. */
export default function MarketingHomeLoading() {
return (
<Box component="main" sx={{ minHeight: "100vh" }}>
<Box
sx={{
minHeight: { xs: 520, md: 640 },
display: "flex",
alignItems: "center",
justifyContent: "center",
px: 2,
}}
>
<Container maxWidth="lg">
<Skeleton
variant="text"
width="70%"
height={56}
animation="wave"
sx={{ mx: "auto", mb: 2 }}
/>
<Skeleton
variant="text"
width="50%"
height={32}
animation="wave"
sx={{ mx: "auto", mb: 4 }}
/>
<Skeleton
variant="rounded"
height={280}
animation="wave"
sx={{ borderRadius: 3 }}
/>
</Container>
</Box>
</Box>
);
}
15 changes: 15 additions & 0 deletions src/app/(default-locale)/(marketing)/privacy/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Box, Container, Skeleton } from "@mui/material";

/** Instant-nav fallback for privacy policy while client chrome hydrates. */
export default function PrivacyLoading() {
return (
<Box component="main" sx={{ minHeight: "70vh", py: 8 }}>
<Container maxWidth="md">
<Skeleton variant="text" width="40%" height={48} animation="wave" />
<Skeleton variant="text" width="100%" animation="wave" sx={{ mt: 3 }} />
<Skeleton variant="text" width="95%" animation="wave" />
<Skeleton variant="text" width="90%" animation="wave" />
</Container>
</Box>
);
}
2 changes: 1 addition & 1 deletion src/app/(default-locale)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { baseLocale } from "#/i18n/i18n-util";

import { LocaleAppLayout } from "#/app/locale-app/LocaleAppLayout";

/** Locale shell — cached i18n + synchronous SSR device hint; session streams via Suspense (P10). */
/** Locale shell — cached i18n + SSR device hint; session resolved in layout (P10). */
export const instant = false;

/** Default-locale (`en`) public App shell at unprefixed URLs (L2). */
Expand Down
9 changes: 3 additions & 6 deletions src/app/AppRootLayoutClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import React, { type ReactNode } from "react";
import "symbol-observable";

import { CookieConsentRoot } from "#/features/cookieConsent/ui/CookieConsentRoot";
import { ProjectBrowserProvider } from "#/features/project/ui/ProjectBrowser/ProjectBrowserContext";
import { type I18nProps } from "#/i18n/getI18nProps";
import type { Locales } from "#/i18n/i18n-types";
import { AppShellProviders } from "#/shared/ui/providers/AppShellProviders";
Expand Down Expand Up @@ -38,11 +37,9 @@ export const AppRootLayoutClient: React.FC<AppRootLayoutClientProps> = ({
<AppShellProviders ssrDeviceType={ssrDeviceType}>
<I18nProvider locale={locale} i18n={i18n}>
<CookieConsentRoot>
<ProjectBrowserProvider>
{children}
<Analytics />
<SpeedInsights />
</ProjectBrowserProvider>
{children}
<Analytics />
<SpeedInsights />
</CookieConsentRoot>
</I18nProvider>
</AppShellProviders>
Expand Down
8 changes: 8 additions & 0 deletions src/app/[lang]/(app)/daily/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ReactNode } from "react";

import { AppChromeLayoutClient } from "#/features/appChrome/ui/AppChromeLayoutClient";

/** Persistent chrome for daily — Apollo prefetch stays in {@link DailyPage}. */
export default function LangDailyLayout({ children }: { children: ReactNode }) {
return <AppChromeLayoutClient>{children}</AppChromeLayoutClient>;
}
24 changes: 24 additions & 0 deletions src/app/[lang]/(app)/daily/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Box, Container, Skeleton } from "@mui/material";

/** Instant-nav fallback for `/daily` while the client view hydrates. */
export default function DailyLoading() {
return (
<Box component="main" sx={{ minHeight: "85vh", py: 8 }}>
<Container maxWidth="lg">
<Skeleton
variant="text"
width="60%"
height={48}
animation="wave"
sx={{ mx: "auto", mb: 4 }}
/>
<Skeleton
variant="rounded"
height={320}
animation="wave"
sx={{ borderRadius: 2 }}
/>
</Container>
</Box>
);
}
File renamed without changes.
8 changes: 8 additions & 0 deletions src/app/[lang]/(app)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** App routes under `(app)` — Apollo mounts on daily/profile segment layouts only. */
export default function LangAppLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}
3 changes: 3 additions & 0 deletions src/app/[lang]/(app)/playground/[[...slug]]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { PlaygroundPageLoading } from "#/features/playground/ui/PlaygroundPageLoading";

export default PlaygroundPageLoading;
9 changes: 9 additions & 0 deletions src/app/[lang]/(app)/playground/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { PlaygroundLayoutClient } from "#/features/playground/ui/PlaygroundLayoutClient";

export default function LangPlaygroundLayout({
children,
}: {
children: React.ReactNode;
}) {
return <PlaygroundLayoutClient>{children}</PlaygroundLayoutClient>;
}
Loading
Loading