feat: enhance API and middleware functionality: add public API routes, implement program and course member checks, and improve rate limiting for invite acceptance. Update OpenAPI spec generation to focus on public API endpoints and add new middleware for automation key scopes. Include new documentation for lesson AI features and public API tests.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'fumadocs-ui/css/neutral.css';
|
||||
@import 'fumadocs-ui/css/preset.css';
|
||||
|
||||
@source "../../../packages/ui/src/**/*.{ts,tsx}";
|
||||
@@ -0,0 +1,15 @@
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
|
||||
export function getMDXComponents(components?: MDXComponents) {
|
||||
return {
|
||||
...defaultMdxComponents,
|
||||
...components
|
||||
} satisfies MDXComponents;
|
||||
}
|
||||
|
||||
export const useMDXComponents = getMDXComponents;
|
||||
|
||||
declare global {
|
||||
type MDXProvidedComponents = ReturnType<typeof getMDXComponents>;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Link } from 'react-router';
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
|
||||
export function NotFound() {
|
||||
return (
|
||||
<HomeLayout
|
||||
nav={{
|
||||
title: 'ClassroomIO'
|
||||
}}
|
||||
className="justify-center py-32 text-center"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<h1 className="text-fd-muted-foreground text-6xl font-bold">404</h1>
|
||||
<h2 className="text-2xl font-semibold">Page Not Found</h2>
|
||||
<p className="text-fd-muted-foreground max-w-md">
|
||||
The page you are looking for might have been removed, had its name changed, or is
|
||||
temporarily unavailable.
|
||||
</p>
|
||||
<Link
|
||||
to="/home"
|
||||
className="bg-fd-primary text-fd-primary-foreground mt-4 rounded-lg px-4 py-2 text-sm font-medium transition-opacity hover:opacity-90"
|
||||
>
|
||||
Back to Home
|
||||
</Link>
|
||||
</div>
|
||||
</HomeLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
import { SquareArrowOutUpRight } from 'lucide-react';
|
||||
|
||||
export function baseOptions(): BaseLayoutProps {
|
||||
return {
|
||||
nav: {
|
||||
title: (
|
||||
<div className="flex items-center gap-2">
|
||||
<img src="/docs/logo-192.png" className="size-8" alt="ClassroomIO logo" />
|
||||
<span className="font-medium">ClassroomIO</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
links: [
|
||||
{
|
||||
text: (
|
||||
<div className="flex items-center gap-2">
|
||||
Contact <SquareArrowOutUpRight />
|
||||
</div>
|
||||
),
|
||||
url: 'https://twitter.com/classroomio',
|
||||
external: true
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<div className="flex items-center gap-2">
|
||||
Demo <SquareArrowOutUpRight />
|
||||
</div>
|
||||
),
|
||||
url: 'https://dub.sh/ciodemo',
|
||||
external: true
|
||||
},
|
||||
{
|
||||
text: (
|
||||
<div className="flex items-center gap-2">
|
||||
Dashboard <SquareArrowOutUpRight />
|
||||
</div>
|
||||
),
|
||||
url: 'https://app.classroomio.com/',
|
||||
external: true
|
||||
},
|
||||
{
|
||||
type: 'icon',
|
||||
label: 'Discord',
|
||||
icon: (
|
||||
<img
|
||||
src="/docs/discord-blue.png"
|
||||
alt="discord icon"
|
||||
className="w-7 transition-all duration-500 hover:scale-90"
|
||||
/>
|
||||
),
|
||||
text: 'Discord',
|
||||
url: 'https://dub.sh/ciodiscord',
|
||||
external: true
|
||||
}
|
||||
],
|
||||
githubUrl: 'https://github.com/classroomio/classroomio'
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { loader, multiple } from 'fumadocs-core/source';
|
||||
import * as icons from 'lucide-static';
|
||||
import { docs } from 'fumadocs-mdx:collections/server';
|
||||
|
||||
export const source = loader({
|
||||
source: multiple({
|
||||
docs: docs.toFumadocsSource()
|
||||
}),
|
||||
baseUrl: '/',
|
||||
icon(icon) {
|
||||
if (!icon) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (icon in icons) {
|
||||
return icons[icon as keyof typeof icons];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export async function getDocsRouteData(slugs: string[]) {
|
||||
const page = source.getPage(slugs);
|
||||
if (!page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (page.data.type === 'docs') {
|
||||
return {
|
||||
tree: source.pageTree as object,
|
||||
slugs,
|
||||
pageType: 'docs' as const,
|
||||
path: page.path
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { LinksFunction, MetaFunction } from 'react-router';
|
||||
import {
|
||||
isRouteErrorResponse,
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
useRouteError
|
||||
} from 'react-router';
|
||||
import { RootProvider } from 'fumadocs-ui/provider/react-router';
|
||||
|
||||
import { NotFound } from '@/components/not-found';
|
||||
import './app.css';
|
||||
|
||||
export const links: LinksFunction = () => [
|
||||
{ rel: 'icon', href: '/docs/favicon.ico' }
|
||||
];
|
||||
|
||||
export const meta: MetaFunction = () => [
|
||||
{
|
||||
title: 'ClassroomIO | The Open Source Learning Management System for Companies'
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
content:
|
||||
'A flexible, user-friendly platform for creating, managing, and delivering courses for companies and training organisations'
|
||||
},
|
||||
{
|
||||
property: 'og:url',
|
||||
content: 'https://classroomio.com/docs'
|
||||
},
|
||||
{
|
||||
property: 'og:type',
|
||||
content: 'website'
|
||||
},
|
||||
{
|
||||
property: 'og:title',
|
||||
content: 'ClassroomIO | The Open Source Learning Management System for Companies'
|
||||
},
|
||||
{
|
||||
property: 'og:description',
|
||||
content:
|
||||
'A flexible, user-friendly platform for creating, managing, and delivering courses for companies and training organisations'
|
||||
},
|
||||
{
|
||||
property: 'og:image:type',
|
||||
content: 'image/png'
|
||||
},
|
||||
{
|
||||
property: 'og:image:width',
|
||||
content: '1920'
|
||||
},
|
||||
{
|
||||
property: 'og:image:height',
|
||||
content: '1080'
|
||||
},
|
||||
{
|
||||
property: 'og:image:secure_url',
|
||||
content: 'https://brand.cdn.clsrio.com/og/classroomio-og.png'
|
||||
},
|
||||
{
|
||||
name: 'twitter:card',
|
||||
content: 'summary_large_image'
|
||||
},
|
||||
{
|
||||
property: 'twitter:domain',
|
||||
content: 'classroomio.com'
|
||||
},
|
||||
{
|
||||
property: 'twitter:url',
|
||||
content: 'https://classroomio.com/docs'
|
||||
},
|
||||
{
|
||||
name: 'twitter:title',
|
||||
content: 'ClassroomIO | The Open Source Learning Management System for Companies'
|
||||
},
|
||||
{
|
||||
name: 'twitter:description',
|
||||
content:
|
||||
'A flexible, user-friendly platform for creating, managing, and delivering courses for companies and training organisations'
|
||||
},
|
||||
{
|
||||
name: 'twitter:creator',
|
||||
content: '@classroomio'
|
||||
},
|
||||
{
|
||||
name: 'twitter:image',
|
||||
content: 'https://brand.cdn.clsrio.com/og/classroomio-og.png'
|
||||
}
|
||||
];
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className="flex min-h-screen flex-col">
|
||||
<RootProvider>{children}</RootProvider>
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
export function ErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
|
||||
if (isRouteErrorResponse(error) && error.status === 404) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<main className="mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center gap-4 px-6 py-24">
|
||||
<h1 className="text-3xl font-semibold">Docs Error</h1>
|
||||
<p className="text-fd-muted-foreground">
|
||||
The docs app hit an unexpected error while rendering this page.
|
||||
</p>
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { type RouteConfig, index, route } from '@react-router/dev/routes';
|
||||
|
||||
export default [
|
||||
index('routes/home.tsx'),
|
||||
route('api/reference', 'routes/api-reference.ts'),
|
||||
route('api/search', 'routes/search.ts'),
|
||||
route('*', 'routes/docs.tsx')
|
||||
] satisfies RouteConfig;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { getHtmlDocument } from '@scalar/core/libs/html-rendering';
|
||||
|
||||
const publicApiSpecUrl = 'https://api.cdn.clsrio.com/openapi/public-api/openapi-latest.json';
|
||||
|
||||
export function loader() {
|
||||
return new Response(
|
||||
getHtmlDocument({
|
||||
url: publicApiSpecUrl,
|
||||
pageTitle: 'ClassroomIO Public API Reference',
|
||||
theme: 'none'
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'content-type': 'text/html; charset=utf-8'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type * as PageTree from 'fumadocs-core/page-tree';
|
||||
import browserCollections from 'fumadocs-mdx:collections/browser';
|
||||
import { isRouteErrorResponse, 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['*'];
|
||||
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
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from 'react-router';
|
||||
|
||||
export function loader() {
|
||||
throw redirect('/home');
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createFromSource } from 'fumadocs-core/search/server';
|
||||
|
||||
export async function loader({ request }: { request: Request }) {
|
||||
const { source } = await import('@/lib/source.server');
|
||||
const server = createFromSource(source, {
|
||||
language: 'english'
|
||||
});
|
||||
|
||||
return server.GET(request);
|
||||
}
|
||||
Reference in New Issue
Block a user