refactor(layouts): overhaul problem and problemset page structures

- Simplify ProblemLayout to use children prop and remove ProblemStoreProvider
- Replace PlaygroundHeader with dedicated ProblemHeader component
- Streamline ProblemsetLayout with new ProblemsetHeader
- Remove deprecated BackButton in favor of NavigateBackButton
- Delete unused ProblemStoreProvider and related dependencies
This commit is contained in:
cfngc4594 2025-05-06 21:04:45 +08:00
parent 829370fafd
commit aed942e7e2
7 changed files with 73 additions and 205 deletions

View File

@ -1,90 +1,25 @@
import prisma from "@/lib/prisma";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { getUserLocale } from "@/i18n/locale"; import { ProblemHeader } from "@/features/problems/components/problem-header";
import ProblemPage from "@/app/(app)/problems/[id]/page";
import { ProblemStoreProvider } from "@/providers/problem-store-provider";
import { PlaygroundHeader } from "@/components/features/playground/header";
interface ProblemProps { interface ProblemLayoutProps {
children: React.ReactNode;
params: Promise<{ id: string }>; params: Promise<{ id: string }>;
Description: React.ReactNode;
Solutions: React.ReactNode;
Submissions: React.ReactNode;
Details: React.ReactNode;
Code: React.ReactNode;
Testcase: React.ReactNode;
Bot: React.ReactNode;
} }
export default async function ProblemLayout({ export default async function ProblemLayout({
children,
params, params,
Description, }: ProblemLayoutProps) {
Solutions,
Submissions,
Details,
Code,
Testcase,
Bot,
}: ProblemProps) {
const { id } = await params; const { id } = await params;
if (!id) { if (!id) {
return notFound(); return notFound();
} }
const [
problem,
editorLanguageConfigs,
languageServerConfigs,
submissions,
] = await Promise.all([
prisma.problem.findUnique({
where: { id },
include: {
templates: true,
testcases: {
include: {
data: true,
},
},
},
}),
prisma.editorLanguageConfig.findMany(),
prisma.languageServerConfig.findMany(),
prisma.submission.findMany({
where: { problemId: id },
}),
]);
if (!problem) {
return notFound();
}
const locale = await getUserLocale();
return ( return (
<div className="flex flex-col h-screen"> <div className="flex flex-col h-screen">
<ProblemStoreProvider <ProblemHeader />
problemId={id} {children}
problem={problem}
editorLanguageConfigs={editorLanguageConfigs}
languageServerConfigs={languageServerConfigs}
submissions={submissions}
>
<PlaygroundHeader />
<main className="flex flex-grow overflow-y-hidden p-2.5 pt-0">
<ProblemPage
locale={locale}
Description={Description}
Solutions={Solutions}
Submissions={Submissions}
Details={Details}
Code={Code}
Testcase={Testcase}
Bot={Bot}
/>
</main>
</ProblemStoreProvider>
</div> </div>
); );
} }

View File

@ -1,5 +1,4 @@
import { Banner } from "@/components/banner"; import { ProblemsetHeader } from "@/features/problemset/components/problemset-header";
import { AvatarButton } from "@/components/avatar-button";
interface ProblemsetLayoutProps { interface ProblemsetLayoutProps {
children: React.ReactNode; children: React.ReactNode;
@ -7,14 +6,9 @@ interface ProblemsetLayoutProps {
export default function ProblemsetLayout({ children }: ProblemsetLayoutProps) { export default function ProblemsetLayout({ children }: ProblemsetLayoutProps) {
return ( return (
<div className="relative h-screen flex flex-col"> <div className="h-full flex flex-col">
<Banner /> <ProblemsetHeader />
<div className="absolute top-2 right-4">
<AvatarButton />
</div>
<main className="h-full container mx-auto p-4">
{children} {children}
</main>
</div> </div>
); );
} }

View File

@ -1,45 +0,0 @@
import { cn } from "@/lib/utils";
import { auth } from "@/lib/auth";
import BackButton from "@/components/back-button";
import { RunCodeButton } from "@/components/run-code";
import { AvatarButton } from "@/components/avatar-button";
import BotVisibilityToggle from "@/components/bot-visibility-toggle";
interface PlaygroundHeaderProps {
className?: string;
}
export async function PlaygroundHeader({
className,
...props
}: PlaygroundHeaderProps) {
const session = await auth();
return (
<header
{...props}
className={cn("relative", className)}
>
<nav className="z-0 relative h-12 w-full flex shrink-0 items-center px-2.5">
<div className="w-full flex justify-between">
<div className="w-full flex items-center justify-between">
<div className="flex items-center">
<BackButton href="/problemset" />
</div>
<div className="relative flex items-center gap-2">
<BotVisibilityToggle />
<AvatarButton />
</div>
</div>
</div>
</nav>
<div className="z-10 absolute left-1/2 top-0 h-full -translate-x-1/2 py-2">
<div className="relative flex">
<div className="relative flex overflow-hidden rounded">
<RunCodeButton session={session} className="bg-muted text-muted-foreground hover:bg-muted/50" />
</div>
</div>
</div>
</header>
);
}

View File

@ -1,20 +1,21 @@
import Link from "next/link"; import Link from "next/link";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { ArrowLeftIcon } from "lucide-react"; import { ArrowLeftIcon } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
interface BackButtonProps { interface NavigateBackButtonProps {
href: string; href: string;
className?: string; className?: string;
} }
export default function BackButton({ const NavigateBackButton = ({ href, className }: NavigateBackButtonProps) => {
href,
className,
...props
}: BackButtonProps) {
const t = useTranslations(); const t = useTranslations();
return ( return (
@ -25,7 +26,6 @@ export default function BackButton({
variant="ghost" variant="ghost"
className={cn("h-8 w-auto p-2", className)} className={cn("h-8 w-auto p-2", className)}
asChild asChild
{...props}
> >
<Link href={href}> <Link href={href}>
<ArrowLeftIcon size={16} aria-hidden="true" /> <ArrowLeftIcon size={16} aria-hidden="true" />
@ -38,4 +38,6 @@ export default function BackButton({
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
); );
} };
export { NavigateBackButton };

View File

@ -0,0 +1,33 @@
import { cn } from "@/lib/utils";
import { Suspense } from "react";
import { UserAvatar, UserAvatarSkeleton } from "@/components/user-avatar";
import { JudgeCodeButton } from "@/features/problems/components/judge-code-button";
import { NavigateBackButton } from "@/features/problems/components/navigate-back-button";
interface ProblemHeaderProps {
className?: string;
}
const ProblemHeader = ({ className }: ProblemHeaderProps) => {
return (
<header
className={cn("relative flex h-12 flex-none items-center", className)}
>
<div className="container mx-auto flex h-full items-center justify-between px-4">
<div className="flex items-center">
<NavigateBackButton href="/problemset" />
</div>
<div className="flex items-center">
<Suspense fallback={<UserAvatarSkeleton />}>
<UserAvatar />
</Suspense>
</div>
</div>
<div className="absolute inset-y-0 left-1/2 z-10 flex -translate-x-1/2 items-center">
<JudgeCodeButton />
</div>
</header>
);
};
export { ProblemHeader };

View File

@ -0,0 +1,18 @@
import { Suspense } from "react";
import { UserAvatar, UserAvatarSkeleton } from "@/components/user-avatar";
const ProblemsetHeader = () => {
return (
<header className="flex h-12 flex-none items-center">
<div className="container mx-auto flex h-full items-center justify-end px-4">
<div className="flex items-center">
<Suspense fallback={<UserAvatarSkeleton />}>
<UserAvatar />
</Suspense>
</div>
</div>
</header>
);
};
export { ProblemsetHeader };

View File

@ -1,69 +0,0 @@
"use client";
import {
EditorLanguage,
type Submission,
type EditorLanguageConfig,
type LanguageServerConfig,
} from "@/generated/client";
import { useStore } from "zustand";
import { type ProblemWithDetails } from "@/types/prisma";
import { type ReactNode, createContext, useRef, useContext } from "react";
import { type ProblemStore, createProblemStore } from "@/stores/problem-store";
export type ProblemStoreApi = ReturnType<typeof createProblemStore>;
export const ProblemStoreContext = createContext<ProblemStoreApi | undefined>(undefined);
export interface ProblemStoreProviderProps {
children: ReactNode;
problemId: string;
problem: ProblemWithDetails;
editorLanguageConfigs: EditorLanguageConfig[];
languageServerConfigs: LanguageServerConfig[];
submissions: Submission[];
}
export const ProblemStoreProvider = ({
children,
problemId,
problem,
editorLanguageConfigs,
languageServerConfigs,
submissions,
}: ProblemStoreProviderProps) => {
const storeRef = useRef<ProblemStoreApi | null>(null);
if (storeRef.current === null) {
storeRef.current = createProblemStore({
hydrated: false,
editor: null,
markers: [],
webSocket: null,
globalLang: EditorLanguage.c,
currentLang: EditorLanguage.c,
currentValue: "",
problemId,
problem,
editorLanguageConfigs,
languageServerConfigs,
submissions,
});
}
return (
<ProblemStoreContext.Provider value={storeRef.current}>
{children}
</ProblemStoreContext.Provider>
);
};
export const useProblemStore = <T,>(selector: (store: ProblemStore) => T): T => {
const problemStoreContext = useContext(ProblemStoreContext);
if (!problemStoreContext) {
throw new Error("useProblemStore must be used within ProblemStoreProvider");
}
return useStore(problemStoreContext, selector);
};