Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 7 additions & 3 deletions src/app/(default-locale)/(app)/profile/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
/** Profile route — Apollo client is created in profile page with server prefetch. */
import type { ReactNode } from "react";

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

/** Persistent chrome for profile — Apollo prefetch stays in {@link ProfilePage}. */
export default function DefaultLocaleProfileLayout({
children,
}: {
children: React.ReactNode;
children: ReactNode;
}) {
return children;
return <AppChromeLayoutClient>{children}</AppChromeLayoutClient>;
}
10 changes: 7 additions & 3 deletions src/app/[lang]/(app)/profile/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
/** Profile route — Apollo client is created in profile page with server prefetch. */
import type { ReactNode } from "react";

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

/** Persistent chrome for profile — Apollo prefetch stays in {@link ProfilePage}. */
export default function LangProfileLayout({
children,
}: {
children: React.ReactNode;
children: ReactNode;
}) {
return children;
return <AppChromeLayoutClient>{children}</AppChromeLayoutClient>;
}
1 change: 0 additions & 1 deletion src/app/locale-app/ApolloHydrationProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ type ApolloHydrationProviderProps = {

/**
* Route-scoped Apollo client with optional server-extracted cache (daily, profile).
* Playground mounts with `initialCache={null}` for {@link ProjectModal} LeetCode import only.
*/
export const ApolloHydrationProvider: React.FC<
ApolloHydrationProviderProps
Expand Down
13 changes: 5 additions & 8 deletions src/features/playground/ui/PlaygroundLayoutClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { PlaygroundPageShell } from "#/features/playground/ui/PlaygroundPageShel
import { ProjectBrowserProvider } from "#/features/project/ui/ProjectBrowser/ProjectBrowserContext";
import { prefetchSplitPanelsLayout } from "#/shared/ui/templates/SplitPanelsLayout/prefetchSplitPanelsLayout";

import { ApolloHydrationProvider } from "#/app/locale-app/ApolloHydrationProvider";
import { ProjectBrowserOverlay } from "#/app/locale-app/ProjectBrowserOverlay";

type PlaygroundLayoutClientProps = {
Expand Down Expand Up @@ -40,12 +39,10 @@ export const PlaygroundLayoutClient: React.FC<PlaygroundLayoutClientProps> = ({
children,
}) => {
return (
<ApolloHydrationProvider initialCache={null}>
<ProjectBrowserProvider>
<PlaygroundRouteEffects />
<PlaygroundPageShell>{children}</PlaygroundPageShell>
<ProjectBrowserOverlay />
</ProjectBrowserProvider>
</ApolloHydrationProvider>
<ProjectBrowserProvider>
<PlaygroundRouteEffects />
<PlaygroundPageShell>{children}</PlaygroundPageShell>
<ProjectBrowserOverlay />
</ProjectBrowserProvider>
);
};
51 changes: 24 additions & 27 deletions src/features/profile/ui/ProfilePageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { UserSettings } from "#/features/profile/ui/UserSettings";
import { useGetUserProfileQuery } from "#/graphql/generated";
import { useI18nContext } from "#/shared/hooks";
import { useProfileUserId } from "#/shared/hooks/useProfileUserId";
import { MainLayout } from "#/shared/ui/templates/MainLayout";

/** Profile dashboard content. Shared by Pages `/profile/[userId]` and App pilot. */
export const ProfilePageView: React.FC = () => {
Expand Down Expand Up @@ -123,32 +122,30 @@ export const ProfilePageView: React.FC = () => {
};

return (
<MainLayout>
<Container>
{renderAuthStatusSection()}
{session.status !== "loading" ? (
<>
<Typography
variant="h5"
sx={{
my: 3,
}}
>
{LL.USER_DASHBOARD({
name: leetCodeUsername || username || "User",
})}
</Typography>
<Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6 }}>
<UserSettings />
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<LeetCodeStats userProfile={userProfileQueryResult} />
</Grid>
<Container>
{renderAuthStatusSection()}
{session.status !== "loading" ? (
<>
<Typography
variant="h5"
sx={{
my: 3,
}}
>
{LL.USER_DASHBOARD({
name: leetCodeUsername || username || "User",
})}
</Typography>
<Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6 }}>
<UserSettings />
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<LeetCodeStats userProfile={userProfileQueryResult} />
</Grid>
</>
) : null}
</Container>
</MainLayout>
</Grid>
</>
) : null}
</Container>
);
};
28 changes: 14 additions & 14 deletions src/features/project/ui/ProjectModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import { categoryLabels } from "#/entities/category/model/categoryLabels";
import { getDifficultyValue } from "#/entities/difficulty/lib/getDifficultyValue";
import { difficultyLabels } from "#/entities/difficulty/model/difficultyLabels";
import { projectSlice } from "#/features/project/model/projectSlice";
import { useQuestionTitleLazyQuery } from "#/graphql/generated";
import {
ProjectCategory,
ProjectDifficulty,
Expand Down Expand Up @@ -88,10 +87,6 @@ export const ProjectModal: React.FC<ProjectModalProps> = ({
const [problemFetchStatus, setProblemFetchStatus] = useState(
ProblemFetchStatus.IDLE,
);
const [getQuestionTitle, { loading: isQuestionLoading }] =
useQuestionTitleLazyQuery({
returnPartialData: true,
});

const prevEditMode = usePrevious(isEditMode);

Expand All @@ -106,6 +101,7 @@ export const ProjectModal: React.FC<ProjectModalProps> = ({
const createProject = api.project.create.useMutation();
const editProject = api.project.update.useMutation();
const deleteProject = api.project.delete.useMutation();
const fetchLeetCodeQuestion = api.leetcode.getQuestionMetadata.useMutation();
const trpcUtils = api.useUtils();

const { setProject, clearSlugs } = usePlaygroundSlugs();
Expand Down Expand Up @@ -250,18 +246,22 @@ export const ProjectModal: React.FC<ProjectModalProps> = ({
return;
}

const { data } = await getQuestionTitle({
variables: {
titleSlug,
},
const question = await fetchLeetCodeQuestion.mutateAsync({
titleSlug,
});
if (!data) return;
if (!question) {
setProblemFetchStatus(ProblemFetchStatus.ERROR);
enqueueSnackbar("Failed to fetch problem data!", {
variant: "error",
});
return;
}

void formik.setValues({
...formik.values,
projectName: data.question.title,
projectSlug: data.question.titleSlug,
projectDifficulty: getDifficultyValue(data.question.difficulty),
projectName: question.title,
projectSlug: question.titleSlug,
projectDifficulty: getDifficultyValue(question.difficulty),
});
setProblemFetchStatus(ProblemFetchStatus.SUCCESS);
enqueueSnackbar("Problem data was successfully fetched 🎉", {
Expand Down Expand Up @@ -440,7 +440,7 @@ export const ProjectModal: React.FC<ProjectModalProps> = ({
>
<span>
<Button
loading={isQuestionLoading}
loading={fetchLeetCodeQuestion.isPending}
disabled={Boolean(
!formik.values.projectLcLink ||
formik.errors.projectLcLink,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

const mockFetchLeetCodeQuestionMetadata = vi.fn();

vi.mock("#/server/leetcode/fetchLeetCodeQuestionMetadata", () => ({
fetchLeetCodeQuestionMetadata: (titleSlug: string) =>
mockFetchLeetCodeQuestionMetadata(titleSlug),
}));

describe("leetcode.getQuestionMetadata", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("returns question metadata from LeetCode", async () => {
mockFetchLeetCodeQuestionMetadata.mockResolvedValue({
title: "Two Sum",
titleSlug: "two-sum",
difficulty: "Easy",
});

const { leetcodeRouter } = await import("#/server/api/routers/leetcode");
const caller = leetcodeRouter.createCaller({
session: null,
db: {} as never,
});

const result = await caller.getQuestionMetadata({ titleSlug: "two-sum" });

expect(mockFetchLeetCodeQuestionMetadata).toHaveBeenCalledWith("two-sum");
expect(result).toEqual({
title: "Two Sum",
titleSlug: "two-sum",
difficulty: "Easy",
});
});

it("returns null when upstream fetch fails", async () => {
mockFetchLeetCodeQuestionMetadata.mockResolvedValue(null);

const { leetcodeRouter } = await import("#/server/api/routers/leetcode");
const caller = leetcodeRouter.createCaller({
session: null,
db: {} as never,
});

const result = await caller.getQuestionMetadata({
titleSlug: "missing-problem",
});

expect(result).toBeNull();
});
});
21 changes: 20 additions & 1 deletion src/server/api/routers/leetcode.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
import { z } from "zod";

import { createTRPCRouter, protectedProcedure } from "#/server/api/trpc";
import {
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from "#/server/api/trpc";
import { fetchLeetCodeQuestionMetadata } from "#/server/leetcode/fetchLeetCodeQuestionMetadata";

export const leetcodeRouter = createTRPCRouter({
getQuestionMetadata: publicProcedure
.input(
z.object({
titleSlug: z.string().min(1),
}),
)
.mutation(async ({ input }) => {
const metadata = await fetchLeetCodeQuestionMetadata(input.titleSlug);
if (!metadata) {
return null;
}
return metadata;
}),

linkUser: protectedProcedure
.input(
z.object({
Expand Down
43 changes: 43 additions & 0 deletions src/server/leetcode/fetchLeetCodeQuestionMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createApolloClient } from "#/graphql/createApolloClient";
import { QuestionTitleDocument } from "#/graphql/generated";
import type { Difficulty } from "#/graphql/generated";

export type LeetCodeQuestionMetadata = {
title: string;
titleSlug: string;
difficulty: Difficulty;
};

/**
* Fetch public LeetCode question metadata for project import (server-side).
*/
export async function fetchLeetCodeQuestionMetadata(
titleSlug: string,
): Promise<LeetCodeQuestionMetadata | null> {
const client = createApolloClient({ ssr: true });

try {
const result = await client.query({
query: QuestionTitleDocument,
variables: { titleSlug },
fetchPolicy: "no-cache",
});

const question = result.data.question;
if (!question?.title || !question.titleSlug || !question.difficulty) {
return null;
}

return {
title: question.title,
titleSlug: question.titleSlug,
difficulty: question.difficulty,
};
} catch (error) {
console.warn(
"fetchLeetCodeQuestionMetadata: LeetCode GraphQL request failed",
error,
);
return null;
}
}
Loading