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",
"@napi-rs/image": "^1.9.2",
"@resvg/resvg-js": "^2.6.2",
"@tanstack/react-query": "^5.59.15",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"astro": "^4.15.4",

@ -50,6 +50,9 @@ importers:
'@resvg/resvg-js':
specifier: ^2.6.2
version: 2.6.2
'@tanstack/react-query':
specifier: ^5.59.15
version: 5.59.15(react@18.3.1)
'@types/react':
specifier: ^18.3.3
version: 18.3.8
@ -1194,6 +1197,14 @@ packages:
peerDependencies:
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':
resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==}
@ -4288,6 +4299,13 @@ snapshots:
postcss-selector-parser: 6.0.10
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':
dependencies:
tslib: 2.7.0

@ -3,65 +3,40 @@ import {
ResizablePanel,
ResizablePanelGroup,
} from '../Resizable';
import { CourseSidebar } from './CourseSidebar';
import { CourseLayout } from './CourseLayout';
import { SqlCodeEditor } from '../SqlCodeEditor/SqlCodeEditor';
import type { ReactNode } from 'react';
import type {
ChapterFileType,
CourseFileType,
LessonFileType,
} from '../../lib/course';
import type { LessonFileType } from '../../lib/course';
type ChallengeViewProps = {
courseId: string;
chapterId: string;
lessonId: string;
title: string;
course: CourseFileType & {
chapters: ChapterFileType[];
};
lesson: LessonFileType;
children: ReactNode;
};
export function ChallengeView(props: ChallengeViewProps) {
const { children, title, course, lesson, courseId, chapterId } = props;
const { chapters } = course;
const { children, lesson } = props;
const { frontmatter } = lesson;
const { defaultValue, initSteps, expectedResults } = frontmatter;
return (
<CourseLayout
courseId={courseId}
chapterId={chapterId}
lessonId={lesson.id}
lesson={lesson}
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>
<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>
</ResizablePanel>
</div>
</ResizablePanel>
<ResizableHandle withHandle={true} />
<ResizableHandle withHandle={true} />
<ResizablePanel defaultSize={40} minSize={20}>
<SqlCodeEditor
defaultValue={defaultValue}
initSteps={initSteps}
expectedResults={expectedResults}
/>
</ResizablePanel>
</ResizablePanelGroup>
</CourseLayout>
<ResizablePanel defaultSize={40} minSize={20}>
<SqlCodeEditor
defaultValue={defaultValue}
initSteps={initSteps}
expectedResults={expectedResults}
/>
</ResizablePanel>
</ResizablePanelGroup>
);
}

@ -1,46 +1,17 @@
import { useState, 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';
import { type ReactNode } from 'react';
type LessonViewProps = {
courseId: string;
chapterId: string;
lessonId: string;
title: string;
course: CourseFileType & {
chapters: ChapterFileType[];
};
lesson: LessonFileType;
children: ReactNode;
};
export function LessonView(props: LessonViewProps) {
const { children, title, course, lesson, courseId, chapterId } = props;
const { chapters } = course;
const { children } = props;
return (
<CourseLayout
courseId={courseId}
chapterId={chapterId}
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 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>
</CourseLayout>
</div>
);
}

@ -1,28 +1,14 @@
import { useState } from 'react';
import { CourseLayout } from './CourseLayout';
import { Circle, CircleCheck, CircleX } from 'lucide-react';
import { cn } from '../../lib/classname';
import type {
ChapterFileType,
CourseFileType,
LessonFileType,
} from '../../lib/course';
import type { LessonFileType } from '../../lib/course';
type QuizViewProps = {
courseId: string;
chapterId: string;
lessonId: string;
title: string;
course: CourseFileType & {
chapters: ChapterFileType[];
};
lesson: LessonFileType;
};
export function QuizView(props: QuizViewProps) {
const { title, course, lesson, courseId, lessonId, chapterId } = props;
const { chapters } = course;
const { lesson } = props;
const { frontmatter } = lesson;
const { questions = [] } = frontmatter;
@ -45,85 +31,75 @@ export function QuizView(props: QuizViewProps) {
}).length;
return (
<CourseLayout
courseId={courseId}
chapterId={chapterId}
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 py-10">
<h3 className="mb-10 text-lg font-semibold">
SQL Quiz: Intermediate
</h3>
<div className="flex flex-col gap-3">
{questions.map((question) => {
return (
<QuizItem
key={question.id}
id={question.id}
title={question.title}
disabled={isSubmitted}
options={question.options.map((option) => {
const selectedOptionId = selectedOptions?.[question.id];
let optionStatus: QuizOptionStatus = 'default';
if (option.isCorrectOption && isSubmitted) {
optionStatus = 'correct';
} else if (selectedOptionId === option.id) {
optionStatus = isSubmitted ? 'wrong' : 'selected';
}
return {
...option,
status: optionStatus,
};
})}
onOptionSelectChange={(id, optionId) => {
setSelectedOptions((prev) => ({
...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>
<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 py-10">
<h3 className="mb-10 text-lg font-semibold">
SQL Quiz: Intermediate
</h3>
<div className="flex flex-col gap-3">
{questions.map((question) => {
return (
<QuizItem
key={question.id}
id={question.id}
title={question.title}
disabled={isSubmitted}
options={question.options.map((option) => {
const selectedOptionId = selectedOptions?.[question.id];
let optionStatus: QuizOptionStatus = 'default';
if (option.isCorrectOption && isSubmitted) {
optionStatus = 'correct';
} else if (selectedOptionId === option.id) {
optionStatus = isSubmitted ? 'wrong' : 'selected';
}
return {
...option,
status: optionStatus,
};
})}
onOptionSelectChange={(id, optionId) => {
setSelectedOptions((prev) => ({
...prev,
[id]: optionId,
}));
}}
selectedOptionId={selectedOptions?.[question.id]}
/>
);
})}
</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 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-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>
</CourseLayout>
</div>
);
}

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

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

@ -2,6 +2,7 @@
import { ChallengeView } from '../../../../components/Course/ChallengeView';
import { LessonView } from '../../../../components/Course/LessonView';
import { QuizView } from '../../../../components/Course/QuizView';
import { CourseLayout } from '../../../../components/Course/CourseLayout';
import SkeletonLayout from '../../../../layouts/SkeletonLayout.astro';
import {
getAllCourses,
@ -70,53 +71,40 @@ const { course, chapter, lesson } = Astro.props;
---
<SkeletonLayout title={course.frontmatter.title}>
{
lesson.frontmatter.type === 'challenge' && (
<ChallengeView
courseId={courseId}
chapterId={chapterId}
lessonId={lesson.id}
title={course.frontmatter.title}
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>
</ChallengeView>
)
}
<CourseLayout
courseId={courseId}
chapterId={chapterId}
lessonId={lesson.id}
lesson={lesson}
title={course.frontmatter.title}
chapters={course.chapters}
completedPercentage={0}
client:load
>
{
lesson.frontmatter.type === 'challenge' && (
<ChallengeView 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>
</ChallengeView>
)
}
{
lesson.frontmatter.type === 'lesson' && (
<LessonView
courseId={courseId}
chapterId={chapterId}
lessonId={lesson.id}
title={course.frontmatter.title}
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 === 'lesson' && (
<LessonView 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' && (
<QuizView
courseId={courseId}
chapterId={chapterId}
lessonId={lesson.id}
title={course.frontmatter.title}
course={course}
lesson={lesson}
client:load
/>
)
}
{
lesson.frontmatter.type === 'quiz' && (
<QuizView lesson={lesson} client:load />
)
}
</CourseLayout>
</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