Initial commit
This commit is contained in:
@@ -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>
|
||||
```
|
||||
Reference in New Issue
Block a user