Adds live-class session scheduling on lessons (join link, date/time, timezone) with automatic 24h and 1h reminder emails plus a calendar invite (.ics), a time-change notification with an updated invite, a teacher-authored welcome email and session invite on enrollment, and a "Notify students" quiz nudge. LMS gets an upcoming live-sessions card and Join buttons, plus a reusable live-session card component (3 states) in @cio/ui with a Storybook story. The sidebar content tree now reflects sequential progression locking like the content list, and the course-completion confetti regression is fixed. Org-based emails no longer reference ClassroomIO by name. Docs add live-class, welcome-email, and course-progression guides, expand the certificates guide (design, signatures, corrected labels), move plan-required callouts to the top, flatten how-to-guides to the docs root with redirects, and add screenshots. Includes an add-docs-image skill.
130 lines
3.5 KiB
TypeScript
130 lines
3.5 KiB
TypeScript
import { useMemo } from 'react';
|
|
|
|
import type * as PageTree from 'fumadocs-core/page-tree';
|
|
import browserCollections from 'fumadocs-mdx:collections/browser';
|
|
import { isRouteErrorResponse, redirect, useLoaderData, useRouteError } from 'react-router';
|
|
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
|
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
|
|
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
|
|
|
import { NotFound } from '@/components/not-found';
|
|
import { baseOptions } from '@/lib/layout.shared';
|
|
|
|
const clientLoader = browserCollections.docs.createClientLoader({
|
|
component({ toc, frontmatter, default: MDX }) {
|
|
return (
|
|
<DocsPage toc={toc}>
|
|
<DocsTitle>{frontmatter.title}</DocsTitle>
|
|
<DocsDescription>{frontmatter.description}</DocsDescription>
|
|
<DocsBody>
|
|
<MDX />
|
|
</DocsBody>
|
|
</DocsPage>
|
|
);
|
|
}
|
|
});
|
|
|
|
export async function loader({ params }: { params: Record<string, string | undefined> }) {
|
|
const slug = params['*'];
|
|
|
|
// How-to guides were flattened to the docs root; permanently redirect the old
|
|
// `.../how-to-guides/<page>` URLs to `.../<page>`.
|
|
if (slug && slug.includes('how-to-guides/')) {
|
|
throw redirect('/' + slug.replace('how-to-guides/', ''), 301);
|
|
}
|
|
|
|
const slugs = slug ? slug.split('/') : ['home'];
|
|
const { getDocsRouteData } = await import('@/lib/source.server');
|
|
const routeData = await getDocsRouteData(slugs);
|
|
if (!routeData) {
|
|
throw new Response('Not Found', { status: 404 });
|
|
}
|
|
|
|
if (routeData.pageType === 'docs') {
|
|
await clientLoader.preload(routeData.path);
|
|
}
|
|
|
|
return routeData;
|
|
}
|
|
|
|
export default function DocsRoute() {
|
|
const data = useLoaderData<typeof loader>();
|
|
const tree = useMemo(() => transformPageTree(data.tree as PageTree.Root), [data.tree]);
|
|
|
|
return (
|
|
<HomeLayout {...baseOptions()}>
|
|
<DocsLayout
|
|
sidebar={{
|
|
collapsible: false
|
|
}}
|
|
themeSwitch={{
|
|
enabled: false
|
|
}}
|
|
searchToggle={{
|
|
enabled: false
|
|
}}
|
|
tree={tree}
|
|
>
|
|
<MdxContent path={data.path} />
|
|
</DocsLayout>
|
|
</HomeLayout>
|
|
);
|
|
}
|
|
|
|
export function ErrorBoundary() {
|
|
const error = useRouteError();
|
|
|
|
if (isRouteErrorResponse(error) && error.status === 404) {
|
|
return <NotFound />;
|
|
}
|
|
|
|
return (
|
|
<HomeLayout {...baseOptions()} className="justify-center py-32 text-center">
|
|
<div className="flex flex-col items-center gap-4">
|
|
<h1 className="text-2xl font-semibold">Docs Error</h1>
|
|
<p className="text-fd-muted-foreground max-w-md">
|
|
The docs page could not be rendered.
|
|
</p>
|
|
</div>
|
|
</HomeLayout>
|
|
);
|
|
}
|
|
|
|
function MdxContent({ path }: { path: string }) {
|
|
const Content = clientLoader.getComponent(path);
|
|
return <Content />;
|
|
}
|
|
|
|
function transformPageTree(root: PageTree.Root): PageTree.Root {
|
|
function mapNode<T extends PageTree.Node>(item: T): T {
|
|
if (typeof item.icon === 'string') {
|
|
item = {
|
|
...item,
|
|
icon: (
|
|
<span
|
|
dangerouslySetInnerHTML={{
|
|
__html: item.icon
|
|
}}
|
|
/>
|
|
)
|
|
};
|
|
}
|
|
|
|
if (item.type === 'folder') {
|
|
return {
|
|
...item,
|
|
index: item.index ? mapNode(item.index) : undefined,
|
|
children: item.children.map(mapNode)
|
|
};
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
return {
|
|
...root,
|
|
children: root.children.map(mapNode),
|
|
fallback: root.fallback ? transformPageTree(root.fallback) : undefined
|
|
};
|
|
}
|