diff --git a/admin/Dockerfile b/admin/Dockerfile
index 9d45def..c992904 100644
--- a/admin/Dockerfile
+++ b/admin/Dockerfile
@@ -28,5 +28,6 @@ ENV NODE_ENV=production
WORKDIR /app
COPY --from=production-deps /app/node_modules /app/node_modules
COPY --from=build /app/build /app
+COPY ./docs /app/docs
EXPOSE 8080
CMD ["node", "./bin/server.js"]
\ No newline at end of file
diff --git a/admin/app/controllers/docs_controller.ts b/admin/app/controllers/docs_controller.ts
index ad69e2a..1c31448 100644
--- a/admin/app/controllers/docs_controller.ts
+++ b/admin/app/controllers/docs_controller.ts
@@ -9,15 +9,13 @@ export default class DocsController {
) { }
async list({ }: HttpContext) {
- const docs = await this.docsService.getDocs();
- return { articles: docs };
+ return await this.docsService.getDocs();
}
async show({ params, inertia }: HttpContext) {
- const content = await this.docsService.parseFile(`${params.slug}.md`);
+ const content = await this.docsService.parseFile(params.slug);
return inertia.render('docs/show', {
content,
- title: "Documentation"
});
}
}
\ No newline at end of file
diff --git a/admin/app/services/docs_service.ts b/admin/app/services/docs_service.ts
index 31bcd89..81692e4 100644
--- a/admin/app/services/docs_service.ts
+++ b/admin/app/services/docs_service.ts
@@ -1,17 +1,28 @@
+import drive from '@adonisjs/drive/services/main';
import Markdoc from '@markdoc/markdoc';
-import { readdir, readFile } from 'node:fs/promises';
-import { join } from 'node:path';
+import { streamToString } from '../../util/docs.js';
export class DocsService {
async getDocs() {
- const docsPath = join(process.cwd(), '/docs');
- console.log(`Resolving docs path: ${docsPath}`);
+ const disk = drive.use('docs');
+ if (!disk) {
+ throw new Error('Docs disk not configured');
+ }
- const files = await readdir(docsPath, { withFileTypes: true });
- const docs = files
- .filter(file => file.isFile() && file.name.endsWith('.md'))
- .map(file => file.name);
- return docs;
+ const contents = await disk.listAll('/');
+ const files: Array<{ title: string; slug: string }> = [];
+
+ for (const item of contents.objects) {
+ if (item.isFile && item.name.endsWith('.md')) {
+ const cleaned = this.prettify(item.name);
+ files.push({
+ title: cleaned,
+ slug: item.name.replace(/\.md$/, '')
+ });
+ }
+ }
+
+ return files.sort((a, b) => a.title.localeCompare(b.title));
}
parse(content: string) {
@@ -26,13 +37,36 @@ export class DocsService {
return Markdoc.transform(ast, config);
}
- async parseFile(filename: string) {
- const fullPath = join(process.cwd(), '/docs', filename);
- console.log(`Resolving file path: ${fullPath}`);
- const content = await readFile(fullPath, 'utf-8')
+ async parseFile(_filename: string) {
+ const disk = drive.use('docs');
+ if (!disk) {
+ throw new Error('Docs disk not configured');
+ }
+
+ if (!_filename) {
+ throw new Error('Filename is required');
+ }
+
+ const filename = _filename.endsWith('.md') ? _filename : `${_filename}.md`;
+
+ const fileExists = await disk.exists(filename);
+ if (!fileExists) {
+ throw new Error(`File not found: ${filename}`);
+ }
+
+ const fileStream = await disk.getStream(filename);
+ if (!fileStream) {
+ throw new Error(`Failed to read file stream: ${filename}`);
+ }
+ const content = await streamToString(fileStream);
return this.parse(content);
}
+ private prettify(filename: string) {
+ const cleaned = filename.replace(/_/g, ' ').replace(/\.md$/, '');
+ return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
+ }
+
private getConfig() {
return {
tags: {
diff --git a/admin/config/drive.ts b/admin/config/drive.ts
index c0b97ad..72aa24e 100644
--- a/admin/config/drive.ts
+++ b/admin/config/drive.ts
@@ -16,6 +16,11 @@ const driveConfig = defineConfig({
routeBasePath: '/storage',
visibility: 'public',
}),
+ docs: services.fs({
+ location: app.makePath('docs'),
+ serveFiles: false, // Don't serve files directly - we handle this via routes/Inertia
+ visibility: 'public',
+ }),
},
})
diff --git a/admin/docs/home.md b/admin/docs/home.md
index 829b744..1516eac 100644
--- a/admin/docs/home.md
+++ b/admin/docs/home.md
@@ -1 +1,71 @@
-# This is a markdown file!
\ No newline at end of file
+# Lorem Ipsum Markdown Showcase
+
+---
+
+## Introduction
+
+This document serves as a comprehensive example of **Markdown's various formatting possibilities**, using the classic *Lorem Ipsum* text as its content. From basic text styling to lists, code blocks, and tables, you'll find a demonstration of common Markdown features here.
+
+---
+
+## Basic Text Formatting
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+* This text is **bold**.
+* This text is *italic*.
+* This text is ***bold and italic***.
+* This text is ~~struck through~~.
+* You can also use `backticks` for `inline code`.
+
+---
+
+## Headers
+
+Markdown supports up to six levels of headers.
+
+# Header 1
+## Header 2
+### Header 3
+#### Header 4
+##### Header 5
+###### Header 6
+
+---
+
+## Lists
+
+### Unordered List
+
+* Lorem ipsum dolor sit amet.
+ * Consectetur adipiscing elit.
+ * Sed do eiusmod tempor.
+* Incididunt ut labore et dolore magna.
+* Aliqua ut enim ad minim veniam.
+
+### Ordered List
+
+1. Quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+2. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+3. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+---
+
+## Blockquotes
+
+> "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
+>
+> — John Doe, *Lorem Ipsum Anthology*
+
+---
+
+## Code Blocks
+
+```python
+def fibonacci(n):
+ a, b = 0, 1
+ for i in range(n):
+ print(a, end=" ")
+ a, b = b, a + b
+
+fibonacci(10)
\ No newline at end of file
diff --git a/admin/inertia/app/app.tsx b/admin/inertia/app/app.tsx
index 9f93e73..1ca0ce4 100644
--- a/admin/inertia/app/app.tsx
+++ b/admin/inertia/app/app.tsx
@@ -9,6 +9,7 @@ import ModalsProvider from '~/providers/ModalProvider'
import { TransmitProvider } from 'react-adonis-transmit'
import { generateUUID } from '~/lib/util'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const appName = import.meta.env.VITE_APP_NAME || 'Project N.O.M.A.D.'
const queryClient = new QueryClient()
@@ -36,6 +37,7 @@ createInertiaApp({
+
diff --git a/admin/inertia/components/StyledSidebar.tsx b/admin/inertia/components/StyledSidebar.tsx
index 8f1e097..75d434a 100644
--- a/admin/inertia/components/StyledSidebar.tsx
+++ b/admin/inertia/components/StyledSidebar.tsx
@@ -9,7 +9,7 @@ interface StyledSidebarProps {
items: Array<{
name: string
href: string
- icon: React.ElementType
+ icon?: React.ElementType
current: boolean
}>
}
@@ -25,7 +25,7 @@ const StyledSidebar: React.FC = ({ title, items }) => {
const ListItem = (item: {
name: string
href: string
- icon: React.ElementType
+ icon?: React.ElementType
current: boolean
}) => {
return (
@@ -39,7 +39,7 @@ const StyledSidebar: React.FC = ({ title, items }) => {
'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold'
)}
>
-
+ {item.icon && }
{item.name}
diff --git a/admin/inertia/layouts/DocsLayout.tsx b/admin/inertia/layouts/DocsLayout.tsx
index a45eab1..6a0e3db 100644
--- a/admin/inertia/layouts/DocsLayout.tsx
+++ b/admin/inertia/layouts/DocsLayout.tsx
@@ -1,35 +1,29 @@
-import { Cog6ToothIcon, CommandLineIcon, FolderIcon } from '@heroicons/react/24/outline'
-import { useEffect, useState } from 'react'
+import { useQuery } from '@tanstack/react-query'
+import { useMemo } from 'react'
import StyledSidebar from '~/components/StyledSidebar'
import api from '~/lib/api'
-const navigation = [
- { name: 'Apps', href: '/settings/apps', icon: CommandLineIcon, current: false },
- { name: 'ZIM Explorer', href: '/settings/zim', icon: FolderIcon, current: false },
- { name: 'System', href: '/settings/system', icon: Cog6ToothIcon, current: true },
-]
-
export default function DocsLayout({ children }: { children: React.ReactNode }) {
- const [docs, setDocs] = useState>([])
+ const { data, isLoading } = useQuery>({
+ queryKey: ['docs'],
+ queryFn: () => api.listDocs(),
+ refetchOnWindowFocus: false,
+ staleTime: Infinity,
+ })
- // Fetch docs when the component mounts
- useEffect(() => {
- fetchDocs()
- }, [])
+ const items = useMemo(() => {
+ if (isLoading || !data) return []
- async function fetchDocs() {
- try {
- const data = await api.listDocs()
- console.log('Fetched docs:', data)
- setDocs(data)
- } catch (error) {
- console.error('Error fetching docs:', error)
- }
- }
+ return data.map((doc) => ({
+ name: doc.title,
+ href: `/docs/${doc.slug}`,
+ current: false,
+ }))
+ }, [data, isLoading])
return (
-
+
{children}
)
diff --git a/admin/inertia/lib/api.ts b/admin/inertia/lib/api.ts
index 68d19d0..e728a92 100644
--- a/admin/inertia/lib/api.ts
+++ b/admin/inertia/lib/api.ts
@@ -15,8 +15,8 @@ class API {
async listDocs() {
try {
- const response = await this.client.get<{ articles: Array<{ title: string; slug: string }> }>("/docs/list");
- return response.data.articles;
+ const response = await this.client.get>("/docs/list");
+ return response.data;
} catch (error) {
console.error("Error listing docs:", error);
throw error;
diff --git a/admin/inertia/pages/docs/show.tsx b/admin/inertia/pages/docs/show.tsx
index d54ed34..8d763e1 100644
--- a/admin/inertia/pages/docs/show.tsx
+++ b/admin/inertia/pages/docs/show.tsx
@@ -2,12 +2,11 @@ import { Head } from '@inertiajs/react'
import MarkdocRenderer from '~/components/MarkdocRenderer'
import DocsLayout from '~/layouts/DocsLayout'
-export default function Show({ content, title }: { content: any; title: string }) {
+export default function Show({ content }: { content: any; }) {
return (
-
+
-
{title}
diff --git a/admin/package-lock.json b/admin/package-lock.json
index 95d8a33..f225f8a 100644
--- a/admin/package-lock.json
+++ b/admin/package-lock.json
@@ -28,6 +28,7 @@
"@tabler/icons-react": "^3.34.0",
"@tailwindcss/vite": "^4.1.10",
"@tanstack/react-query": "^5.81.5",
+ "@tanstack/react-query-devtools": "^5.83.0",
"@tanstack/react-virtual": "^3.13.12",
"@vinejs/vine": "^3.0.1",
"@vitejs/plugin-react": "^4.6.0",
@@ -3668,9 +3669,19 @@
}
},
"node_modules/@tanstack/query-core": {
- "version": "5.81.5",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.81.5.tgz",
- "integrity": "sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q==",
+ "version": "5.83.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz",
+ "integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/query-devtools": {
+ "version": "5.81.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.81.2.tgz",
+ "integrity": "sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg==",
"license": "MIT",
"funding": {
"type": "github",
@@ -3678,12 +3689,12 @@
}
},
"node_modules/@tanstack/react-query": {
- "version": "5.81.5",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.81.5.tgz",
- "integrity": "sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw==",
+ "version": "5.83.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz",
+ "integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==",
"license": "MIT",
"dependencies": {
- "@tanstack/query-core": "5.81.5"
+ "@tanstack/query-core": "5.83.0"
},
"funding": {
"type": "github",
@@ -3693,6 +3704,23 @@
"react": "^18 || ^19"
}
},
+ "node_modules/@tanstack/react-query-devtools": {
+ "version": "5.83.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.83.0.tgz",
+ "integrity": "sha512-yfp8Uqd3I1jgx8gl0lxbSSESu5y4MO2ThOPBnGNTYs0P+ZFu+E9g5IdOngyUGuo6Uz6Qa7p9TLdZEX3ntik2fQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-devtools": "5.81.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "@tanstack/react-query": "^5.83.0",
+ "react": "^18 || ^19"
+ }
+ },
"node_modules/@tanstack/react-virtual": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
diff --git a/admin/package.json b/admin/package.json
index ac3120f..757fc37 100644
--- a/admin/package.json
+++ b/admin/package.json
@@ -74,6 +74,7 @@
"@tabler/icons-react": "^3.34.0",
"@tailwindcss/vite": "^4.1.10",
"@tanstack/react-query": "^5.81.5",
+ "@tanstack/react-query-devtools": "^5.83.0",
"@tanstack/react-virtual": "^3.13.12",
"@vinejs/vine": "^3.0.1",
"@vitejs/plugin-react": "^4.6.0",
diff --git a/admin/start/kernel.ts b/admin/start/kernel.ts
index 3886b0a..740125a 100644
--- a/admin/start/kernel.ts
+++ b/admin/start/kernel.ts
@@ -24,10 +24,10 @@ server.errorHandler(() => import('#exceptions/handler'))
*/
server.use([
() => import('#middleware/container_bindings_middleware'),
- () => import('@adonisjs/static/static_middleware'),
() => import('@adonisjs/cors/cors_middleware'),
() => import('@adonisjs/vite/vite_middleware'),
- () => import('@adonisjs/inertia/inertia_middleware')
+ () => import('@adonisjs/inertia/inertia_middleware'),
+ () => import('@adonisjs/static/static_middleware')
])
/**
diff --git a/admin/util/docs.ts b/admin/util/docs.ts
new file mode 100644
index 0000000..86f026f
--- /dev/null
+++ b/admin/util/docs.ts
@@ -0,0 +1,9 @@
+import { Readable } from 'stream';
+
+export const streamToString = async (stream: Readable): Promise => {
+ const chunks: Buffer[] = [];
+ for await (const chunk of stream) {
+ chunks.push(Buffer.from(chunk));
+ }
+ return Buffer.concat(chunks).toString('utf-8');
+};
\ No newline at end of file