From 22650cefc1c63fd2701e7a2ecba65ebe6d1d6485 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 18:14:07 +0000 Subject: [PATCH 1/5] feat(daily): RSC shell with streamed Apollo island Split DailyPageView into server-rendered DailyPageContent (title, lead) and client DailyPageInteractive (Apollo-driven widgets). Prefetch runs in async DailyApolloIsland so the shell streams without blocking on LeetCode. Add shared getServerTranslationFunctions and resolvePageLocale helpers; refactor privacy page to use them. Remove unused SessionWidget. Co-authored-by: maxim.kayander1 --- .../__tests__/resolvePageLocale.test.ts | 29 +++++ src/app/locale-app/pages/dailyPage.tsx | 22 ++-- src/app/locale-app/pages/privacyPage.tsx | 29 +---- src/app/locale-app/resolvePageLocale.ts | 20 ++++ .../homePage/ui/DailyApolloIsland.tsx | 15 +++ src/features/homePage/ui/DailyPageContent.tsx | 76 ++++++++++++ .../homePage/ui/DailyPageInteractive.tsx | 49 ++++++++ src/features/homePage/ui/DailyPageView.tsx | 110 ------------------ src/features/homePage/ui/SessionWidget.tsx | 105 ----------------- src/i18n/getServerTranslationFunctions.ts | 16 +++ 10 files changed, 224 insertions(+), 247 deletions(-) create mode 100644 src/app/locale-app/__tests__/resolvePageLocale.test.ts create mode 100644 src/app/locale-app/resolvePageLocale.ts create mode 100644 src/features/homePage/ui/DailyApolloIsland.tsx create mode 100644 src/features/homePage/ui/DailyPageContent.tsx create mode 100644 src/features/homePage/ui/DailyPageInteractive.tsx delete mode 100644 src/features/homePage/ui/DailyPageView.tsx delete mode 100644 src/features/homePage/ui/SessionWidget.tsx create mode 100644 src/i18n/getServerTranslationFunctions.ts diff --git a/src/app/locale-app/__tests__/resolvePageLocale.test.ts b/src/app/locale-app/__tests__/resolvePageLocale.test.ts new file mode 100644 index 00000000..995a2b88 --- /dev/null +++ b/src/app/locale-app/__tests__/resolvePageLocale.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; + +import { baseLocale } from "#/i18n/i18n-util"; + +import { resolvePageLocale } from "#/app/locale-app/resolvePageLocale"; + +describe("resolvePageLocale", () => { + it("returns base locale when params are omitted", async () => { + await expect(resolvePageLocale()).resolves.toBe(baseLocale); + }); + + it("returns base locale when lang param is absent", async () => { + await expect(resolvePageLocale(Promise.resolve({}))).resolves.toBe( + baseLocale, + ); + }); + + it("returns the resolved locale for supported lang params", async () => { + await expect( + resolvePageLocale(Promise.resolve({ lang: "de" })), + ).resolves.toBe("de"); + }); + + it("falls back to base locale for unsupported lang params", async () => { + await expect( + resolvePageLocale(Promise.resolve({ lang: "zz" })), + ).resolves.toBe(baseLocale); + }); +}); diff --git a/src/app/locale-app/pages/dailyPage.tsx b/src/app/locale-app/pages/dailyPage.tsx index fee0d8c6..b7411cdb 100644 --- a/src/app/locale-app/pages/dailyPage.tsx +++ b/src/app/locale-app/pages/dailyPage.tsx @@ -1,12 +1,13 @@ -import { DailyPageView } from "#/features/homePage/ui/DailyPageView"; +import { DailyApolloIsland } from "#/features/homePage/ui/DailyApolloIsland"; +import { DailyPageContent } from "#/features/homePage/ui/DailyPageContent"; +import { getServerTranslationFunctions } from "#/i18n/getServerTranslationFunctions"; import type { Translation } from "#/i18n/i18n-types"; -import { getDailyInitialData } from "#/server/daily/getDailyInitialData"; -import { ApolloHydrationProvider } from "#/app/locale-app/ApolloHydrationProvider"; import { createDefaultLocaleRouteMetadata, createLangRouteMetadata, } from "#/app/locale-app/createLocaleRouteMetadata"; +import { resolvePageLocale } from "#/app/locale-app/resolvePageLocale"; /** Marketing daily — instant client navigations to sibling routes (L5). */ export const instant = true; @@ -24,12 +25,17 @@ export const generateLangDailyMetadata = createLangRouteMetadata( pickDailyCopy, ); -export async function DailyPage() { - const initialCache = await getDailyInitialData(); +type DailyPageProps = { + params?: Promise<{ lang?: string }>; +}; + +export async function DailyPage({ params }: DailyPageProps = {}) { + const locale = await resolvePageLocale(params); + const LL = await getServerTranslationFunctions(locale); return ( - - - + + + ); } diff --git a/src/app/locale-app/pages/privacyPage.tsx b/src/app/locale-app/pages/privacyPage.tsx index 929ca9d3..5fcf3255 100644 --- a/src/app/locale-app/pages/privacyPage.tsx +++ b/src/app/locale-app/pages/privacyPage.tsx @@ -1,14 +1,13 @@ import { PrivacyPageContent } from "#/features/privacy/ui/PrivacyPageContent"; -import { createTranslationFunctions } from "#/i18n/createTranslationFunctions"; -import type { Locales, Translation } from "#/i18n/i18n-types"; +import { getServerTranslationFunctions } from "#/i18n/getServerTranslationFunctions"; +import type { Translation } from "#/i18n/i18n-types"; import { baseLocale } from "#/i18n/i18n-util"; -import { loadI18nForLocale } from "#/i18n/loadI18nForLocale"; import { createDefaultLocaleRouteMetadata, createLangRouteMetadata, } from "#/app/locale-app/createLocaleRouteMetadata"; -import { resolveLangParamSync } from "#/app/locale-app/resolveLangParam"; +import { resolvePageLocale } from "#/app/locale-app/resolvePageLocale"; /** Marketing privacy — instant client navigations; body is server-rendered (RSC). */ export const instant = true; @@ -30,27 +29,9 @@ type PrivacyPageProps = { params?: Promise<{ lang?: string }>; }; -async function resolvePrivacyLocale( - params?: Promise<{ lang?: string }>, -): Promise { - if (!params) { - return baseLocale; - } - const { lang: langParam } = await params; - if (!langParam) { - return baseLocale; - } - return resolveLangParamSync(langParam) ?? baseLocale; -} - export async function PrivacyPage({ params }: PrivacyPageProps = {}) { - const locale = await resolvePrivacyLocale(params); - const { translations } = await loadI18nForLocale(locale); - const translation = translations[locale]; - if (!translation) { - throw new Error(`Missing translations for locale: ${locale}`); - } - const LL = createTranslationFunctions(locale, translation); + const locale = await resolvePageLocale(params); + const LL = await getServerTranslationFunctions(locale); const homePath = locale === baseLocale ? "/" : `/${locale}`; return ; diff --git a/src/app/locale-app/resolvePageLocale.ts b/src/app/locale-app/resolvePageLocale.ts new file mode 100644 index 00000000..6f5a041f --- /dev/null +++ b/src/app/locale-app/resolvePageLocale.ts @@ -0,0 +1,20 @@ +import type { Locales } from "#/i18n/i18n-types"; +import { baseLocale } from "#/i18n/i18n-util"; + +import { resolveLangParamSync } from "#/app/locale-app/resolveLangParam"; + +/** Resolves locale from optional App `[lang]` route params (default locale when absent). */ +export async function resolvePageLocale( + params?: Promise<{ lang?: string }>, +): Promise { + if (!params) { + return baseLocale; + } + + const { lang: langParam } = await params; + if (!langParam) { + return baseLocale; + } + + return resolveLangParamSync(langParam) ?? baseLocale; +} diff --git a/src/features/homePage/ui/DailyApolloIsland.tsx b/src/features/homePage/ui/DailyApolloIsland.tsx new file mode 100644 index 00000000..0143ffdc --- /dev/null +++ b/src/features/homePage/ui/DailyApolloIsland.tsx @@ -0,0 +1,15 @@ +import { DailyPageInteractive } from "#/features/homePage/ui/DailyPageInteractive"; +import { getDailyInitialData } from "#/server/daily/getDailyInitialData"; + +import { ApolloHydrationProvider } from "#/app/locale-app/ApolloHydrationProvider"; + +/** Async server island: prefetch LeetCode daily data without blocking the page shell. */ +export async function DailyApolloIsland() { + const initialCache = await getDailyInitialData(); + + return ( + + + + ); +} diff --git a/src/features/homePage/ui/DailyPageContent.tsx b/src/features/homePage/ui/DailyPageContent.tsx new file mode 100644 index 00000000..6846c759 --- /dev/null +++ b/src/features/homePage/ui/DailyPageContent.tsx @@ -0,0 +1,76 @@ +import { Box, Container, Link as MuiLink, Typography } from "@mui/material"; +import type { ReactNode } from "react"; +import React from "react"; + +import type { TranslationFunctions } from "#/i18n/i18n-types"; + +type DailyPageContentProps = { + LL: TranslationFunctions; + children: ReactNode; +}; + +/** Server-rendered daily page shell (marketing chrome supplies client app bar). */ +export const DailyPageContent: React.FC = ({ + LL, + children, +}) => ( + + + + + {LL.HOME_DAILY_SECTION_TITLE()} + + + {LL.HOME_DAILY_SECTION_LEAD()}{" "} + + LeetCode + + + + + {children} + + +); diff --git a/src/features/homePage/ui/DailyPageInteractive.tsx b/src/features/homePage/ui/DailyPageInteractive.tsx new file mode 100644 index 00000000..84e59cac --- /dev/null +++ b/src/features/homePage/ui/DailyPageInteractive.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { Alert, Grid } from "@mui/material"; +import React from "react"; + +import { useDailyQuestionData } from "#/api"; +import { DailyProblem } from "#/features/homePage/ui/DailyProblem/DailyProblem"; +import { QuestionSummary } from "#/features/homePage/ui/QuestionSummary"; +import { useI18nContext } from "#/shared/hooks"; + +/** Client island: daily question data, summary, and interactive problem UI. */ +export const DailyPageInteractive: React.FC = () => { + const { LL } = useI18nContext(); + const questionDataQuery = useDailyQuestionData(); + + return ( + <> + {questionDataQuery.error ? ( + + {LL.HOME_DAILY_QUESTION_ERROR()} + + ) : null} + + + + + + + + + + + ); +}; diff --git a/src/features/homePage/ui/DailyPageView.tsx b/src/features/homePage/ui/DailyPageView.tsx deleted file mode 100644 index bcd4395f..00000000 --- a/src/features/homePage/ui/DailyPageView.tsx +++ /dev/null @@ -1,110 +0,0 @@ -"use client"; - -import { - Alert, - Box, - Container, - Grid, - Link as MuiLink, - Typography, - useMediaQuery, - useTheme, -} from "@mui/material"; -import React from "react"; - -import { useDailyQuestionData } from "#/api"; -import { DailyProblem } from "#/features/homePage/ui/DailyProblem/DailyProblem"; -import { QuestionSummary } from "#/features/homePage/ui/QuestionSummary"; -import { useI18nContext } from "#/shared/hooks"; - -/** Daily problem page content. Shared by Pages `/daily` and App Router pilot. */ -export const DailyPageView: React.FC = () => { - const { LL } = useI18nContext(); - const theme = useTheme(); - const questionDataQuery = useDailyQuestionData(); - const isMediumScreen = useMediaQuery(theme.breakpoints.between("sm", "lg")); - - return ( - - - - - {LL.HOME_DAILY_SECTION_TITLE()} - - - {LL.HOME_DAILY_SECTION_LEAD()}{" "} - - LeetCode - - - - - {questionDataQuery.error ? ( - - {LL.HOME_DAILY_QUESTION_ERROR()} - - ) : null} - - - - - - - - - - - - ); -}; diff --git a/src/features/homePage/ui/SessionWidget.tsx b/src/features/homePage/ui/SessionWidget.tsx deleted file mode 100644 index 22b5ec57..00000000 --- a/src/features/homePage/ui/SessionWidget.tsx +++ /dev/null @@ -1,105 +0,0 @@ -"use client"; - -import { Box, Button, CircularProgress, Typography } from "@mui/material"; -import { signIn, signOut, useSession } from "next-auth/react"; -import Image from "next/image"; -import React from "react"; - -const isNextImageOptimizedAvatarUrl = (url: string): boolean => { - try { - const host = new URL(url).hostname; - return ( - host === "avatars.githubusercontent.com" || - host === "lh3.googleusercontent.com" - ); - } catch { - return false; - } -}; - -export const SessionWidget: React.FC = () => { - const { data: session, status } = useSession(); - const loading = status === "loading"; - - if (loading) { - return ( - - - - ); - } - - const handleSignIn: React.MouseEventHandler = async ( - event, - ) => { - event.preventDefault(); - await signIn(); - }; - - const handleSignOut: React.MouseEventHandler = async ( - event, - ) => { - event.preventDefault(); - await signOut(); - }; - - return ( - - {!session?.user ? ( - <> - - You are not signed in - - - - ) : ( - <> - - You are signed in as: - - {session.user.image && ( - user avatar - )} - - {session.user.name}
- {session.user.email}
- {session.user.image}
-
- - - )} -
- ); -}; diff --git a/src/i18n/getServerTranslationFunctions.ts b/src/i18n/getServerTranslationFunctions.ts new file mode 100644 index 00000000..383ab42c --- /dev/null +++ b/src/i18n/getServerTranslationFunctions.ts @@ -0,0 +1,16 @@ +import { createTranslationFunctions } from "#/i18n/createTranslationFunctions"; +import type { Locales, TranslationFunctions } from "#/i18n/i18n-types"; +import { loadI18nForLocale } from "#/i18n/loadI18nForLocale"; + +/** Server-side `LL` helpers for RSC pages (cached via `loadI18nForLocale`). */ +export async function getServerTranslationFunctions( + locale: Locales, +): Promise { + const { translations } = await loadI18nForLocale(locale); + const translation = translations[locale]; + if (!translation) { + throw new Error(`Missing translations for locale: ${locale}`); + } + + return createTranslationFunctions(locale, translation); +} From 041bcb7ab8b164793a08dbcbd2323294daee0721 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 18:49:14 +0000 Subject: [PATCH 2/5] fix(daily): wrap async RSC island in Suspense to prevent React #412 React error #412 ('Connection closed') occurs when client navigations consume a partial RSC payload with an uncached async child. Match the profile page pattern: sync page export with Suspense boundaries around DailyPageWithShell and DailyApolloIsland. Co-authored-by: maxim.kayander1 --- next-env.d.ts | 4 +-- .../(default-locale)/(app)/daily/loading.tsx | 26 ++-------------- src/app/[lang]/(app)/daily/loading.tsx | 26 ++-------------- src/app/locale-app/pages/dailyPage.tsx | 18 +++++++++-- .../homePage/ui/DailyInteractiveSkeleton.tsx | 30 +++++++++++++++++++ .../homePage/ui/DailyPageSkeleton.tsx | 23 ++++++++++++++ 6 files changed, 77 insertions(+), 50 deletions(-) create mode 100644 src/features/homePage/ui/DailyInteractiveSkeleton.tsx create mode 100644 src/features/homePage/ui/DailyPageSkeleton.tsx diff --git a/next-env.d.ts b/next-env.d.ts index ce4e94a6..a419cbe4 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,7 +1,7 @@ /// /// -import "./.next/types/routes.d.ts"; -import "./.next/types/root-params.d.ts"; +import "./.next/dev/types/routes.d.ts"; +import "./.next/dev/types/root-params.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/app/(default-locale)/(app)/daily/loading.tsx b/src/app/(default-locale)/(app)/daily/loading.tsx index bb3b63b9..4b79618e 100644 --- a/src/app/(default-locale)/(app)/daily/loading.tsx +++ b/src/app/(default-locale)/(app)/daily/loading.tsx @@ -1,24 +1,4 @@ -import { Box, Container, Skeleton } from "@mui/material"; +import { DailyPageSkeleton } from "#/features/homePage/ui/DailyPageSkeleton"; -/** Instant-nav fallback for `/daily` while the client view hydrates. */ -export default function DailyLoading() { - return ( - - - - - - - ); -} +/** Instant-nav fallback for `/daily` while the page shell and data stream in. */ +export default DailyPageSkeleton; diff --git a/src/app/[lang]/(app)/daily/loading.tsx b/src/app/[lang]/(app)/daily/loading.tsx index bb3b63b9..4b79618e 100644 --- a/src/app/[lang]/(app)/daily/loading.tsx +++ b/src/app/[lang]/(app)/daily/loading.tsx @@ -1,24 +1,4 @@ -import { Box, Container, Skeleton } from "@mui/material"; +import { DailyPageSkeleton } from "#/features/homePage/ui/DailyPageSkeleton"; -/** Instant-nav fallback for `/daily` while the client view hydrates. */ -export default function DailyLoading() { - return ( - - - - - - - ); -} +/** Instant-nav fallback for `/daily` while the page shell and data stream in. */ +export default DailyPageSkeleton; diff --git a/src/app/locale-app/pages/dailyPage.tsx b/src/app/locale-app/pages/dailyPage.tsx index b7411cdb..7120c2cc 100644 --- a/src/app/locale-app/pages/dailyPage.tsx +++ b/src/app/locale-app/pages/dailyPage.tsx @@ -1,5 +1,9 @@ +import { Suspense } from "react"; + import { DailyApolloIsland } from "#/features/homePage/ui/DailyApolloIsland"; +import { DailyInteractiveSkeleton } from "#/features/homePage/ui/DailyInteractiveSkeleton"; import { DailyPageContent } from "#/features/homePage/ui/DailyPageContent"; +import { DailyPageSkeleton } from "#/features/homePage/ui/DailyPageSkeleton"; import { getServerTranslationFunctions } from "#/i18n/getServerTranslationFunctions"; import type { Translation } from "#/i18n/i18n-types"; @@ -29,13 +33,23 @@ type DailyPageProps = { params?: Promise<{ lang?: string }>; }; -export async function DailyPage({ params }: DailyPageProps = {}) { +async function DailyPageWithShell({ params }: DailyPageProps) { const locale = await resolvePageLocale(params); const LL = await getServerTranslationFunctions(locale); return ( - + }> + + ); } + +export function DailyPage({ params }: DailyPageProps = {}) { + return ( + }> + + + ); +} diff --git a/src/features/homePage/ui/DailyInteractiveSkeleton.tsx b/src/features/homePage/ui/DailyInteractiveSkeleton.tsx new file mode 100644 index 00000000..e0c96eb2 --- /dev/null +++ b/src/features/homePage/ui/DailyInteractiveSkeleton.tsx @@ -0,0 +1,30 @@ +import { Grid, Skeleton } from "@mui/material"; +import React from "react"; + +/** Fallback while LeetCode daily data streams in below the server-rendered title. */ +export const DailyInteractiveSkeleton: React.FC = () => ( + + + + + + + + +); diff --git a/src/features/homePage/ui/DailyPageSkeleton.tsx b/src/features/homePage/ui/DailyPageSkeleton.tsx new file mode 100644 index 00000000..bec4ae94 --- /dev/null +++ b/src/features/homePage/ui/DailyPageSkeleton.tsx @@ -0,0 +1,23 @@ +import { Box, Container, Skeleton } from "@mui/material"; +import React from "react"; + +/** Instant-nav fallback for `/daily` while the page shell and data stream in. */ +export const DailyPageSkeleton: React.FC = () => ( + + + + + + +); From 65b82db8a7ad3bbc20f2fc087aed0be57d6e59fd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 18:55:55 +0000 Subject: [PATCH 3/5] fix(daily): cap LeetCode prefetch at 10s so Suspense can resolve Co-authored-by: maxim.kayander1 --- src/server/daily/getDailyInitialData.ts | 50 +++++++++++++++---------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/server/daily/getDailyInitialData.ts b/src/server/daily/getDailyInitialData.ts index 81d9bcee..09429469 100644 --- a/src/server/daily/getDailyInitialData.ts +++ b/src/server/daily/getDailyInitialData.ts @@ -6,31 +6,43 @@ import { QuestionOfTodayDocument, } from "#/graphql/generated"; -/** - * Prefetch today's LeetCode daily question for RSC + Apollo cache hydration. - * Returns null when upstream GraphQL is unavailable (client hooks refetch). - */ -export async function getDailyInitialData(): Promise { +const DAILY_PREFETCH_TIMEOUT_MS = 10_000; + +async function fetchDailyInitialData(): Promise { const client = createApolloClient({ ssr: true }); - try { - const todayResult = await client.query({ - query: QuestionOfTodayDocument, + const todayResult = await client.query({ + query: QuestionOfTodayDocument, + fetchPolicy: "no-cache", + }); + + const titleSlug = + todayResult.data.activeDailyCodingChallengeQuestion?.question?.titleSlug; + + if (titleSlug) { + await client.query({ + query: QuestionDataDocument, + variables: { titleSlug }, fetchPolicy: "no-cache", }); + } - const titleSlug = - todayResult.data.activeDailyCodingChallengeQuestion?.question?.titleSlug; - - if (titleSlug) { - await client.query({ - query: QuestionDataDocument, - variables: { titleSlug }, - fetchPolicy: "no-cache", - }); - } + return client.cache.extract(); +} - return client.cache.extract(); +/** + * Prefetch today's LeetCode daily question for RSC + Apollo cache hydration. + * Returns null when upstream GraphQL is unavailable (client hooks refetch). + */ +export async function getDailyInitialData(): Promise { + try { + const result = await Promise.race([ + fetchDailyInitialData(), + new Promise((resolve) => { + setTimeout(() => resolve(null), DAILY_PREFETCH_TIMEOUT_MS); + }), + ]); + return result; } catch (error) { console.warn( "getDailyInitialData: LeetCode GraphQL prefetch failed", From 22afa3e9d0c95fa3ed96aa5ec098baec11ebc898 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 19:21:17 +0000 Subject: [PATCH 4/5] fix(daily): revert RSC shell to fix React #412 on instant nav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The async server island + nested Suspense pattern is incompatible with instant=true client navigations — RSC stream closes with pending rows (React error #412). Restore the pre-refactor client DailyPageView with server-side Apollo prefetch at the page level. Keep getServerTranslationFunctions, resolvePageLocale (privacy), prefetch timeout, and DailyPageSkeleton for loading.tsx. Co-authored-by: maxim.kayander1 --- src/app/locale-app/pages/dailyPage.tsx | 36 ++---- .../homePage/ui/DailyApolloIsland.tsx | 15 --- .../homePage/ui/DailyInteractiveSkeleton.tsx | 30 ----- src/features/homePage/ui/DailyPageContent.tsx | 76 ------------ .../homePage/ui/DailyPageInteractive.tsx | 49 -------- src/features/homePage/ui/DailyPageView.tsx | 110 ++++++++++++++++++ 6 files changed, 118 insertions(+), 198 deletions(-) delete mode 100644 src/features/homePage/ui/DailyApolloIsland.tsx delete mode 100644 src/features/homePage/ui/DailyInteractiveSkeleton.tsx delete mode 100644 src/features/homePage/ui/DailyPageContent.tsx delete mode 100644 src/features/homePage/ui/DailyPageInteractive.tsx create mode 100644 src/features/homePage/ui/DailyPageView.tsx diff --git a/src/app/locale-app/pages/dailyPage.tsx b/src/app/locale-app/pages/dailyPage.tsx index 7120c2cc..fee0d8c6 100644 --- a/src/app/locale-app/pages/dailyPage.tsx +++ b/src/app/locale-app/pages/dailyPage.tsx @@ -1,17 +1,12 @@ -import { Suspense } from "react"; - -import { DailyApolloIsland } from "#/features/homePage/ui/DailyApolloIsland"; -import { DailyInteractiveSkeleton } from "#/features/homePage/ui/DailyInteractiveSkeleton"; -import { DailyPageContent } from "#/features/homePage/ui/DailyPageContent"; -import { DailyPageSkeleton } from "#/features/homePage/ui/DailyPageSkeleton"; -import { getServerTranslationFunctions } from "#/i18n/getServerTranslationFunctions"; +import { DailyPageView } from "#/features/homePage/ui/DailyPageView"; import type { Translation } from "#/i18n/i18n-types"; +import { getDailyInitialData } from "#/server/daily/getDailyInitialData"; +import { ApolloHydrationProvider } from "#/app/locale-app/ApolloHydrationProvider"; import { createDefaultLocaleRouteMetadata, createLangRouteMetadata, } from "#/app/locale-app/createLocaleRouteMetadata"; -import { resolvePageLocale } from "#/app/locale-app/resolvePageLocale"; /** Marketing daily — instant client navigations to sibling routes (L5). */ export const instant = true; @@ -29,27 +24,12 @@ export const generateLangDailyMetadata = createLangRouteMetadata( pickDailyCopy, ); -type DailyPageProps = { - params?: Promise<{ lang?: string }>; -}; - -async function DailyPageWithShell({ params }: DailyPageProps) { - const locale = await resolvePageLocale(params); - const LL = await getServerTranslationFunctions(locale); - - return ( - - }> - - - - ); -} +export async function DailyPage() { + const initialCache = await getDailyInitialData(); -export function DailyPage({ params }: DailyPageProps = {}) { return ( - }> - - + + + ); } diff --git a/src/features/homePage/ui/DailyApolloIsland.tsx b/src/features/homePage/ui/DailyApolloIsland.tsx deleted file mode 100644 index 0143ffdc..00000000 --- a/src/features/homePage/ui/DailyApolloIsland.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { DailyPageInteractive } from "#/features/homePage/ui/DailyPageInteractive"; -import { getDailyInitialData } from "#/server/daily/getDailyInitialData"; - -import { ApolloHydrationProvider } from "#/app/locale-app/ApolloHydrationProvider"; - -/** Async server island: prefetch LeetCode daily data without blocking the page shell. */ -export async function DailyApolloIsland() { - const initialCache = await getDailyInitialData(); - - return ( - - - - ); -} diff --git a/src/features/homePage/ui/DailyInteractiveSkeleton.tsx b/src/features/homePage/ui/DailyInteractiveSkeleton.tsx deleted file mode 100644 index e0c96eb2..00000000 --- a/src/features/homePage/ui/DailyInteractiveSkeleton.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Grid, Skeleton } from "@mui/material"; -import React from "react"; - -/** Fallback while LeetCode daily data streams in below the server-rendered title. */ -export const DailyInteractiveSkeleton: React.FC = () => ( - - - - - - - - -); diff --git a/src/features/homePage/ui/DailyPageContent.tsx b/src/features/homePage/ui/DailyPageContent.tsx deleted file mode 100644 index 6846c759..00000000 --- a/src/features/homePage/ui/DailyPageContent.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Box, Container, Link as MuiLink, Typography } from "@mui/material"; -import type { ReactNode } from "react"; -import React from "react"; - -import type { TranslationFunctions } from "#/i18n/i18n-types"; - -type DailyPageContentProps = { - LL: TranslationFunctions; - children: ReactNode; -}; - -/** Server-rendered daily page shell (marketing chrome supplies client app bar). */ -export const DailyPageContent: React.FC = ({ - LL, - children, -}) => ( - - - - - {LL.HOME_DAILY_SECTION_TITLE()} - - - {LL.HOME_DAILY_SECTION_LEAD()}{" "} - - LeetCode - - - - - {children} - - -); diff --git a/src/features/homePage/ui/DailyPageInteractive.tsx b/src/features/homePage/ui/DailyPageInteractive.tsx deleted file mode 100644 index 84e59cac..00000000 --- a/src/features/homePage/ui/DailyPageInteractive.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { Alert, Grid } from "@mui/material"; -import React from "react"; - -import { useDailyQuestionData } from "#/api"; -import { DailyProblem } from "#/features/homePage/ui/DailyProblem/DailyProblem"; -import { QuestionSummary } from "#/features/homePage/ui/QuestionSummary"; -import { useI18nContext } from "#/shared/hooks"; - -/** Client island: daily question data, summary, and interactive problem UI. */ -export const DailyPageInteractive: React.FC = () => { - const { LL } = useI18nContext(); - const questionDataQuery = useDailyQuestionData(); - - return ( - <> - {questionDataQuery.error ? ( - - {LL.HOME_DAILY_QUESTION_ERROR()} - - ) : null} - - - - - - - - - - - ); -}; diff --git a/src/features/homePage/ui/DailyPageView.tsx b/src/features/homePage/ui/DailyPageView.tsx new file mode 100644 index 00000000..bcd4395f --- /dev/null +++ b/src/features/homePage/ui/DailyPageView.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { + Alert, + Box, + Container, + Grid, + Link as MuiLink, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import React from "react"; + +import { useDailyQuestionData } from "#/api"; +import { DailyProblem } from "#/features/homePage/ui/DailyProblem/DailyProblem"; +import { QuestionSummary } from "#/features/homePage/ui/QuestionSummary"; +import { useI18nContext } from "#/shared/hooks"; + +/** Daily problem page content. Shared by Pages `/daily` and App Router pilot. */ +export const DailyPageView: React.FC = () => { + const { LL } = useI18nContext(); + const theme = useTheme(); + const questionDataQuery = useDailyQuestionData(); + const isMediumScreen = useMediaQuery(theme.breakpoints.between("sm", "lg")); + + return ( + + + + + {LL.HOME_DAILY_SECTION_TITLE()} + + + {LL.HOME_DAILY_SECTION_LEAD()}{" "} + + LeetCode + + + + + {questionDataQuery.error ? ( + + {LL.HOME_DAILY_QUESTION_ERROR()} + + ) : null} + + + + + + + + + + + + ); +}; From 489522152889134f77e12bb18357d20a0f3e3424 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 8 Sep 2026 20:46:12 +0000 Subject: [PATCH 5/5] fix(privacy): restore inline i18n load to fix preview RSC stall getServerTranslationFunctions() broke privacy page rendering on Vercel preview (loading skeleton stuck, no h1/canonical). Revert to calling loadI18nForLocale directly from the page component. Co-authored-by: maxim.kayander1 --- .../__tests__/resolvePageLocale.test.ts | 29 ------------------- src/app/locale-app/pages/privacyPage.tsx | 29 +++++++++++++++---- src/app/locale-app/resolvePageLocale.ts | 20 ------------- src/i18n/getServerTranslationFunctions.ts | 16 ---------- 4 files changed, 24 insertions(+), 70 deletions(-) delete mode 100644 src/app/locale-app/__tests__/resolvePageLocale.test.ts delete mode 100644 src/app/locale-app/resolvePageLocale.ts delete mode 100644 src/i18n/getServerTranslationFunctions.ts diff --git a/src/app/locale-app/__tests__/resolvePageLocale.test.ts b/src/app/locale-app/__tests__/resolvePageLocale.test.ts deleted file mode 100644 index 995a2b88..00000000 --- a/src/app/locale-app/__tests__/resolvePageLocale.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { baseLocale } from "#/i18n/i18n-util"; - -import { resolvePageLocale } from "#/app/locale-app/resolvePageLocale"; - -describe("resolvePageLocale", () => { - it("returns base locale when params are omitted", async () => { - await expect(resolvePageLocale()).resolves.toBe(baseLocale); - }); - - it("returns base locale when lang param is absent", async () => { - await expect(resolvePageLocale(Promise.resolve({}))).resolves.toBe( - baseLocale, - ); - }); - - it("returns the resolved locale for supported lang params", async () => { - await expect( - resolvePageLocale(Promise.resolve({ lang: "de" })), - ).resolves.toBe("de"); - }); - - it("falls back to base locale for unsupported lang params", async () => { - await expect( - resolvePageLocale(Promise.resolve({ lang: "zz" })), - ).resolves.toBe(baseLocale); - }); -}); diff --git a/src/app/locale-app/pages/privacyPage.tsx b/src/app/locale-app/pages/privacyPage.tsx index 5fcf3255..929ca9d3 100644 --- a/src/app/locale-app/pages/privacyPage.tsx +++ b/src/app/locale-app/pages/privacyPage.tsx @@ -1,13 +1,14 @@ import { PrivacyPageContent } from "#/features/privacy/ui/PrivacyPageContent"; -import { getServerTranslationFunctions } from "#/i18n/getServerTranslationFunctions"; -import type { Translation } from "#/i18n/i18n-types"; +import { createTranslationFunctions } from "#/i18n/createTranslationFunctions"; +import type { Locales, Translation } from "#/i18n/i18n-types"; import { baseLocale } from "#/i18n/i18n-util"; +import { loadI18nForLocale } from "#/i18n/loadI18nForLocale"; import { createDefaultLocaleRouteMetadata, createLangRouteMetadata, } from "#/app/locale-app/createLocaleRouteMetadata"; -import { resolvePageLocale } from "#/app/locale-app/resolvePageLocale"; +import { resolveLangParamSync } from "#/app/locale-app/resolveLangParam"; /** Marketing privacy — instant client navigations; body is server-rendered (RSC). */ export const instant = true; @@ -29,9 +30,27 @@ type PrivacyPageProps = { params?: Promise<{ lang?: string }>; }; +async function resolvePrivacyLocale( + params?: Promise<{ lang?: string }>, +): Promise { + if (!params) { + return baseLocale; + } + const { lang: langParam } = await params; + if (!langParam) { + return baseLocale; + } + return resolveLangParamSync(langParam) ?? baseLocale; +} + export async function PrivacyPage({ params }: PrivacyPageProps = {}) { - const locale = await resolvePageLocale(params); - const LL = await getServerTranslationFunctions(locale); + const locale = await resolvePrivacyLocale(params); + const { translations } = await loadI18nForLocale(locale); + const translation = translations[locale]; + if (!translation) { + throw new Error(`Missing translations for locale: ${locale}`); + } + const LL = createTranslationFunctions(locale, translation); const homePath = locale === baseLocale ? "/" : `/${locale}`; return ; diff --git a/src/app/locale-app/resolvePageLocale.ts b/src/app/locale-app/resolvePageLocale.ts deleted file mode 100644 index 6f5a041f..00000000 --- a/src/app/locale-app/resolvePageLocale.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Locales } from "#/i18n/i18n-types"; -import { baseLocale } from "#/i18n/i18n-util"; - -import { resolveLangParamSync } from "#/app/locale-app/resolveLangParam"; - -/** Resolves locale from optional App `[lang]` route params (default locale when absent). */ -export async function resolvePageLocale( - params?: Promise<{ lang?: string }>, -): Promise { - if (!params) { - return baseLocale; - } - - const { lang: langParam } = await params; - if (!langParam) { - return baseLocale; - } - - return resolveLangParamSync(langParam) ?? baseLocale; -} diff --git a/src/i18n/getServerTranslationFunctions.ts b/src/i18n/getServerTranslationFunctions.ts deleted file mode 100644 index 383ab42c..00000000 --- a/src/i18n/getServerTranslationFunctions.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createTranslationFunctions } from "#/i18n/createTranslationFunctions"; -import type { Locales, TranslationFunctions } from "#/i18n/i18n-types"; -import { loadI18nForLocale } from "#/i18n/loadI18nForLocale"; - -/** Server-side `LL` helpers for RSC pages (cached via `loadI18nForLocale`). */ -export async function getServerTranslationFunctions( - locale: Locales, -): Promise { - const { translations } = await loadI18nForLocale(locale); - const translation = translations[locale]; - if (!translation) { - throw new Error(`Missing translations for locale: ${locale}`); - } - - return createTranslationFunctions(locale, translation); -}