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.
@@ -15,6 +15,7 @@ node_modules
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
.tanstack
|
||||
.react-router
|
||||
.dev.vars
|
||||
|
||||
src/routeTree.gen.ts
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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') {
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
"---Core Features---",
|
||||
"course",
|
||||
"programs",
|
||||
"api",
|
||||
"mcp",
|
||||
"tags",
|
||||
"audience",
|
||||
|
||||
@@ -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>
|
||||
@@ -1,10 +0,0 @@
|
||||
import { defineNitroConfig } from 'nitro/config';
|
||||
|
||||
export default defineNitroConfig({
|
||||
compatibilityDate: '2026-03-13',
|
||||
preset: 'cloudflare_module',
|
||||
cloudflare: {
|
||||
deployConfig: true,
|
||||
nodeCompat: true
|
||||
}
|
||||
});
|
||||
@@ -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 |
@@ -0,0 +1,5 @@
|
||||
import type { Config } from '@react-router/dev/config';
|
||||
|
||||
export default {
|
||||
basename: '/docs'
|
||||
} satisfies Config;
|
||||
@@ -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];
|
||||
},
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
beforeLoad: () => {
|
||||
throw redirect({
|
||||
to: '/$',
|
||||
params: {
|
||||
_splat: 'home'
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
]
|
||||
});
|
||||
|
||||