chore: import codes

This commit is contained in:
2026-04-12 12:50:14 -04:00
parent 4bcb4898e8
commit 65ae3c6788
241 changed files with 48834 additions and 1 deletions
+284
View File
@@ -0,0 +1,284 @@
# Module System
Modules are self-contained features that can be enabled/disabled via environment variables.
## File Structure
```
src/modules/your-module/
├── module.config.js # Required — navigation, pages, widgets
├── db.js # Database schema (createTables / dropTables)
├── crud.js # CRUD operations
├── actions.js # Server actions (for public pages)
├── metadata.js # SEO metadata generators
├── api.js # API route handlers
├── cron.config.js # Scheduled tasks
├── index.js # Public API re-exports
├── .env.example # Environment variable documentation
├── admin/ # Admin pages (lazy-loaded)
│ └── index.js # Re-exports admin components
├── pages/ # Public pages (lazy-loaded)
│ └── index.js
├── dashboard/ # Dashboard widgets
│ ├── statsActions.js
│ └── Widget.js
└── sub-feature/ # Optional sub-modules (e.g. items/, categories/)
├── db.js
├── crud.js
└── admin/
```
> Not all files are required. Only create what the module actually needs.
---
## Step 1 — Create `module.config.js`
```javascript
import { lazy } from 'react';
export default {
// Module identity
name: 'your-module',
displayName: 'Your Module',
version: '1.0.0',
description: 'Description of your module',
// Other modules this one depends on (must be enabled too)
dependencies: ['clients'],
// Environment variables this module uses (documentation only)
envVars: [
'YOUR_MODULE_API_KEY',
],
// Admin navigation — single section object or array of section objects
navigation: {
id: 'your-module',
title: 'Your Module',
icon: 'SomeIcon', // String icon name from shared/Icons.js
items: [
{ name: 'Items', href: '/admin/your-module/items', icon: 'PackageIcon' },
{ name: 'New', href: '/admin/your-module/items/new', icon: 'PlusSignIcon' },
],
},
// Admin pages — path → lazy component
adminPages: {
'/admin/your-module/items': lazy(() => import('./admin/ItemsListPage.js')),
'/admin/your-module/items/new': lazy(() => import('./admin/ItemCreatePage.js')),
'/admin/your-module/items/edit': lazy(() => import('./admin/ItemEditPage.js')),
},
// (Optional) Custom resolver for dynamic paths not known at build time.
// Called before the adminPages map. Return the lazy component or null.
pageResolver(path) {
const parts = path.split('/').filter(Boolean);
// example: /admin/your-module/{type}/list
if (parts[2] === 'list') return lazy(() => import('./admin/ItemsListPage.js'));
return null;
},
// Public pages — keyed by 'default' (one component handles all public routes)
publicPages: {
default: lazy(() => import('./pages/YourModulePublicPages.js')),
},
// Public route patterns for SEO/route matching (relative to /zen/your-module/)
publicRoutes: [
{ pattern: ':id', description: 'View item' },
{ pattern: ':id/pdf', description: 'PDF viewer' },
],
// Dashboard widgets (lazy-loaded, rendered on the admin dashboard)
dashboardWidgets: [
lazy(() => import('./dashboard/Widget.js')),
],
};
```
### Navigation as multiple sections
When a module provides several distinct sections (like the `posts` module with one section per post type), set `navigation` to an array:
```javascript
navigation: [
{ id: 'your-module-foo', title: 'Foo', icon: 'Book02Icon', items: [...] },
{ id: 'your-module-bar', title: 'Bar', icon: 'Layers01Icon', items: [...] },
],
```
---
## Step 2 — Create `db.js`
Every module that uses a database must expose a `createTables` function:
```javascript
import { query } from '@hykocx/zen/database';
export async function createTables() {
const created = [];
const skipped = [];
const exists = await query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = $1
)`, ['zen_your_module']);
if (!exists.rows[0].exists) {
await query(`
CREATE TABLE zen_your_module (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
)
`);
created.push('zen_your_module');
} else {
skipped.push('zen_your_module');
}
return { created, skipped };
}
export async function dropTables() {
await query(`DROP TABLE IF EXISTS zen_your_module CASCADE`);
}
```
> **Never create migrations.** Instead, provide the SQL to the user so they can run it manually.
---
## Step 3 — Create `.env.example`
Document every environment variable the module reads:
```bash
#################################
# MODULE YOUR-MODULE
ZEN_MODULE_YOUR_MODULE=false
ZEN_MODULE_YOUR_MODULE_API_KEY=
ZEN_MODULE_YOUR_MODULE_SOME_OPTION=default_value
#################################
```
---
## Step 4 — Create `cron.config.js` (optional)
Only needed if the module requires scheduled tasks:
```javascript
import { doSomething } from './reminders.js';
export default {
jobs: [
{
name: 'your-module-task',
description: 'Description of what this job does',
schedule: '*/5 * * * *', // cron expression
handler: doSomething,
timezone: process.env.ZEN_TIMEZONE || 'America/Toronto',
},
],
};
```
---
## Step 5 — Register the module in 5 files
### `modules/modules.registry.js` — add the module name
```javascript
export const AVAILABLE_MODULES = [
'clients',
'invoice',
'your-module',
];
```
### `modules/modules.pages.js` — import the config
```javascript
'use client';
import yourModuleConfig from './your-module/module.config.js';
const MODULE_CONFIGS = {
// ...existing modules...
'your-module': yourModuleConfig,
};
```
### `modules/modules.actions.js` — import server actions (if public pages or dashboard widgets)
```javascript
import { yourPublicAction } from './your-module/actions.js';
import { getYourModuleDashboardStats } from './your-module/dashboard/statsActions.js';
export const MODULE_ACTIONS = {
// ...existing modules...
'your-module': { yourPublicAction },
};
export const MODULE_DASHBOARD_ACTIONS = {
// ...existing modules...
'your-module': getYourModuleDashboardStats,
};
```
### `modules/modules.metadata.js` — import metadata generators (if SEO needed)
```javascript
import * as yourModuleMetadata from './your-module/metadata.js';
export const MODULE_METADATA = {
// ...existing modules...
'your-module': yourModuleMetadata,
};
```
### `modules/init.js` — register the database initializer
```javascript
import { createTables as createYourModuleTables } from './your-module/db.js';
const MODULE_DB_INITIALIZERS = {
// ...existing modules...
'your-module': createYourModuleTables,
};
```
---
## Step 6 — Enable the module
```bash
ZEN_MODULE_YOUR_MODULE=true
```
The environment variable is derived from the module name: `ZEN_MODULE_` + module name uppercased (hyphens become underscores).
---
## Sub-modules
For complex modules, group related features into sub-directories. Each sub-module follows the same pattern (`db.js`, `crud.js`, `admin/`). The parent `module.config.js` registers all sub-module admin pages, and the parent `index.js` re-exports everything publicly.
See `src/modules/invoice/` for a complete example with `items/`, `categories/`, `transactions/`, and `recurrences/` sub-modules.
---
## Reference implementations
| Module | Features demonstrated |
|--------|-----------------------|
| `src/modules/invoice/` | Sub-modules, public pages, cron jobs, Stripe, email, PDF, dashboard widgets, metadata |
| `src/modules/posts/` | Dynamic config from env vars, `pageResolver`, multiple navigation sections |
| `src/modules/clients/` | Simple module, dependencies, no public pages |