diff --git a/.vscode/settings.json b/.vscode/settings.json index 964af63..9f3ba47 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,5 @@ "mode": "auto" } ], - "cSpell.words": ["mantine", "Menlo", "Millis", "Pandang", "Segoe", "Ujung", "WITA"] + "cSpell.words": ["mantine", "Menlo", "mgmt", "Millis", "Pandang", "Segoe", "Ujung", "WITA"] } diff --git a/apps/docs-dev/src/.vitepress/config.mts b/apps/docs-dev/src/.vitepress/config.mts index 1783a76..3e884b5 100644 --- a/apps/docs-dev/src/.vitepress/config.mts +++ b/apps/docs-dev/src/.vitepress/config.mts @@ -1,25 +1,23 @@ -import { defineConfig } from 'vitepress' -import { withMermaid } from 'vitepress-plugin-mermaid' +import { defineConfig } from 'vitepress'; +import { withMermaid } from 'vitepress-plugin-mermaid'; const config = withMermaid( defineConfig({ // title: "Frontend Monorepo", title: 'Frontend Arch', - description: "Centralized documentation for the Enterprise Frontend Monorepo", + description: 'Centralized documentation for the Enterprise Frontend Monorepo', head: [ - ['link', { rel: 'icon', href: '/favicon.svg' }] // Jika Anda menggunakan favicon.svg - ], + ['link', { rel: 'icon', href: '/favicon.svg' }], // Jika Anda menggunakan favicon.svg + ], themeConfig: { search: { provider: 'local', options: { - detailedView: true - } + detailedView: true, + }, }, logo: '/logo.svg', - nav: [ - { text: 'Docs', link: '/overview' }, - ], + nav: [{ text: 'Docs', link: '/overview' }], sidebar: [ { @@ -33,13 +31,22 @@ const config = withMermaid( text: 'Core Architecture', collapsed: false, items: [ - { text: 'API & Domain Logic', link: '/packages/core-api/' }, + // { text: 'API & Domain Logic', link: '/packages/core-api/' }, + { + text: 'API & Domain Logic', + collapsed: false, + items: [ + { text: 'API Engine', link: '/packages/core-api' }, + { text: 'Data Transformers', link: '/packages/core-api/transformers' }, + ], + }, + { text: 'Event Bus System', link: '/packages/core-events/' }, { text: 'Storage & Persistence', link: '/packages/core-storage/' }, { text: 'I18n & Localization', link: '/packages/core-i18n/' }, ], }, - { + { text: 'UI System', collapsed: false, items: [ @@ -52,23 +59,23 @@ const config = withMermaid( text: 'Desktop Ecosystem', collapsed: false, items: [ - { text: 'Overview', link: '/apps/desktop/' }, - { text: 'Lifecycle & Configuration', link: '/apps/desktop/CONFIGURATION' }, - { text: 'IPC & Bridge Architecture', link: '/apps/desktop/IPC_ARCHITECTURE' }, - { text: 'Distribution & Auto-Update', link: '/apps/desktop/AUTO_UPDATER' }, + { text: 'Overview', link: '/apps/desktop/' }, + { text: 'Lifecycle & Configuration', link: '/apps/desktop/CONFIGURATION' }, + { text: 'IPC & Bridge Architecture', link: '/apps/desktop/IPC_ARCHITECTURE' }, + { text: 'Distribution & Auto-Update', link: '/apps/desktop/AUTO_UPDATER' }, ], }, ], outline: { level: [2, 3] }, socialLinks: [ - { - icon: { - svg: 'Gitea' + { + icon: { + svg: 'Gitea', + }, + link: 'https://git.eigen.co.id/eigen/fe-monorepo-template', }, - link: 'https://git.eigen.co.id/eigen/fe-monorepo-template' - } - ] + ], }, // Mermaid configuration @@ -79,22 +86,20 @@ const config = withMermaid( // Fix cascading CJS/ESM SyntaxErrors caused by Vite dynamically discovering mermaid vite: { optimizeDeps: { - include: [ - 'mermaid' - ] - } - } - }) + include: ['mermaid'], + }, + }, + }), ); -// Pnpm strict workspace workaround: +// Pnpm strict workspace workaround: // vitepress-plugin-mermaid aggressively injects sub-dependencies into optimizeDeps.include. // Because pnpm uses strict symlinks, Vite fails to resolve these sub-dependencies from the project root, // causing pre-bundling to fail and cascading CJS/ESM SyntaxErrors in the browser. // We strip them out so esbuild can naturally inline them into the 'mermaid' chunk instead. if (config.vite?.optimizeDeps?.include) { config.vite.optimizeDeps.include = config.vite.optimizeDeps.include.filter( - (dep) => !['@braintree/sanitize-url', 'debug', 'cytoscape-cose-bilkent', 'cytoscape'].includes(dep) + (dep) => !['@braintree/sanitize-url', 'debug', 'cytoscape-cose-bilkent', 'cytoscape'].includes(dep), ); } diff --git a/apps/web/package.json b/apps/web/package.json index 90c80ec..bb1468b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -24,7 +24,7 @@ "dayjs": "^1.11.19", "events": "^3.3.0", "i18next": "^24.2.2", - "lucide-react": "^1.17.0", + "lucide-react": "^1.22.0", "react": "^19.2.3", "react-dom": "^19.2.3", "react-hook-form": "^7.56.4", diff --git a/apps/web/src/apps/index.tsx b/apps/web/src/apps/index.tsx index 1f5a777..5d6914d 100644 --- a/apps/web/src/apps/index.tsx +++ b/apps/web/src/apps/index.tsx @@ -2,6 +2,7 @@ import { lazy, Suspense, useState } from 'react'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; import { ThemeProvider, ColorSchemeType, DensityType } from '@repo/ui/provider'; import { NotFound, Forbidden, Maintenance, ComingSoon } from '@repo/ui/components'; +import { LoadingScreen } from '../core/components/loading-screen'; const AuthModule = lazy(() => import('./auth')); const AppModule = lazy(() => import('./modules')); @@ -15,7 +16,7 @@ export default function App() { return ( - Loading...}> + }> } /> } /> diff --git a/apps/web/src/apps/modules/example/example.page.tsx b/apps/web/src/apps/modules/example/example.page.tsx deleted file mode 100644 index e6c1363..0000000 --- a/apps/web/src/apps/modules/example/example.page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function ExamplePage() { - return
example
; -} diff --git a/apps/web/src/apps/modules/example/full-page/data/full-page.remote.service.ts b/apps/web/src/apps/modules/example/full-page/data/full-page.remote.service.ts new file mode 100644 index 0000000..2d63b7c --- /dev/null +++ b/apps/web/src/apps/modules/example/full-page/data/full-page.remote.service.ts @@ -0,0 +1,25 @@ +import { BaseRemoteDataServices } from '@repo/core-api/data-services'; +import { FullPageDTO, FullPageEntity } from '../domain/entities'; + +/** + * Full Page Remote Data Services + * + * Provides core data services for the full-page module by extending the base remote data services. + * While this class automatically handles standard CRUD operations and data transformations out-of-the-box, + * implementers can freely extend it by adding custom methods to support domain-specific API endpoints + * or complex business logic as needed. + * + * @example + * ```ts + * export class FullPageRemoteDataServices extends BaseRemoteDataServices { + * // Example of adding a custom method tailored to specific module needs + * public async getDashboardMetrics(status: string): Promise { + * const response = await this.httpClient.get(`${this.apiUrl}/metrics`, { + * params: { status } + * }); + * return response.data; + * } + * } + * ``` + */ +export class FullPageRemoteDataServices extends BaseRemoteDataServices {} diff --git a/apps/web/src/apps/modules/example/full-page/domain/constants/full-page.constants.ts b/apps/web/src/apps/modules/example/full-page/domain/constants/full-page.constants.ts new file mode 100644 index 0000000..3823bf4 --- /dev/null +++ b/apps/web/src/apps/modules/example/full-page/domain/constants/full-page.constants.ts @@ -0,0 +1,22 @@ +import { ModuleConfigEntity } from '@repo/ui/foundations'; + +/** + * Core configuration and constants for the Full Page module. + * Used across Domain, Data, and Presentation layers. + */ +export const FullPageModuleConfig: ModuleConfigEntity = { + /** Unique identifier for permissions, caching, and i18n */ + moduleKey: 'EXAMPLE_FULL_PAGE', + + /** Translation namespace — must match the namespace used in registerModuleNamespace() */ + translationNamespace: 'EXAMPLE_FULL_PAGE', + + /** Base API endpoint for remote data services */ + apiUrl: '/full-page', + + /** Base Web Router URL for UI navigation */ + webUrl: '/app/full-page', + + /** Architectural category of the module, used for rendering and routing logic */ + moduleCategory: 'FULL_PAGE', +} as const; diff --git a/apps/web/src/apps/modules/example/full-page/domain/constants/index.ts b/apps/web/src/apps/modules/example/full-page/domain/constants/index.ts new file mode 100644 index 0000000..1f3aff8 --- /dev/null +++ b/apps/web/src/apps/modules/example/full-page/domain/constants/index.ts @@ -0,0 +1 @@ +export * from './full-page.constants'; diff --git a/apps/web/src/apps/modules/example/full-page/domain/entities/full-page.entity.ts b/apps/web/src/apps/modules/example/full-page/domain/entities/full-page.entity.ts new file mode 100644 index 0000000..046e669 --- /dev/null +++ b/apps/web/src/apps/modules/example/full-page/domain/entities/full-page.entity.ts @@ -0,0 +1,26 @@ +import { BaseEntity } from '@repo/core-api/data-services'; + +/** + * Represents a full-page entity in the frontend domain model. + * + * This entity is derived from the API's `FullPageDTO` via the + * {@link FullPageTransformer}, which handles field name mapping + * and computed field derivation. + * + */ +export interface FullPageEntity extends BaseEntity { + status?: string; + name?: string; + code?: string; + description?: string; +} + +/** + * Represents the raw data structure returned by the API for a full-page resource. + * + * This DTO uses snake_case field names matching the backend's JSON serialization. + * It is transformed into a {@link FullPageEntity} by the {@link FullPageTransformer}. + */ +export interface FullPageDTO extends FullPageEntity { + [key: string]: any; +} diff --git a/apps/web/src/apps/modules/example/full-page/domain/entities/index.ts b/apps/web/src/apps/modules/example/full-page/domain/entities/index.ts new file mode 100644 index 0000000..de64115 --- /dev/null +++ b/apps/web/src/apps/modules/example/full-page/domain/entities/index.ts @@ -0,0 +1 @@ +export * from './full-page.entity'; diff --git a/apps/web/src/apps/modules/example/full-page/domain/factories/index.ts b/apps/web/src/apps/modules/example/full-page/domain/factories/index.ts new file mode 100644 index 0000000..45bf752 --- /dev/null +++ b/apps/web/src/apps/modules/example/full-page/domain/factories/index.ts @@ -0,0 +1,30 @@ +/** + * Full Page Factory + * + * This module acts as the dependency injection and configuration center for the full-page feature. + * It pre-configures and exports singleton instances of the data transformer and data service. + * By centralizing the instantiation here, it ensures that all UI components and hooks + * within the module share the same API client, configuration, and transformation logic. + */ + +import { apiClient } from '../../../../../../core/lib/api-client'; +import { FullPageRemoteDataServices } from '../../data/full-page.remote.service'; +import { FullPageModuleConfig } from '../constants/full-page.constants'; +import { FullPageRemoteDataTransformer } from '../transformers/full-page.remote.transformer'; + +/** + * Singleton instance of the FullPageRemoteDataTransformer. + * Exported for potential direct usage if manual data mapping is required outside the standard API flow. + */ +export const fullPageDataTransformer = new FullPageRemoteDataTransformer(); + +/** + * Pre-configured singleton instance of the FullPageRemoteDataServices. + * Ready to be consumed by UI components, state managers, or module providers. + * It is fully wired with the HTTP client and automatically handles data mapping via the injected transformer. + */ +export const fullPageDataService = new FullPageRemoteDataServices(apiClient, { + apiUrl: FullPageModuleConfig.apiUrl, + moduleKey: FullPageModuleConfig.moduleKey, + transformer: fullPageDataTransformer, +}); diff --git a/apps/web/src/apps/modules/example/full-page/domain/transformers/full-page.remote.transformer.ts b/apps/web/src/apps/modules/example/full-page/domain/transformers/full-page.remote.transformer.ts new file mode 100644 index 0000000..67a2d6a --- /dev/null +++ b/apps/web/src/apps/modules/example/full-page/domain/transformers/full-page.remote.transformer.ts @@ -0,0 +1,45 @@ +import { BaseDataTransformer } from '@repo/core-api/data-services'; +import { FullPageEntity, FullPageDTO } from '../entities'; + +/** + * Full Page Remote Data Transformer + * + * Responsible for transforming data between the raw API data transfer objects (DTOs) + * and the frontend domain entities for the full-page module. By extending the base transformer, + * it ensures strict type safety and decouples data parsing logic from the API service layer. + * + * Implementers must define the core mapping rules (`transformToEntity`, `transformToDTO`) + * and can freely add custom transformation methods for specific API responses. + * + * @example + * ```ts + * export class FullPageRemoteDataTransformer extends BaseDataTransformer { + * // Map snake_case API payload to camelCase frontend entity + * public transformToEntity(dto: FullPageDTO): FullPageEntity { + * return { + * id: dto.id, + * documentNumber: dto.document_number, + * createdAt: new Date(dto.created_at), + * // ... other property mappings + * }; + * } + * + * // Map camelCase frontend entity back to snake_case API payload + * public transformToDTO(entity: FullPageEntity): FullPageDTO { + * return { + * id: entity.id, + * document_number: entity.documentNumber, + * // ... other property mappings + * }; + * } + * + * // Example of adding a custom transformation method for a specific feature + * public transformMetrics(rawData: any): MetricsEntity { + * return { + * totalActive: rawData.total_active_count ?? 0, + * }; + * } + * } + * ``` + */ +export class FullPageRemoteDataTransformer extends BaseDataTransformer {} diff --git a/apps/web/src/apps/modules/example/full-page/domain/validators/full-page.validator.ts b/apps/web/src/apps/modules/example/full-page/domain/validators/full-page.validator.ts new file mode 100644 index 0000000..a32d8e6 --- /dev/null +++ b/apps/web/src/apps/modules/example/full-page/domain/validators/full-page.validator.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; +import { compose, required, rangeLength } from '@repo/ui/validators'; + +/** + * Factory function to generate the Zod validation schema for the Full Page module. + * It accepts a translation object `t` to maintain domain purity while supporting + * dynamic, localized error messages (i18n). + * + * @param t - The translation object (typically derived from `useTranslation()` in the UI layer). + * @returns The configured Zod object schema. + */ +export const createFullPageSchema = (t: any) => { + return z.object({ + // Code: Required, length between 3 and 10 characters. + code: compose(z.string(), required(t.fields.code), rangeLength(3, 10, t.fields.code)), + + // Name: Required, length between 3 and 50 characters. + name: compose(z.string(), required(t.fields.name), rangeLength(3, 50, t.fields.name)), + + // Status: Required selection (typically from a dropdown/select). + status: compose(z.string(), required(t.fields.status)), + + // Description: Optional text field. + description: z.string().optional(), + }); +}; + +/** + * Data Transfer Object (DTO) for the Full Page form. + * This type is automatically inferred from the Zod schema factory. + * Use this type as a generic for form initialization, e.g., `useForm()`. + */ +export type FullPageFormDTO = z.infer>; diff --git a/apps/web/src/apps/modules/example/full-page/presentation/factory/index.tsx b/apps/web/src/apps/modules/example/full-page/presentation/factory/index.tsx new file mode 100644 index 0000000..d3fdc07 --- /dev/null +++ b/apps/web/src/apps/modules/example/full-page/presentation/factory/index.tsx @@ -0,0 +1,43 @@ +import { lazy } from 'react'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import { EnterpriseModuleProvider } from '@repo/ui/foundations'; +import { registerModuleNamespace } from '@repo/core-i18n'; +import { FullPageModuleConfig } from '../../domain/constants'; +import { fullPageDataService } from '../../domain/factories'; +import { FullPageEntity } from '../../domain/entities'; + +import fullPageId from '../locales/id/full-page.json'; +import fullPageEn from '../locales/en/full-page.json'; + +const IndexPage = lazy(() => import('../pages/full-page.page.index')); +const FormPage = lazy(() => import('../pages/full-page.page.form')); +const DetailPage = lazy(() => import('../pages/full-page.page.detail')); + +// --------------------------------------------------------------------------- +// Namespace Registration (Module Scope) +// --------------------------------------------------------------------------- +// Called once at import time — safe, idempotent, outside React render cycle. +// The namespace 'full-page' must match config.translationNamespace. +registerModuleNamespace(FullPageModuleConfig.translationNamespace, { + id: fullPageId, + en: fullPageEn, +}); + +// --------------------------------------------------------------------------- +// Module Factory +// --------------------------------------------------------------------------- +export default function FullPageModule() { + return ( + config={FullPageModuleConfig} dataServices={fullPageDataService}> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); +} diff --git a/apps/web/src/apps/modules/example/full-page/presentation/locales/en/full-page.json b/apps/web/src/apps/modules/example/full-page/presentation/locales/en/full-page.json new file mode 100644 index 0000000..fe973f1 --- /dev/null +++ b/apps/web/src/apps/modules/example/full-page/presentation/locales/en/full-page.json @@ -0,0 +1,9 @@ +{ + "title": "Full Page Management", + "fields": { + "status": "Status", + "name": "Name", + "code": "Code", + "description": "Description" + } +} \ No newline at end of file diff --git a/apps/web/src/apps/modules/example/full-page/presentation/locales/id/full-page.json b/apps/web/src/apps/modules/example/full-page/presentation/locales/id/full-page.json new file mode 100644 index 0000000..0d7d6ac --- /dev/null +++ b/apps/web/src/apps/modules/example/full-page/presentation/locales/id/full-page.json @@ -0,0 +1,9 @@ +{ + "title": "Manajemen Halaman Penuh", + "fields": { + "status": "Status", + "name": "Nama", + "code": "Kode", + "description": "Deskripsi" + } +} \ No newline at end of file diff --git a/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.detail.tsx b/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.detail.tsx new file mode 100644 index 0000000..d68059d --- /dev/null +++ b/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.detail.tsx @@ -0,0 +1,15 @@ +import { useEnterpriseModuleTranslationContext } from '@repo/ui/foundations'; + +export default function FullPagePageDetail() { + const { t } = useEnterpriseModuleTranslationContext(); + + return ( +
+
full-page.page.detail
+
{t('fields.code')}
+
{t('fields.name')}
+
{t('fields.status')}
+
{t('fields.description')}
+
+ ); +} diff --git a/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.form.tsx b/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.form.tsx new file mode 100644 index 0000000..c489d62 --- /dev/null +++ b/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.form.tsx @@ -0,0 +1,18 @@ +import { useEnterpriseModuleTranslationContext } from '@repo/ui/foundations'; + +export default function FullPagePageForm({ formPageType }: { formPageType: 'edit' | 'create' | 'duplicate' }) { + const { t } = useEnterpriseModuleTranslationContext(); + + return ( +
+
full-page.page.form
+
Form Type: {formPageType}
+
{t('fields.code')}
+
{t('fields.name')}
+
{t('fields.status')}
+
{t('fields.description')}
+
{t('validation:required')}
+
{t('common:save')}
+
+ ); +} diff --git a/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.index.tsx b/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.index.tsx new file mode 100644 index 0000000..480e528 --- /dev/null +++ b/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.index.tsx @@ -0,0 +1,102 @@ +import { Table, Box, Title, Paper } from '@repo/ui/components'; +import { PageActions } from '@repo/ui/components'; +import { + useEnterpriseModuleTranslationContext, + useEnterpriseModuleNavigationContext, +} from '@repo/ui/foundations'; +import { FullPageEntity } from '../../domain/entities'; +import { Edit2, Eye, Copy, Trash2 } from 'lucide-react'; +import { useCallback } from 'react'; + +// TODO: Replace this mock data with real API data loaded from DataService via useEnterpriseModuleDataServiceContext +const MOCK_DATA: FullPageEntity[] = [ + { id: '1', name: 'Dashboard Widget', code: 'WID-001', status: 'ACTIVE', description: 'Main dashboard widget' }, + { id: '2', name: 'Report Generator', code: 'REP-002', status: 'INACTIVE', description: 'Generates monthly reports' }, + { + id: '3', + name: 'User Management', + code: 'USR-003', + status: 'ACTIVE', + description: 'Manages user roles and permissions', + }, +]; + +export default function FullPagePageIndex() { + // Translation is scoped to ['full-page', 'common'] — no prefix needed for module keys + const { t } = useEnterpriseModuleTranslationContext(); + + const { navigateToDetail, navigateToEdit, navigateToDuplicate } = useEnterpriseModuleNavigationContext(); + + const getRowActions = useCallback( + (row: FullPageEntity) => [ + { + key: 'view', + label: t('common:view'), + icon: , + onClick: () => navigateToDetail(row.id as string), + }, + { + key: 'edit', + label: t('common:edit'), + icon: , + onClick: () => navigateToEdit(row.id as string), + }, + { + key: 'duplicate', + label: t('common:duplicate'), + icon: , + onClick: () => navigateToDuplicate(row.id as string), + }, + { + type: 'divider' as const, + key: 'div-1', + }, + { + key: 'delete', + label: t('common:delete'), + icon: , + intent: 'destructive' as const, + onClick: () => { + // Mock delete action + alert(`Delete ${row.name}`); + }, + }, + ], + [t, navigateToDetail, navigateToEdit, navigateToDuplicate], + ); + + const rows = MOCK_DATA.map((item) => ( + + {item.code} + {item.name} + {item.status} + {item.description} + + + + + )); + + return ( + + + {t('title')} + + + + + + + {t('fields.code')} + {t('fields.name')} + {t('fields.status')} + {t('fields.description')} + {t('common:edit')} + + + {rows} +
+
+
+ ); +} diff --git a/apps/web/src/apps/modules/index.tsx b/apps/web/src/apps/modules/index.tsx index f36fdfb..dba5012 100644 --- a/apps/web/src/apps/modules/index.tsx +++ b/apps/web/src/apps/modules/index.tsx @@ -1,13 +1,17 @@ import { lazy } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; +import ModuleLayout from './layouts/module.layout'; -const ExamplePage = lazy(() => import('./example/example.page')); +const FullPageModule = lazy(() => import('./example/full-page/presentation/factory')); export default function AppModule() { return ( - - } /> - } /> - + + + } /> + } /> + } /> + + ); } diff --git a/apps/web/src/apps/modules/layouts/components/header.layout.tsx b/apps/web/src/apps/modules/layouts/components/header.layout.tsx new file mode 100644 index 0000000..a240641 --- /dev/null +++ b/apps/web/src/apps/modules/layouts/components/header.layout.tsx @@ -0,0 +1,35 @@ +import { useTranslation } from '@repo/core-i18n'; +import { Burger, Group, Select, Text, useCoreAppShell } from '@repo/ui/components'; +import { Globe } from 'lucide-react'; +import { AppStorageKey, secureStorage } from '../../../../core/storage/local'; + +export default function HeaderLayout() { + const { i18n } = useTranslation(); + const { mobileOpened, toggleMobile } = useCoreAppShell(); + return ( + + + + + Mock Header (bg="green.1") + +