Skip to content

FEAT: Added tabs to easily navigate between description and solution #177

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
16 changes: 8 additions & 8 deletions app/components/CodeEditor/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,14 @@ export default function CodeEditor({
chapterIndex: number;
outputResult: OutputResult;
}) {
const { colorMode } = useColorMode();
const [monaco, setMonaco] = useState<any>(null);
const [isValidating, setIsValidating] = useState(false);
const editorStore = useEditorStore();
const editorRef = useRef<any>(null);

// Apply custom hooks
useEditorTheme(monaco, colorMode);
const { colorMode } = useColorMode();
const [monaco, setMonaco] = useState<any>(null);
const [isValidating, setIsValidating] = useState(false);
const editorStore = useEditorStore();
const editorRef = useRef<any>(null);

// Apply custom hooks
useEditorTheme(monaco, colorMode);
Comment on lines +173 to +180
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove these formatting chagnes


const handleValidate = () => {
setIsValidating(true);
Expand Down
1 change: 1 addition & 0 deletions app/components/ContentViewer/ContentViewer.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
8 changes: 5 additions & 3 deletions app/components/EditorNOutput/EditorNOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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));
Expand Down Expand Up @@ -64,6 +63,9 @@ export default function EditorNOutput({
};

useEffect(() => {
// Load the initial code
setCodeString(JSON.stringify(codeFile.code, null, 2));
Copy link
Preview

Copilot AI Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Include codeFile in the useEffect dependency array to ensure the editor state resets if the content changes.

Copilot uses AI. Check for mistakes.


const topHeight = localStorage.getItem("verticalTopHeight");
if (topHeight) {
setTopWidth(Number(topHeight));
Expand Down
3 changes: 3 additions & 0 deletions app/components/MyBtn/MyBtn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ export default function MyBtn({
isDisabled,
tooltip,
size = "xs",
position = "right",
}: {
children: React.ReactNode;
variant: "success" | "error" | "default";
onClick: () => void;
isDisabled?: boolean;
tooltip?: string;
size?: "xs" | "sm" | "md" | "lg";
position?: "left" | "right";
}) {
return (
<Tooltip label={tooltip} aria-label={tooltip} placement="top">
Expand All @@ -31,6 +33,7 @@ export default function MyBtn({
textTransform={"uppercase"}
isDisabled={isDisabled}
fontWeight={"bold"}
style={{float: position}}
Copy link
Preview

Copilot AI Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using inline float styling can lead to layout issues; consider leveraging Chakra UI props (e.g., alignSelf) or a container layout instead.

Copilot uses AI. Check for mistakes.

>
{children}
</Button>
Expand Down
10 changes: 10 additions & 0 deletions app/components/SolutionTab/SolutionTab.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.container {
height: 70vh;
width: 100%;
border: 1px solid #ccc;
margin-bottom: 16px;
padding: 16px 10px;
box-sizing: border-box;

}

66 changes: 66 additions & 0 deletions app/components/SolutionTab/SolutionTab.tsx
Original file line number Diff line number Diff line change
@@ -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<any>(null);

const { setCodeString } = useUserSolutionStore();

function useSolution() {
setCodeString(solution);
}

// Apply custom hooks
useEditorTheme(monaco, colorMode);
return (
<div className={styles.container}>
<Editor
language="json"
theme={colorMode === "light" ? "light" : "my-theme"}
value={String(solution)}
height="90%"
options={{
minimap: { enabled: false },
fontSize: 14,
}}
// dont allow to edit the solution
onMount={(editor, monacoInstance) => {
setMonaco(monacoInstance);
editor.updateOptions({ readOnly: true });
}}
/>
<MyBtn onClick={useSolution} variant={"default"} size="sm" position="right">
Use this solution
</MyBtn>
</div>
);
};

export default SolutionTab;
1 change: 1 addition & 0 deletions app/components/SolutionTab/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as default } from "./SolutionTab";
40 changes: 40 additions & 0 deletions app/components/TabHeader/TabHeader.module.css
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just use the chakraUI tabs instead (of course with customized styles)

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.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;
position: relative;
color: hsl(var(--text));
}

.tabButton {
position: relative;
padding-bottom: 8px;
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));
}
48 changes: 48 additions & 0 deletions app/components/TabHeader/TabHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use client";
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;
}) => {
Comment on lines +12 to +15
Copy link
Preview

Copilot AI Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline intersection with TabHeaderProps is redundant; use TabHeaderProps alone for clearer, DRY type annotations.

Suggested change
}: TabHeaderProps & {
currentSelectedTab: "description" | "solution";
setCurrentSelectedTab: (tab: "description" | "solution") => void;
}) => {
}: TabHeaderProps) => {

Copilot uses AI. Check for mistakes.


return (
<div className={styles.wrapper}>
<div className={styles.tabContainer}>
<button
onClick={() => setCurrentSelectedTab("description")}
className={`${styles.tabButton} ${
currentSelectedTab === "description" ? styles.activeTab : ""
}`}
Comment on lines +19 to +24
Copy link
Preview

Copilot AI Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding ARIA roles (e.g., role="tablist" on the container and role="tab" with aria-selected on buttons) to improve keyboard navigation and screen reader support.

Suggested change
<div className={styles.tabContainer}>
<button
onClick={() => setCurrentSelectedTab("description")}
className={`${styles.tabButton} ${
currentSelectedTab === "description" ? styles.activeTab : ""
}`}
<div className={styles.tabContainer} role="tablist">
<button
onClick={() => setCurrentSelectedTab("description")}
className={`${styles.tabButton} ${
currentSelectedTab === "description" ? styles.activeTab : ""
}`}
role="tab"
aria-selected={currentSelectedTab === "description"}

Copilot uses AI. Check for mistakes.

>
Description
{currentSelectedTab === "description" && (
<div className={styles.underline} />
)}
</button>

<button
onClick={() => setCurrentSelectedTab("solution")}
className={`${styles.tabButton} ${
currentSelectedTab === "solution" ? styles.activeTab : ""
}`}
>
Solution
{currentSelectedTab === "solution" && (
<div className={styles.underline} />
)}
</button>
</div>
</div>
);
};

export default TabHeader;
1 change: 1 addition & 0 deletions app/components/TabHeader/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as default } from "./TabHeader";
4 changes: 4 additions & 0 deletions app/components/Tabs/Tabs.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.container {
height: 100%;
width: 100%;
}
31 changes: 31 additions & 0 deletions app/components/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -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<TabType>("description");
return (
<div className={styles.container}>
<TabHeader currentSelectedTab={currentSelectedTab} setCurrentSelectedTab={setCurrentSelectedTab} />

<ContentViewer>
{
currentSelectedTab=="description"?
Copy link
Preview

Copilot AI Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use strict equality (===) instead of loose equality (==) for string comparisons to avoid unintended type coercion.

Suggested change
currentSelectedTab=="description"?
currentSelectedTab==="description"?

Copilot uses AI. Check for mistakes.

Page:
<SolutionTab solution={solution}/>
}
</ContentViewer>
</div>
);
}

export default Tabs;
1 change: 1 addition & 0 deletions app/components/Tabs/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as default } from "./Tabs";
Copy link
Preview

Copilot AI Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Simplify the re-export to export { default } from "./Tabs"; for clearer syntax.

Suggested change
export { default as default } from "./Tabs";
export { default } from "./Tabs";

Copilot uses AI. Check for mistakes.

6 changes: 6 additions & 0 deletions app/content/[...markdownPath]/page.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@
height: 100%;
overflow-y: auto;
}
.tabContainer {
display: flex;
flex-direction: column;
width: 50%;
position: relative;
}
12 changes: 7 additions & 5 deletions app/content/[...markdownPath]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ 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 Tabs from "@/app/components/Tabs/Tabs";

export function generateMetadata({
params,
Expand Down Expand Up @@ -37,12 +37,14 @@ export default async function Content({
const { mdPath, nextStepPath, stepIndex, codePath, chapterIndex } =
contentManager.getPageMeta(urlPath);
const { Page, metadata, codeFile } = parseLessonFolder(mdPath, codePath);

return (
<div className={styles.mainArea}>
<ContentViewer>
<Page />
</ContentViewer>
<div className={styles.tabContainer}>
<Tabs
Page={<Page />}
solution={JSON.stringify(codeFile.solution, null, 2)}
/>
</div>
<EditorNOutput
codeFile={codeFile}
nextStepPath={nextStepPath}
Expand Down
8 changes: 8 additions & 0 deletions lib/stores.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { create } from "zustand";
import { TabType } from "./types";

type Store = {
editor: any;
Expand Down Expand Up @@ -30,6 +31,8 @@ type UserSolutionStore = {
lesson: number,
) => string | null;
clearAllCode: () => void;
codeString: string;
setCodeString: (code: string) => void;
};

export const useUserSolutionStore = create<UserSolutionStore>()((set, get) => ({
Expand Down Expand Up @@ -66,4 +69,9 @@ export const useUserSolutionStore = create<UserSolutionStore>()((set, get) => ({
localStorage.removeItem("codeData");
set({ userSolutionsByLesson: {} });
},

codeString: "",
setCodeString: (code: string) => {
set({ codeString: code });
},
}));
3 changes: 3 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -59,4 +61,5 @@ export type OutputResult = {
testCaseResults?: TestCaseResult[];
totalTestCases?: number;
errors?: InvalidSchemaError | string;
selectedTab?: TabType;
};