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:
Rotimi Best
2026-04-12 08:58:41 +01:00
parent 5a8f50f67b
commit d6e812d08b
573 changed files with 31964 additions and 8708 deletions
+1
View File
@@ -15,6 +15,7 @@ node_modules
/blob-report/
/playwright/.cache/
.tanstack
.react-router
.dev.vars
src/routeTree.gen.ts
+5
View File
@@ -0,0 +1,5 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
@source "../../../packages/ui/src/**/*.{ts,tsx}";
+15
View File
@@ -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>;
}
@@ -1,4 +1,4 @@
import { Link } from '@tanstack/react-router';
import { Link } from 'react-router';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
export function NotFound() {
@@ -17,7 +17,7 @@ export function NotFound() {
temporarily unavailable.
</p>
<Link
to="/"
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
@@ -6,7 +6,7 @@ export function baseOptions(): BaseLayoutProps {
nav: {
title: (
<div className="flex items-center gap-2">
<img src="/logo-192.png" className="size-8" alt="ClassroomIO logo" />
<img src="/docs/logo-192.png" className="size-8" alt="ClassroomIO logo" />
<span className="font-medium">ClassroomIO</span>
</div>
)
@@ -44,7 +44,7 @@ export function baseOptions(): BaseLayoutProps {
label: 'Discord',
icon: (
<img
src="/discord-blue.png"
src="/docs/discord-blue.png"
alt="discord icon"
className="w-7 transition-all duration-500 hover:scale-90"
/>
+35
View File
@@ -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
};
}
}
+134
View File
@@ -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>
);
}
+8
View File
@@ -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;
+18
View File
@@ -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'
}
}
);
}
@@ -1,39 +1,14 @@
import { useMemo } from 'react';
import defaultMdxComponents from 'fumadocs-ui/mdx';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { createServerFn } from '@tanstack/react-start';
import type * as PageTree from 'fumadocs-core/page-tree';
import browserCollections from 'fumadocs-mdx:collections/browser';
import { createFileRoute, notFound } from '@tanstack/react-router';
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 { source } from '@/lib/source';
import { baseOptions } from '@/lib/layout.shared';
import { HomeLayout } from 'fumadocs-ui/layouts/home';
export const Route = createFileRoute('/$')({
component: Page,
loader: async ({ params }) => {
const slugs = params._splat?.split('/') ?? [];
const data = await loader({ data: slugs });
await clientLoader.preload(data.path);
return data;
}
});
const loader = createServerFn({
method: 'GET'
})
.inputValidator((slugs: string[]) => slugs)
.handler(async ({ data: slugs }) => {
const page = source.getPage(slugs);
if (!page) throw notFound();
return {
tree: source.pageTree as object,
path: page.path
};
});
import { NotFound } from '@/components/not-found';
import { baseOptions } from '@/lib/layout.shared';
const clientLoader = browserCollections.docs.createClientLoader({
component({ toc, frontmatter, default: MDX }) {
@@ -42,21 +17,32 @@ const clientLoader = browserCollections.docs.createClientLoader({
<DocsTitle>{frontmatter.title}</DocsTitle>
<DocsDescription>{frontmatter.description}</DocsDescription>
<DocsBody>
<MDX
components={{
...defaultMdxComponents
}}
/>
<MDX />
</DocsBody>
</DocsPage>
);
}
});
function Page() {
const data = Route.useLoaderData();
const Content = clientLoader.getComponent(data.path);
const tree = useMemo(() => transformPageTree(data.tree as PageTree.Folder), [data.tree]);
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()}>
@@ -72,12 +58,36 @@ function Page() {
}}
tree={tree}
>
<Content />
<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') {
+5
View File
@@ -0,0 +1,5 @@
import { redirect } from 'react-router';
export function loader() {
throw redirect('/home');
}
+10
View File
@@ -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);
}
+64
View File
@@ -0,0 +1,64 @@
---
title: API
description: Use the ClassroomIO public API to manage audience members and courses with organization-scoped API keys.
---
import { Callout } from 'fumadocs-ui/components/callout';
The ClassroomIO public API is available under:
```text
https://api.classroomio.com/public-api/v1
```
Authentication uses organization-scoped bearer tokens created from `Automation -> API` in the ClassroomIO dashboard.
## Interactive Reference
- [Open the interactive API reference](/docs/api/reference)
## Plan Requirement
<Callout type="warn" title="Paid Access">
API key creation is available only on Early Adopter and Enterprise plans.
</Callout>
## Current Surface
Current public API groups:
- `audience`
- `courses`
Implemented route families:
- `GET /public-api/v1/audience`
- `POST /public-api/v1/audience`
- `POST /public-api/v1/audience/assign-courses`
- `GET /public-api/v1/audience/:memberId`
- `PUT /public-api/v1/audience/:memberId`
- `DELETE /public-api/v1/audience/:memberId`
- `GET /public-api/v1/courses`
- `GET /public-api/v1/courses/:courseId`
- `GET /public-api/v1/courses/:courseId/students`
- `GET /public-api/v1/courses/:courseId/export`
## OpenAPI
Raw OpenAPI JSON:
- [Public API OpenAPI JSON](https://api.cdn.clsrio.com/openapi/public-api/openapi-latest.json)
- [Interactive API Reference](/docs/api/reference)
## Authentication Example
```bash
curl https://api.classroomio.com/public-api/v1/audience \
-H "Authorization: Bearer <cio_api_key>"
```
## Notes
- API keys are organization-scoped.
- Enrollment is handled through `POST /public-api/v1/audience/assign-courses`.
- The API documentation will expand as the remaining public API write endpoints are added.
@@ -40,7 +40,7 @@ import { t } from '$lib/utils/functions/translations';
Once you've updated the English file, you need to update the other files. To do this, run a command. Firstly ensure you're still in the `/apps/dashboard` directory, then execute:
```zsh title="Terminal"
pnpm run script:translate
pnpm run translate
```
This command runs a script that updates the other translation files.
+2
View File
@@ -7,6 +7,8 @@
"---Core Features---",
"course",
"programs",
"api",
"mcp",
"tags",
"audience",
+10
View File
@@ -0,0 +1,10 @@
---
title: Programs
description: Learn when to use Programs in ClassroomIO, the kinds of training they support, and how they work for both admins and learners.
---
import { Callout } from 'fumadocs-ui/components/callout';
<Callout type="warn" title="Coming Soon">
Full customer documentation for Programs is still being prepared. For now, this page is a placeholder and the draft content has been moved into the product PRD.
</Callout>
-10
View File
@@ -1,10 +0,0 @@
import { defineNitroConfig } from 'nitro/config';
export default defineNitroConfig({
compatibilityDate: '2026-03-13',
preset: 'cloudflare_module',
cloudflare: {
deployConfig: true,
nodeCompat: true
}
});
+11 -11
View File
@@ -2,23 +2,26 @@
"name": "@cio/docs",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "wrangler dev --port 4174",
"start": "wrangler dev --port 3001",
"dev": "react-router dev",
"build": "react-router build",
"preview": "react-router-serve ./build/server/index.js",
"start": "react-router-serve ./build/server/index.js",
"types:check": "fumadocs-mdx && tsc --noEmit",
"postinstall": "fumadocs-mdx"
},
"dependencies": {
"@tanstack/react-router": "^1.134.12",
"@tanstack/react-router-devtools": "^1.134.12",
"@tanstack/react-start": "^1.143.4",
"@scalar/core": "^0.3.28",
"@react-router/dev": "^7.9.4",
"@react-router/node": "^7.9.4",
"@react-router/serve": "^7.9.4",
"fumadocs-core": "16.2.1",
"fumadocs-mdx": "14.0.4",
"fumadocs-ui": "16.2.1",
"isbot": "^5.1.30",
"lucide-react": "^0.562.0",
"lucide-static": "^0.552.0",
"react": "^19.2.0",
"react-router": "^7.9.4",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.3.1",
"vite": "^7.2.0"
@@ -29,11 +32,8 @@
"@types/node": "^24.10.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0",
"nitro": "3.0.1-alpha.1",
"tailwindcss": "^4.1.16",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^5.1.4",
"wrangler": "^4.21.2"
"vite-tsconfig-paths": "^5.1.4"
}
}
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900" fill="none">
<rect width="1600" height="900" fill="#F8FAFC"/>
<rect x="48" y="48" width="1504" height="804" rx="28" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="4"/>
<rect x="96" y="96" width="300" height="28" rx="14" fill="#0F172A"/>
<rect x="96" y="170" width="1040" height="56" rx="18" fill="#E2E8F0"/>
<rect x="96" y="280" width="1360" height="420" rx="24" fill="#FFFFFF" stroke="#94A3B8" stroke-dasharray="14 10" stroke-width="4"/>
<text x="776" y="490" text-anchor="middle" font-family="Arial, sans-serif" font-size="42" font-weight="700" fill="#0F172A">LMS Programs Placeholder</text>
<text x="776" y="548" text-anchor="middle" font-family="Arial, sans-serif" font-size="28" fill="#475569">Replace with the learner programs list or learner program entry view.</text>
</svg>

After

Width:  |  Height:  |  Size: 885 B

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900" fill="none">
<rect width="1600" height="900" fill="#F8FAFC"/>
<rect x="48" y="48" width="1504" height="804" rx="28" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="4"/>
<rect x="96" y="96" width="340" height="28" rx="14" fill="#0F172A"/>
<rect x="1220" y="88" width="236" height="52" rx="16" fill="#0F172A"/>
<text x="1338" y="122" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" font-weight="700" fill="#FFFFFF">Add Course</text>
<rect x="96" y="172" width="1360" height="60" rx="18" fill="#E2E8F0"/>
<rect x="96" y="276" width="1360" height="440" rx="24" fill="#FFFFFF" stroke="#94A3B8" stroke-dasharray="14 10" stroke-width="4"/>
<text x="776" y="492" text-anchor="middle" font-family="Arial, sans-serif" font-size="42" font-weight="700" fill="#0F172A">Program Courses Placeholder</text>
<text x="776" y="550" text-anchor="middle" font-family="Arial, sans-serif" font-size="28" fill="#475569">Replace with the Courses page in either list or grid view.</text>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900" fill="none">
<rect width="1600" height="900" fill="#F8FAFC"/>
<rect x="48" y="48" width="1504" height="804" rx="28" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="4"/>
<rect x="96" y="96" width="360" height="28" rx="14" fill="#0F172A"/>
<rect x="1256" y="88" width="200" height="52" rx="16" fill="#0F172A"/>
<text x="1356" y="122" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" font-weight="700" fill="#FFFFFF">New Post</text>
<rect x="240" y="190" width="1072" height="500" rx="24" fill="#FFFFFF" stroke="#94A3B8" stroke-dasharray="14 10" stroke-width="4"/>
<text x="776" y="426" text-anchor="middle" font-family="Arial, sans-serif" font-size="42" font-weight="700" fill="#0F172A">Program Newsfeed Placeholder</text>
<text x="776" y="484" text-anchor="middle" font-family="Arial, sans-serif" font-size="28" fill="#475569">Replace with a screenshot of posts, reactions, and comments.</text>
</svg>

After

Width:  |  Height:  |  Size: 1023 B

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900" fill="none">
<rect width="1600" height="900" fill="#F8FAFC"/>
<rect x="48" y="48" width="1504" height="804" rx="28" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="4"/>
<rect x="96" y="96" width="320" height="28" rx="14" fill="#0F172A"/>
<rect x="1234" y="88" width="222" height="52" rx="16" fill="#0F172A"/>
<text x="1345" y="122" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" font-weight="700" fill="#FFFFFF">Invite People</text>
<rect x="96" y="182" width="760" height="52" rx="16" fill="#E2E8F0"/>
<rect x="884" y="182" width="180" height="52" rx="16" fill="#E2E8F0"/>
<rect x="96" y="286" width="1360" height="430" rx="24" fill="#FFFFFF" stroke="#94A3B8" stroke-dasharray="14 10" stroke-width="4"/>
<text x="776" y="500" text-anchor="middle" font-family="Arial, sans-serif" font-size="42" font-weight="700" fill="#0F172A">Program People Placeholder</text>
<text x="776" y="558" text-anchor="middle" font-family="Arial, sans-serif" font-size="28" fill="#475569">Replace with the People page and membership table.</text>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900" fill="none">
<rect width="1600" height="900" fill="#F8FAFC"/>
<rect x="48" y="48" width="1504" height="804" rx="28" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="4"/>
<rect x="96" y="96" width="340" height="28" rx="14" fill="#0F172A"/>
<rect x="1220" y="88" width="236" height="52" rx="16" fill="#0F172A"/>
<text x="1338" y="122" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" font-weight="700" fill="#FFFFFF">Save Changes</text>
<rect x="248" y="198" width="1104" height="470" rx="24" fill="#FFFFFF" stroke="#94A3B8" stroke-dasharray="14 10" stroke-width="4"/>
<text x="800" y="432" text-anchor="middle" font-family="Arial, sans-serif" font-size="42" font-weight="700" fill="#0F172A">Program Settings Placeholder</text>
<text x="800" y="490" text-anchor="middle" font-family="Arial, sans-serif" font-size="28" fill="#475569">Replace with the Settings page showing status and general fields.</text>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900" fill="none">
<rect width="1600" height="900" fill="#F8FAFC"/>
<rect x="48" y="48" width="1504" height="804" rx="28" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="4"/>
<rect x="96" y="96" width="280" height="660" rx="24" fill="#E2E8F0"/>
<rect x="136" y="150" width="200" height="22" rx="11" fill="#0F172A"/>
<rect x="136" y="224" width="200" height="56" rx="16" fill="#FFFFFF"/>
<rect x="136" y="300" width="200" height="56" rx="16" fill="#FFFFFF"/>
<rect x="136" y="376" width="200" height="56" rx="16" fill="#FFFFFF"/>
<rect x="136" y="452" width="200" height="56" rx="16" fill="#FFFFFF"/>
<rect x="424" y="96" width="1032" height="92" rx="24" fill="#E2E8F0"/>
<rect x="424" y="224" width="1032" height="532" rx="24" fill="#FFFFFF" stroke="#94A3B8" stroke-dasharray="14 10" stroke-width="4"/>
<text x="940" y="470" text-anchor="middle" font-family="Arial, sans-serif" font-size="42" font-weight="700" fill="#0F172A">Program Shell Placeholder</text>
<text x="940" y="528" text-anchor="middle" font-family="Arial, sans-serif" font-size="28" fill="#475569">Replace with a screenshot showing the sidebar and header layout.</text>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900" fill="none">
<rect width="1600" height="900" fill="#F8FAFC"/>
<rect x="48" y="48" width="1504" height="804" rx="28" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="4"/>
<rect x="96" y="96" width="360" height="28" rx="14" fill="#0F172A"/>
<rect x="96" y="146" width="520" height="18" rx="9" fill="#94A3B8"/>
<rect x="1200" y="92" width="256" height="52" rx="16" fill="#0F172A"/>
<text x="1328" y="126" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" font-weight="700" fill="#FFFFFF">Create Program</text>
<rect x="96" y="206" width="960" height="52" rx="16" fill="#E2E8F0"/>
<rect x="1080" y="206" width="180" height="52" rx="16" fill="#E2E8F0"/>
<rect x="1280" y="206" width="176" height="52" rx="16" fill="#E2E8F0"/>
<rect x="96" y="296" width="1360" height="420" rx="24" fill="#FFFFFF" stroke="#94A3B8" stroke-dasharray="14 10" stroke-width="4"/>
<text x="776" y="500" text-anchor="middle" font-family="Arial, sans-serif" font-size="42" font-weight="700" fill="#0F172A">Programs List Placeholder</text>
<text x="776" y="558" text-anchor="middle" font-family="Arial, sans-serif" font-size="28" fill="#475569">Replace with the org dashboard programs index page.</text>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900" fill="none">
<rect width="1600" height="900" fill="#F8FAFC"/>
<rect x="48" y="48" width="1504" height="804" rx="28" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="4"/>
<rect x="96" y="96" width="420" height="28" rx="14" fill="#0F172A"/>
<rect x="96" y="144" width="760" height="18" rx="9" fill="#94A3B8"/>
<rect x="96" y="210" width="1408" height="560" rx="24" fill="#E2E8F0"/>
<rect x="144" y="274" width="1312" height="96" rx="20" fill="#FFFFFF" stroke="#94A3B8" stroke-dasharray="14 10" stroke-width="4"/>
<text x="800" y="330" text-anchor="middle" font-family="Arial, sans-serif" font-size="42" font-weight="700" fill="#0F172A">Programs Overview Placeholder</text>
<text x="800" y="388" text-anchor="middle" font-family="Arial, sans-serif" font-size="28" fill="#475569">Replace with a broad screenshot that explains the Programs feature at a glance.</text>
</svg>

After

Width:  |  Height:  |  Size: 972 B

+5
View File
@@ -0,0 +1,5 @@
import type { Config } from '@react-router/dev/config';
export default {
basename: '/docs'
} satisfies Config;
-15
View File
@@ -1,15 +0,0 @@
import { loader } from 'fumadocs-core/source';
import * as icons from 'lucide-static';
import { docs } from 'fumadocs-mdx:collections/server';
export const source = loader({
source: docs.toFumadocsSource(),
baseUrl: '/',
icon(icon) {
if (!icon) {
return;
}
if (icon in icons) return icons[icon as keyof typeof icons];
},
});
-13
View File
@@ -1,13 +0,0 @@
import { NotFound } from '@/components/not-found';
import { createRouter as createTanStackRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen';
export function getRouter() {
return createTanStackRouter({
basepath: '/docs',
defaultNotFoundComponent: NotFound,
defaultPreload: 'intent',
routeTree,
scrollRestoration: true
});
}
-118
View File
@@ -1,118 +0,0 @@
import * as React from 'react';
import { HeadContent, Outlet, Scripts, createRootRoute } from '@tanstack/react-router';
import { RootProvider } from 'fumadocs-ui/provider/tanstack';
import appCss from '@/styles/app.css?url';
export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: 'utf-8'
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1'
},
{
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',
itemProp: 'image',
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'
}
],
links: [
{ rel: 'stylesheet', href: appCss },
{ rel: 'icon', href: '/favicon.ico' }
]
}),
component: RootComponent
});
function RootComponent() {
return (
<RootDocument>
<Outlet />
</RootDocument>
);
}
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html suppressHydrationWarning>
<head>
<HeadContent />
</head>
<body className="flex min-h-screen flex-col">
<RootProvider>{children}</RootProvider>
<Scripts />
</body>
</html>
);
}
-16
View File
@@ -1,16 +0,0 @@
import { createFileRoute } from '@tanstack/react-router';
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';
const server = createFromSource(source, {
// https://docs.orama.com/docs/orama-js/supported-languages
language: 'english',
});
export const Route = createFileRoute('/api/search')({
server: {
handlers: {
GET: async ({ request }) => server.GET(request),
},
},
});
-12
View File
@@ -1,12 +0,0 @@
import { createFileRoute, redirect } from '@tanstack/react-router';
export const Route = createFileRoute('/')({
beforeLoad: () => {
throw redirect({
to: '/$',
params: {
_splat: 'home'
}
});
}
});
-7
View File
@@ -1,7 +0,0 @@
@import 'tailwindcss' source('../');
@import 'fumadocs-ui/css/black.css';
@import 'fumadocs-ui/css/preset.css';
body {
--color-fd-primary: var(--color-blue-700);
}
+2 -2
View File
@@ -1,5 +1,5 @@
{
"include": ["**/*.ts", "**/*.tsx"],
"include": ["app/**/*.ts", "app/**/*.tsx", "react-router.config.ts", "source.config.ts", "vite.config.ts"],
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
@@ -16,7 +16,7 @@
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/*": ["./app/*"],
"fumadocs-mdx:collections/*": [".source/*"]
},
"noEmit": true
+3 -10
View File
@@ -1,13 +1,12 @@
import { defineConfig } from 'vite';
import mdx from 'fumadocs-mdx/vite';
import { nitro } from 'nitro/vite';
import react from '@vitejs/plugin-react';
import { reactRouter } from '@react-router/dev/vite';
import tailwindcss from '@tailwindcss/vite';
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import tsConfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
server: {
host: '127.0.0.1',
port: 3000
},
base: '/docs',
@@ -17,12 +16,6 @@ export default defineConfig({
tsConfigPaths({
projects: ['./tsconfig.json']
}),
tanstackStart(),
react(),
// see https://tanstack.com/start/latest/docs/framework/react/guide/hosting for hosting config
// we configured nitro by default
nitro({
baseURL: '/docs'
})
reactRouter()
]
});