From af3f899fac915e089c74b510d839aa6561492cd5 Mon Sep 17 00:00:00 2001 From: AQIB-NAWAB Date: Sun, 8 Jun 2025 11:23:23 +0500 Subject: [PATCH 1/3] FEAT: Added tabs to easily navigate between description and solution --- app/components/CodeEditor/CodeEditor.tsx | 16 ++--- .../ContentViewer/ContentViewer.module.css | 1 + .../EditorNOutput/EditorNOutput.tsx | 8 ++- app/components/MyBtn/MyBtn.tsx | 3 + .../SolutionTab/SolutionTab.module.css | 10 +++ app/components/SolutionTab/SolutionTab.tsx | 66 +++++++++++++++++++ app/components/SolutionTab/index.tsx | 1 + app/components/TabHeader/TabHeader.module.css | 41 ++++++++++++ app/components/TabHeader/TabHeader.tsx | 49 ++++++++++++++ app/components/TabHeader/index.tsx | 1 + app/components/Tabs/Tabs.module.css | 4 ++ app/components/Tabs/Tabs.tsx | 31 +++++++++ app/components/Tabs/index.tsx | 1 + app/content/[...markdownPath]/page.module.css | 6 ++ app/content/[...markdownPath]/page.tsx | 13 ++-- lib/stores.ts | 8 +++ lib/types.ts | 3 + 17 files changed, 246 insertions(+), 16 deletions(-) create mode 100644 app/components/SolutionTab/SolutionTab.module.css create mode 100644 app/components/SolutionTab/SolutionTab.tsx create mode 100644 app/components/SolutionTab/index.tsx create mode 100644 app/components/TabHeader/TabHeader.module.css create mode 100644 app/components/TabHeader/TabHeader.tsx create mode 100644 app/components/TabHeader/index.tsx create mode 100644 app/components/Tabs/Tabs.module.css create mode 100644 app/components/Tabs/Tabs.tsx create mode 100644 app/components/Tabs/index.tsx diff --git a/app/components/CodeEditor/CodeEditor.tsx b/app/components/CodeEditor/CodeEditor.tsx index 32870c3..6b26c1e 100644 --- a/app/components/CodeEditor/CodeEditor.tsx +++ b/app/components/CodeEditor/CodeEditor.tsx @@ -170,14 +170,14 @@ export default function CodeEditor({ chapterIndex: number; outputResult: OutputResult; }) { - const { colorMode } = useColorMode(); - const [monaco, setMonaco] = useState(null); - const [isValidating, setIsValidating] = useState(false); - const editorStore = useEditorStore(); - const editorRef = useRef(null); - - // Apply custom hooks - useEditorTheme(monaco, colorMode); + const { colorMode } = useColorMode(); + const [monaco, setMonaco] = useState(null); + const [isValidating, setIsValidating] = useState(false); + const editorStore = useEditorStore(); + const editorRef = useRef(null); + + // Apply custom hooks + useEditorTheme(monaco, colorMode); const handleValidate = () => { setIsValidating(true); diff --git a/app/components/ContentViewer/ContentViewer.module.css b/app/components/ContentViewer/ContentViewer.module.css index 8e98663..02af120 100644 --- a/app/components/ContentViewer/ContentViewer.module.css +++ b/app/components/ContentViewer/ContentViewer.module.css @@ -7,6 +7,7 @@ padding-right: 14px; padding-bottom: 12px; padding-left: 14px; + margin-top: 50px; } .contentWrapper { border-right: 1px solid hsl(var(--border-color)); diff --git a/app/components/EditorNOutput/EditorNOutput.tsx b/app/components/EditorNOutput/EditorNOutput.tsx index 05957f7..50c8054 100644 --- a/app/components/EditorNOutput/EditorNOutput.tsx +++ b/app/components/EditorNOutput/EditorNOutput.tsx @@ -7,6 +7,7 @@ import { Box } from "@chakra-ui/react"; import Output from "../Output"; import { CodeFile } from "@/lib/types"; import { outputReducer } from "@/lib/reducers"; +import { useUserSolutionStore } from "@/lib/stores"; export default function EditorNOutput({ codeFile, @@ -19,9 +20,7 @@ export default function EditorNOutput({ stepIndex: number; chapterIndex: number; }) { - const [codeString, setCodeString] = useState( - JSON.stringify(codeFile.code, null, 2), - ); + const { setCodeString, codeString } = useUserSolutionStore(); const showSolution = () => { setCodeString(JSON.stringify(codeFile.solution, null, 2)); @@ -64,6 +63,9 @@ export default function EditorNOutput({ }; useEffect(() => { + // Load the initial code + setCodeString(JSON.stringify(codeFile.code, null, 2)); + const topHeight = localStorage.getItem("verticalTopHeight"); if (topHeight) { setTopWidth(Number(topHeight)); diff --git a/app/components/MyBtn/MyBtn.tsx b/app/components/MyBtn/MyBtn.tsx index f8d135c..daab3ec 100644 --- a/app/components/MyBtn/MyBtn.tsx +++ b/app/components/MyBtn/MyBtn.tsx @@ -8,6 +8,7 @@ export default function MyBtn({ isDisabled, tooltip, size = "xs", + position = "right", }: { children: React.ReactNode; variant: "success" | "error" | "default"; @@ -15,6 +16,7 @@ export default function MyBtn({ isDisabled?: boolean; tooltip?: string; size?: "xs" | "sm" | "md" | "lg"; + position?: "left" | "right"; }) { return ( @@ -31,6 +33,7 @@ export default function MyBtn({ textTransform={"uppercase"} isDisabled={isDisabled} fontWeight={"bold"} + style={{float: position}} > {children} diff --git a/app/components/SolutionTab/SolutionTab.module.css b/app/components/SolutionTab/SolutionTab.module.css new file mode 100644 index 0000000..bedcee7 --- /dev/null +++ b/app/components/SolutionTab/SolutionTab.module.css @@ -0,0 +1,10 @@ +.container { + height: 70vh; + width: 100%; + border: 1px solid #ccc; + margin-bottom: 16px; + padding: 16px; + box-sizing: border-box; + +} + diff --git a/app/components/SolutionTab/SolutionTab.tsx b/app/components/SolutionTab/SolutionTab.tsx new file mode 100644 index 0000000..bff945c --- /dev/null +++ b/app/components/SolutionTab/SolutionTab.tsx @@ -0,0 +1,66 @@ +"use client"; + +import Editor, { Monaco } from "@monaco-editor/react"; +import { useColorMode } from "@chakra-ui/react"; +import { useEffect, useState } from "react"; +import { useUserSolutionStore } from "@/lib/stores"; +import styles from "./SolutionTab.module.css"; +import MyBtn from "../MyBtn/MyBtn"; + +type SolutionTabProps = { + solution: string; +}; + +const useEditorTheme = (monaco: Monaco, colorMode: "dark" | "light") => { + useEffect(() => { + if (monaco) { + monaco.editor.defineTheme("my-theme", { + base: "vs-dark", + inherit: true, + rules: [], + colors: { + "editor.background": "#1f1f1f", + }, + }); + monaco.editor.setTheme(colorMode === "light" ? "light" : "my-theme"); + } + }, [monaco, colorMode]); +}; + +const SolutionTab = ({ solution }: SolutionTabProps) => { + const { colorMode } = useColorMode(); + const [monaco, setMonaco] = useState(null); + + const { setCodeString } = useUserSolutionStore(); + + function useSolution() { + setCodeString(solution); + } + + // Apply custom hooks + useEditorTheme(monaco, colorMode); + return ( +
+ { + setMonaco(monacoInstance); + editor.updateOptions({ readOnly: true }); + }} + /> + + Use this solution + +
+ ); +}; + +export default SolutionTab; diff --git a/app/components/SolutionTab/index.tsx b/app/components/SolutionTab/index.tsx new file mode 100644 index 0000000..ccfc666 --- /dev/null +++ b/app/components/SolutionTab/index.tsx @@ -0,0 +1 @@ +export { default as default } from "./SolutionTab"; diff --git a/app/components/TabHeader/TabHeader.module.css b/app/components/TabHeader/TabHeader.module.css new file mode 100644 index 0000000..486d046 --- /dev/null +++ b/app/components/TabHeader/TabHeader.module.css @@ -0,0 +1,41 @@ +.wrapper { + width: 100%; + padding: 12px 16px; + position: fixed; + top: 40px; + margin: 12px 0; +} + +.tabContainer { + display: flex; + gap: 1.5rem; + font-size: 0.875rem; + font-weight: 500; + color: #4b5563; + position: relative; +} + +.tabButton { + position: relative; + padding-bottom: 8px; + color: #6b7280; + background: none; + font-size: 16px; + border: none; + cursor: pointer; + font-family: Verdana, Geneva, Tahoma, sans-serif; + color: hsl(var(--text)); +} + +.activeTab { + color: hsl(var(--text)); +} + +.underline { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background-color:hsl(var(--text)); +} diff --git a/app/components/TabHeader/TabHeader.tsx b/app/components/TabHeader/TabHeader.tsx new file mode 100644 index 0000000..5dd5817 --- /dev/null +++ b/app/components/TabHeader/TabHeader.tsx @@ -0,0 +1,49 @@ +"use client"; +import { useUserSolutionStore } from "@/lib/stores"; +import styles from "./TabHeader.module.css"; + +type TabHeaderProps = { + currentSelectedTab: "description" | "solution"; + setCurrentSelectedTab: (tab: "description" | "solution") => void; +}; + +const TabHeader = ({ + currentSelectedTab, + setCurrentSelectedTab, +}: TabHeaderProps & { + currentSelectedTab: "description" | "solution"; + setCurrentSelectedTab: (tab: "description" | "solution") => void; +}) => { + + return ( +
+
+ + + +
+
+ ); +}; + +export default TabHeader; diff --git a/app/components/TabHeader/index.tsx b/app/components/TabHeader/index.tsx new file mode 100644 index 0000000..9dc32fb --- /dev/null +++ b/app/components/TabHeader/index.tsx @@ -0,0 +1 @@ +export { default as default } from "./TabHeader"; diff --git a/app/components/Tabs/Tabs.module.css b/app/components/Tabs/Tabs.module.css new file mode 100644 index 0000000..7303f2a --- /dev/null +++ b/app/components/Tabs/Tabs.module.css @@ -0,0 +1,4 @@ +.container { + height: 100%; + width: 100%; +} diff --git a/app/components/Tabs/Tabs.tsx b/app/components/Tabs/Tabs.tsx new file mode 100644 index 0000000..e8769b4 --- /dev/null +++ b/app/components/Tabs/Tabs.tsx @@ -0,0 +1,31 @@ +"use client"; +import ContentViewer from "../ContentViewer/ContentViewer"; +import { ReactElement, useState } from "react"; +import SolutionTab from "../SolutionTab/SolutionTab"; +import TabHeader from "../TabHeader/TabHeader"; +import { TabType } from "@/lib/types"; +import styles from "./Tabs.module.css"; + +type ContentTabProps = { + Page?: ReactElement; + solution: string +}; + +function Tabs({ Page,solution}: ContentTabProps) { + const [currentSelectedTab,setCurrentSelectedTab] = useState("description"); + return ( +
+ + + + { + currentSelectedTab=="description"? + Page: + + } + +
+ ); +} + +export default Tabs; diff --git a/app/components/Tabs/index.tsx b/app/components/Tabs/index.tsx new file mode 100644 index 0000000..24e48f0 --- /dev/null +++ b/app/components/Tabs/index.tsx @@ -0,0 +1 @@ +export { default as default } from "./Tabs"; diff --git a/app/content/[...markdownPath]/page.module.css b/app/content/[...markdownPath]/page.module.css index 7669d38..94f2325 100644 --- a/app/content/[...markdownPath]/page.module.css +++ b/app/content/[...markdownPath]/page.module.css @@ -5,3 +5,9 @@ height: 100%; overflow-y: auto; } +.tabContainer { + display: flex; + flex-direction: column; + width: 50%; + position: relative; +} diff --git a/app/content/[...markdownPath]/page.tsx b/app/content/[...markdownPath]/page.tsx index c2b94a4..13f6140 100644 --- a/app/content/[...markdownPath]/page.tsx +++ b/app/content/[...markdownPath]/page.tsx @@ -2,8 +2,9 @@ import { contentManager } from "@/lib/contentManager"; import styles from "./page.module.css"; import React from "react"; import { parseLessonFolder } from "@/lib/server-functions"; -import ContentViewer from "@/app/components/ContentViewer"; import EditorNOutput from "@/app/components/EditorNOutput"; +import TabHeader from "@/app/components/TabHeader/TabHeader"; +import Tabs from "@/app/components/Tabs/Tabs"; export function generateMetadata({ params, @@ -37,12 +38,14 @@ export default async function Content({ const { mdPath, nextStepPath, stepIndex, codePath, chapterIndex } = contentManager.getPageMeta(urlPath); const { Page, metadata, codeFile } = parseLessonFolder(mdPath, codePath); - return (
- - - +
+ } + solution={JSON.stringify(codeFile.solution, null, 2)} + /> +
string | null; clearAllCode: () => void; + codeString: string; + setCodeString: (code: string) => void; }; export const useUserSolutionStore = create()((set, get) => ({ @@ -66,4 +69,9 @@ export const useUserSolutionStore = create()((set, get) => ({ localStorage.removeItem("codeData"); set({ userSolutionsByLesson: {} }); }, + + codeString: "", + setCodeString: (code: string) => { + set({ codeString: code }); + }, })); diff --git a/lib/types.ts b/lib/types.ts index 3d3b031..e9e84ee 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,5 +1,7 @@ import { InvalidSchemaError } from "@hyperjump/json-schema/draft-2020-12"; +export type TabType = "description" | "solution"; + export type ChapterStep = { title: string; fileName: string; @@ -59,4 +61,5 @@ export type OutputResult = { testCaseResults?: TestCaseResult[]; totalTestCases?: number; errors?: InvalidSchemaError | string; + selectedTab?: TabType; }; From ed66257f80da01e6c2e7d5fe75b251933bffb4d1 Mon Sep 17 00:00:00 2001 From: AQIB-NAWAB Date: Sun, 8 Jun 2025 11:28:42 +0500 Subject: [PATCH 2/3] necessary tweaks --- app/components/SolutionTab/SolutionTab.module.css | 2 +- app/components/TabHeader/TabHeader.module.css | 3 +-- app/components/TabHeader/TabHeader.tsx | 1 - app/content/[...markdownPath]/page.tsx | 1 - 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/app/components/SolutionTab/SolutionTab.module.css b/app/components/SolutionTab/SolutionTab.module.css index bedcee7..2c4cd5a 100644 --- a/app/components/SolutionTab/SolutionTab.module.css +++ b/app/components/SolutionTab/SolutionTab.module.css @@ -3,7 +3,7 @@ width: 100%; border: 1px solid #ccc; margin-bottom: 16px; - padding: 16px; + padding: 16px 0px; box-sizing: border-box; } diff --git a/app/components/TabHeader/TabHeader.module.css b/app/components/TabHeader/TabHeader.module.css index 486d046..42aa36b 100644 --- a/app/components/TabHeader/TabHeader.module.css +++ b/app/components/TabHeader/TabHeader.module.css @@ -11,14 +11,13 @@ gap: 1.5rem; font-size: 0.875rem; font-weight: 500; - color: #4b5563; position: relative; + color: hsl(var(--text)); } .tabButton { position: relative; padding-bottom: 8px; - color: #6b7280; background: none; font-size: 16px; border: none; diff --git a/app/components/TabHeader/TabHeader.tsx b/app/components/TabHeader/TabHeader.tsx index 5dd5817..3bea330 100644 --- a/app/components/TabHeader/TabHeader.tsx +++ b/app/components/TabHeader/TabHeader.tsx @@ -1,5 +1,4 @@ "use client"; -import { useUserSolutionStore } from "@/lib/stores"; import styles from "./TabHeader.module.css"; type TabHeaderProps = { diff --git a/app/content/[...markdownPath]/page.tsx b/app/content/[...markdownPath]/page.tsx index 13f6140..7626e65 100644 --- a/app/content/[...markdownPath]/page.tsx +++ b/app/content/[...markdownPath]/page.tsx @@ -3,7 +3,6 @@ import styles from "./page.module.css"; import React from "react"; import { parseLessonFolder } from "@/lib/server-functions"; import EditorNOutput from "@/app/components/EditorNOutput"; -import TabHeader from "@/app/components/TabHeader/TabHeader"; import Tabs from "@/app/components/Tabs/Tabs"; export function generateMetadata({ From af27a131565dd4161ec273a69602e1c650aab6da Mon Sep 17 00:00:00 2001 From: AQIB-NAWAB Date: Sun, 8 Jun 2025 11:32:24 +0500 Subject: [PATCH 3/3] add spacing for btn and codeEditor --- app/components/SolutionTab/SolutionTab.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/SolutionTab/SolutionTab.module.css b/app/components/SolutionTab/SolutionTab.module.css index 2c4cd5a..9948716 100644 --- a/app/components/SolutionTab/SolutionTab.module.css +++ b/app/components/SolutionTab/SolutionTab.module.css @@ -3,7 +3,7 @@ width: 100%; border: 1px solid #ccc; margin-bottom: 16px; - padding: 16px 0px; + padding: 16px 10px; box-sizing: border-box; }