Initial commit
This commit is contained in:
@@ -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.
|
||||
@@ -0,0 +1,392 @@
|
||||
# CRUX React Frontend Architecture
|
||||
|
||||
This guide describes the recommended React frontend architecture for apps in the CRUX monorepo.
|
||||
|
||||
Its main goal is to make frontend development easier, more structured, and consistent across teams, so that every developer can write and understand each other's code quickly.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Base Concepts](#base-concepts)
|
||||
- [Disclaimer](#disclaimer)
|
||||
- [Core Principles](#core-principles)
|
||||
- [Recommended Layering](#recommended-layering)
|
||||
- [Architecture Scope](#architecture-scope)
|
||||
- [Layer Dependency Rules](#layer-dependency-rules)
|
||||
- [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)
|
||||
- [Providers Layer](#3-providers-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)
|
||||
|
||||
## Base Concepts
|
||||
|
||||
- **Purpose:** Provide a shared frontend foundation with predefined packages, utilities, and patterns for React apps and frontend modules in this monorepo.
|
||||
- **Goals:**
|
||||
- Reduce boilerplate code.
|
||||
- Standardize folder structure, file naming, and coding practices in the company context.
|
||||
- Assist in onboarding of new developers.
|
||||
- **Included:**
|
||||
- Preconfigured `React` + `TypeScript` setup.
|
||||
- Common utilities and helper functions.
|
||||
- Shared UI components, hooks, and state management helpers.
|
||||
- Integrated i18n, form validation, and API utilities.
|
||||
|
||||
## 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/
|
||||
├── data/ # API, react-query hooks, DTOs
|
||||
├── entities/ # global types and interfaces
|
||||
├── infrastructure/ # technical adapters: HTTP, query, storage, error handling
|
||||
├── providers/ # global providers (router, query, session, i18n, theme)
|
||||
├── refdata/ # API-backed common reference datasets
|
||||
├── shared/ # reusable global components, hooks, libs, assets, styles
|
||||
├── view/ # UI layer (blocks)
|
||||
│ ├── components/ # Feature-level reusable components (ProductCard, LoginButton, VehicleSearchInput)
|
||||
│ ├── forms/ # smart form components (ProductForm, UserForm)
|
||||
│ └── widgets/ # composed smart, multi-entity components (DashboardStatsWidget, UserProfileWidget)
|
||||
└── pages/ # route-level pages
|
||||
```
|
||||
|
||||
## 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`, `providers`, `pages`, `view`, and `shared`. |
|
||||
| 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.
|
||||
|
||||
### Admin App Interpretation
|
||||
|
||||
`apps/admin` is a reusable admin panel template, not a simple single-purpose React app.
|
||||
|
||||
The app shell owns cross-cutting concerns:
|
||||
|
||||
- routing and route registration
|
||||
- global providers
|
||||
- layouts
|
||||
- i18n setup
|
||||
- shared UI
|
||||
- technical infrastructure
|
||||
- module registration
|
||||
|
||||
Admin app infrastructure that is shared by every module lives in `apps/admin/src/infrastructure`.
|
||||
Examples include HTTP clients, React Query helpers, storage adapters, and error normalization.
|
||||
|
||||
Admin-wide API-backed reference datasets live in `apps/admin/src/refdata`.
|
||||
Examples include country lists, common option datasets, and other reusable data that comes from the backend but is not owned by one feature module.
|
||||
|
||||
Each feature module owns its feature concerns:
|
||||
|
||||
- API calls and query hooks in module `data`
|
||||
- module domain types in module `entities`
|
||||
- module-local contexts and stores in module `providers`
|
||||
- route-level screens in module `pages`
|
||||
- module UI in module `view`
|
||||
- module-only helpers in module `shared`
|
||||
|
||||
If a helper or component becomes useful across multiple modules, move it upward to `apps/admin/src/shared` or a package under `packages/`.
|
||||
|
||||
## Layer Dependency Rules
|
||||
|
||||
Each layer has defined set of layers it is allowed to import from.
|
||||
Imports must only flow **downward** — a lower layer never knows about a higher one.
|
||||
Violating this creates circular dependencies and couples layers that should be independent.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
pages["pages/"]
|
||||
view["view/"]
|
||||
data["data/"]
|
||||
providers["providers/"]
|
||||
infrastructure["infrastructure/"]
|
||||
refdata["refdata/"]
|
||||
entities["entities/"]
|
||||
shared["shared/"]
|
||||
|
||||
pages --> view
|
||||
pages --> data
|
||||
pages --> refdata
|
||||
pages --> providers
|
||||
pages --> infrastructure
|
||||
pages --> entities
|
||||
pages --> shared
|
||||
|
||||
view --> data
|
||||
view --> refdata
|
||||
view --> infrastructure
|
||||
view --> entities
|
||||
view --> shared
|
||||
|
||||
refdata --> infrastructure
|
||||
refdata --> shared
|
||||
|
||||
providers --> data
|
||||
providers --> refdata
|
||||
providers --> infrastructure
|
||||
providers --> shared
|
||||
|
||||
data --> infrastructure
|
||||
data --> entities
|
||||
data --> shared
|
||||
|
||||
infrastructure --> shared
|
||||
```
|
||||
|
||||
| Layer | Can import from |
|
||||
| ----------------- | --------------------------------------------------------------------------- |
|
||||
| `shared/` | nothing — zero app-layer dependencies |
|
||||
| `entities/` | nothing — zero app-layer dependencies |
|
||||
| `infrastructure/` | `shared/`, external packages, app runtime config |
|
||||
| `refdata/` | `infrastructure/`, `shared/` |
|
||||
| `data/` | `infrastructure/`, `entities/`, `shared/` |
|
||||
| `providers/` | `infrastructure/`, `data/`, `refdata/`, `shared/` |
|
||||
| `view/` | `data/`, `refdata/`, `entities/`, `shared/`, generic infrastructure helpers |
|
||||
| `pages/` | all layers |
|
||||
|
||||
### Rules
|
||||
|
||||
1. **`shared/` has no imports** — it is infrastructure and UI primitives only. It must not reference any domain type or application layer. If a utility needs a domain type, it does not belong in `shared/`.
|
||||
|
||||
2. **`entities/` has no imports** — No logic, no React, no external libraries.
|
||||
|
||||
3. **`data/` never imports from `view/` or `pages/`** — data hooks and API functions are completely independent of the UI.
|
||||
|
||||
4. **`view/` never imports from `pages/`** — components, forms, and widgets do not depend on page-level composition.
|
||||
|
||||
5. **The boundary between `shared/ui/` and `view/components/` is enforced by this rule** — if a component needs to import from `entities/`, it cannot live in `shared/`.
|
||||
|
||||
6. **`infrastructure/` contains technical adapters, not feature logic** — HTTP clients, React Query, storage adapters, and formatting service wrappers live here.
|
||||
|
||||
7. **`refdata/` contains app-wide lookup data** — country lists, languages, currencies, and other shared backend datasets live here instead of inside one feature module.
|
||||
|
||||
### 1. **Entities** Layer
|
||||
|
||||
The `entities` layer contains global types and interfaces that define your domain models.
|
||||
|
||||
- Define entity types for your business objects: User, Product, Message
|
||||
- Define enums or constants
|
||||
- No Framework
|
||||
- No API or hook logic
|
||||
|
||||
### 2. **Data** layer (Data Access Layer)
|
||||
|
||||
#### Purpose
|
||||
|
||||
The `data` layer is responsible for all interactions with external or internal data sources and state management hooks.
|
||||
|
||||
It is the only layer that knows how to fetch, mutate, and map data, while higher layers (`forms`, `widgets`, `pages`) just consume the hooks or data from this layer.
|
||||
|
||||
#### Responsibilities
|
||||
|
||||
- Handle API calls
|
||||
- Provide React Query hooks or other state management hooks for data fetching/mutations
|
||||
- Define DTOs (Data Transfer Objects)
|
||||
- Provide DTO mappers (map raw API data to app entities or view models)
|
||||
- Define types/interfaces specific to API responses
|
||||
- Isolate side-effects like HTTP requests, error handling, or caching
|
||||
|
||||
#### Rules and Guidelines
|
||||
|
||||
1. Only data and state logic belongs here
|
||||
|
||||
- No UI, forms, or layout logic
|
||||
|
||||
2. Use React Query (or other state library) for fetching/mutations
|
||||
|
||||
- Expose hooks, not raw API functions, to the view layer
|
||||
|
||||
3. DTOs + Mappers
|
||||
|
||||
- Map API response to internal entities/ types
|
||||
- Keep external API structure isolated from your app
|
||||
- Mappers should be covered by unit tests because they define the data-layer contract between API DTOs and app entities
|
||||
|
||||
4. Types/Interfaces
|
||||
|
||||
- Define API-specific types
|
||||
- Use entities/ for app-wide types
|
||||
|
||||
5. No direct imports from view/ or pages/
|
||||
- data/ is completely independent
|
||||
|
||||
### Infrastructure Layer
|
||||
|
||||
The `infrastructure` layer contains app-wide technical adapters.
|
||||
|
||||
In the admin app, it lives in `apps/admin/src/infrastructure/`.
|
||||
|
||||
Examples:
|
||||
|
||||
- configured HTTP clients and interceptors
|
||||
- React Query helpers, response types, and error normalization
|
||||
- storage or browser adapters
|
||||
- app-wide formatting service wrappers such as date and name formatters
|
||||
|
||||
This layer should stay feature-agnostic. Feature API modules should use infrastructure clients instead of creating their own clients.
|
||||
|
||||
### Reference Data Layer
|
||||
|
||||
The `refdata` layer contains app-wide reference datasets.
|
||||
|
||||
In the admin app, it lives in `apps/admin/src/refdata/`.
|
||||
|
||||
Examples:
|
||||
|
||||
- countries
|
||||
- languages
|
||||
- currencies
|
||||
- time zones
|
||||
- common option dictionaries
|
||||
|
||||
Use this layer for backend-backed lookup data shared by multiple modules. If the data belongs to one feature module, keep it in that module's `data/` layer.
|
||||
|
||||
### 3. **Providers** layer
|
||||
|
||||
The `providers` layer is responsible for global context, app-wide state, and services that need to be available throughout the React application.
|
||||
It sets up everything that any component or page might need: routing, state management, internationalization, theme, notifications, etc.
|
||||
|
||||
### 4. **Pages** layer
|
||||
|
||||
- `pages` = the highest-level blocks
|
||||
- Compose `widgets` + `forms` + `components` + `data` into full pages
|
||||
- Only page-level layout, routing, and orchestration logic lives here
|
||||
- Example: `UserPage` → uses `UserProfileWidget` + `UserActivityWidget`
|
||||
|
||||
### 5. **View** layer
|
||||
|
||||
1. **view/components/**
|
||||
|
||||
- “Dumb” components
|
||||
- Pure UI, no business logic
|
||||
- Example: `ProductCard`, `UserAvatar`
|
||||
|
||||
2. **view/forms/**
|
||||
|
||||
- Form components
|
||||
- Encapsulate form state, validation, and submit logic
|
||||
- Use hooks from `data/` for mutations/queries
|
||||
- Example: `CreateProductForm` → uses `useCreateProduct()` from `data/hooks`
|
||||
|
||||
3. **view/widgets/**
|
||||
- Composed components
|
||||
- Combine `components` + `forms` + `shared/ui`
|
||||
- Feature blocks that can be reused on multiple pages
|
||||
- Example: `UserProfileWidget` → contains `UserAvatar`, `UserForm`, `UserStatsTable`
|
||||
|
||||
### 6. **Shared** layer
|
||||
|
||||
The `shared` layer contains global reusable building blocks for the frontend:
|
||||
|
||||
- Components, hooks, utilities, assets, styles
|
||||
- Not tied to a specific feature or entity
|
||||
- Can be used by `view`, `pages`, `widgets`, or `forms`
|
||||
|
||||
This is where common "Lego" blocks live.
|
||||
|
||||
#### Responsibilities
|
||||
|
||||
- Provide reusable UI components (`Button`, `Input`, `Modal`, `Card`)
|
||||
- Provide reusable hooks (`useToggle`, `useDebounce`, `useLocalStorage`)
|
||||
- Provide utility functions (`formatDate`, `errorHandler`, http helpers)
|
||||
- Provide assets (icons, images, fonts)
|
||||
- Provide global styles (CSS variables, global CSS, Tailwind overrides)
|
||||
|
||||
#### Rules
|
||||
|
||||
- Components here are dumb, reusable, feature-agnostic
|
||||
- Hooks here are generic, not tied to a single entity or feature
|
||||
- Assets can be imported anywhere
|
||||
- Styles are global or utility-level, not component-specific
|
||||
|
||||
## Full documentation
|
||||
|
||||
- [Entities Layer](./layers/entities.md)
|
||||
- [Data Layer](./layers/data.md)
|
||||
- [Infrastructure Layer](./layers/infrastructure.md)
|
||||
- [Reference Data Layer](./layers/refdata.md)
|
||||
- [Providers Layer](./layers/providers.md)
|
||||
- [View Layer](./layers/view.md)
|
||||
- [Shared Layer](./layers/shared.md)
|
||||
- [File & Folder Naming Convention](./naming.md)
|
||||
@@ -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,96 @@
|
||||
# Providers Layer
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Providers Layer](#providers-layer)
|
||||
- [Purpose](#purpose)
|
||||
- [Responsibilities](#responsibilities)
|
||||
- [Recommended Structure](#recommended-structure)
|
||||
- [App Provider Composition](#app-provider-composition)
|
||||
- [Example: AppProviders](#example-appproviders)
|
||||
|
||||
## Purpose
|
||||
|
||||
The `providers` layer is responsible for **global application context and environment configuration**.
|
||||
|
||||
It acts as the **composition root of the application**, where all global React providers are initialized and combined.
|
||||
|
||||
Providers make global services available across the entire application, such as:
|
||||
|
||||
- routing
|
||||
- data fetching clients
|
||||
- theme configuration
|
||||
- authentication/session context
|
||||
- internationalization
|
||||
- notifications
|
||||
- global state management
|
||||
|
||||
Instead of importing these providers in many places, they are **composed once and applied at the application root**.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
The `providers` layer is responsible for:
|
||||
|
||||
- initializing global React context providers
|
||||
- configuring global services
|
||||
- composing multiple providers into a single entry point
|
||||
- exposing the provider tree to the application root
|
||||
|
||||
## Recommended Structure
|
||||
|
||||
```plaintext
|
||||
providers/
|
||||
|
|
||||
| --- AppProviders.tsx # main provider composition
|
||||
|
|
||||
| --- QueryProvider.tsx # React Query setup
|
||||
| --- RouterProvider.tsx # routing configuration
|
||||
| --- ThemeProvider.tsx # theme context
|
||||
| --- AuthProvider.tsx # authentication/session provider
|
||||
| --- I18nProvider.tsx # internationalization provider
|
||||
|
|
||||
| --- index.ts # re-export providers (optional)
|
||||
```
|
||||
|
||||
## App Provider Composition
|
||||
|
||||
Instead of wrapping the application with multiple providers manually, they are composed in a single root provider component.
|
||||
This centralizes the provider tree and simplifies application initialization.
|
||||
Example usage in the application entry point:
|
||||
|
||||
```tsx
|
||||
import { AppProviders } from "@/providers/AppProviders";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<AppProviders>
|
||||
<App />
|
||||
</AppProviders>,
|
||||
);
|
||||
```
|
||||
|
||||
### Example: AppProviders
|
||||
|
||||
This component acts as the global application wrapper.
|
||||
|
||||
```tsx
|
||||
import { ReactNode, FC, PropsWithChildren } from "react";
|
||||
import { QueryProvider } from "./QueryProvider";
|
||||
import { ThemeProvider } from "./ThemeProvider";
|
||||
import { AuthProvider } from "./AuthProvider";
|
||||
import { RouterProvider } from "./RouterProvider";
|
||||
import { I18nProvider } from "./I18nProvider";
|
||||
|
||||
export const AppProviders: FC<PropsWithChildren> = ({ children }) => {
|
||||
return (
|
||||
<QueryProvider>
|
||||
<I18nProvider>
|
||||
<AuthProvider>
|
||||
<ThemeProvider>
|
||||
<RouterProvider>{children}</RouterProvider>
|
||||
</ThemeProvider>
|
||||
</AuthProvider>
|
||||
</I18nProvider>
|
||||
</QueryProvider>
|
||||
);
|
||||
};
|
||||
```
|
||||
@@ -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,249 @@
|
||||
# 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**:
|
||||
|
||||
These conventions apply to frontend apps and to module folders inside those apps. For example, `apps/admin/src/core/FileManager` may use PascalCase because it is a product module folder, while its layer folders such as `data`, `entities`, `providers`, `pages`, `view`, and `shared` stay lowercase.
|
||||
|
||||
| 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
|
||||
```
|
||||
|
||||
#### `providers/`
|
||||
|
||||
```plaintext
|
||||
providers/
|
||||
├── AppProviders.tsx
|
||||
├── QueryProvider.tsx
|
||||
├── RouterProvider.tsx
|
||||
├── ThemeProvider.tsx
|
||||
├── AuthProvider.tsx
|
||||
├── I18nProvider.tsx
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
#### `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
|
||||
Reference in New Issue
Block a user