Add initial project files

This commit is contained in:
Igor Martyniuk
2026-09-01 13:03:41 +03:00
parent 22e111bb44
commit 74cb5b6176
1841 changed files with 242819 additions and 30 deletions
@@ -0,0 +1,33 @@
# App Layer
The composition root: owns concerns that exist once for the whole app and coordinates layers and modules without containing feature business logic. It composes global providers, configures app-wide settings and environment values, initializes i18n and global styles, defines top-level routing, provides layouts and menus, and registers core/project/development modules.
```plaintext
app/
├── config/ # global settings, environment values, and config types
├── css/ # global styles and app-level third-party overrides
├── i18n/ # languages, messages, and internationalization config
├── layouts/ # top-level Admin layout variants
├── providers/ # providers composed once for the entire application
│ ├── AppProviders.tsx
│ ├── I18nProvider.tsx
│ ├── QueryProvider.tsx
│ ├── SettingsProvider.tsx
│ └── ThemeProvider.tsx
├── routing/
│ ├── AppRouting.tsx
│ └── AppRoutingSetup.tsx
├── services/
│ └── modules/ # module registry, routes, menus, and messages
└── menu.tsx # app-level menu composition
```
Files may evolve, but ownership stays app-wide — a concern used by only one feature belongs to that feature module.
## Notes
- **Providers** — `app/providers/AppProviders.tsx` is the single composition point: TanStack Query, auth context (from the Auth core module), settings, tooltip/notification UI, theme, i18n and text direction, React Router.
- **Routing** — `app/routing/` composes top-level routes from routes contributed by registered modules; feature pages stay in their modules.
- **Config & i18n** — centralize environment access through `app/config/` instead of repeating it in feature code; `app/i18n/` owns languages and messages.
- **Layouts & CSS** — `app/layouts/` holds the Admin shell variants, `app/css/` global and third-party overrides; feature styles stay with the feature.
- **Module registration** — `app/services/modules/` wires modules in through their routes, menus, and messages; module implementation stays under `core/`, `modules/`, or `devtools/`.
@@ -0,0 +1,149 @@
# Data Layer
The `data` layer owns all data access and transformation: API calls, DTO types, DTO → entity mappers, React Query hooks, caching and errors. Higher layers (`forms`, `widgets`, `pages`) never call APIs directly — they consume data-layer hooks.
## Organization
Two ways to structure the layer:
| Approach | Layout | Best for | Trade-off |
| --------- | ------------------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------------- |
| By Module | `data/api/`, `data/hooks/`, `data/dto/`, `data/mappers/` | Small apps, simple data layers | One feature's files scattered; hard to scale |
| By Entity | `data/user/`, `data/product/` — each with `api.ts`, `hooks.ts`, `dto.ts`, `mapper.ts`, `types.ts` | Medium/large apps | Harder to see all APIs/hooks globally |
```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)
```
## 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 and always return DTOs.
```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 need unit tests whenever they rename fields, normalize values (paths, dates, statuses, enums, defaults), map nested DTOs, arrays, paginated data, or optional fields. Cover: `toEntity` on a representative DTO, `toEntities` on arrays, defaults for missing fields, nested mapper shapes, enum/status casts. Test only transformation — no React, no rendering, no real API calls.
Keep tests at the app root, mirroring the source path:
```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 — keep it 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,25 @@
# Entities
Core domain models of the application — pure type definitions for business objects (`User`, `Product`, `Message`), their enums, and related constants. Stable, reusable, usable from any layer.
```plaintext
entities/
| --- user.ts # { id: string; name: string; email: string }
| --- product.ts # { id: string; name: string; price: number }
| --- enums.ts # { UserRole, ProductCategory }
```
Larger entities can use a folder (`user/` with `interfaces.ts`, `enums.ts`, `rules.ts`, `index.ts`).
```ts
export type User = {
id: number;
name: string;
email: string;
};
export enum UserRole {
ADMIN = "admin",
SIMLPE_JACK = "simple_jack",
}
```
@@ -0,0 +1,51 @@
# Infrastructure Layer
App-wide technical adapters connecting the app to technical systems and shared runtime services: HTTP clients and interceptors, shared React Query helpers (keys, response types, error normalization), storage/browser adapters, formatting service adapters, technical config wrappers. No feature business logic.
```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. Feature-agnostic: no module business rules, no page/component composition, no entity mapping that belongs in a module `data/mapper`.
2. API modules use infrastructure clients (`import { axiosClient } from "#infrastructure/axios"`) instead of creating their own.
3. React Query helpers shared across modules (key builders, list response types, pagination, error extraction) live here.
4. UI never calls raw HTTP clients — view/page code uses data-layer hooks; direct `#infrastructure/axios` usage belongs in API/data modules.
5. Generic formatting adapters (`IntlDate`, `IntlName`) may be consumed by UI.
6. Keep app-state coupling narrow — prefer dependency injection; don't import feature modules into infrastructure.
## Examples
```ts
import { axiosClient } from "#infrastructure/axios";
export class UserAPI {
getDetailed(id: number) {
return axiosClient.get(`/users/${id}`).then((response) => response.data);
}
}
```
```ts
import {
createKeyBuilder,
getPaginationInfo,
} from "#infrastructure/reactQuery";
export const usersCacheKey = createKeyBuilder("USERS", "LIST");
```
@@ -0,0 +1,55 @@
# Reference Data Layer
App-wide API-backed lookup datasets reused across modules — countries, languages, currencies, time zones, common option dictionaries. The layer fetches them, exposes reusable query hooks, and owns their cache keys.
Small dataset → one file; larger → a folder:
```plaintext
refdata/
├── countries.ts
├── languages.ts
└── currencies/
├── api.ts
├── dto.ts
├── mapper.ts
├── queries.ts
└── keys.ts
```
## Rules
1. App-wide datasets only — a dataset owned by one feature belongs in that module's `data/` layer.
2. May use `#infrastructure/axios` and `#infrastructure/reactQuery`; must not import from `view/`, `pages/`, or feature modules.
3. Expose hooks or small service functions — consumers should not know endpoints or cache keys.
4. Add mappers when backend DTOs differ from app-facing data, tested like module data mappers.
## Example
```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,56 @@
# Shared Layer
Global reusable building blocks used across the entire application — framework-level, never feature-specific.
```plaintext
shared/
| --- ui/ # primitive UI components (Button, Modal, Card)
| --- widgets/ # reusable composed UI blocks (DataGrid, screens)
| --- forms/ # reusable form composition (composed inputs)
| --- editors/ # reusable editor wrappers (tinymce, image_input)
| --- hooks/ # generic reusable hooks (useDebounce, useToggle, useLocalStorage)
| --- utils/ # pure helper functions (formatDate, generateId, sleep)
| --- lib/ # pure helpers and utility services (time/, phone_number/, resolveFileURL)
| --- assets/ # images, icons, fonts
| --- styles/ # global CSS, variables, Tailwind overrides
| --- constants/ # global constants
```
## UI Components
Simple, composable, customizable building blocks — Lego pieces that combine into larger UI:
- **Simple** — Button, Input, Card, Modal, Dropdown.
- **Composable** — complex UI is built by composition, not by modifying base components:
```tsx
<Card>
<CardHeader />
<CardContent />
<CardFooter />
</Card>
```
- **Customizable** — through `className`, variants, props, and composition:
```tsx
<Button variant="outline" size="lg">
Save
</Button>
```
- **No business logic** — presentation only (local state allowed): no API calls, React Query, form submission, or domain logic.
### Styling
Utility-first, design tokens, variants instead of style duplication, minimal centralized custom CSS.
Stack: **TailwindCSS** (primary styling), **class-variance-authority** (variants), **CSS variables** (design tokens), **Radix primitives** (accessible building blocks).
## Other Folders
- `hooks/` — generic hooks with no entity or business-logic dependencies.
- `utils/` — stateless pure helper functions.
- `lib/` — pure helpers and utility services (date, name, path, phone formatting).
- `assets/` — static resources (icons, fonts).
- `styles/` — CSS variables, Tailwind extensions, global resets.
@@ -0,0 +1,150 @@
# Views Layer
## Components
Reusable UI blocks: render UI, accept props, handle simple UI state (hover, toggle, open/close), compose other components. `UserAvatar`, `ProductBadge`.
No API calls, mutations, complex state management, or business workflows.
## Widgets
Self-contained UI blocks that implement a complete piece of functionality — panels, sections, complex interactive blocks used to compose pages. A widget combines UI, logic, and interactions: it composes multiple components, may include forms, dialogs, or actions, may fetch data via data hooks or take it from props, and exposes a simple API to pages while hiding its internals.
A widget can use another widget when the nested widget is a clear functional sub-block, the parent is grouping or orchestrating UI, and the dependency stays loose:
```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 # validation schema, types, default values
| --- Component.tsx # UI + react-hook-form integration
| --- submit.ts # submission logic
| --- constants.ts # Optional: form IDs, UI constants
| --- schema.ts # Optional validation schema. Good for large or multistep forms
| --- index.ts # public exports
```
### Form Logic
`form.ts` contains pure form logic: validation schema, form types, default values, helper templates.
```ts
export const validationSchema = z.object({
first_name: z.string().min(3),
last_name: z.string().min(3),
});
```
With `i18n`, create a function that returns the schema:
```ts
function createValidationSchema(
translationFn: TFunction<"translation", undefined>, // Depend on library 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
Default values come from factory functions, not inline objects:
```ts
export function createFormValues(): FormValues {
return { first_name: "", last_name: "" };
}
export function toFormValues(data: SomeData): FormValues {
return {
first_name: data?.first_name ?? "",
last_name: data?.last_name ?? "",
};
}
```
Types for values and submit functions:
```ts
export type FormValues = z.infer<typeof validationSchema>;
export type SubmitFunc = (
values: FormValues,
handlers: UseFormReturn<FormValues>,
) => void;
```
### Submit Functions
A `useSubmit` hook centralizes submission: it picks create vs update by `values.id`, calls the `data/` layer mutations, invalidates list/detail queries on success, maps API validation errors onto form fields, and exposes `submit` + `isSubmitting`.
```ts
export const useSubmit = (params?: { onSuccess?: (id?: number) => void }) => {
const { mutate: createMutation, isPending: isCreatePending } = useCreate();
const { mutate: updateMutation, isPending: isUpdatePending } = useUpdate();
const handleSubmit: SubmitFunc = (values, handlers) => {
const mutation = values.id ? updateMutation : createMutation;
mutation(values, {
onSuccess() {
invalidateList();
if (values.id) invalidateDetailed(values.id);
params?.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 || isUpdatePending,
};
};
```
### Composed Form Elements
To avoid form boilerplate, wrap React Hook Form + shared UI into composed inputs. Each combines label, input, validation message, and layout, works with `FormProvider` via `useFormContext`, and can encapsulate any input type: text, switch, select, date, textarea, etc.
```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>
```