Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/TheSerchCp/SEAM/llms.txt

Use this file to discover all available pages before exploring further.

SEAM uses a layered extension pattern: every new module you build goes through the same five steps — repository, service, page, router registration, and (optionally) real-time operation listeners. Following the pattern keeps each concern isolated and makes new pages as maintainable as the existing ones. The templates below are derived directly from the users module, which is the canonical reference implementation.
1

Create the repository file

The repository is the only place that talks to the backend. Create js/repositories/mimodulo.repository.js and call ApiClient directly for each HTTP operation your module needs:
js/repositories/mimodulo.repository.js
import { ApiClient } from '../core/ApiClient.js';

export const getAllItems   = ()           => ApiClient.get('/mimodulo');
export const getItemById   = (id)         => ApiClient.get(`/mimodulo/${id}`);
export const createItem    = (data)       => ApiClient.post('/mimodulo', data);
export const updateItem    = (id, data)   => ApiClient.put(`/mimodulo/${id}`, data);
export const deleteItemById = (id)        => ApiClient.delete(`/mimodulo/${id}`);
Each export is a one-liner that returns the Promise from ApiClient. No error handling, no transformation — that belongs in the service layer.
2

Create the service file

The service layer imports from the repository and exposes intention-revealing function names to the rest of the app. Create js/services/mimodulo.service.js:
js/services/mimodulo.service.js
import * as MimoduloRepo from '../repositories/mimodulo.repository.js';

export const getItems      = ()           => MimoduloRepo.getAllItems();
export const getItemById   = (id)         => MimoduloRepo.getItemById(id);
export const createItem    = (data)       => MimoduloRepo.createItem(data);
export const updateItem    = (id, data)   => MimoduloRepo.updateItem(id, data);
export const removeItem    = (id)         => MimoduloRepo.deleteItemById(id);
If you need to transform data before returning it to the page (mapping field names, computing derived values, filtering), do it here — never in the repository or the page.
3

Create the page file

The page file is an async function that returns an HTML string. It wraps its content in PrivateLayout, subscribes to EventBus events, and registers a cleanup function so listeners are removed when the user navigates away.Create js/pages/mimodulo/mimodulo.page.js:
js/pages/mimodulo/mimodulo.page.js
import { PrivateLayout }       from '../../layout/PrivateLayout.js';
import { getItems, removeItem } from '../../services/mimodulo.service.js';
import { EventBus }            from '../../core/EventBus.js';
import { shouldUpdatePage }    from '../../core/OperationListeners.js';
import { registerPageCleanup } from '../../core/Router.js';

async function renderPage() {
    const items = await getItems();

    return `
        <div class="w-full space-y-5 rounded-xl border border-gray-700/60 bg-gray-900/80 p-4 sm:p-6 shadow-xl shadow-black/20">
            <div class="flex flex-col gap-3 border-b border-gray-700/40 pb-4 sm:pb-6">
                <span class="text-xs font-bold uppercase tracking-[0.25em] text-blue-400">Módulo</span>
                <h1 class="text-xl font-bold text-white sm:text-2xl">Mi Módulo</h1>
                <p class="mt-1 text-xs text-gray-400 sm:text-sm">
                    ${items.length} registros encontrados
                </p>
            </div>
            <!-- render items here -->
            <div id="modal-container"></div>
        </div>`;
}

export async function MimoduloPage() {
    const html = await renderPage();
    const _unsubscribers = [];

    const initEvents = async () => {
        const modalContainer = document.getElementById('modal-container');
        if (!modalContainer) { setTimeout(initEvents, 50); return; }

        // Re-render the page when a local action completes
        const unsubReload = EventBus.on('page:reload', async () => {
            document.getElementById('app').innerHTML = await MimoduloPage();
        });
        _unsubscribers.push(unsubReload);

        // React to real-time changes pushed by other clients via Socket.io
        const unsubSocket = EventBus.on('data:changed', async (payload) => {
            if (shouldUpdatePage('mimodulo', payload?.operation)) {
                if (payload?.initiatorSocketId === EventBus.socketId) return;
                document.getElementById('app').innerHTML = await MimoduloPage();
            }
        });
        _unsubscribers.push(unsubSocket);
    };

    // Clean up all EventBus subscriptions when the user navigates away
    registerPageCleanup(() => {
        _unsubscribers.forEach(unsub => unsub?.());
        _unsubscribers.length = 0;
    });

    setTimeout(initEvents, 0);

    return PrivateLayout(html);
}
Always call registerPageCleanup and pass a function that invokes every unsubscribe handle stored in _unsubscribers. If you skip this step, EventBus.on listeners accumulate across navigations and will fire against DOM nodes that no longer exist, causing silent errors and potential memory leaks.
4

Register the route in Router.js

Open js/core/Router.js and add your new route to the routes object. For most pages, use the lazy dynamic import() pattern so the module is only loaded when the user navigates to that route:
js/core/Router.js
import { HomePage }     from '../pages/home/home.page.js';
import { UsersPage }    from '../pages/users/users.page.js';
import { LoginPage }    from '../pages/auth/auth.page.js';
import { session }      from '../state/session.state.js';
import { Error404Page } from '../pages/error/error.404.page.js';

const routes = {
    "/":            LoginPage,
    "/home":        HomePage,
    "/usuarios":    UsersPage,
    // Add your new route using the lazy import pattern:
    "/mimodulo":    () => import('../pages/mimodulo/mimodulo.page.js').then(m => m.MimoduloPage()),
    "/reportes":    () => import('../pages/reportes/reportes.page.js').then(m => m.ReportesPage()),
    "/permisos":    () => import('../pages/permissions/permissions.page.js').then(m => m.PermisosPage()),
    "/calificaciones": () => import('../pages/calificaciones/calificaciones.page.js').then(m => m.CalificacionesPage()),
    "/tareas":      () => import('../pages/tareas/tareas.page.js').then(m => m.TareasPage()),
    "/proyectos":   () => import('../pages/proyectos/proyectos.page.js').then(m => m.ProyectosPage()),
    "/roles":       () => import('../pages/roles/roles.page.js').then(m => m.RolesPage()),
    "/login":       LoginPage,
};
The lazy import() pattern — () => import('...').then(m => m.PageFn()) — means the JavaScript for that page is only fetched and parsed when the user first visits that route. Use it for every route except those that must be available immediately on page load (like LoginPage and HomePage). This keeps the initial load fast.
The router strips the hash prefix and resolves routes by their base path segment, so #/mimodulo/123 resolves to the /mimodulo entry automatically.
5

Add operation listeners for real-time updates (optional)

If your module’s data can change in real time because other users are modifying the same records, register your page key and the relevant socket operation strings in js/core/OperationListeners.js:
js/core/OperationListeners.js
export const OPERATION_LISTENERS = {
    users: [
        'auth:register', 'auth:login',
        'users:create', 'users:update', 'users:delete'
    ],
    permissions: [
        'permissions:create', 'permissions:update', 'permissions:delete',
        'permissions:assign', 'permissions:unassign',
        'roles:create', 'roles:update', 'roles:delete'
    ],
    roles:   ['roles:create', 'roles:update', 'roles:delete'],
    sidebar: ['sidebar:create', 'sidebar:update', 'sidebar:delete'],
    // Add your module and the operations it should react to:
    mimodulo: ['mimodulo:create', 'mimodulo:update', 'mimodulo:delete'],
};

export const shouldUpdatePage = (pageType, operation) => {
    return OPERATION_LISTENERS[pageType]?.includes(operation);
};
Back in your page file, shouldUpdatePage('mimodulo', payload?.operation) will return true only when the incoming data:changed socket event carries one of the operations you listed here, preventing unnecessary re-renders on unrelated events.

Summary checklist

Repository

js/repositories/mimodulo.repository.js — one export per HTTP verb, ApiClient calls only.

Service

js/services/mimodulo.service.js — re-exports with clean names, transformation logic if needed.

Page

js/pages/mimodulo/mimodulo.page.jsrenderPage() returns HTML, MimoduloPage() wraps it in PrivateLayout and wires events.

Router

One new entry in the routes object inside js/core/Router.js, using the lazy import() pattern.

Build docs developers (and LLMs) love