Reorganize app workspaces and align Admin architecture
This commit is contained in:
@@ -1,79 +1,9 @@
|
||||
# Admin App Documentation
|
||||
|
||||
The admin app is a reusable React admin panel template. It includes the app shell, layouts, shared infrastructure, built-in core modules, development tools, and generated project modules.
|
||||
|
||||
Source root:
|
||||
|
||||
```txt
|
||||
apps/admin/
|
||||
```
|
||||
|
||||
Frontend architecture guide:
|
||||
|
||||
```txt
|
||||
docs/apps-architecture/react-frontend/Readme.md
|
||||
```
|
||||
|
||||
## Current Purpose
|
||||
|
||||
This page is a documentation stub. The admin app will be documented in more detail while its modules are migrated or aligned to the shared React frontend architecture.
|
||||
|
||||
For now, use this page to define ownership and the target documentation structure.
|
||||
The admin app is a reusable React admin panel template.
|
||||
It includes the app shell, layouts, shared infrastructure,
|
||||
built-in core modules, development tools, and generated project modules.
|
||||
|
||||
## Architecture Direction
|
||||
|
||||
`apps/admin` follows the general React architecture described in [React Frontend Architecture](../../apps-architecture/react-frontend/Readme.md).
|
||||
|
||||
Because the admin app is a template and module host, the architecture applies at multiple levels:
|
||||
|
||||
1. App shell architecture under `apps/admin/src/app` and `apps/admin/src/shared`.
|
||||
2. Core module architecture under `apps/admin/src/core/*`.
|
||||
3. Generated or project module architecture under `apps/admin/src/modules/*`.
|
||||
|
||||
Large modules should be structured like small apps inside their own folder:
|
||||
|
||||
```txt
|
||||
ModuleName/
|
||||
config/
|
||||
data/
|
||||
entities/
|
||||
providers/
|
||||
pages/
|
||||
shared/
|
||||
view/
|
||||
index.ts
|
||||
```
|
||||
|
||||
Small modules may start with fewer folders. Add layers when the module actually owns API calls, entities, providers, pages, UI blocks, or module-only helpers.
|
||||
|
||||
## Source Areas
|
||||
|
||||
| Area | Path | Purpose |
|
||||
| ----------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| App shell | `apps/admin/src/app/` | App routing, providers, layouts, i18n, styles, and global configuration. |
|
||||
| Infrastructure | `apps/admin/src/infrastructure/` | HTTP, React Query helpers, storage adapters, and technical error handling shared by modules. |
|
||||
| Reference data | `apps/admin/src/refdata/` | Common API-backed datasets and option lists that are not owned by a single feature module. |
|
||||
| Core modules | `apps/admin/src/core/` | Built-in admin template modules such as File Manager, auth, account, permissions, dashboard, and admin management. |
|
||||
| Generated modules | `apps/admin/src/modules/` | Project-specific or generated business modules. |
|
||||
| Devtools | `apps/admin/src/devtools/` | Developer tools documented in the [Devtools guide](../../devtools/Readme.md). |
|
||||
| Shared frontend | `apps/admin/src/shared/` | Admin-wide reusable UI, widgets, forms, editors, hooks, and pure utilities. |
|
||||
|
||||
## Module Documentation Plan
|
||||
|
||||
Each core module or generated module should eventually have documentation that covers:
|
||||
|
||||
1. Module purpose.
|
||||
2. Source path.
|
||||
3. Routes and pages.
|
||||
4. Data layer API and query ownership.
|
||||
5. Entities and DTO mapping.
|
||||
6. Providers, stores, and local state ownership.
|
||||
7. View widgets, forms, and reusable components.
|
||||
8. Backend integration, if the module has backend routes.
|
||||
9. Known migration gaps from the target architecture.
|
||||
|
||||
## Current Module Docs
|
||||
|
||||
| Module | Source | Docs |
|
||||
| ------------ | ---------------------------------- | -------------------------------------------------------------------- |
|
||||
| File Manager | `apps/admin/src/core/FileManager/` | [File Manager frontend](../../core-modules/file-manager/frontend.md) |
|
||||
`apps/admin` follows the general React architecture described in [React Frontend Architecture](./architecture/Readme.md).
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# CRUX React Admin Panel Architecture
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Disclaimer](#disclaimer)
|
||||
- [Core Principles](#core-principles)
|
||||
- [Recommended Layering](#recommended-layering)
|
||||
- [Architecture Scope](#architecture-scope)
|
||||
- [Entities Layer](#1-entities-layer)
|
||||
- [Data Layer (Data Access Layer)](#2-data-layer-data-access-layer)
|
||||
- [Purpose](#purpose)
|
||||
- [Responsibilities](#responsibilities-2)
|
||||
- [Rules and Guidelines](#rules-and-guidelines)
|
||||
- [Infrastructure Layer](#infrastructure-layer)
|
||||
- [Reference Data Layer](#reference-data-layer)
|
||||
- [App Layer](#3-app-layer)
|
||||
- [Pages Layer](#4-pages-layer)
|
||||
- [View Layer](#5-view-layer)
|
||||
- [view/components/](#viewcomponents)
|
||||
- [view/forms/](#viewforms)
|
||||
- [view/widgets/](#viewwidgets)
|
||||
- [Shared Layer](#6-shared-layer)
|
||||
- [Responsibilities](#responsibilities-4)
|
||||
- [Rules](#rules-2)
|
||||
- [Full documentation](#full-documentation)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This architecture guide is intended as a **recommendation**, not a strict rulebook. **Feel free to adapt and modify it based on your team’s needs and project context**.
|
||||
|
||||
Key points to keep in mind:
|
||||
|
||||
1. **Maintainability comes first**.
|
||||
The main goal of this structure is to help someone new to the project quickly understand where to make changes.
|
||||
It’s about finding the right place to touch the code now, rather than strictly following a pattern for its own sake.
|
||||
**Ultimately, the main goal is to make development easier for everyone**
|
||||
2. **Naming debates are inevitable**.
|
||||
Any architecture guide may spark discussions about file, folder, or function naming. Use this guide as a reference, but remember that practicality and clarity are more important than strict adherence.
|
||||
3. **Flexibility is allowed**.
|
||||
- You can adjust layers, module structure, or naming conventions if it improves readability or reduces complexity.
|
||||
- New patterns or exceptions can be added as the project evolves.
|
||||
|
||||
In short: this guide exists to make the project easier to understand and maintain, not to create unnecessary friction. Use judgment and prioritize clarity over rigid consistency.
|
||||
|
||||
---
|
||||
|
||||
Цей посібник з архітектури є **рекомендацією**, а не суворим зводом правил. **Не соромтеся адаптувати та змінювати його відповідно до потреб вашої команди та контексту проєкту**.
|
||||
|
||||
Ключові моменти, які варто мати на увазі:
|
||||
|
||||
1. **Насамперед — зручність підтримки**.
|
||||
Головна мета цієї структури — допомогти новому учаснику проєкту швидко зрозуміти, де вносити зміни.
|
||||
Йдеться про те, щоб знайти правильне місце в коді просто зараз, а не сліпо дотримуватися патерну заради самого патерну.
|
||||
**Зрештою, головна мета — зробити розробку простішою для всіх.**
|
||||
2. **Дискусії щодо назв неминучі**.
|
||||
Будь-який посібник з архітектури може спровокувати суперечки щодо назв файлів, папок або функцій. Використовуйте цей посібник як орієнтир, але пам'ятайте, що практичність і зрозумілість важливіші за суворе дотримання правил.
|
||||
3. **Гнучкість допускається**.
|
||||
- Ви можете коригувати шари, структуру модулів або угоди щодо найменування, якщо це покращує читабельність або зменшує складність.
|
||||
- У міру розвитку проєкту можна додавати нові патерни або винятки.
|
||||
|
||||
Коротко: цей посібник існує для того, щоб проєкт був зрозумілішим і простішим у підтримці, а не для того, щоб створювати зайві труднощі. Керуйтеся здоровим глуздом і надавайте перевагу ясності, а не жорсткій послідовності.
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. Blocks are composable and isolated
|
||||
|
||||
- Each block (component, form, widget) has a single responsibility.
|
||||
- Higher-level blocks (widgets, pages) compose lower-level blocks.
|
||||
|
||||
2. Data vs. UI separation
|
||||
|
||||
- `data` handles API, state, and business data.
|
||||
- `view` handles UI and interaction.
|
||||
- No API logic in UI components — UI calls hooks from `data`.
|
||||
|
||||
3. Entities are global types/interfaces
|
||||
|
||||
- Placing them in `entities`.
|
||||
- Any block (data hook, form, widget, page) can use these.
|
||||
|
||||
4. Shared
|
||||
|
||||
- shared/ = globally reusable components, hooks, utilities.
|
||||
|
||||
5. Infrastructure
|
||||
|
||||
- infrastructure/ = app-wide technical adapters such as HTTP clients, React Query, storage adapters, and formatting service wrappers.
|
||||
|
||||
6. Reference data
|
||||
- refdata/ = app-wide API-backed lookup datasets shared by multiple modules.
|
||||
|
||||
## Recommended Layering
|
||||
|
||||
```plaintext
|
||||
src/
|
||||
├── app/ # composition root: providers, routing, config, i18n, layouts
|
||||
├── infrastructure/ # technical adapters: HTTP, query, storage, error handling
|
||||
├── refdata/ # API-backed common reference datasets
|
||||
├── shared/ # reusable app-wide UI, hooks, utilities, and widgets
|
||||
├── core/ # built-in product modules
|
||||
├── modules/ # project-specific modules
|
||||
└── devtools/ # development-only modules and tools
|
||||
```
|
||||
|
||||
## Architecture Scope
|
||||
|
||||
The layer structure can be used at three levels:
|
||||
|
||||
| Scope | Example | Meaning |
|
||||
| --------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| App-level architecture | `apps/admin/src/app`, `apps/admin/src/shared` | Global app shell, providers, routing, layout, infrastructure, reusable app-wide building blocks. |
|
||||
| Core module architecture | `apps/admin/src/core/FileManager` | Built-in product modules shipped with the admin template. Each large core module can own its own `data`, `entities`, `pages`, `view`, and `shared`, plus feature-scoped context when needed. |
|
||||
| Project module architecture | `apps/admin/src/modules/*` | Generated or project-specific modules. Each module should follow the same layer rules inside its own folder when it becomes more than a simple page. |
|
||||
|
||||
For small modules, do not create empty layers only to match the template. Add a layer when the module has real ownership for that layer.
|
||||
|
||||
## Full documentation
|
||||
|
||||
- [Entities Layer](./layers/entities.md)
|
||||
- [Data Layer](./layers/data.md)
|
||||
- [App Layer](./layers/app.md)
|
||||
- [Infrastructure Layer](./layers/infrastructure.md)
|
||||
- [Reference Data Layer](./layers/refdata.md)
|
||||
- [View Layer](./layers/view.md)
|
||||
- [Shared Layer](./layers/shared.md)
|
||||
- [File & Folder Naming Convention](./naming.md)
|
||||
@@ -0,0 +1,83 @@
|
||||
# App Layer
|
||||
|
||||
## Purpose
|
||||
|
||||
The `app` layer is the composition root of the Admin application. It owns the concerns
|
||||
that exist once for the whole app and coordinates lower-level layers and feature
|
||||
modules without containing feature business logic.
|
||||
|
||||
Global React providers belong inside `app/providers/`. They are an implementation
|
||||
detail of the app layer, not a separate architectural layer.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- Compose global React providers in one root component.
|
||||
- Configure application-wide settings and environment-derived values.
|
||||
- Initialize internationalization and global styles.
|
||||
- Define top-level routing and connect routes registered by modules.
|
||||
- Provide application layouts, navigation, and menu composition.
|
||||
- Register core, project, and development modules with the app shell.
|
||||
|
||||
## Current Structure
|
||||
|
||||
```plaintext
|
||||
app/
|
||||
├── config/ # global settings, environment values, and config types
|
||||
├── css/ # global styles and app-level third-party overrides
|
||||
├── i18n/ # languages, messages, and internationalization config
|
||||
├── layouts/ # top-level Admin layout variants
|
||||
├── providers/ # providers composed once for the entire application
|
||||
│ ├── AppProviders.tsx
|
||||
│ ├── I18nProvider.tsx
|
||||
│ ├── QueryProvider.tsx
|
||||
│ ├── SettingsProvider.tsx
|
||||
│ └── ThemeProvider.tsx
|
||||
├── routing/
|
||||
│ ├── AppRouting.tsx
|
||||
│ └── AppRoutingSetup.tsx
|
||||
├── services/
|
||||
│ └── modules/ # module registry, routes, menus, and messages
|
||||
└── menu.tsx # app-level menu composition
|
||||
```
|
||||
|
||||
The exact files may evolve, but their ownership should remain app-wide. A concern
|
||||
used by only one feature belongs to that feature module instead.
|
||||
|
||||
## Global Providers
|
||||
|
||||
`app/providers/AppProviders.tsx` is the single provider composition point. The
|
||||
current provider tree supplies:
|
||||
|
||||
- TanStack Query configuration and error handling
|
||||
- authentication context from the Auth core module
|
||||
- application settings
|
||||
- shared tooltip and notification UI
|
||||
- theme state
|
||||
- internationalization and text direction
|
||||
- React Router with the configured base URL
|
||||
|
||||
## Routing
|
||||
|
||||
`app/routing/` owns top-level route composition. It connects the application shell
|
||||
to routes contributed by registered modules. Feature pages and route-specific logic
|
||||
remain inside their owning modules.
|
||||
|
||||
## Configuration and Internationalization
|
||||
|
||||
- `app/config/` owns global settings, application metadata, and configuration types.
|
||||
- `app/i18n/` owns supported languages, translated messages, and i18n configuration.
|
||||
- Environment access should be centralized through app configuration instead of
|
||||
being repeated throughout feature code.
|
||||
|
||||
## Layouts and Global CSS
|
||||
|
||||
`app/layouts/` contains the Admin shell variants. `app/css/` contains global styles,
|
||||
layout styles, and app-wide overrides for third-party components. Feature-specific
|
||||
styles should stay with the feature that owns them.
|
||||
|
||||
## Module Registration
|
||||
|
||||
`app/services/modules/` integrates modules into the application through their
|
||||
routes, menus, and translated messages. It is integration code owned by the app
|
||||
shell; module implementation details remain under `core/`, `modules/`, or
|
||||
`devtools/`.
|
||||
@@ -0,0 +1,260 @@
|
||||
# Data Layer
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Data Layer](#data-layer)
|
||||
- [Purpose](#purpose)
|
||||
- [Responsibilities](#responsibilities)
|
||||
- [Data Layer Organization Approaches](#data-layer-organization-approaches)
|
||||
- [By Module](#by-module)
|
||||
- [By Entity](#by-entity)
|
||||
- [Data Flow](#data-flow)
|
||||
- [DTO Types](#1-dto-types)
|
||||
- [API Layer](#2-api-layer)
|
||||
- [Mapper](#3-mapper)
|
||||
- [Mapper Testing](#mapper-testing)
|
||||
- [Query Hooks](#4-query-hooks)
|
||||
- [Pagination](#pagination)
|
||||
|
||||
## Purpose
|
||||
|
||||
The `data` layer is responsible for **all data access and data transformation** in the application.
|
||||
|
||||
It is the only layer that knows:
|
||||
|
||||
- how to call APIs
|
||||
- how to map API responses (DTOs) to application entities
|
||||
- how to handle caching, queries, and mutations
|
||||
|
||||
Higher layers (`forms`, `widgets`, `pages`) **do not interact with APIs directly**.
|
||||
Instead, they consume **hooks provided by the data layer**.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
The `data/` layer is responsible for:
|
||||
|
||||
- performing **API calls**
|
||||
- defining **DTO types (Data Transfer Objects)**
|
||||
- converting DTOs → **entities** using **mappers**
|
||||
- providing **React Query hooks**
|
||||
- handling **side effects** (requests, caching, errors)
|
||||
- defining **API-related types**
|
||||
|
||||
## Data Layer Organization Approaches
|
||||
|
||||
```plaintext
|
||||
data/
|
||||
| --- user/
|
||||
| | --- api.ts # user-related API functions (getUsers, createUser, etc.)
|
||||
| | --- hooks.ts # React Query hooks (useUsersQuery, useCreateUserMutation)
|
||||
| | --- dto.ts # API DTO types for user
|
||||
| | --- mapper.ts # map user DTO → entity
|
||||
| | --- types.ts # API types/interfaces
|
||||
|
|
||||
| --- pagination/
|
||||
| | --- dto.ts
|
||||
| | --- mapper.ts
|
||||
| | --- types.ts
|
||||
```
|
||||
|
||||
There are two main ways to structure the `data` layer:
|
||||
|
||||
- By Module — grouped by type of file (API, hooks, DTOs, mappers, types)
|
||||
- By Entity — grouped by business feature/entity (user, product, message)
|
||||
|
||||
| Approach | Best For | Pros | Cons |
|
||||
| --------- | ------------------------------- | -------------------------------------------- | ---------------------------------------------- |
|
||||
| By Module | Small apps, simple data layers | Easy to find all API/hooks/DTOs in one place | Files for one feature scattered; hard to scale |
|
||||
| By Entity | Medium/large apps, scalable CRM | Encapsulated, modular, easier to maintain | Harder to see all APIs/hooks globally |
|
||||
|
||||
### By Module
|
||||
|
||||
```plaintext
|
||||
data/
|
||||
| --- api/ # API request functions
|
||||
| | --- userAPI.ts
|
||||
| | --- productAPI.ts
|
||||
| | --- messageAPI.ts
|
||||
|
|
||||
| --- hooks/ # React Query hooks or other data hooks
|
||||
| | --- useUserQuery.ts
|
||||
| | --- useCreateProductMutation.ts
|
||||
| | --- useMessages.ts
|
||||
|
|
||||
| --- dto/ # API DTO types
|
||||
| | --- userDTO.ts
|
||||
| | --- productDTO.ts
|
||||
| | --- messageDTO.ts
|
||||
|
|
||||
├ --- mappers/ # DTO → entity mappers
|
||||
| | --- userMapper.ts
|
||||
| | --- productMapper.ts
|
||||
|
|
||||
| --- types/ # API types/interfaces
|
||||
```
|
||||
|
||||
### By Entity
|
||||
|
||||
```plaintext
|
||||
data/
|
||||
| --- user/
|
||||
| | --- api.ts # user-related API functions (getUsers, createUser, etc.)
|
||||
| | --- hooks.ts # React Query hooks (useUsersQuery, useCreateUserMutation)
|
||||
| | --- dto.ts # API DTO types for user
|
||||
| | --- mapper.ts # map user DTO → entity
|
||||
| | --- types.ts # API types/interfaces (optional)
|
||||
|
|
||||
| --- product/
|
||||
| | --- api.ts
|
||||
| | --- hooks.ts
|
||||
| | --- dto.ts
|
||||
| | --- mapper.ts
|
||||
| | --- types.ts
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### 1. DTO Types
|
||||
|
||||
```ts
|
||||
export type UserDTO = {
|
||||
id: number;
|
||||
full_name: string;
|
||||
email_address: string;
|
||||
};
|
||||
export type CreateUserDTO = {
|
||||
full_name: string;
|
||||
email_address: string;
|
||||
};
|
||||
```
|
||||
|
||||
### 2. API Layer
|
||||
|
||||
API functions or class methods perform HTTP requests. API always returns DTO objects.
|
||||
|
||||
```ts
|
||||
export class UserApi {
|
||||
private http: HttpClient;
|
||||
|
||||
async getDetailed(id: number): Promise<UserDTO> {
|
||||
return this.http.get(`/users/${id}`);
|
||||
}
|
||||
|
||||
async getList(): Promise<UserDTO[]> {
|
||||
return this.http.get(`/users`);
|
||||
}
|
||||
|
||||
async create(dto: CreateUserDTO): Promise<UserDTO> {
|
||||
return this.http.post(`/users`, dto);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Mapper
|
||||
|
||||
Mappers convert `DTO` to `Entity`
|
||||
|
||||
```ts
|
||||
import { User } from "@/entities/user";
|
||||
|
||||
export class UserMapper {
|
||||
toEntity(dto: UserDTO): User {
|
||||
return {
|
||||
id: dto.id,
|
||||
name: dto.full_name,
|
||||
email: dto.email_address,
|
||||
};
|
||||
}
|
||||
|
||||
toEntities(dtos: UserDTO[]): User[] {
|
||||
return dtos.map((dto) => this.toEntity(dto));
|
||||
}
|
||||
|
||||
toPaginated(dto: PaginatedDataDTO<UserDTO[]>): PaginatedResult<User[]> => {
|
||||
return {
|
||||
meta: this.paginationMetaMapper.toMeta(dto.meta),
|
||||
data: this.toEntities(dto.data),
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Mapper Testing
|
||||
|
||||
Mappers are part of the data layer contract and should have unit tests.
|
||||
|
||||
Mapper tests are required when a mapper:
|
||||
|
||||
- renames fields from API DTOs to app entities
|
||||
- normalizes values, paths, dates, sizes, statuses, enums, or defaults
|
||||
- maps nested DTOs through other mappers
|
||||
- maps arrays, paginated data, or optional API fields
|
||||
- protects the app from backend response shape changes
|
||||
|
||||
These tests should focus only on transformation behavior. Do not mock React, render components, or call real APIs in mapper tests.
|
||||
|
||||
Recommended test coverage:
|
||||
|
||||
- `toEntity` maps a representative DTO to the expected entity
|
||||
- `toEntities` maps arrays consistently
|
||||
- optional or missing DTO fields use the correct defaults
|
||||
- nested mappers preserve the expected entity shape
|
||||
- enum/status casts are covered with expected values
|
||||
|
||||
For apps that keep tests outside production source, use a test folder at the app root:
|
||||
|
||||
```plaintext
|
||||
apps/admin/
|
||||
├── src/
|
||||
│ └── core/FileManager/data/mappers/entryMapper.ts
|
||||
└── tests/
|
||||
└── unit/core/FileManager/data/mappers/entryMapper.test.ts
|
||||
```
|
||||
|
||||
### 4. Query Hooks
|
||||
|
||||
Hooks combine API + mapper + React Query.
|
||||
|
||||
```ts
|
||||
export const useUserDetailed = (id: number) => {
|
||||
const api = new UserApi(http);
|
||||
const mapper = new UserMapper();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["user", id],
|
||||
queryFn: async () => {
|
||||
const dto = await api.getDetailed(id);
|
||||
return mapper.toEntity(dto);
|
||||
},
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
Pagination is not **entity-specific**, so it should live in a shared data module.
|
||||
|
||||
```ts
|
||||
export type PaginatedMetaDTO = {
|
||||
total: number;
|
||||
perPage: number;
|
||||
currentPage: number;
|
||||
lastPage: number;
|
||||
};
|
||||
|
||||
export type PaginatedDTO<T> = {
|
||||
data: T;
|
||||
meta: PaginatedMetaDTO;
|
||||
};
|
||||
|
||||
export class PaginationMapper {
|
||||
toMeta(dto: PaginatedMetaDTO) {
|
||||
return {
|
||||
total: dto.total,
|
||||
perPage: dto.perPage,
|
||||
currentPage: dto.currentPage,
|
||||
lastPage: dto.lastPage,
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,53 @@
|
||||
# Entities
|
||||
|
||||
## Purpose
|
||||
|
||||
The `entities` layer defines the **core domain models of the application**.
|
||||
|
||||
Entities represent **business objects** used throughout the system, such as `User`, `Product`, `Project`, or `Message`.
|
||||
|
||||
This layer contains **pure type definitions** that describe how data is structured inside the application.
|
||||
Entities are **stable, reusable models** that can be used across any layer.
|
||||
|
||||
```plaintext
|
||||
entities/
|
||||
| --- user.ts # { id: string; name: string; email: string }
|
||||
| --- product.ts # { id: string; name: string; price: number }
|
||||
| --- message.ts # { id: string; content: string; createdAt: string }
|
||||
| --- enums.ts # { UserRole, ProductCategory }
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```plaintext
|
||||
entities/
|
||||
| --- user/
|
||||
| | --- interfaces.ts # { id: string; name: string; email: string }
|
||||
| | --- types.ts # { id: string; name: string; email: string }
|
||||
| | --- enums.ts # { UserRole }
|
||||
| | --- rules.ts # MIN_ALLOWED_AGE = 18
|
||||
| | --- index.ts # export all entity types
|
||||
| --- product.ts # { id: string; name: string; price: number }
|
||||
| --- message.ts # { id: string; content: string; createdAt: string }
|
||||
```
|
||||
|
||||
## Responsibilities
|
||||
|
||||
The entities layer is responsible for:
|
||||
|
||||
- defining **domain models**
|
||||
- defining **entity-related enums or constants**
|
||||
- providing **shared types used across the application**
|
||||
|
||||
```ts
|
||||
export type User = {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export enum UserRole {
|
||||
ADMIN = "admin",
|
||||
SIMLPE_JACK = "simple_jack",
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,106 @@
|
||||
# Infrastructure Layer
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Purpose](#purpose)
|
||||
- [Responsibilities](#responsibilities)
|
||||
- [Recommended Structure](#recommended-structure)
|
||||
- [Rules](#rules)
|
||||
- [Examples](#examples)
|
||||
|
||||
## Purpose
|
||||
|
||||
The `infrastructure/` layer contains app-wide technical adapters and integration helpers.
|
||||
|
||||
Use this layer for code that connects the frontend application to technical systems or shared runtime services, not for feature-specific business logic.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
The `infrastructure/` layer is responsible for:
|
||||
|
||||
- HTTP clients and request/response interceptors
|
||||
- React Query shared helpers, response types, query key helpers, and error normalization
|
||||
- storage, browser, or runtime adapters
|
||||
- shared formatting/service adapters such as date or name formatting services
|
||||
- app-wide technical configuration wrappers
|
||||
|
||||
## Recommended Structure
|
||||
|
||||
```plaintext
|
||||
infrastructure/
|
||||
├── axios/
|
||||
│ ├── index.ts # configured axios client
|
||||
│ ├── interceptors.ts # request/response interceptors
|
||||
│ └── axios.d.ts # axios type augmentation
|
||||
│
|
||||
├── reactQuery/
|
||||
│ ├── index.ts
|
||||
│ ├── errors.ts # query error handling helpers
|
||||
│ ├── helpers.ts # query key and pagination helpers
|
||||
│ ├── queryParams.ts # table/query-param mapping helpers
|
||||
│ └── types.ts # shared API response/query types
|
||||
│
|
||||
├── IntlDate.ts # date formatting service adapter
|
||||
└── IntlName.ts # name formatting service adapter
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
1. Keep infrastructure feature-agnostic.
|
||||
|
||||
- No module-specific business rules
|
||||
- No page or component composition
|
||||
- No entity mapping that belongs in a module `data/mapper`
|
||||
|
||||
2. API modules should use infrastructure clients instead of creating their own clients.
|
||||
|
||||
```ts
|
||||
import { axiosClient } from "#infrastructure/axios";
|
||||
```
|
||||
|
||||
3. React Query helpers that are shared across modules belong here.
|
||||
|
||||
Examples: query key builders, list response types, pagination helpers, error extraction helpers.
|
||||
|
||||
4. UI layers must not call raw HTTP clients directly.
|
||||
|
||||
View and page code should call data-layer hooks. Direct `#infrastructure/axios` usage belongs in API/data modules, not in components.
|
||||
|
||||
5. Generic formatting adapters may be consumed by UI when they are not tied to a feature.
|
||||
|
||||
Examples: `IntlDate`, `IntlName`.
|
||||
|
||||
6. Prefer dependency injection for app-state coupling.
|
||||
|
||||
If an interceptor needs auth or query-client behavior, keep that coupling narrow and documented. Avoid importing random feature modules into infrastructure.
|
||||
|
||||
## Examples
|
||||
|
||||
API modules use the configured HTTP client:
|
||||
|
||||
```ts
|
||||
import { axiosClient } from "#infrastructure/axios";
|
||||
|
||||
export class UserAPI {
|
||||
getDetailed(id: number) {
|
||||
return axiosClient.get(`/users/${id}`).then((response) => response.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Data/query modules use shared query helpers:
|
||||
|
||||
```ts
|
||||
import {
|
||||
createKeyBuilder,
|
||||
getPaginationInfo,
|
||||
} from "#infrastructure/reactQuery";
|
||||
|
||||
export const usersCacheKey = createKeyBuilder("USERS", "LIST");
|
||||
```
|
||||
|
||||
UI can consume generic app-wide formatters:
|
||||
|
||||
```ts
|
||||
import { IntlDate } from "#infrastructure/IntlDate";
|
||||
```
|
||||
@@ -0,0 +1,109 @@
|
||||
# Reference Data Layer
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Purpose](#purpose)
|
||||
- [Responsibilities](#responsibilities)
|
||||
- [Recommended Structure](#recommended-structure)
|
||||
- [Rules](#rules)
|
||||
- [Examples](#examples)
|
||||
|
||||
## Purpose
|
||||
|
||||
The `refdata/` layer contains app-wide reference datasets that are reused across modules.
|
||||
|
||||
Reference data is data that usually comes from the backend, but is not owned by one feature module.
|
||||
|
||||
Examples:
|
||||
|
||||
- countries
|
||||
- languages
|
||||
- currencies
|
||||
- time zones
|
||||
- common option dictionaries
|
||||
- shared lookup datasets
|
||||
|
||||
## Responsibilities
|
||||
|
||||
The `refdata/` layer is responsible for:
|
||||
|
||||
- fetching common reference datasets
|
||||
- exposing reusable query hooks for those datasets
|
||||
- defining cache keys for reference data
|
||||
- keeping app-wide lookup data out of feature modules
|
||||
|
||||
## Recommended Structure
|
||||
|
||||
For small datasets, a single file per dataset is enough:
|
||||
|
||||
```plaintext
|
||||
refdata/
|
||||
├── countries.ts
|
||||
├── languages.ts
|
||||
└── currencies.ts
|
||||
```
|
||||
|
||||
For larger datasets, use a folder:
|
||||
|
||||
```plaintext
|
||||
refdata/
|
||||
└── countries/
|
||||
├── api.ts
|
||||
├── dto.ts
|
||||
├── mapper.ts
|
||||
├── queries.ts
|
||||
└── keys.ts
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
1. Use `refdata/` only for app-wide datasets.
|
||||
|
||||
If a dataset belongs to one feature module, keep it in that module's `data/` layer.
|
||||
|
||||
2. `refdata/` may use infrastructure.
|
||||
|
||||
Reference data hooks can use `#infrastructure/axios` and `#infrastructure/reactQuery`.
|
||||
|
||||
3. `refdata/` should not import from `view/`, `pages/`, or feature modules.
|
||||
|
||||
It should remain reusable across the app.
|
||||
|
||||
4. Expose hooks or small service functions.
|
||||
|
||||
Consumers should not need to know backend endpoints or cache key details.
|
||||
|
||||
5. Add mappers when backend DTOs differ from app-facing reference data.
|
||||
|
||||
If a reference dataset performs DTO transformation, test the mapper the same way module data mappers are tested.
|
||||
|
||||
## Examples
|
||||
|
||||
```ts
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { axiosClient } from "#infrastructure/axios";
|
||||
import { createKeyBuilder } from "#infrastructure/reactQuery";
|
||||
|
||||
export const countriesCacheKey = createKeyBuilder(
|
||||
"REF_DATA",
|
||||
"COUNTRIES",
|
||||
"LIST",
|
||||
);
|
||||
|
||||
interface CountryData {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const useCountries = () => {
|
||||
return useQuery({
|
||||
queryKey: countriesCacheKey.store(),
|
||||
placeholderData: keepPreviousData,
|
||||
queryFn: async () => {
|
||||
return axiosClient
|
||||
.get<Array<CountryData>>("/datasets/countries")
|
||||
.then((data) => data.data);
|
||||
},
|
||||
});
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,252 @@
|
||||
# Shared Layer
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Purpose](#purpose)
|
||||
- [Recommended Structure](#recommended-structure)
|
||||
- [UI Components](#ui-components)
|
||||
- [Purpose](#ui-components-purpose)
|
||||
- [Core Principles](#ui-components-core-principles)
|
||||
1. [Simple Components](#ui-components-core-principles-1)
|
||||
2. [Composable Design](#ui-components-core-principles-2)
|
||||
3. [Easily Customizable](#ui-components-core-principles-3)
|
||||
4. [No Business Logic](#ui-components-core-principles-4)
|
||||
- [Styling Strategy](#ui-components-styling-strategy)
|
||||
- [Styling Philosophy](#ui-components-styling-strategy-philosophy)
|
||||
- [Technology Stack](#ui-components-styling-strategy-tech-stack)
|
||||
- [Hooks](#hooks)
|
||||
- [Utilities](#utilities)
|
||||
- [Libraries](#libraries)
|
||||
- [Assets](#assets)
|
||||
- [Styles](#styles)
|
||||
|
||||
## Purpose
|
||||
|
||||
The `shared` layer contains **global reusable building blocks used across the entire application**.
|
||||
The goal is to keep these utilities **framework-level and reusable across the entire application**.
|
||||
|
||||
```plaintext
|
||||
shared/
|
||||
├── ui/ # dumb or basic UI components
|
||||
│ ├── Button.tsx
|
||||
│ ├── Modal.tsx
|
||||
│ └── Card.tsx
|
||||
│
|
||||
├── widgets/ # reusable composed UI blocks
|
||||
│ ├── DataGrid/
|
||||
│ └── screens/
|
||||
│
|
||||
├── forms/ # reusable form composition helpers
|
||||
│ └── composed/
|
||||
│
|
||||
├── editors/ # reusable editor integrations
|
||||
│ ├── tinymce/
|
||||
│ └── image_input/
|
||||
│
|
||||
├── hooks/ # reusable hooks
|
||||
│ ├── useDebounce.ts
|
||||
│ ├── useToggle.ts
|
||||
│ └── useLocalStorage.ts
|
||||
│
|
||||
├── lib/ # pure helpers & utilities
|
||||
│ └── formatDate.ts
|
||||
│
|
||||
├── assets/ # images, icons, fonts
|
||||
│ ├── icons/
|
||||
│ └── fonts/
|
||||
│
|
||||
└── styles/ # global CSS, variables, Tailwind overrides
|
||||
├── globals.css
|
||||
└── variables.css
|
||||
```
|
||||
|
||||
## Recommended Structure
|
||||
|
||||
```plaintext
|
||||
shared/
|
||||
|
|
||||
| --- ui/ # primitive UI components
|
||||
|
|
||||
| --- widgets/ # reusable composed UI blocks
|
||||
|
|
||||
| --- forms/ # reusable form composition
|
||||
|
|
||||
| --- editors/ # reusable editor wrappers
|
||||
|
|
||||
| --- hooks/ # generic reusable hooks
|
||||
|
|
||||
| --- utils/ # pure helper functions
|
||||
|
|
||||
| --- lib/ # pure helpers and utility services
|
||||
|
|
||||
| --- assets/ # images, icons, fonts
|
||||
|
|
||||
| --- styles/ # global styling
|
||||
|
|
||||
| --- constants/ # global constants
|
||||
```
|
||||
|
||||
### UI Components
|
||||
|
||||
#### Purpose {#ui-components-purpose}
|
||||
|
||||
The goal of the UI layer is to provide simple, composable, and customizable components that can be reused across the application.
|
||||
|
||||
- components are simple building blocks
|
||||
- components are composable
|
||||
- components are fully customizable
|
||||
- components are not tied to business logic
|
||||
|
||||
UI components should behave like Lego blocks that can be combined into larger UI structures.
|
||||
|
||||
#### Core Principles {#ui-components-core-principles}
|
||||
|
||||
##### 1. Simple Components {#ui-components-core-principles-1}
|
||||
|
||||
Examples:
|
||||
|
||||
- Button
|
||||
- Input
|
||||
- Card
|
||||
- Modal
|
||||
- Dropdown
|
||||
|
||||
##### 2. Composable Design {#ui-components-core-principles-2}
|
||||
|
||||
Components must support composition.
|
||||
Composition allows developers to build complex UI without modifying base components.
|
||||
|
||||
Example:
|
||||
|
||||
```tsx
|
||||
<Card>
|
||||
<CardHeader />
|
||||
<CardContent />
|
||||
<CardFooter />
|
||||
</Card>
|
||||
```
|
||||
|
||||
```tsx
|
||||
<Button>
|
||||
<Icon />
|
||||
Save
|
||||
</Button>
|
||||
```
|
||||
|
||||
##### 3. Easily Customizable {#ui-components-core-principles-3}
|
||||
|
||||
UI components should allow customization through:
|
||||
|
||||
- className
|
||||
- variants
|
||||
- props
|
||||
- composition
|
||||
|
||||
```tsx
|
||||
<Button variant="outline" size="lg">
|
||||
Save
|
||||
</Button>
|
||||
```
|
||||
|
||||
##### 4. No Business Logic {#ui-components-core-principles-4}
|
||||
|
||||
UI components must remain presentation-only. May include local state.
|
||||
|
||||
They should NOT:
|
||||
|
||||
- call API
|
||||
- use React Query
|
||||
- contain form submission logic
|
||||
- contain domain-specific logic
|
||||
|
||||
#### Styling Strategy {#ui-components-styling-strategy}
|
||||
|
||||
##### Styling Philosophy {#ui-components-styling-strategy-philosophy}
|
||||
|
||||
Styling follows these principles:
|
||||
|
||||
- Utility-first styling
|
||||
- Design tokens for consistency
|
||||
- Component-level styling
|
||||
- Variants instead of style duplication
|
||||
- Minimal custom CSS
|
||||
|
||||
##### Technology Stack {#ui-components-styling-strategy-tech-stack}
|
||||
|
||||
- **TailwindCSS** - primary styling system
|
||||
- **class-variance-authority (CVA)** - component variants
|
||||
- **CSS variables** - design tokens
|
||||
- **Radix primitives** - accessible UI building blocks
|
||||
|
||||
Custom CSS should be minimal and centralized.
|
||||
|
||||
### Hooks
|
||||
|
||||
The `shared/hooks/` folder contains generic reusable hooks.
|
||||
These hooks must not depend on entities or business logic.
|
||||
|
||||
```plaintext
|
||||
shared/hooks/
|
||||
| --- useDebounce.ts
|
||||
| --- useToggle.ts
|
||||
| --- usePrevious.ts
|
||||
| --- useLocalStorage.ts
|
||||
```
|
||||
|
||||
### Utilities
|
||||
|
||||
The `shared/utils/` folder contains pure helper functions.
|
||||
These functions should be stateless and reusable
|
||||
|
||||
```plaintext
|
||||
shared/utils/
|
||||
| --- formatDate.ts
|
||||
| --- formatCurrency.ts
|
||||
| --- generateId.ts
|
||||
| --- sleep.ts
|
||||
```
|
||||
|
||||
### Libraries
|
||||
|
||||
The `shared/lib/` folder contains pure helpers and utility services.
|
||||
|
||||
Examples include:
|
||||
|
||||
- date formatting helpers
|
||||
- name formatting helpers
|
||||
- URL/file path helpers
|
||||
- phone-number formatting helpers
|
||||
|
||||
```plaintext
|
||||
shared/lib/
|
||||
| --- time/
|
||||
| --- phone_number/
|
||||
| --- resolveFileURL.ts
|
||||
```
|
||||
|
||||
### Assets
|
||||
|
||||
The `shared/assets/` folder stores static resources used across the application.
|
||||
|
||||
```plaintext
|
||||
shared/assets/
|
||||
| --- icons/
|
||||
| --- fonts/
|
||||
```
|
||||
|
||||
### Styles
|
||||
|
||||
The `shared/styles/` folder contains global styling resources.
|
||||
|
||||
Typical usage:
|
||||
|
||||
- CSS variables
|
||||
- Tailwind configuration extensions
|
||||
- global resets
|
||||
|
||||
```plaintext
|
||||
shared/styles/
|
||||
| --- globals.css
|
||||
| --- variables.css
|
||||
| --- tailwind.css
|
||||
```
|
||||
@@ -0,0 +1,345 @@
|
||||
# Views Layer
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Views Layer](#views-layer)
|
||||
- [Components](#components)
|
||||
- [Components Purpose](#components-purpose)
|
||||
- [Components Responsibilities](#components-responsibilities)
|
||||
- [What Components should NOT do](#what-components-should-not-do)
|
||||
- [Widgets](#widgets)
|
||||
- [Widget Purpose](#widget-purpose)
|
||||
- [Widget Characteristics](#widget-characteristics)
|
||||
- [Widget Responsibilities](#widget-responsibilities)
|
||||
- [Forms](#forms)
|
||||
- [Form Architecture](#form-architecture)
|
||||
- [Form Logic](#form-logic)
|
||||
- [Form Validation](#form-validation)
|
||||
- [Form Values](#form-values)
|
||||
- [Form Types](#form-types)
|
||||
- [Submit Functions](#submit-functions)
|
||||
- [Purpose](#submit-functions-purpose)
|
||||
- [API Example](#api-example)
|
||||
- [Responsibilities](#responsibilities)
|
||||
- [Composed Form Elements](#composed-form-elements)
|
||||
- [Example: InputSwitch](#example-inputswitch)
|
||||
- [Usage in a Form](#usage-in-a-form)
|
||||
|
||||
## Components
|
||||
|
||||
### Components Purpose
|
||||
|
||||
Components are reusable UI blocks used to build user interfaces.
|
||||
|
||||
They encapsulate presentation and simple UI behavior, but do not contain business logic or data fetching. `UserAvatar`, `ProductBadge`
|
||||
|
||||
Components are designed to be small, composable, and reusable across multiple parts of the application.
|
||||
|
||||
### Components Responsibilities:
|
||||
|
||||
- render UI
|
||||
- accept props
|
||||
- handle simple UI state (hover, toggle, open/close)
|
||||
- compose other components
|
||||
|
||||
### What **Components** should NOT do
|
||||
|
||||
Components should not contain logic such as:
|
||||
|
||||
- API calls
|
||||
- mutations
|
||||
- complex state management
|
||||
- business workflows
|
||||
|
||||
## Widgets
|
||||
|
||||
### Widget Purpose
|
||||
|
||||
Widgets are **self-contained UI blocks that implement a specific piece of functionality**.
|
||||
|
||||
Unlike simple components, widgets combine **UI, logic, and interactions** to deliver a complete user-facing feature.
|
||||
|
||||
Widgets are typically used to **compose pages** and represent meaningful parts of the interface such as panels, sections, or complex interactive blocks. Widgets should remain modular building blocks.
|
||||
|
||||
### Widget Characteristics
|
||||
|
||||
A widget usually:
|
||||
|
||||
- encapsulates **UI and feature logic**
|
||||
- composes **multiple components**
|
||||
- may include **forms, dialogs, or actions**
|
||||
- can interact with **data**
|
||||
- exposes a **simple API for pages**
|
||||
|
||||
Widgets should hide their internal implementation and provide a **clean interface** to the outside.
|
||||
|
||||
### Widget Responsibilities
|
||||
|
||||
Widgets may:
|
||||
|
||||
- orchestrate UI interactions
|
||||
- manage local UI state
|
||||
- connect to features or data hooks
|
||||
- combine multiple components/forms into a functional block
|
||||
- may fetch data or rely on data passed in props
|
||||
- A **widget** can use another **widget** if:
|
||||
- the nested widget represents a clear functional sub-block
|
||||
- the parent widget is grouping or orchestrating UI
|
||||
- the dependency does not create tight coupling
|
||||
|
||||
```ts
|
||||
export const ProjectOverviewWidget = ({ projectId }: Props) => {
|
||||
return (
|
||||
<div>
|
||||
<ProjectStatsWidget projectId={projectId} />
|
||||
<ProjectActivityWidget projectId={projectId} />
|
||||
<ProjectMembersWidget projectId={projectId} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Forms
|
||||
|
||||
### Form Architecture
|
||||
|
||||
```plaintext
|
||||
view/forms/SomeForm/
|
||||
|
||||
| --- form.ts
|
||||
| --- Component.tsx
|
||||
| --- submit.ts
|
||||
| --- constants.ts # Optional
|
||||
| --- schema.ts # Optional validation schema. Good for large forms or multistep forms
|
||||
| --- index.ts
|
||||
```
|
||||
|
||||
| File | Responsibility |
|
||||
| --------------- | ---------------------------------- |
|
||||
| `form.ts` | validation schema, types, defaults |
|
||||
| `Component.tsx` | UI + react-hook-form integration |
|
||||
| `submit.ts` | Submission logic |
|
||||
| `constants.ts` | form IDs, UI constants |
|
||||
| `index.ts` | public exports |
|
||||
|
||||
### Form Logic
|
||||
|
||||
`form.ts` contains pure form logic.
|
||||
|
||||
It should include:
|
||||
|
||||
- validation schema
|
||||
- form types
|
||||
- default values
|
||||
- helper templates
|
||||
|
||||
#### Form Validation
|
||||
|
||||
```ts
|
||||
export const validationSchema = z.object({
|
||||
first_name: z.string().min(3),
|
||||
last_name: z.string().min(3),
|
||||
});
|
||||
```
|
||||
|
||||
If you use `i18n` you can create function that returns validation schema.
|
||||
|
||||
```ts
|
||||
function createValidationSchema(
|
||||
translationFn: TFunction<"translation", undefined>, // Depend on libraty you use
|
||||
) {
|
||||
return z.object({
|
||||
first_name: z.string({ message: translationFn("required") }),
|
||||
last_name: z.string({ message: translationFn("required") }),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
> [! WARNING]
|
||||
> Dont include `i18n` singleton last_name: z.string().min(1, i18n.t('required'))
|
||||
> It will not translate errors when language state change
|
||||
|
||||
#### Form Values
|
||||
|
||||
Default values should be defined using factory functions.
|
||||
Avoid inline default objects.
|
||||
|
||||
```ts
|
||||
export function createFormValues(): FormValues {
|
||||
return {
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
export function toFormValues(data: SomeData): FormValues {
|
||||
return {
|
||||
first_name: data?.first_name ?? "",
|
||||
last_name: data?.last_name ?? "",
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### Form Types
|
||||
|
||||
Define types for form values and submit functiuons
|
||||
|
||||
```ts
|
||||
export type FormValues = z.infer<typeof validationSchema>;
|
||||
export type SubmitFunc = (
|
||||
values: FormValues,
|
||||
handlers: UseFormReturn<FormValues>,
|
||||
) => void;
|
||||
```
|
||||
|
||||
#### Submit Functions
|
||||
|
||||
##### Purpose {#submit-functions-purpose}
|
||||
|
||||
The `useSubmit` hook standardizes form submission logic:
|
||||
|
||||
- Handles create and update operations
|
||||
- Integrates with `data/` layer hooks (`useCreate`, `useUpdate`)
|
||||
- Automatically handles validation errors from the API
|
||||
- Invalidates lists or detailed queries to refresh UI
|
||||
- Exposes a unified API for forms
|
||||
|
||||
This pattern allows keep submission logic centralized.
|
||||
|
||||
##### API Example
|
||||
|
||||
```ts
|
||||
interface Params {
|
||||
onSuccess?: (id?: number) => void;
|
||||
}
|
||||
export const useSubmit = (params?: Params) => {
|
||||
const onSuccess = params?.onSuccess;
|
||||
|
||||
const { mutate: createMutation, isPending: isCreatePending } = useCreate();
|
||||
const { mutate: updateMutation, isPending: isUpdateending } = useUpdate();
|
||||
|
||||
const handleSubmit: SubmitFunc = (values, handlers) => {
|
||||
if (values.id) {
|
||||
handleUpdate(values, handlers);
|
||||
} else {
|
||||
handleCreate(values, handlers);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate: SubmitFunc = (values, handlers) => {
|
||||
createMutation(values, {
|
||||
onSuccess() {
|
||||
invalidateList();
|
||||
if (typeof onSuccess === "function") onSuccess();
|
||||
},
|
||||
onError(error) {
|
||||
extractValidationErrors(error).forEach((e) => {
|
||||
handlers.setError(e.field as keyof typeof values, {
|
||||
type: "apiError",
|
||||
message: e.message,
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdate: SubmitFunc = (values, handlers) => {
|
||||
if (!values.id) return;
|
||||
|
||||
updateMutation(
|
||||
{ ...values, id: values.id },
|
||||
{
|
||||
onSuccess() {
|
||||
invalidateList();
|
||||
if (values.id) invalidateDetailed(values.id);
|
||||
if (typeof onSuccess === "function") {
|
||||
onSuccess(values.id ?? undefined);
|
||||
}
|
||||
},
|
||||
onError(error) {
|
||||
extractValidationErrors(error).forEach((e) => {
|
||||
handlers.setError(e.field as keyof typeof values, {
|
||||
type: "apiError",
|
||||
message: e.message,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
submit: handleSubmit,
|
||||
isSubmitting: isCreatePending || isUpdateending,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
##### Responsibilities
|
||||
|
||||
1. Determine operation type
|
||||
- If values.id exists -> update
|
||||
- Otherwise -> create
|
||||
2. Call the appropriate mutation
|
||||
- `createMutation` or `updateMutation` from `data` layer
|
||||
- Hooks handle API calls internally
|
||||
3. Handle success
|
||||
- Invalidate queries (list/detail)
|
||||
- Trigger optional `onSuccess` callback
|
||||
4. Handle errors
|
||||
- Extract validation errors from API
|
||||
- Set errors on form fields using `handlers.setError`
|
||||
5. Expose state
|
||||
- `submit` -> function to call from form
|
||||
- `isSubmitting` -> combines pending state from both mutations
|
||||
|
||||
#### Composed Form Elements
|
||||
|
||||
To avoid repetitive boilerplate code when building forms, we encourages creating composed form elements that integrate React Hook Form with shared UI components.
|
||||
|
||||
- Combines label, input, validation message, and layout in one reusable component.
|
||||
- Can encapsulate any input type: text, switch, select, date, textarea, etc.
|
||||
- Works seamlessly with FormProvider for context propagation.
|
||||
|
||||
##### Example: InputSwitch
|
||||
|
||||
InputSwitch wraps a Switch component into a reusable form field:
|
||||
|
||||
```tsx
|
||||
<InputSwitch
|
||||
name="notificationsEnabled"
|
||||
label="Enable notifications"
|
||||
description="Toggle notifications on/off"
|
||||
required
|
||||
/>
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
- Uses `useFormContext` to access form control.
|
||||
- Wraps `FormField`, `FormItem`, `FormLabel`, `FormMessage`, and `FormControl`.
|
||||
- Handles required and disabled states.
|
||||
- This allows you to reuse the same input type across forms without writing repetitive boilerplate.
|
||||
|
||||
##### Usage in a Form
|
||||
|
||||
```tsx
|
||||
<FormProvider {...form}>
|
||||
<Component.FormElement
|
||||
id={MAIN_FORM_ID}
|
||||
onSubmit={form.handleSubmit((values) => onSubmit(values, form))}
|
||||
>
|
||||
<Component.FormGrid columns={2}>
|
||||
<Component.GridElement colSpan={1}>
|
||||
<Component.InputSwitch
|
||||
name="notificationsEnabled"
|
||||
required
|
||||
label="Enable notifications"
|
||||
/>
|
||||
</Component.GridElement>
|
||||
</Component.FormGrid>
|
||||
</Component.FormElement>
|
||||
</FormProvider>
|
||||
```
|
||||
@@ -0,0 +1,251 @@
|
||||
# File & Folder Naming
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Naming Convention](#naming-convention)
|
||||
- [Folders](#folders)
|
||||
- [Component Files](#component-files)
|
||||
- [Hook Files](#hook-files)
|
||||
- [Utility & Helper Files](#utility--helper-files)
|
||||
- [Other TypeScript Files](#other-typescript-files)
|
||||
- [Examples by Layer](#examples-by-layer)
|
||||
- [Case-Only Rename Problem (macOS + Git)](#case-only-rename-problem-macos--git)
|
||||
- [Required: Correct Rename Procedure](#required-correct-rename-procedure)
|
||||
- [Making Git Case-Sensitive on macOS](#making-git-case-sensitive-on-macos)
|
||||
- [Important Reality Check](#important-reality-check)
|
||||
|
||||
## Naming Convention
|
||||
|
||||
We follow **standard React community naming conventions**:
|
||||
|
||||
| Type | Convention | Example |
|
||||
| ---------------------- | -------------------------------------------- | ------------------------------------- |
|
||||
| Folders | lowercase (PascalCase for component folders) | `user/`, `shared/`, `CreateUserForm/` |
|
||||
| React component files | PascalCase | `UserProfile.tsx`, `Button.tsx` |
|
||||
| Hook files | camelCase (`use` prefix) | `useDebounce.ts`, `useToggle.ts` |
|
||||
| Utility / helper files | camelCase | `formatDate.ts`, `generateId.ts` |
|
||||
| API / service files | camelCase (acronyms uppercase) | `userAPI.ts`, `productAPI.ts` |
|
||||
| DTO / mapper files | camelCase (acronyms uppercase) | `userDTO.ts`, `userMapper.ts` |
|
||||
| Type / interface files | camelCase | `types.ts`, `dto.ts`, `interfaces.ts` |
|
||||
| Constants files | camelCase | `constants.ts`, `formConstants.ts` |
|
||||
| Config / entry files | camelCase | `index.ts`, `queryClient.ts` |
|
||||
|
||||
### Folders
|
||||
|
||||
Layer and infrastructure folders use **lowercase**.
|
||||
Component, form, and widget folders use **PascalCase** — matching the component they contain.
|
||||
|
||||
```
|
||||
src/
|
||||
data/ # lowercase — layer folder
|
||||
user/ # lowercase — entity folder
|
||||
product/
|
||||
view/ # lowercase — layer folder
|
||||
components/
|
||||
forms/
|
||||
CreateUserForm/ # PascalCase — component folder
|
||||
widgets/
|
||||
UserProfileWidget/
|
||||
shared/
|
||||
ui/
|
||||
hooks/
|
||||
lib/
|
||||
```
|
||||
|
||||
### Component Files
|
||||
|
||||
React component files use **PascalCase** — matching the name of the exported component.
|
||||
|
||||
```
|
||||
UserProfile.tsx
|
||||
ProductCard.tsx
|
||||
LoginButton.tsx
|
||||
UserProfileWidget.tsx
|
||||
CreateProductForm.tsx
|
||||
```
|
||||
|
||||
### Hook Files
|
||||
|
||||
Hook files use **camelCase** and always start with `use`.
|
||||
|
||||
```
|
||||
useDebounce.ts
|
||||
useToggle.ts
|
||||
useLocalStorage.ts
|
||||
useUserQuery.ts
|
||||
useCreateProductMutation.ts
|
||||
```
|
||||
|
||||
### Utility & Helper Files
|
||||
|
||||
Utility and helper files use **camelCase**.
|
||||
|
||||
```
|
||||
formatDate.ts
|
||||
formatCurrency.ts
|
||||
generateId.ts
|
||||
sleep.ts
|
||||
```
|
||||
|
||||
### Other TypeScript Files
|
||||
|
||||
All other `.ts` files (APIs, DTOs, mappers, types, config) use **camelCase**, with acronyms kept uppercase.
|
||||
|
||||
```
|
||||
userAPI.ts
|
||||
userDTO.ts
|
||||
userMapper.ts
|
||||
types.ts
|
||||
constants.ts
|
||||
queryClient.ts
|
||||
axiosInstance.ts
|
||||
```
|
||||
|
||||
### Examples by Layer
|
||||
|
||||
#### `shared/`
|
||||
|
||||
```plaintext
|
||||
shared/
|
||||
├── ui/
|
||||
│ ├── Button.tsx
|
||||
│ ├── Modal.tsx
|
||||
│ └── Card.tsx
|
||||
│
|
||||
├── hooks/
|
||||
│ ├── useDebounce.ts
|
||||
│ ├── useToggle.ts
|
||||
│ └── useLocalStorage.ts
|
||||
│
|
||||
├── lib/
|
||||
│ ├── axios/
|
||||
│ │ └── axiosInstance.ts
|
||||
│ └── query/
|
||||
│ └── queryClient.ts
|
||||
│
|
||||
└── utils/
|
||||
├── formatDate.ts
|
||||
└── formatCurrency.ts
|
||||
```
|
||||
|
||||
#### `data/`
|
||||
|
||||
```plaintext
|
||||
data/
|
||||
├── user/
|
||||
│ ├── userAPI.ts
|
||||
│ ├── hooks.ts # or useUserQuery.ts, useCreateUserMutation.ts
|
||||
│ ├── userDTO.ts
|
||||
│ └── userMapper.ts
|
||||
│
|
||||
└── product/
|
||||
├── productAPI.ts
|
||||
├── hooks.ts
|
||||
├── productDTO.ts
|
||||
└── productMapper.ts
|
||||
```
|
||||
|
||||
#### `app/`
|
||||
|
||||
```plaintext
|
||||
app/
|
||||
├── config/
|
||||
├── i18n/
|
||||
├── layouts/
|
||||
├── providers/
|
||||
│ ├── AppProviders.tsx
|
||||
│ ├── I18nProvider.tsx
|
||||
│ ├── QueryProvider.tsx
|
||||
│ ├── SettingsProvider.tsx
|
||||
│ └── ThemeProvider.tsx
|
||||
├── routing/
|
||||
└── services/
|
||||
```
|
||||
|
||||
#### `view/`
|
||||
|
||||
```plaintext
|
||||
view/
|
||||
├── components/
|
||||
│ ├── UserAvatar.tsx
|
||||
│ └── ProductCard.tsx
|
||||
│
|
||||
├── widgets/
|
||||
│ └── UserProfileWidget/
|
||||
│ ├── index.ts
|
||||
│ └── UserProfileWidget.tsx
|
||||
│
|
||||
└── forms/
|
||||
└── CreateUserForm/
|
||||
├── form.ts
|
||||
├── Component.tsx
|
||||
├── submit.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Case-Only Rename Problem (macOS + Git)
|
||||
|
||||
macOS file systems are **case-insensitive by default**, while Git is **case-sensitive internally**.
|
||||
|
||||
Renaming a file like `api.ts` → `Api.ts` may:
|
||||
|
||||
- Not be detected by Git
|
||||
- Break CI (Linux environments are case-sensitive)
|
||||
- Cause inconsistent repository state
|
||||
|
||||
### REQUIRED: Correct Rename Procedure
|
||||
|
||||
If you **must rename a file or folder by case only**, you are required to perform a two-step rename.
|
||||
|
||||
#### Step 1 — Rename to Temporary Name
|
||||
|
||||
```bash
|
||||
git mv userApi.ts userApi-temp.ts
|
||||
git commit -m "chore: temporary rename userApi.ts to userApi-temp.ts"
|
||||
```
|
||||
|
||||
#### Step 2 — Rename to Final Name
|
||||
|
||||
```bash
|
||||
git mv userApi-temp.ts UserApi.ts
|
||||
git commit -m "chore: rename userApi-temp.ts to UserApi.ts (case change)"
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
||||
- Never rename by case only in one step
|
||||
- Always use an intermediate name
|
||||
- Always commit both steps
|
||||
- Applies to files and folders
|
||||
- Do not rely only on IDE rename
|
||||
- Always verify with `git status`
|
||||
|
||||
### Making Git Case-Sensitive on macOS
|
||||
|
||||
By default, Git on macOS usually has:
|
||||
|
||||
`core.ignorecase=true`
|
||||
|
||||
This makes Git ignore case-only changes.
|
||||
|
||||
#### Set Case Sensitivity for This Repository
|
||||
|
||||
```bash
|
||||
git config core.ignorecase false
|
||||
```
|
||||
|
||||
#### Verify:
|
||||
|
||||
```bash
|
||||
git config core.ignorecase
|
||||
```
|
||||
|
||||
It should return: `false`
|
||||
|
||||
### Important Reality Check
|
||||
|
||||
Even if `core.ignorecase=false`:
|
||||
|
||||
- macOS file system is still case-insensitive
|
||||
- Case-only renames can still behave unexpectedly
|
||||
- The two-step rename process is still the safest approach
|
||||
@@ -0,0 +1,61 @@
|
||||
# Backend Architecture
|
||||
|
||||
This guide is a short backend architecture reference for CRUX backend apps.
|
||||
|
||||
The current backend is based on AdonisJS v6. Use the official AdonisJS folder structure as the default baseline:
|
||||
|
||||
https://v6-docs.adonisjs.com/guides/getting-started/folder-structure
|
||||
|
||||
## Main Idea
|
||||
|
||||
Keep the backend close to the standard AdonisJS structure. Add project-specific folders only when they make ownership clearer.
|
||||
|
||||
## Default Folders
|
||||
|
||||
| Folder | Purpose |
|
||||
| ----------- | -------------------------------------------------------------------------------------------- |
|
||||
| `app/` | Application code: controllers, services, middleware, policies, validators, and domain logic. |
|
||||
| `start/` | Boot lifecycle files such as routes, kernel setup, env loading, and event registration. |
|
||||
| `config/` | Runtime configuration for AdonisJS and installed packages. |
|
||||
| `database/` | Migrations and seeders. |
|
||||
| `bin/` | Entrypoints for server, console, and tests. |
|
||||
| `public/` | Static files and generated public assets. |
|
||||
| `tests/` | Backend tests. |
|
||||
|
||||
## Basic Flow
|
||||
|
||||
```txt
|
||||
start/routes.ts
|
||||
-> controller
|
||||
-> validator / policy
|
||||
-> service
|
||||
-> model / database / external service
|
||||
```
|
||||
|
||||
Controllers should handle HTTP concerns. Services should hold business logic. Validators should own request validation. Policies and abilities should own authorization.
|
||||
|
||||
## Module Folders
|
||||
|
||||
If a backend app has reusable or generated modules, keep each module's files together when that improves maintainability.
|
||||
|
||||
Example:
|
||||
|
||||
```txt
|
||||
app/base_modules/ModuleName/
|
||||
controller/
|
||||
services/
|
||||
validator/
|
||||
policy/
|
||||
routes.ts
|
||||
```
|
||||
|
||||
This is an app convention, not an AdonisJS requirement. Prefer the default AdonisJS structure unless a module boundary is useful.
|
||||
|
||||
## Rules
|
||||
|
||||
- Follow AdonisJS conventions first.
|
||||
- Keep HTTP logic out of services.
|
||||
- Keep business logic out of controllers.
|
||||
- Validate request input before calling services.
|
||||
- Use policies or abilities for protected operations.
|
||||
- Document module-specific architecture in module docs, not in this general guide.
|
||||
Reference in New Issue
Block a user