Feat: interactive courses (#736)

* add courses page

* add api for courses

* add course page

* update course type

* update client

* update course page

* refactor courses pages

* render chapter content

* shadow in course

* fix video handler

* update style

* mark finished examples

* fix media player

* update locale

* finish chapter

* refactor

* auto update chapter status

* audo finish chapter

* fix media provider

* fix wavesurfer player

* update continue btn

* refactor chapters & page

* minor fix

* fix undefined

* refactor

* refactor

* disable sentry in dev

* clean markdown format before alignment

* refactor

* fix regenerate

* fix transcription pre-process for `-` connector

* upgrade deps

* handle no chapters

* add llm chat api

* create llm chat

* display llm message

* create message

* handle error

* generate llm message

* display llm datetime

* scroll to message

* tts for llm message

* add course provider

* refactor

* translate llm message

* fix llm chat introduction

* refacotr

* upgrade deps

* refactor style

* handle undefined

* fix posts

* update locales

* update courses api

* add enrollments count

* upgrade yarn

* upgrade deps

* restore dep to fix package in mac

* upgrade deps
This commit is contained in:
an-lee
2024-07-11 19:14:40 +08:00
committed by GitHub
parent d9523269a3
commit 728bfae82f
72 changed files with 3083 additions and 1973 deletions

View File

@@ -5,6 +5,7 @@ export * from "./layout";
export * from "./loader-spin";
export * from "./login-form";
export * from "./github-login-form";
export * from "./markdown-wrapper";
export * from "./mixin-login-form";
export * from "./no-records-found";
export * from "./page-placeholder";

View File

@@ -0,0 +1,30 @@
import Markdown from "react-markdown";
export const MarkdownWrapper = ({
children,
className,
...props
}: {
children: string;
className?: string;
}) => {
return (
<Markdown
className={className}
components={{
a({ node, children, ...props }) {
try {
new URL(props.href ?? "");
props.target = "_blank";
props.rel = "noopener noreferrer";
} catch (e) {}
return <a {...props}>{children}</a>;
},
}}
{...props}
>
{children}
</Markdown>
);
};

View File

@@ -31,6 +31,7 @@ import {
ExternalLinkIcon,
NotebookPenIcon,
SpeechIcon,
GraduationCapIcon,
} from "lucide-react";
import { useLocation, Link } from "react-router-dom";
import { t } from "i18next";
@@ -71,6 +72,14 @@ export const Sidebar = () => {
Icon={HomeIcon}
/>
<SidebarItem
href="/courses"
label={t("sidebar.courses")}
tooltip={t("sidebar.courses")}
active={activeTab.startsWith("/courses")}
Icon={GraduationCapIcon}
/>
<Separator className="hidden xl:block" />
<SidebarItem

View File

@@ -49,12 +49,9 @@ export const WavesurferPlayer = (props: {
wavesurfer.isPlaying() ? wavesurfer.pause() : wavesurfer.play();
}, [wavesurfer]);
useEffect(() => {
// use the intersection observer to only create the wavesurfer instance
// when the player is visible
if (!entry?.isIntersecting) return;
const initialize = () => {
if (!containerRef.current) return;
if (!src) return;
if (wavesurfer) return;
const ws = WaveSurfer.create({
container: containerRef.current,
@@ -73,7 +70,14 @@ export const WavesurferPlayer = (props: {
});
setWavesurfer(ws);
}, [src, entry]);
};
useEffect(() => {
if (!entry?.isIntersecting) return;
if (wavesurfer?.options?.url === src) return;
initialize();
}, [src, entry, containerRef]);
useEffect(() => {
if (!wavesurfer) return;