diff --git a/src/app/(app)/problems/[problemId]/layout.tsx b/src/app/(app)/problems/[problemId]/layout.tsx index def7321..fe22fac 100644 --- a/src/app/(app)/problems/[problemId]/layout.tsx +++ b/src/app/(app)/problems/[problemId]/layout.tsx @@ -1,15 +1,12 @@ import { notFound } from "next/navigation"; import { ProblemHeader } from "@/features/problems/components/header"; -interface ProblemLayoutProps { +interface LayoutProps { children: React.ReactNode; params: Promise<{ problemId: string }>; } -export default async function ProblemLayout({ - children, - params, -}: ProblemLayoutProps) { +const Layout = async ({ children, params }: LayoutProps) => { const { problemId } = await params; if (!problemId) { @@ -24,4 +21,6 @@ export default async function ProblemLayout({ ); -} +}; + +export default Layout; diff --git a/src/app/(app)/problems/[problemId]/page.tsx b/src/app/(app)/problems/[problemId]/page.tsx index 1ee7a32..8ae3f1e 100644 --- a/src/app/(app)/problems/[problemId]/page.tsx +++ b/src/app/(app)/problems/[problemId]/page.tsx @@ -1,40 +1,17 @@ -import { TestcasePanel } from "@/features/problems/testcase/panel"; -// import { BotPanel } from "@/features/problems/bot/components/panel"; -import { CodePanel } from "@/features/problems/code/components/panel"; -import { DetailPanel } from "@/features/problems/detail/components/panel"; -import { SolutionPanel } from "@/features/problems/solution/components/panel"; -import { SubmissionPanel } from "@/features/problems/submission/components/panel"; -import { DescriptionPanel } from "@/features/problems/description/components/panel"; -import { ProblemFlexLayout } from "@/features/problems/components/problem-flexlayout"; -import { AnalysisPanel } from "@/features/problems/analysis/components/panel"; +import { ProblemView } from "@/features/problems/ui/views/problem-view"; -interface ProblemPageProps { +interface PageProps { params: Promise<{ problemId: string }>; searchParams: Promise<{ submissionId: string | undefined; }>; } -export default async function ProblemPage({ - params, - searchParams, -}: ProblemPageProps) { +const Page = async ({ params, searchParams }: PageProps) => { const { problemId } = await params; const { submissionId } = await searchParams; - const components: Record = { - description: , - solution: , - submission: , - detail: , - code: , - testcase: , - bot: , - }; + return ; +}; - return ( -
- -
- ); -} +export default Page; diff --git a/src/app/(protected)/admin/problems/[problemId]/edit/layout.tsx b/src/app/(protected)/admin/problems/[problemId]/edit/layout.tsx new file mode 100644 index 0000000..fb46c63 --- /dev/null +++ b/src/app/(protected)/admin/problems/[problemId]/edit/layout.tsx @@ -0,0 +1,19 @@ +import { notFound } from "next/navigation"; +import { ProblemEditLayout } from "@/features/admin/ui/layouts/problem-edit-layout"; + +interface LayoutProps { + children: React.ReactNode; + params: Promise<{ problemId: string }>; +} + +const Layout = async ({ children, params }: LayoutProps) => { + const { problemId } = await params; + + if (!problemId) { + return notFound(); + } + + return {children}; +}; + +export default Layout; diff --git a/src/app/(protected)/admin/problems/[problemId]/edit/page.tsx b/src/app/(protected)/admin/problems/[problemId]/edit/page.tsx new file mode 100644 index 0000000..8f8302f --- /dev/null +++ b/src/app/(protected)/admin/problems/[problemId]/edit/page.tsx @@ -0,0 +1,13 @@ +import { ProblemEditView } from "@/features/admin/ui/views/problem-edit-view"; + +interface PageProps { + params: Promise<{ problemId: string }>; +} + +const Page = async ({ params }: PageProps) => { + const { problemId } = await params; + + return ; +}; + +export default Page; diff --git a/src/app/(protected)/layout.tsx b/src/app/(protected)/layout.tsx new file mode 100644 index 0000000..8a017f9 --- /dev/null +++ b/src/app/(protected)/layout.tsx @@ -0,0 +1,11 @@ +import { AdminProtectedLayout } from "@/features/admin/ui/layouts/admin-protected-layout"; + +interface LayoutProps { + children: React.ReactNode; +} + +const Layout = ({ children }: LayoutProps) => { + return {children}; +}; + +export default Layout; diff --git a/src/components/content/pre-detail.tsx b/src/components/content/pre-detail.tsx index 2293ce7..c535fa8 100644 --- a/src/components/content/pre-detail.tsx +++ b/src/components/content/pre-detail.tsx @@ -1,13 +1,13 @@ "use client"; import { cn } from "@/lib/utils"; +import { Actions } from "flexlayout-react"; import { Button } from "@/components/ui/button"; import { ReactNode, useRef, useState } from "react"; import { CheckIcon, CopyIcon, RepeatIcon } from "lucide-react"; -import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { useProblemEditorStore } from "@/stores/problem-editor"; -import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout"; -import { Actions } from "flexlayout-react"; +import { useProblemFlexLayoutStore } from "@/stores/flexlayout"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; interface PreDetailProps { children?: ReactNode; diff --git a/src/features/admin/ui/components/problem-edit-flexlayout.tsx b/src/features/admin/ui/components/problem-edit-flexlayout.tsx new file mode 100644 index 0000000..90a45d9 --- /dev/null +++ b/src/features/admin/ui/components/problem-edit-flexlayout.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { useProblemEditFlexLayoutStore } from "@/stores/flexlayout"; +import { FlexLayout } from "@/features/problems/components/flexlayout"; + +interface ProblemEditFlexLayoutProps { + components: Record; +} + +export const ProblemEditFlexLayout = ({ + components, +}: ProblemEditFlexLayoutProps) => { + const { hasHydrated, model, jsonModel, setModel, setJsonModel } = + useProblemEditFlexLayoutStore(); + + return ( + + ); +}; diff --git a/src/features/admin/ui/layouts/admin-protected-layout.tsx b/src/features/admin/ui/layouts/admin-protected-layout.tsx new file mode 100644 index 0000000..5af0b59 --- /dev/null +++ b/src/features/admin/ui/layouts/admin-protected-layout.tsx @@ -0,0 +1,30 @@ +import prisma from "@/lib/prisma"; +import { auth, signIn } from "@/lib/auth"; +import { redirect } from "next/navigation"; + +interface AdminProtectedLayoutProps { + children: React.ReactNode; +} + +export const AdminProtectedLayout = async ({ + children, +}: AdminProtectedLayoutProps) => { + const session = await auth(); + const userId = session?.user?.id; + if (!userId) { + await signIn(); + } + + const user = await prisma.user.findUnique({ + select: { + role: true, + }, + where: { + id: userId, + }, + }); + + if (user?.role !== "ADMIN") redirect("/unauthorized"); + + return <>{children}; +}; diff --git a/src/features/admin/ui/layouts/problem-edit-layout.tsx b/src/features/admin/ui/layouts/problem-edit-layout.tsx new file mode 100644 index 0000000..a1857cb --- /dev/null +++ b/src/features/admin/ui/layouts/problem-edit-layout.tsx @@ -0,0 +1,29 @@ +import { Suspense } from "react"; +import { BackButton } from "@/components/back-button"; +import { UserAvatar, UserAvatarSkeleton } from "@/components/user-avatar"; + +interface ProblemEditLayoutProps { + children: React.ReactNode; +} + +export const ProblemEditLayout = ({ children }: ProblemEditLayoutProps) => { + return ( +
+
+
+
+ +
+
+ }> + + +
+
+
+
+ {children} +
+
+ ); +}; diff --git a/src/features/admin/ui/views/problem-edit-view.tsx b/src/features/admin/ui/views/problem-edit-view.tsx new file mode 100644 index 0000000..add476f --- /dev/null +++ b/src/features/admin/ui/views/problem-edit-view.tsx @@ -0,0 +1,26 @@ +import EditCodePanel from "@/components/creater/edit-code-panel"; +import EditDetailPanel from "@/components/creater/edit-detail-panel"; +import EditSolutionPanel from "@/components/creater/edit-solution-panel"; +import EditTestcasePanel from "@/components/creater/edit-testcase-panel"; +import EditDescriptionPanel from "@/components/creater/edit-description-panel"; +import { ProblemEditFlexLayout } from "@/features/admin/ui/components/problem-edit-flexlayout"; + +interface ProblemEditViewProps { + problemId: string; +} + +export const ProblemEditView = ({ problemId }: ProblemEditViewProps) => { + const components: Record = { + description: , + solution: , + detail: , + code: , + testcase: , + }; + + return ( +
+ +
+ ); +}; diff --git a/src/features/problems/bot/components/view-bot-button.tsx b/src/features/problems/bot/components/view-bot-button.tsx index e2abb06..e4ae598 100644 --- a/src/features/problems/bot/components/view-bot-button.tsx +++ b/src/features/problems/bot/components/view-bot-button.tsx @@ -11,7 +11,7 @@ import { useEffect, useState } from "react"; import { useTranslations } from "next-intl"; import { Toggle } from "@/components/ui/toggle"; import { Actions, DockLocation } from "flexlayout-react"; -import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout"; +import { useProblemFlexLayoutStore } from "@/stores/flexlayout"; export const ViewBotButton = () => { const t = useTranslations(); diff --git a/src/features/problems/components/flexlayout.tsx b/src/features/problems/components/flexlayout.tsx new file mode 100644 index 0000000..2996a6a --- /dev/null +++ b/src/features/problems/components/flexlayout.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { + BotIcon, + CircleCheckBigIcon, + FileTextIcon, + FlaskConicalIcon, + SquareCheckIcon, + SquarePenIcon, +} from "lucide-react"; +import { + IJsonModel, + ITabRenderValues, + Layout, + Model, + TabNode, +} from "flexlayout-react"; +import "@/styles/flexlayout.css"; +import { useTranslations } from "next-intl"; +import { useCallback, useEffect } from "react"; +import { Skeleton } from "@/components/ui/skeleton"; + +interface FlexLayoutProps { + components: Record; + hasHydrated: boolean; + model: Model | null; + jsonModel: IJsonModel; + setModel: (model: Model) => void; + setJsonModel: (jsonModel: IJsonModel) => void; +} + +export const FlexLayout = ({ + components, + hasHydrated, + model, + jsonModel, + setModel, + setJsonModel, +}: FlexLayoutProps) => { + const t = useTranslations("ProblemPage"); + + useEffect(() => { + if (hasHydrated && !model) { + const model = Model.fromJson(jsonModel); + setModel(model); + } + }, [hasHydrated, jsonModel, model, setModel]); + + const onModelChange = useCallback( + (model: Model) => { + const jsonModel = model.toJson(); + setJsonModel(jsonModel); + }, + [setJsonModel] + ); + + const factory = useCallback( + (node: TabNode) => { + const component = node.getComponent(); + return component ? components[component] : null; + }, + [components] + ); + + const onRenderTab = useCallback( + (node: TabNode, renderValues: ITabRenderValues) => { + const Icon = getIconForTab(node.getId()); + renderValues.leading = Icon ? ( +