Module System Documentation
Overview
The Module System is the backbone of the Baldr Dashboard's dynamic feature architecture. It allows administrators to enable, disable, configure, and organize application features without requiring code changes or deployments.
What is a Module?
A module represents a distinct feature area or section of the application. Each module:
- Has its own data model and API endpoints
- Can be independently enabled/disabled
- Appears as a menu item when active
- Has its own icon, label, and routing
- Can support multiple languages
- May display notification badges (via stats)
Common Modules
| Module | Type | Purpose | Icon |
|---|---|---|---|
| News | news | News article management | newspaper |
| Events | events | Event calendar and management | event |
| Products | products | Product catalog | inventory_2 |
| Gallery | gallery | Image gallery management | photo_library |
| FAQ | faq | Frequently asked questions | help |
| Staff | staff | Team member profiles | group |
| Bookings | bookings | Reservation system | book_online |
| Contact | contact | Contact form submissions | mail |
Module Structure
Interface Definition
interface IModule {
_id: string; // Unique identifier
active: boolean; // Enabled/disabled state
title: string; // Display name (e.g., "News & Articles")
slug: string; // URL path (e.g., "news")
type: string; // Backend type identifier
icon: string; // Material Symbols icon name
config?: Config; // Optional configuration
createdAt?: Date; // Creation timestamp
updatedAt?: Date; // Last update timestamp
}
Configuration Object
interface Config {
languages?: string[]; // Supported language codes ["fr", "en", "es"]
order?: number; // Display order in menu (1 = first)
deletable?: boolean; // Can be deleted by users
stats?: string; // Stats key for notification badges
endpoint?: string; // API endpoint path
}
Module Lifecycle
┌─────────────────────────────────────────────────────────┐
│ Module Lifecycle │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. AVAILABLE │
│ ↓ │
│ Module template exists but not created │
│ (shown in "Available Modules" list) │
│ │
│ 2. CREATED │
│ ↓ │
│ Module instance created in database │
│ active: false by default │
│ │
│ 3. CONFIGURED │
│ ↓ │
│ Admin sets: title, icon, languages, order │
│ Still active: false │
│ │
│ 4. ACTIVATED │
│ ↓ │
│ Set active: true │
│ Appears in navigation menu │
│ Available to all users │
│ │
│ 5. DEACTIVATED (optional) │
│ ↓ │
│ Set active: false │
│ Hidden from menu but data preserved │
│ │
│ 6. DELETED (optional, if deletable) │
│ ↓ │
│ Module removed from database │
│ Associated data may be affected │
│ │
└─────────────────────────────────────────────────────────┘
Module API
Available Operations
import Module from "~/api/module.api";
// 1. Get all modules
const { items: modules } = await Module.getAll();
// 2. Get available module templates
const available = await Module.getAvailable();
// 3. Get single module
const module = await Module.getById(moduleId);
// 4. Create new module
const newModule = await Module.create({
name: "products",
title: "Products",
slug: "products",
type: "products",
icon: "inventory_2",
active: true,
config: {
languages: ["fr", "en"],
order: 3,
deletable: true,
stats: "products_count"
}
});
// 5. Update module
await Module.update(moduleId, {
title: "Updated Title",
order: 1,
active: false
});
// 6. Delete module
await Module.deleteMany({ ids: [moduleId] });
Integration Points
1. ModulesContext
The ModulesContext provides global access to the module list:
import { useModules } from "~/context/modules.context";
function MyComponent() {
const { modules, isLoading } = useModules();
// Filter active modules
const activeModules = modules.filter(m => m.active);
// Sort by order
const sortedModules = [...activeModules].sort((a, b) =>
(a.config?.order || 0) - (b.config?.order || 0)
);
return (
<nav>
{sortedModules.map(module => (
<NavLink key={module._id} to={`/${module.slug}`}>
<span className="material-symbols-outlined">{module.icon}</span>
{module.title}
</NavLink>
))}
</nav>
);
}
2. Dynamic Menu
The Menu organism automatically renders modules:
// app/components/organisms/menu.organism.tsx
const { modules } = useModules();
return (
<nav>
{modules
.filter(m => m.active)
.sort((a, b) => (a.config?.order || 0) - (b.config?.order || 0))
.map(module => (
<MenuItem
key={module._id}
icon={module.icon}
label={module.title}
to={`/${module.slug}`}
badge={getModuleStats(module.config?.stats)}
/>
))
}
</nav>
);
3. Routing
Modules automatically create routes:
// app/routes.ts
import { modules } from "~/context/modules.context";
// For each active module, routes are generated:
// /news → News listing page
// /news/:id → News detail page
// /news/new → Create news page
// /news/:id/edit → Edit news page
4. Stats Integration
Modules can display notification badges:
import { useStats } from "~/context/stats.context";
function ModuleMenuItem({ module }) {
const { stats } = useStats();
// Find stats for this module
const moduleStats = stats.find(s => s.key === module.config?.stats);
return (
<NavLink to={`/${module.slug}`}>
{module.title}
{moduleStats && moduleStats.value > 0 && (
<Badge value={moduleStats.value} />
)}
</NavLink>
);
}
Creating a New Module
Step 1: Define Module Template (Backend)
// Backend: module.templates.ts
{
name: "products",
title: "Products",
description: "Manage product catalog",
icon: "inventory_2",
type: "products",
endpoint: "/products",
stats: "products_count",
deletable: true
}
Step 2: Create Module Instance
// Admin clicks "Add Module" and selects "Products"
await Module.create({
name: "products",
title: "Products",
slug: "products",
type: "products",
icon: "inventory_2",
active: false, // Start disabled
config: {
languages: ["fr", "en"],
order: 5,
deletable: true
}
});
Step 3: Configure Module
// Admin configures the module
await Module.update(moduleId, {
title: "Product Catalog",
icon: "shopping_cart",
config: {
languages: ["fr", "en", "es", "de"],
order: 3
}
});
Step 4: Activate Module
// Admin enables the module
await Module.update(moduleId, {
active: true
});
// Module now appears in navigation menu
// Users can access /products
Module Configuration Best Practices
1. Naming Conventions
// ✅ Good
{
name: "products", // Lowercase, singular or plural
slug: "products", // Same as name
type: "products", // Matches backend route
title: "Products" // Human-readable
}
// ❌ Bad
{
name: "Product", // Should be lowercase
slug: "product-list", // Doesn't match name
type: "items", // Doesn't match name
title: "products" // Should be capitalized
}
2. Icon Selection
Use Material Symbols icons that clearly represent the module:
// ✅ Good choices
{ name: "news", icon: "newspaper" }
{ name: "events", icon: "event" }
{ name: "products", icon: "inventory_2" }
{ name: "gallery", icon: "photo_library" }
{ name: "staff", icon: "group" }
{ name: "contact", icon: "mail" }
// ❌ Avoid generic icons
{ name: "news", icon: "star" } // Too generic
{ name: "events", icon: "check" } // Not descriptive
3. Module Ordering
// Typical order (lower = higher in menu)
1. News (most important/updated)
2. Events
3. Products
4. Gallery
5. Staff
6. FAQ
7. Contact (less frequently accessed)
4. Language Configuration
// ✅ Good - Specify all supported languages
config: {
languages: ["fr", "en", "es", "de"]
}
// ❌ Bad - Empty or missing
config: {
languages: [] // No languages enabled
}
// This would break translation system
Module Permissions
Checking Module Access
import { useModules } from "~/context/modules.context";
function ProtectedFeature() {
const { modules } = useModules();
// Check if module exists and is active
const hasProductsModule = modules.some(
m => m.type === "products" && m.active
);
if (!hasProductsModule) {
return <div>Products feature not available</div>;
}
return <ProductsPage />;
}
Role-Based Module Access
import { useUser } from "~/context/user.context";
import { useModules } from "~/context/modules.context";
function AdminModuleManager() {
const { credential } = useUser();
const { modules } = useModules();
// Only admins can manage modules
if (credential?.role !== "admin") {
return <AccessDenied />;
}
return (
<div>
<h1>Manage Modules</h1>
{modules.map(module => (
<ModuleCard key={module._id} module={module} />
))}
</div>
);
}
Module Stats System
How Stats Work
- Backend calculates counts/metrics for each module
- Stats API returns array of
{ key, value, label } - Module's
config.statsfield links to a stats key - Menu displays badge with the stats value
Example Stats Configuration
// Module configuration
{
type: "news",
config: {
stats: "news_draft" // Key to match in stats array
}
}
// Stats API returns
[
{ key: "news_draft", value: 5, label: "Draft articles" },
{ key: "news_published", value: 42, label: "Published articles" },
{ key: "events_upcoming", value: 3, label: "Upcoming events" }
]
// Menu displays badge with "5" for News module
Custom Stats Display
function ModuleStatsDisplay({ module }: { module: IModule }) {
const { stats } = useStats();
// Get all stats for this module type
const moduleStats = stats.filter(s =>
s.key.startsWith(module.type)
);
return (
<div>
<h3>{module.title} Statistics</h3>
{moduleStats.map(stat => (
<div key={stat.key}>
{stat.label}: {stat.value}
</div>
))}
</div>
);
}
Troubleshooting
Issue: Module not appearing in menu
Possible causes:
active: false- Module is disabled- Module not in database - Check
Module.getAll() - User doesn't have permission - Check role-based access
- Frontend cache stale - Refresh page or invalidate query
Solution:
// Check module state
const module = await Module.getById(moduleId);
console.log("Active:", module.active);
// Activate module
await Module.update(moduleId, { active: true });
// Invalidate cache
queryClient.invalidateQueries({ queryKey: ["modules"] });
Issue: Module appears in wrong order
Solution:
// Update module order
await Module.update(moduleId, {
config: {
...module.config,
order: 2 // Position in menu
}
});
Issue: Module stats not updating
Possible causes:
- Stats key mismatch - Module's
config.statsdoesn't match stats API key - Stats not refreshing - Stats context refetches every 30 seconds
- Backend not calculating stats correctly
Solution:
// Check stats key
console.log("Module stats key:", module.config?.stats);
// Check available stats
const { stats } = useStats();
console.log("Available stats:", stats.map(s => s.key));
// Manually refetch stats
queryClient.invalidateQueries({ queryKey: ["stats"] });
Advanced: Dynamic Module Loading
Lazy Loading Module Components
// app/routes.ts
import { lazy } from "react";
const moduleRoutes = modules.map(module => ({
path: `/${module.slug}`,
component: lazy(() => import(`~/pages/${module.type}.page`))
}));
Module-Specific Layouts
function ModuleLayout({ module }: { module: IModule }) {
return (
<div>
<header>
<span className="material-symbols-outlined">{module.icon}</span>
<h1>{module.title}</h1>
</header>
<main>
{/* Module content */}
</main>
</div>
);
}
Summary
| Feature | Description | Key Point |
|---|---|---|
| Dynamic | Modules can be added/removed without code changes | No deployment required |
| Configurable | Each module has its own settings | Icons, labels, languages |
| Ordered | Modules can be reordered in menu | Drag-and-drop support |
| Stateful | Modules track active/inactive state | Enable/disable anytime |
| Integrated | Works with routing, stats, permissions | Seamless UX |
| Scalable | Easy to add new modules | Template-based creation |
Last Updated: October 28, 2025
Version: 1.0.0