fix: refactor layout

feat/course
Arik Chakma 1 month ago
parent 088d4e2e2d
commit 0abddab414
  1. 1
      package.json
  2. 18
      pnpm-lock.yaml
  3. 61
      src/components/Course/ChallengeView.tsx
  4. 41
      src/components/Course/LessonView.tsx
  5. 158
      src/components/Course/QuizView.tsx
  6. 9
      src/components/SqlCodeEditor/sql-code-editor-theme.ts
  7. 12
      src/data/courses/sql/chapters/introduction/lessons/challenge-1.md
  8. 82
      src/pages/learn/[courseId]/[chapterId]/[lessonId].astro
  9. 11
      src/stores/query-client.ts

@ -43,6 +43,7 @@
"@nanostores/react": "^0.7.2", "@nanostores/react": "^0.7.2",
"@napi-rs/image": "^1.9.2", "@napi-rs/image": "^1.9.2",
"@resvg/resvg-js": "^2.6.2", "@resvg/resvg-js": "^2.6.2",
"@tanstack/react-query": "^5.59.15",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"astro": "^4.15.4", "astro": "^4.15.4",

@ -50,6 +50,9 @@ importers:
'@resvg/resvg-js': '@resvg/resvg-js':
specifier: ^2.6.2 specifier: ^2.6.2
version: 2.6.2 version: 2.6.2
'@tanstack/react-query':
specifier: ^5.59.15
version: 5.59.15(react@18.3.1)
'@types/react': '@types/react':
specifier: ^18.3.3 specifier: ^18.3.3
version: 18.3.8 version: 18.3.8
@ -1194,6 +1197,14 @@ packages:
peerDependencies: peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20' tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20'
'@tanstack/query-core@5.59.13':
resolution: {integrity: sha512-Oou0bBu/P8+oYjXsJQ11j+gcpLAMpqW42UlokQYEz4dE7+hOtVO9rVuolJKgEccqzvyFzqX4/zZWY+R/v1wVsQ==}
'@tanstack/react-query@5.59.15':
resolution: {integrity: sha512-QbVlAkTI78wB4Mqgf2RDmgC0AOiJqer2c5k9STOOSXGv1S6ZkY37r/6UpE8DbQ2Du0ohsdoXgFNEyv+4eDoPEw==}
peerDependencies:
react: ^18 || ^19
'@tybys/wasm-util@0.9.0': '@tybys/wasm-util@0.9.0':
resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==}
@ -4288,6 +4299,13 @@ snapshots:
postcss-selector-parser: 6.0.10 postcss-selector-parser: 6.0.10
tailwindcss: 3.4.13 tailwindcss: 3.4.13
'@tanstack/query-core@5.59.13': {}
'@tanstack/react-query@5.59.15(react@18.3.1)':
dependencies:
'@tanstack/query-core': 5.59.13
react: 18.3.1
'@tybys/wasm-util@0.9.0': '@tybys/wasm-util@0.9.0':
dependencies: dependencies:
tslib: 2.7.0 tslib: 2.7.0

@ -3,65 +3,40 @@ import {
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
} from '../Resizable'; } from '../Resizable';
import { CourseSidebar } from './CourseSidebar';
import { CourseLayout } from './CourseLayout';
import { SqlCodeEditor } from '../SqlCodeEditor/SqlCodeEditor'; import { SqlCodeEditor } from '../SqlCodeEditor/SqlCodeEditor';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import type { import type { LessonFileType } from '../../lib/course';
ChapterFileType,
CourseFileType,
LessonFileType,
} from '../../lib/course';
type ChallengeViewProps = { type ChallengeViewProps = {
courseId: string;
chapterId: string;
lessonId: string;
title: string;
course: CourseFileType & {
chapters: ChapterFileType[];
};
lesson: LessonFileType; lesson: LessonFileType;
children: ReactNode; children: ReactNode;
}; };
export function ChallengeView(props: ChallengeViewProps) { export function ChallengeView(props: ChallengeViewProps) {
const { children, title, course, lesson, courseId, chapterId } = props; const { children, lesson } = props;
const { chapters } = course;
const { frontmatter } = lesson; const { frontmatter } = lesson;
const { defaultValue, initSteps, expectedResults } = frontmatter; const { defaultValue, initSteps, expectedResults } = frontmatter;
return ( return (
<CourseLayout <ResizablePanelGroup direction="horizontal">
courseId={courseId} <ResizablePanel defaultSize={60} minSize={20}>
chapterId={chapterId} <div className="relative h-full">
lessonId={lesson.id} <div className="absolute inset-0 overflow-y-auto [scrollbar-color:#3f3f46_#27272a;]">
lesson={lesson} <div className="mx-auto max-w-xl p-4">{children}</div>
title={title}
chapters={chapters}
completedPercentage={0}
>
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={60} minSize={20}>
<div className="relative h-full">
<div className="absolute inset-0 overflow-y-auto [scrollbar-color:#3f3f46_#27272a;]">
<div className="mx-auto max-w-xl p-4">{children}</div>
</div>
</div> </div>
</ResizablePanel> </div>
</ResizablePanel>
<ResizableHandle withHandle={true} /> <ResizableHandle withHandle={true} />
<ResizablePanel defaultSize={40} minSize={20}> <ResizablePanel defaultSize={40} minSize={20}>
<SqlCodeEditor <SqlCodeEditor
defaultValue={defaultValue} defaultValue={defaultValue}
initSteps={initSteps} initSteps={initSteps}
expectedResults={expectedResults} expectedResults={expectedResults}
/> />
</ResizablePanel> </ResizablePanel>
</ResizablePanelGroup> </ResizablePanelGroup>
</CourseLayout>
); );
} }

@ -1,46 +1,17 @@
import { useState, type ReactNode } from 'react'; import { type ReactNode } from 'react';
import { CourseSidebar } from './CourseSidebar';
import { CourseLayout } from './CourseLayout';
import { Circle, CircleCheck, CircleX } from 'lucide-react';
import { cn } from '../../lib/classname';
import type {
ChapterFileType,
CourseFileType,
LessonFileType,
} from '../../lib/course';
type LessonViewProps = { type LessonViewProps = {
courseId: string;
chapterId: string;
lessonId: string;
title: string;
course: CourseFileType & {
chapters: ChapterFileType[];
};
lesson: LessonFileType;
children: ReactNode; children: ReactNode;
}; };
export function LessonView(props: LessonViewProps) { export function LessonView(props: LessonViewProps) {
const { children, title, course, lesson, courseId, chapterId } = props; const { children } = props;
const { chapters } = course;
return ( return (
<CourseLayout <div className="relative h-full">
courseId={courseId} <div className="absolute inset-0 overflow-y-auto [scrollbar-color:#3f3f46_#27272a;]">
chapterId={chapterId} <div className="mx-auto max-w-xl p-4">{children}</div>
lessonId={lesson.id}
lesson={lesson}
title={title}
chapters={chapters}
completedPercentage={0}
>
<div className="relative h-full">
<div className="absolute inset-0 overflow-y-auto [scrollbar-color:#3f3f46_#27272a;]">
<div className="mx-auto max-w-xl p-4">{children}</div>
</div>
</div> </div>
</CourseLayout> </div>
); );
} }

@ -1,28 +1,14 @@
import { useState } from 'react'; import { useState } from 'react';
import { CourseLayout } from './CourseLayout';
import { Circle, CircleCheck, CircleX } from 'lucide-react'; import { Circle, CircleCheck, CircleX } from 'lucide-react';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';
import type { import type { LessonFileType } from '../../lib/course';
ChapterFileType,
CourseFileType,
LessonFileType,
} from '../../lib/course';
type QuizViewProps = { type QuizViewProps = {
courseId: string;
chapterId: string;
lessonId: string;
title: string;
course: CourseFileType & {
chapters: ChapterFileType[];
};
lesson: LessonFileType; lesson: LessonFileType;
}; };
export function QuizView(props: QuizViewProps) { export function QuizView(props: QuizViewProps) {
const { title, course, lesson, courseId, lessonId, chapterId } = props; const { lesson } = props;
const { chapters } = course;
const { frontmatter } = lesson; const { frontmatter } = lesson;
const { questions = [] } = frontmatter; const { questions = [] } = frontmatter;
@ -45,85 +31,75 @@ export function QuizView(props: QuizViewProps) {
}).length; }).length;
return ( return (
<CourseLayout <div className="relative h-full">
courseId={courseId} <div className="absolute inset-0 overflow-y-auto [scrollbar-color:#3f3f46_#27272a;]">
chapterId={chapterId} <div className="mx-auto max-w-xl p-4 py-10">
lessonId={lesson.id} <h3 className="mb-10 text-lg font-semibold">
lesson={lesson} SQL Quiz: Intermediate
title={title} </h3>
chapters={chapters}
completedPercentage={0} <div className="flex flex-col gap-3">
> {questions.map((question) => {
<div className="relative h-full"> return (
<div className="absolute inset-0 overflow-y-auto [scrollbar-color:#3f3f46_#27272a;]"> <QuizItem
<div className="mx-auto max-w-xl p-4 py-10"> key={question.id}
<h3 className="mb-10 text-lg font-semibold"> id={question.id}
SQL Quiz: Intermediate title={question.title}
</h3> disabled={isSubmitted}
options={question.options.map((option) => {
<div className="flex flex-col gap-3"> const selectedOptionId = selectedOptions?.[question.id];
{questions.map((question) => {
return ( let optionStatus: QuizOptionStatus = 'default';
<QuizItem if (option.isCorrectOption && isSubmitted) {
key={question.id} optionStatus = 'correct';
id={question.id} } else if (selectedOptionId === option.id) {
title={question.title} optionStatus = isSubmitted ? 'wrong' : 'selected';
disabled={isSubmitted} }
options={question.options.map((option) => {
const selectedOptionId = selectedOptions?.[question.id]; return {
...option,
let optionStatus: QuizOptionStatus = 'default'; status: optionStatus,
if (option.isCorrectOption && isSubmitted) { };
optionStatus = 'correct'; })}
} else if (selectedOptionId === option.id) { onOptionSelectChange={(id, optionId) => {
optionStatus = isSubmitted ? 'wrong' : 'selected'; setSelectedOptions((prev) => ({
} ...prev,
[id]: optionId,
return { }));
...option, }}
status: optionStatus, selectedOptionId={selectedOptions?.[question.id]}
}; />
})} );
onOptionSelectChange={(id, optionId) => { })}
setSelectedOptions((prev) => ({ </div>
...prev,
[id]: optionId,
}));
}}
selectedOptionId={selectedOptions?.[question.id]}
/>
);
})}
</div>
<div className="mt-8 flex items-center justify-end">
<button
className="rounded-xl border border-zinc-700 bg-zinc-800 p-2 px-4 text-sm font-medium text-white focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
disabled={isSubmitted || !isAllAnswered}
onClick={() => {
setIsSubmitted(true);
}}
>
Submit my Answers
</button>
</div>
{isSubmitted && ( <div className="mt-8 flex items-center justify-end">
<div className="mt-8 flex items-center justify-between gap-2 rounded-xl border border-zinc-800 p-4"> <button
<span> className="rounded-xl border border-zinc-700 bg-zinc-800 p-2 px-4 text-sm font-medium text-white focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
You got {correctAnswerCount} out of {questions.length}{' '} disabled={isSubmitted || !isAllAnswered}
questions right onClick={() => {
</span> setIsSubmitted(true);
}}
<a className="disabled:cusror-not-allowed rounded-xl border border-zinc-700 bg-zinc-800 p-2 px-4 text-sm font-medium text-white focus:outline-none"> >
Move to Next Lesson Submit my Answers
</a> </button>
</div>
)}
</div> </div>
{isSubmitted && (
<div className="mt-8 flex items-center justify-between gap-2 rounded-xl border border-zinc-800 p-4">
<span>
You got {correctAnswerCount} out of {questions.length} questions
right
</span>
<a className="disabled:cusror-not-allowed rounded-xl border border-zinc-700 bg-zinc-800 p-2 px-4 text-sm font-medium text-white focus:outline-none">
Move to Next Lesson
</a>
</div>
)}
</div> </div>
</div> </div>
</CourseLayout> </div>
); );
} }

@ -14,8 +14,8 @@ export const editorDarkTheme = EditorView.theme(
'.cm-content': {}, '.cm-content': {},
// Line number styles // Line number styles
'.cm-lineNumbers .cm-gutterElement': { '.cm-lineNumbers .cm-gutterElement': {
color: '#757575', // Text color for line numbers color: '#757575',
paddingRight: '1em', minWidth: '24px',
}, },
// Scrollbar styles // Scrollbar styles
'.cm-scroller': { '.cm-scroller': {
@ -25,7 +25,7 @@ export const editorDarkTheme = EditorView.theme(
'.cm-scroller::-webkit-scrollbar': {}, '.cm-scroller::-webkit-scrollbar': {},
// Highlight active line // Highlight active line
'.cm-activeLine': { '.cm-activeLine': {
backgroundColor: '#27272a', // Active line background color backgroundColor: '#27272a',
}, },
// Cursor styles // Cursor styles
'.cm-cursor': { '.cm-cursor': {
@ -56,6 +56,9 @@ export const editorDarkTheme = EditorView.theme(
border: 'none', border: 'none',
backgroundColor: 'transparent', backgroundColor: 'transparent',
}, },
'& .cm-foldGutter .cm-gutterElement': {
paddingLeft: '4px',
},
}, },
{ {
dark: true, dark: true,

@ -4,9 +4,15 @@ description: Write a SQL query to find the total number of orders in the `orders
order: 300 order: 300
type: challenge type: challenge
defaultValue: | defaultValue: |
SELECT * FROM orders; SELECT
*
SELECT COUNT(*) FROM orders; FROM
orders;
SELECT
COUNT(*)
FROM
orders;
initSteps: initSteps:
- CREATE TABLE orders ( - CREATE TABLE orders (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,

@ -2,6 +2,7 @@
import { ChallengeView } from '../../../../components/Course/ChallengeView'; import { ChallengeView } from '../../../../components/Course/ChallengeView';
import { LessonView } from '../../../../components/Course/LessonView'; import { LessonView } from '../../../../components/Course/LessonView';
import { QuizView } from '../../../../components/Course/QuizView'; import { QuizView } from '../../../../components/Course/QuizView';
import { CourseLayout } from '../../../../components/Course/CourseLayout';
import SkeletonLayout from '../../../../layouts/SkeletonLayout.astro'; import SkeletonLayout from '../../../../layouts/SkeletonLayout.astro';
import { import {
getAllCourses, getAllCourses,
@ -70,53 +71,40 @@ const { course, chapter, lesson } = Astro.props;
--- ---
<SkeletonLayout title={course.frontmatter.title}> <SkeletonLayout title={course.frontmatter.title}>
{ <CourseLayout
lesson.frontmatter.type === 'challenge' && ( courseId={courseId}
<ChallengeView chapterId={chapterId}
courseId={courseId} lessonId={lesson.id}
chapterId={chapterId} lesson={lesson}
lessonId={lesson.id} title={course.frontmatter.title}
title={course.frontmatter.title} chapters={course.chapters}
course={course} completedPercentage={0}
lesson={lesson} client:load
client:load >
> {
<div class='course-content prose prose-lg prose-invert mt-8 text-zinc-300 prose-headings:mb-3 prose-headings:mt-8 prose-code:text-zinc-100 prose-li:my-1 prose-thead:border-zinc-800 prose-tr:border-zinc-800'> lesson.frontmatter.type === 'challenge' && (
<lesson.Content /> <ChallengeView lesson={lesson} client:load>
</div> <div class='course-content prose prose-lg prose-invert mt-8 text-zinc-300 prose-headings:mb-3 prose-headings:mt-8 prose-code:text-zinc-100 prose-li:my-1 prose-thead:border-zinc-800 prose-tr:border-zinc-800'>
</ChallengeView> <lesson.Content />
) </div>
} </ChallengeView>
)
}
{ {
lesson.frontmatter.type === 'lesson' && ( lesson.frontmatter.type === 'lesson' && (
<LessonView <LessonView client:load>
courseId={courseId} <div class='course-content prose prose-lg prose-invert mt-8 text-zinc-300 prose-headings:mb-3 prose-headings:mt-8 prose-code:text-zinc-100 prose-li:my-1 prose-thead:border-zinc-800 prose-tr:border-zinc-800'>
chapterId={chapterId} <lesson.Content />
lessonId={lesson.id} </div>
title={course.frontmatter.title} </LessonView>
course={course} )
lesson={lesson} }
client:load
>
<div class='course-content prose prose-lg prose-invert mt-8 text-zinc-300 prose-headings:mb-3 prose-headings:mt-8 prose-code:text-zinc-100 prose-li:my-1 prose-thead:border-zinc-800 prose-tr:border-zinc-800'>
<lesson.Content />
</div>
</LessonView>
)
}
{ {
lesson.frontmatter.type === 'quiz' && ( lesson.frontmatter.type === 'quiz' && (
<QuizView <QuizView lesson={lesson} client:load />
courseId={courseId} )
chapterId={chapterId} }
lessonId={lesson.id} </CourseLayout>
title={course.frontmatter.title}
course={course}
lesson={lesson}
client:load
/>
)
}
</SkeletonLayout> </SkeletonLayout>

@ -0,0 +1,11 @@
import { QueryCache, QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
queryCache: new QueryCache({}),
defaultOptions: {
queries: {
retry: false,
enabled: !import.meta.env.SSR,
},
},
});
Loading…
Cancel
Save