build ok,
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import type { StepIconProps } from '@mui/material/StepIcon';
|
||||
import { Check as CheckIcon } from '@phosphor-icons/react/dist/ssr/Check';
|
||||
|
||||
export function ChapterStepIcon({ active, completed, icon }: StepIconProps): React.JSX.Element {
|
||||
const highlight = active || completed;
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
sx={{
|
||||
'--Avatar-size': '24px',
|
||||
...(highlight
|
||||
? { bgcolor: 'var(--mui-palette-primary-main)', color: 'var(--mui-palette-primary-contrastText)' }
|
||||
: { bgcolor: 'var(--mui-palette-background-level2)', color: 'var(--mui-palette-text-primary)' }),
|
||||
}}
|
||||
variant="circular"
|
||||
>
|
||||
{completed ? <CheckIcon /> : icon}
|
||||
</Avatar>
|
||||
);
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ArrowLeft as ArrowLeftIcon } from '@phosphor-icons/react/dist/ssr/ArrowLeft';
|
||||
import { ArrowRight as ArrowRightIcon } from '@phosphor-icons/react/dist/ssr/ArrowRight';
|
||||
|
||||
import { Lesson } from './lesson';
|
||||
import type { Chapter } from './types';
|
||||
|
||||
export interface ChapterViewProps {
|
||||
chapter: Chapter;
|
||||
}
|
||||
|
||||
export function ChapterView({ chapter }: ChapterViewProps): React.JSX.Element {
|
||||
return (
|
||||
<Box sx={{ position: 'relative', pb: 6 }}>
|
||||
<Card>
|
||||
<CardHeader subheader={chapter.description} title={chapter.title} />
|
||||
<Tabs sx={{ px: 3 }} value="lesson">
|
||||
<Tab label="Lesson" tabIndex={0} value="lesson" />
|
||||
<Tab label="Resources" tabIndex={0} value="resources" />
|
||||
</Tabs>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<Lesson content={chapter.lesson} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Box
|
||||
sx={{
|
||||
bottom: 20,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<Card sx={{ boxShadow: 'var(--mui-shadows-16)' }}>
|
||||
<Stack direction="row" spacing={3} sx={{ alignItems: 'center', p: 1 }}>
|
||||
<Button color="secondary" size="small" startIcon={<ArrowLeftIcon />}>
|
||||
Prev
|
||||
</Button>
|
||||
<Typography color="text.secondary" variant="subtitle2">
|
||||
1/3
|
||||
</Typography>
|
||||
<Button color="secondary" size="small" startIcon={<ArrowRightIcon />}>
|
||||
Next
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
import * as React from 'react';
|
||||
import RouterLink from 'next/link';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardMedia from '@mui/material/CardMedia';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ArrowRight as ArrowRightIcon } from '@phosphor-icons/react/dist/ssr/ArrowRight';
|
||||
import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock';
|
||||
|
||||
import { paths } from '@/paths';
|
||||
|
||||
import type { Course } from './types';
|
||||
|
||||
export interface CourseCardProps {
|
||||
course: Course;
|
||||
}
|
||||
|
||||
export function CourseCard({ course }: CourseCardProps): React.JSX.Element {
|
||||
return (
|
||||
<Card variant="outlined">
|
||||
<CardMedia
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.academy.details('1')}
|
||||
image={course.media}
|
||||
sx={{ height: '180px' }}
|
||||
/>
|
||||
<CardContent>
|
||||
<Stack spacing={1}>
|
||||
<Link
|
||||
color="text.primary"
|
||||
component={RouterLink}
|
||||
href={paths.dashboard.academy.details('1')}
|
||||
underline="none"
|
||||
variant="subtitle1"
|
||||
>
|
||||
{course.title}
|
||||
</Link>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{course.description}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
||||
<ClockIcon fontSize="var(--icon-fontSize-sm)" />
|
||||
<Typography color="text.secondary" variant="caption">
|
||||
{course.duration}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
<LinearProgress value={course.progress} variant="determinate" />
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', p: 1 }}>
|
||||
<Button
|
||||
color="secondary"
|
||||
component={RouterLink}
|
||||
endIcon={<ArrowRightIcon />}
|
||||
href={paths.dashboard.academy.details('1')}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
import * as React from 'react';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Step from '@mui/material/Step';
|
||||
import StepLabel from '@mui/material/StepLabel';
|
||||
import Stepper from '@mui/material/Stepper';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock';
|
||||
|
||||
import { ChapterStepIcon } from './chapter-step-icon';
|
||||
import type { Chapter, Course } from './types';
|
||||
|
||||
export interface CourseSummaryProps {
|
||||
chapters: Chapter[];
|
||||
course: Course;
|
||||
currentChapterNumber?: number;
|
||||
}
|
||||
|
||||
export function CourseSummary({ chapters, course, currentChapterNumber }: CourseSummaryProps): React.JSX.Element {
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<Stack spacing={1}>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<LinearProgress sx={{ flex: '1 1 auto', height: '8px' }} value={course.progress} variant="determinate" />
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(
|
||||
course.progress / 100
|
||||
)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
||||
<ClockIcon fontSize="var(--icon-fontSize-sm)" />
|
||||
<Typography color="text.secondary" variant="caption">
|
||||
{course.duration}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<div>
|
||||
<Typography variant="subtitle1">{course.title}</Typography>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{course.description}
|
||||
</Typography>
|
||||
</div>
|
||||
<Stepper
|
||||
activeStep={currentChapterNumber ? currentChapterNumber - 1 : 0}
|
||||
orientation="vertical"
|
||||
sx={{
|
||||
'& .MuiStepLabel-iconContainer': { pr: 3 },
|
||||
'& .MuiStepConnector-line': { borderLeft: '2px solid var(--mui-palette-divider)' },
|
||||
}}
|
||||
>
|
||||
{chapters.map((chapter, index) => {
|
||||
const isCompleted = currentChapterNumber ? index < currentChapterNumber - 1 : false;
|
||||
|
||||
return (
|
||||
<Step key={chapter.title}>
|
||||
<StepLabel StepIconComponent={ChapterStepIcon}>
|
||||
<Typography color={isCompleted ? 'primary.main' : 'text.primary'} variant="subtitle2">
|
||||
{chapter.title}
|
||||
</Typography>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{chapter.description}
|
||||
</Typography>
|
||||
</StepLabel>
|
||||
</Step>
|
||||
);
|
||||
})}
|
||||
</Stepper>
|
||||
</Stack>
|
||||
);
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
import Select from '@mui/material/Select';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import { MagnifyingGlass as MagnifyingGlassIcon } from '@phosphor-icons/react/dist/ssr/MagnifyingGlass';
|
||||
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
import { Option } from '@/components/core/option';
|
||||
|
||||
export function CoursesFilters(): React.JSX.Element {
|
||||
return (
|
||||
<Card>
|
||||
<Stack direction="row" spacing={3} sx={{ alignItems: 'flex-end', flexWrap: 'wrap', p: 3 }}>
|
||||
<FormControl sx={{ maxWidth: '100%', width: '240px' }}>
|
||||
<InputLabel>Search</InputLabel>
|
||||
<OutlinedInput fullWidth name="query" placeholder="Keywords" />
|
||||
</FormControl>
|
||||
<FormControl sx={{ maxWidth: '100%', width: '240px' }}>
|
||||
<InputLabel>Platform</InputLabel>
|
||||
<Select defaultValue="" fullWidth name="category">
|
||||
<Option value="">All categories</Option>
|
||||
<Option value="fullstack">Fullstack</Option>
|
||||
<Option value="devops">DevOps</Option>
|
||||
<Option value="design">Design</Option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<DatePicker
|
||||
format="MMM D, YYYY"
|
||||
label="From"
|
||||
onChange={() => {
|
||||
// noop
|
||||
}}
|
||||
sx={{ maxWidth: '100%', width: '240px' }}
|
||||
value={dayjs().subtract(1, 'month')}
|
||||
/>
|
||||
<DatePicker
|
||||
format="MMM D, YYYY"
|
||||
label="To"
|
||||
onChange={() => {
|
||||
// noop
|
||||
}}
|
||||
sx={{ maxWidth: '100%', width: '240px' }}
|
||||
value={dayjs()}
|
||||
/>
|
||||
<Button startIcon={<MagnifyingGlassIcon />} variant="contained">
|
||||
Search
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -0,0 +1,102 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { RadialBar, RadialBarChart } from 'recharts';
|
||||
|
||||
import { NoSsr } from '@/components/core/no-ssr';
|
||||
|
||||
export interface DailyProgressProps {
|
||||
timeCurrent: number;
|
||||
timeGoal: number;
|
||||
}
|
||||
|
||||
export function DailyProgress({ timeCurrent, timeGoal }: DailyProgressProps): React.JSX.Element {
|
||||
const chartSize = 250;
|
||||
|
||||
const timeLeft = timeGoal - timeCurrent;
|
||||
const progress = Math.round((timeCurrent / timeGoal) * 100);
|
||||
|
||||
const data = [
|
||||
{ name: 'Empty', value: 100 },
|
||||
{ name: 'Usage', value: progress },
|
||||
] satisfies { name: string; value: number }[];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Stack spacing={2}>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<NoSsr fallback={<Box sx={{ height: `${chartSize}px` }} />}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
// hide the empty bar
|
||||
'& .recharts-layer path[name="Empty"]': { display: 'none' },
|
||||
'& .recharts-layer .recharts-radial-bar-background-sector': {
|
||||
fill: 'var(--mui-palette-neutral-100)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RadialBarChart
|
||||
barSize={20}
|
||||
data={data}
|
||||
endAngle={-10}
|
||||
height={chartSize}
|
||||
innerRadius={166}
|
||||
startAngle={190}
|
||||
width={chartSize}
|
||||
>
|
||||
<RadialBar
|
||||
animationDuration={300}
|
||||
background
|
||||
cornerRadius={10}
|
||||
dataKey="value"
|
||||
endAngle={-320}
|
||||
fill="var(--mui-palette-primary-main)"
|
||||
startAngle={20}
|
||||
/>
|
||||
</RadialBarChart>
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ textAlign: 'center', mt: '-50px' }}>
|
||||
<Typography variant="subtitle1">Time left</Typography>
|
||||
<Typography variant="body2">{timeLeft} min</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</NoSsr>
|
||||
</Box>
|
||||
<Box sx={{ mt: '-80px' }}>
|
||||
<Typography variant="h6">Today's progress of your {timeGoal}-minutes goal</Typography>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
You have used{' '}
|
||||
{new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(progress / 100)} of
|
||||
your available spots. Upgrade plan to create more projects.
|
||||
</Typography>
|
||||
</Box>
|
||||
<div>
|
||||
<Button variant="contained">Continue: React and Redux Tutorial</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
58
002_source/cms/src/components/dashboard/academy/help.tsx
Normal file
58
002_source/cms/src/components/dashboard/academy/help.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ArrowRight as ArrowRightIcon } from '@phosphor-icons/react/dist/ssr/ArrowRight';
|
||||
import { File as FileIcon } from '@phosphor-icons/react/dist/ssr/File';
|
||||
import { UserPlus as UserPlusIcon } from '@phosphor-icons/react/dist/ssr/UserPlus';
|
||||
|
||||
export function Help(): React.JSX.Element {
|
||||
return (
|
||||
<Card sx={{ height: '100%' }} variant="outlined">
|
||||
<CardContent>
|
||||
<Stack divider={<Divider />} spacing={2}>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'flex-start' }}>
|
||||
<Box sx={{ flex: '0 0 auto' }}>
|
||||
<FileIcon fontSize="var(--icon-fontSize-lg)" />
|
||||
</Box>
|
||||
<Stack spacing={2}>
|
||||
<div>
|
||||
<Typography variant="subtitle1">Find courses</Typography>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Browse the latest written articles
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Button endIcon={<ArrowRightIcon />} size="small" variant="contained">
|
||||
Courses
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'flex-start' }}>
|
||||
<Box sx={{ flex: '0 0 auto' }}>
|
||||
<UserPlusIcon fontSize="var(--icon-fontSize-lg)" />
|
||||
</Box>
|
||||
<Stack spacing={2}>
|
||||
<div>
|
||||
<Typography variant="subtitle1">Find tutors</Typography>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Find tutors to help you with your studies
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Button color="secondary" endIcon={<ArrowRightIcon />} size="small" variant="contained">
|
||||
Tutors
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
26
002_source/cms/src/components/dashboard/academy/lesson.tsx
Normal file
26
002_source/cms/src/components/dashboard/academy/lesson.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Markdown from 'react-markdown';
|
||||
import type { Components } from 'react-markdown';
|
||||
|
||||
import { CodeHighlighter } from '@/components/core/code-highlighter';
|
||||
|
||||
const components = { code: CodeHighlighter as Components['code'] } satisfies Components;
|
||||
|
||||
export interface LessonProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export function Lesson({ content }: LessonProps): React.JSX.Element {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
'& h2': { fontWeight: 500, fontSize: '1.5rem', lineHeight: 1.2, mb: 3 },
|
||||
'& h3': { fontWeight: 500, fontSize: '1.25rem', lineHeight: 1.2, mb: 3 },
|
||||
'& p': { fontWeight: 400, fontSize: '1rem', lineHeight: 1.5, mb: 2, mt: 0 },
|
||||
}}
|
||||
>
|
||||
<Markdown components={components}>{content}</Markdown>
|
||||
</Box>
|
||||
);
|
||||
}
|
16
002_source/cms/src/components/dashboard/academy/types.d.ts
vendored
Normal file
16
002_source/cms/src/components/dashboard/academy/types.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface Course {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
duration: string;
|
||||
media?: string;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
export interface Chapter {
|
||||
id: string;
|
||||
number: number;
|
||||
description: string;
|
||||
lesson: string;
|
||||
title: string;
|
||||
}
|
Reference in New Issue
Block a user