@pplancq/react-template 2.8.17 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/_README.md +57 -52
- package/mise.toml +2 -2
- package/mocks/browser.ts +1 -1
- package/mocks/fixtures/user.ts +14 -14
- package/mocks/handlers/userHandlers.ts +3 -3
- package/mocks/server.ts +1 -1
- package/package.json +30 -30
- package/playwright-report/html/index.html +90 -0
- package/playwright-report/sonar-results.xml +20 -0
- package/playwright.config.ts +3 -3
- package/scripts/removeDemo.cjs +105 -66
- package/src/README.md +81 -0
- package/src/app/App.tsx +18 -0
- package/src/{bootstrap.tsx → app/AppHTMLElement.tsx} +5 -5
- package/src/app/README.md +69 -0
- package/src/app/assets/css/index.ts +2 -0
- package/src/{assets → app/assets}/css/reset.css +3 -3
- package/src/app/config/queryClientConfig.ts +3 -0
- package/src/app/config/serviceContainer.ts +6 -0
- package/src/app/providers/Providers/Providers.tsx +18 -0
- package/src/app/providers/QueryClientProvider/QueryClientProvider.tsx +16 -0
- package/src/app/providers/ServiceProvider/ServiceContext.ts +8 -0
- package/src/app/providers/ServiceProvider/ServiceProvider.tsx +14 -0
- package/src/app/routing/Router.tsx +14 -0
- package/src/app/routing/appRoutes.ts +3 -0
- package/src/app/routing/routes.tsx +9 -0
- package/src/demo/README.md +149 -0
- package/src/demo/application/dtos/ArchitecturePrinciple.ts +5 -0
- package/src/{types/profileTypes.ts → demo/application/dtos/ProfileFormValues.ts} +1 -1
- package/src/demo/application/use-cases/GetArchitecturePrinciplesUseCase.ts +11 -0
- package/src/demo/application/use-cases/GetArchitecturePrinciplesUseCaseInterface.ts +5 -0
- package/src/demo/application/use-cases/GetDemoUsersUseCase.ts +11 -0
- package/src/demo/application/use-cases/GetDemoUsersUseCaseInterface.ts +5 -0
- package/src/demo/domain/entities/DemoUser.ts +7 -0
- package/src/demo/domain/repositories/ArchitecturePrincipleRepositoryInterface.d.ts +5 -0
- package/src/demo/domain/repositories/DemoUserRepositoryInterface.d.ts +5 -0
- package/src/demo/infrastructure/repositories/HttpDemoUserRepository.ts +29 -0
- package/src/demo/infrastructure/repositories/StaticArchitecturePrincipleRepository.ts +33 -0
- package/src/demo/serviceDemo.ts +40 -0
- package/src/demo/serviceIdentifiers.ts +6 -0
- package/src/demo/ui/components/DemoFooter/DemoFooter.tsx +17 -0
- package/src/demo/ui/components/DemoFooter/index.ts +1 -0
- package/src/demo/ui/components/DemoHeader/DemoHeader.tsx +25 -0
- package/src/demo/ui/components/DemoHeader/index.ts +1 -0
- package/src/{ui/molecules/Navigation/Navigation.tsx → demo/ui/components/DemoNavigation/DemoNavigation.tsx} +6 -6
- package/src/demo/ui/components/DemoNavigation/index.ts +1 -0
- package/src/{ui/atoms → demo/ui/components}/Input/Input.tsx +3 -3
- package/src/demo/ui/components/Input/index.ts +1 -0
- package/src/{ui/atoms → demo/ui/components}/Logo/Logo.tsx +2 -2
- package/src/demo/ui/components/Logo/index.ts +1 -0
- package/src/{ui/atoms → demo/ui/components}/NavLink/NavLink.tsx +3 -3
- package/src/demo/ui/components/NavLink/index.ts +1 -0
- package/src/{components → demo/ui/components}/TextInput/TextInput.tsx +3 -3
- package/src/demo/ui/components/TextInput/index.ts +1 -0
- package/src/{forms → demo/ui/forms}/ProfileForm/ProfileForm.tsx +13 -13
- package/src/demo/ui/forms/ProfileForm/index.ts +1 -0
- package/src/demo/ui/hooks/useDemoUsersQuery/useDemoUsersQuery.ts +13 -0
- package/src/demo/ui/layouts/DemoLayout/DemoLayout.tsx +12 -0
- package/src/demo/ui/layouts/MainTemplate/MainTemplate.module.css +21 -0
- package/src/demo/ui/layouts/MainTemplate/MainTemplate.tsx +26 -0
- package/src/demo/ui/layouts/MainTemplate/index.ts +1 -0
- package/src/{pages/Demo/Demo.module.scss → demo/ui/pages/DemoHome/DemoHome.module.scss} +5 -5
- package/src/demo/ui/pages/DemoHome/DemoHome.tsx +117 -0
- package/src/demo/ui/pages/DependencyInjectionDemo/DependencyInjectionDemo.tsx +47 -0
- package/src/{pages/Demo → demo/ui/pages}/ReactHookFormDemo/ReactHookFormDemo.tsx +8 -8
- package/src/demo/ui/pages/ReactQueryDemo/ReactQueryDemo.module.scss +38 -0
- package/src/{pages/Demo → demo/ui/pages}/ReactQueryDemo/ReactQueryDemo.tsx +6 -5
- package/src/demo/ui/routing/demoAppRoutes.ts +6 -0
- package/src/demo/ui/routing/demoRoutes.tsx +31 -0
- package/src/main.ts +5 -5
- package/src/react-app-env.d.ts +1 -1
- package/src/shared/README.md +53 -0
- package/src/shared/application/README.md +49 -0
- package/src/shared/domain/README.md +52 -0
- package/src/shared/infrastructure/README.md +51 -0
- package/src/shared/infrastructure/http/constant.ts +16 -0
- package/src/{api → shared/infrastructure/http}/fetchApi.ts +3 -3
- package/src/shared/ui/README.md +50 -0
- package/src/shared/ui/hooks/useService/useService.ts +13 -0
- package/test-results/.last-run.json +4 -0
- package/tests/e2e/demo.test.ts +17 -0
- package/tests/e2e/fixture/playwright.fixture.ts +15 -0
- package/tests/{utils → e2e/utils}/createNetworkFixture.ts +2 -2
- package/tests/{utils → e2e/utils}/createTestWithCoverageFixture.ts +8 -8
- package/tests/{utils → e2e/utils}/playwright.globalSetup.ts +4 -4
- package/tests/{utils → e2e/utils}/playwright.globalTeardown.ts +3 -3
- package/{src/__tests__/bootstraps.test.tsx → tests/unit/app/AppHTMLElement.test.tsx} +14 -14
- package/tests/unit/demo/ui/components/DemoFooter/Footer.test.tsx +15 -0
- package/{src/components/Footer/__tests__ → tests/unit/demo/ui/components/DemoFooter}/__snapshots__/Footer.test.tsx.snap +3 -3
- package/tests/unit/demo/ui/components/DemoHeader/Header.test.tsx +15 -0
- package/tests/unit/demo/ui/components/DemoHeader/__snapshots__/Header.test.tsx.snap +64 -0
- package/tests/unit/demo/ui/components/Logo/Logo.test.tsx +12 -0
- package/tsconfig.json +4 -2
- package/vitest.config.mts +6 -2
- package/postcss.config.mjs +0 -5
- package/src/api/README.md +0 -65
- package/src/api/constant.ts +0 -16
- package/src/api/demoApi.ts +0 -10
- package/src/assets/README.md +0 -54
- package/src/assets/css/index.ts +0 -2
- package/src/components/App/App.tsx +0 -18
- package/src/components/App/index.ts +0 -1
- package/src/components/Footer/Footer.tsx +0 -16
- package/src/components/Footer/__tests__/Footer.test.tsx +0 -15
- package/src/components/Footer/index.ts +0 -1
- package/src/components/Header/Header.tsx +0 -17
- package/src/components/Header/__tests__/Header.test.tsx +0 -15
- package/src/components/Header/__tests__/__snapshots__/Header.test.tsx.snap +0 -37
- package/src/components/Header/index.ts +0 -1
- package/src/components/README.md +0 -33
- package/src/components/TextInput/index.ts +0 -1
- package/src/config/README.md +0 -63
- package/src/config/index.ts +0 -1
- package/src/config/queryClientConfig.ts +0 -3
- package/src/forms/ProfileForm/index.ts +0 -1
- package/src/forms/README.md +0 -142
- package/src/hooks/README.md +0 -83
- package/src/hooks/api/queryKey.ts +0 -1
- package/src/hooks/api/useDemoApi.ts +0 -16
- package/src/pages/Demo/Demo.tsx +0 -236
- package/src/pages/Demo/ReactHookFormDemo/index.ts +0 -1
- package/src/pages/Demo/ReactHookFormDemo/routes.tsx +0 -7
- package/src/pages/Demo/ReactQueryDemo/index.ts +0 -1
- package/src/pages/Demo/ReactQueryDemo/routes.tsx +0 -7
- package/src/pages/Demo/__tests__/Demo.test.tsx +0 -15
- package/src/pages/Demo/__tests__/__snapshots__/Demo.test.tsx.snap +0 -557
- package/src/pages/Demo/index.ts +0 -1
- package/src/pages/Demo/routes.tsx +0 -8
- package/src/pages/Layout/Layout.tsx +0 -12
- package/src/pages/Layout/index.ts +0 -1
- package/src/pages/README.md +0 -91
- package/src/pages/UnexpectedError/UnexpectedError.module.css +0 -8
- package/src/pages/UnexpectedError/UnexpectedError.tsx +0 -22
- package/src/pages/UnexpectedError/index.ts +0 -1
- package/src/providers/QueryClientProvider/QueryClientProvider.tsx +0 -16
- package/src/providers/QueryClientProvider/index.ts +0 -1
- package/src/providers/README.md +0 -112
- package/src/providers/index.ts +0 -1
- package/src/providers/withProviders/index.ts +0 -1
- package/src/providers/withProviders/withProviders.tsx +0 -18
- package/src/routing/README.md +0 -76
- package/src/routing/appRoutes.ts +0 -5
- package/src/routing/routerFactory.ts +0 -12
- package/src/routing/routes.tsx +0 -13
- package/src/types/README.md +0 -62
- package/src/types/demoApi.ts +0 -25
- package/src/ui/README.md +0 -144
- package/src/ui/atoms/Input/index.ts +0 -1
- package/src/ui/atoms/Logo/__tests__/Logo.test.tsx +0 -12
- package/src/ui/atoms/Logo/index.ts +0 -1
- package/src/ui/atoms/NavLink/index.ts +0 -1
- package/src/ui/molecules/Navigation/index.ts +0 -1
- package/src/ui/organisms/Footer/Footer.module.css +0 -10
- package/src/ui/organisms/Footer/Footer.tsx +0 -10
- package/src/ui/organisms/Footer/index.ts +0 -1
- package/src/ui/organisms/Header/Header.module.css +0 -17
- package/src/ui/organisms/Header/Header.tsx +0 -16
- package/src/ui/organisms/Header/index.ts +0 -1
- package/src/ui/organisms/MainContent/MainContent.module.css +0 -3
- package/src/ui/organisms/MainContent/MainContent.tsx +0 -6
- package/src/ui/organisms/MainContent/index.ts +0 -1
- package/src/ui/templates/MainTemplate/MainTemplate.module.css +0 -5
- package/src/ui/templates/MainTemplate/MainTemplate.tsx +0 -26
- package/src/ui/templates/MainTemplate/index.ts +0 -1
- package/src/utils/README.md +0 -70
- package/tests/demo.test.ts +0 -17
- package/tests/fixture/playwright.fixture.ts +0 -15
- /package/src/{assets → app/assets}/css/mainBody.css +0 -0
- /package/src/{assets → demo/ui/assets}/images/logo.png +0 -0
- /package/src/{assets → demo/ui/assets}/images/templatePlugins.png +0 -0
- /package/src/{components/Footer/Footer.module.css → demo/ui/components/DemoFooter/DemoFooter.module.css} +0 -0
- /package/src/{components/Header/Header.module.css → demo/ui/components/DemoHeader/DemoHeader.module.css} +0 -0
- /package/src/{ui/molecules/Navigation/Navigation.module.css → demo/ui/components/DemoNavigation/DemoNavigation.module.css} +0 -0
- /package/src/{ui/atoms → demo/ui/components}/Input/Input.module.scss +0 -0
- /package/src/{ui/atoms → demo/ui/components}/Logo/Logo.module.css +0 -0
- /package/src/{forms → demo/ui/forms}/ProfileForm/ProfileForm.module.scss +0 -0
- /package/src/{pages/Demo/ReactQueryDemo/ReactQueryDemo.module.scss → demo/ui/pages/DependencyInjectionDemo/DependencyInjectionDemo.module.scss} +0 -0
- /package/src/{pages/Demo/ReactHookFormDemo/ReactHookFormDemo.module.css → demo/ui/pages/ReactHookFormDemo/ReactHookFormDemo.module.scss} +0 -0
- /package/src/{types/api.ts → shared/infrastructure/http/fetchApi.type.d.ts} +0 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# `demo`
|
|
2
|
+
|
|
3
|
+
The `demo` folder is the template's **example Bounded Context**.
|
|
4
|
+
|
|
5
|
+
Path alias: `@Demo/*`
|
|
6
|
+
|
|
7
|
+
## What is a Bounded Context?
|
|
8
|
+
|
|
9
|
+
In Domain-Driven Design (DDD), a **Bounded Context** is an isolated, autonomous slice of the application that
|
|
10
|
+
owns its own domain model, its own ubiquitous language, and its own internal implementation. It communicates
|
|
11
|
+
with the outside world (other contexts, the app layer) through well-defined entry points only.
|
|
12
|
+
|
|
13
|
+
Practical rules:
|
|
14
|
+
|
|
15
|
+
- A bounded context **never imports directly from another bounded context**. Cross-context communication goes
|
|
16
|
+
through clearly defined contracts (events, DTOs, or a shared kernel type).
|
|
17
|
+
- A bounded context **owns its full stack**: from the domain model all the way to the UI.
|
|
18
|
+
- Every concept inside a context is named consistently — this is the **Ubiquitous Language** principle. The
|
|
19
|
+
same word must mean the same thing everywhere inside the context.
|
|
20
|
+
|
|
21
|
+
## Layer structure
|
|
22
|
+
|
|
23
|
+
The `demo` context follows Clean Architecture's four-layer model:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
demo/
|
|
27
|
+
├── domain/ ← pure business contracts, entities, repository interfaces
|
|
28
|
+
├── application/ ← use cases, DTOs, orchestration
|
|
29
|
+
├── infrastructure/ ← concrete implementations, adapters
|
|
30
|
+
├── ui/ ← React pages, components, hooks, routing
|
|
31
|
+
├── serviceDemo.ts ← Inversify ContainerModule that wires this context's DI
|
|
32
|
+
└── serviceIdentifiers.ts ← unique Symbols for each service in this context
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### `domain/`
|
|
36
|
+
|
|
37
|
+
The heart of the context. Contains:
|
|
38
|
+
|
|
39
|
+
- **Repository interfaces** — describe the data access operations the domain needs (e.g.,
|
|
40
|
+
`DemoUserRepositoryInterface`, `ArchitecturePrincipleRepositoryInterface`). These are TypeScript interfaces;
|
|
41
|
+
the implementation lives in `infrastructure/`.
|
|
42
|
+
- **Entities** — objects with identity and lifecycle (e.g., `DemoUser`).
|
|
43
|
+
- **Value Objects** — immutable objects defined by their values (no identity).
|
|
44
|
+
|
|
45
|
+
Zero external dependencies. No React, no Inversify, no HTTP.
|
|
46
|
+
|
|
47
|
+
### `application/`
|
|
48
|
+
|
|
49
|
+
The orchestration layer. Contains:
|
|
50
|
+
|
|
51
|
+
- **Use cases** — named, testable business operations (e.g., `GetDemoUsersUseCase`). Each use case implements
|
|
52
|
+
an interface (`GetDemoUsersUseCaseInterface`) so it can be replaced by a test double without modifying the UI.
|
|
53
|
+
- **DTOs** — plain, typed structures that carry data across layer boundaries (e.g., `ArchitecturePrinciple`,
|
|
54
|
+
`ProfileFormValues`).
|
|
55
|
+
|
|
56
|
+
Can import from `domain`. Must not import from `infrastructure` or `ui`.
|
|
57
|
+
|
|
58
|
+
### `infrastructure/`
|
|
59
|
+
|
|
60
|
+
The adapter layer. Contains:
|
|
61
|
+
|
|
62
|
+
- **Concrete repository implementations** (e.g., `HttpDemoUserRepository`, `StaticArchitecturePrincipleRepository`).
|
|
63
|
+
Each class implements an interface defined in `domain/`, satisfying the **Dependency Inversion Principle**.
|
|
64
|
+
|
|
65
|
+
Can import from `domain` (the interfaces it implements) and from `@Shared/infrastructure` (shared adapters).
|
|
66
|
+
Must not import from `application` or `ui`.
|
|
67
|
+
|
|
68
|
+
### `ui/`
|
|
69
|
+
|
|
70
|
+
The presentation layer. Contains:
|
|
71
|
+
|
|
72
|
+
- **Pages** — top-level route components
|
|
73
|
+
- **Components** — context-specific visual components
|
|
74
|
+
- **Layouts** — page-level layout wrappers
|
|
75
|
+
- **Forms** — form composition (React Hook Form)
|
|
76
|
+
- **Hooks** — React Query hooks that bridge use cases to the React component tree
|
|
77
|
+
- **Routing** — route declarations exported to `@App/routing`
|
|
78
|
+
|
|
79
|
+
The UI layer uses `useService` from `@Shared/ui/hooks/useService` to resolve use cases from the DI container.
|
|
80
|
+
It calls use cases; it does not call repositories or infrastructure directly.
|
|
81
|
+
|
|
82
|
+
## Dependency direction within the context
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
ui → application → domain
|
|
86
|
+
infrastructure -.→ domain (implements its interfaces)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The arrows show allowed import direction. Reversing them is a Clean Architecture violation.
|
|
90
|
+
|
|
91
|
+
## DI wiring
|
|
92
|
+
|
|
93
|
+
### `serviceIdentifiers.ts`
|
|
94
|
+
|
|
95
|
+
Declares unique `Symbol` identifiers for every service in the context:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
export const DEMO_SERVICES = Object.freeze({
|
|
99
|
+
DemoUserRepository: Symbol.for("Demo.DemoUserRepository"),
|
|
100
|
+
GetDemoUsersUseCase: Symbol.for("Demo.GetDemoUsersUseCase"),
|
|
101
|
+
// ...
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Using `Symbol.for` with a namespaced string (`Demo.*`) avoids collisions with other contexts.
|
|
106
|
+
|
|
107
|
+
### `serviceDemo.ts`
|
|
108
|
+
|
|
109
|
+
A `ContainerModule` that binds each service identifier to its concrete implementation using Inversify's
|
|
110
|
+
`toDynamicValue`. This pattern avoids TypeScript decorators entirely, keeping the domain and application layers
|
|
111
|
+
free of framework metadata:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
export const serviceDemo: ContainerModule = new ContainerModule((options) => {
|
|
115
|
+
options
|
|
116
|
+
.bind<DemoUserRepositoryInterface>(DEMO_SERVICES.DemoUserRepository)
|
|
117
|
+
.toDynamicValue(() => new HttpDemoUserRepository())
|
|
118
|
+
.inSingletonScope();
|
|
119
|
+
|
|
120
|
+
options
|
|
121
|
+
.bind<GetDemoUsersUseCaseInterface>(DEMO_SERVICES.GetDemoUsersUseCase)
|
|
122
|
+
.toDynamicValue(
|
|
123
|
+
(services) =>
|
|
124
|
+
new GetDemoUsersUseCase(services.get<DemoUserRepositoryInterface>(DEMO_SERVICES.DemoUserRepository)),
|
|
125
|
+
)
|
|
126
|
+
.inSingletonScope();
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`serviceDemo` is loaded into the global container in `@App/config/serviceContainer.ts`.
|
|
131
|
+
|
|
132
|
+
## UI organization principle
|
|
133
|
+
|
|
134
|
+
The UI layer uses practical, readable sub-folders (`components`, `layouts`, `pages`, `forms`, `hooks`,
|
|
135
|
+
`routing`). Atomic Design can still guide how you think about component responsibility — keeping components
|
|
136
|
+
small and composable — but strict `atoms/molecules/organisms/templates` directories add indirection without
|
|
137
|
+
real value in most projects.
|
|
138
|
+
|
|
139
|
+
## Important: demo as a model, not a constraint
|
|
140
|
+
|
|
141
|
+
The `demo` context exists to show the path. When you start your real project:
|
|
142
|
+
|
|
143
|
+
1. Rename or replace `demo` with your first real bounded context.
|
|
144
|
+
2. Add its alias to `tsconfig.json` (e.g., `"@Orders/*": ["src/orders/*"]`).
|
|
145
|
+
3. Create its `serviceIdentifiers.ts` and `service[ContextName].ts` (e.g., `serviceHome.ts`).
|
|
146
|
+
4. Register its container module in `app/config/serviceContainer.ts`.
|
|
147
|
+
|
|
148
|
+
Keep visual components in their context's `ui/` folder until there is a proven, concrete reason to move them
|
|
149
|
+
to `shared`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ArchitecturePrinciple } from "@Demo/application/dtos/ArchitecturePrinciple";
|
|
2
|
+
import type { GetArchitecturePrinciplesUseCaseInterface } from "@Demo/application/use-cases/GetArchitecturePrinciplesUseCaseInterface";
|
|
3
|
+
import type { ArchitecturePrincipleRepositoryInterface } from "@Demo/domain/repositories/ArchitecturePrincipleRepositoryInterface";
|
|
4
|
+
|
|
5
|
+
export class GetArchitecturePrinciplesUseCase implements GetArchitecturePrinciplesUseCaseInterface {
|
|
6
|
+
public constructor(private readonly architecturePrincipleRepository: ArchitecturePrincipleRepositoryInterface) {}
|
|
7
|
+
|
|
8
|
+
public readonly execute = (): ArchitecturePrinciple[] => {
|
|
9
|
+
return this.architecturePrincipleRepository.getAll();
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { GetDemoUsersUseCaseInterface } from "@Demo/application/use-cases/GetDemoUsersUseCaseInterface";
|
|
2
|
+
import type { DemoUser } from "@Demo/domain/entities/DemoUser";
|
|
3
|
+
import type { DemoUserRepositoryInterface } from "@Demo/domain/repositories/DemoUserRepositoryInterface";
|
|
4
|
+
|
|
5
|
+
export class GetDemoUsersUseCase implements GetDemoUsersUseCaseInterface {
|
|
6
|
+
public constructor(private readonly demoUserRepository: DemoUserRepositoryInterface) {}
|
|
7
|
+
|
|
8
|
+
public readonly execute = async (): Promise<DemoUser[]> => {
|
|
9
|
+
return this.demoUserRepository.getAll();
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DemoUser } from "@Demo/domain/entities/DemoUser";
|
|
2
|
+
import type { DemoUserRepositoryInterface } from "@Demo/domain/repositories/DemoUserRepositoryInterface";
|
|
3
|
+
import { fetchApi } from "@Shared/infrastructure/http/fetchApi";
|
|
4
|
+
|
|
5
|
+
type DemoUserApiResponse = {
|
|
6
|
+
id: number;
|
|
7
|
+
name: string;
|
|
8
|
+
username: string;
|
|
9
|
+
email: string;
|
|
10
|
+
website: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export class HttpDemoUserRepository implements DemoUserRepositoryInterface {
|
|
14
|
+
private readonly apiPath = "https://jsonplaceholder.typicode.com/users";
|
|
15
|
+
|
|
16
|
+
public readonly getAll = async (): Promise<DemoUser[]> => {
|
|
17
|
+
const users = await fetchApi<DemoUserApiResponse[]>({
|
|
18
|
+
path: this.apiPath,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return users.map((user) => ({
|
|
22
|
+
id: user.id,
|
|
23
|
+
name: user.name,
|
|
24
|
+
username: user.username.toUpperCase(),
|
|
25
|
+
email: user.email.toLowerCase(),
|
|
26
|
+
website: user.website,
|
|
27
|
+
}));
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ArchitecturePrinciple } from "@Demo/application/dtos/ArchitecturePrinciple";
|
|
2
|
+
import type { ArchitecturePrincipleRepositoryInterface } from "@Demo/domain/repositories/ArchitecturePrincipleRepositoryInterface";
|
|
3
|
+
|
|
4
|
+
export class StaticArchitecturePrincipleRepository implements ArchitecturePrincipleRepositoryInterface {
|
|
5
|
+
private readonly architecturePrinciples: ArchitecturePrinciple[] = [
|
|
6
|
+
{
|
|
7
|
+
title: "Domain stays framework-free",
|
|
8
|
+
description: "The demo domain only defines contracts and pure data structures. React and Inversify stay outside.",
|
|
9
|
+
layer: "domain",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
title: "Application orchestrates use cases",
|
|
13
|
+
description: "Use cases expose intent-driven APIs such as GetDemoUsersUseCase instead of leaking infrastructure.",
|
|
14
|
+
layer: "application",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
title: "Infrastructure implements contracts",
|
|
18
|
+
description:
|
|
19
|
+
"Concrete adapters, such as the HTTP repository, live in infrastructure and are selected by the container.",
|
|
20
|
+
layer: "infrastructure",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
title: "UI depends on abstractions",
|
|
24
|
+
description:
|
|
25
|
+
"React pages use hooks and useService to resolve use cases without importing infrastructure directly.",
|
|
26
|
+
layer: "ui",
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
public readonly getAll = (): ArchitecturePrinciple[] => {
|
|
31
|
+
return this.architecturePrinciples;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { GetArchitecturePrinciplesUseCase } from "@Demo/application/use-cases/GetArchitecturePrinciplesUseCase";
|
|
2
|
+
import type { GetArchitecturePrinciplesUseCaseInterface } from "@Demo/application/use-cases/GetArchitecturePrinciplesUseCaseInterface";
|
|
3
|
+
import { GetDemoUsersUseCase } from "@Demo/application/use-cases/GetDemoUsersUseCase";
|
|
4
|
+
import type { GetDemoUsersUseCaseInterface } from "@Demo/application/use-cases/GetDemoUsersUseCaseInterface";
|
|
5
|
+
import type { ArchitecturePrincipleRepositoryInterface } from "@Demo/domain/repositories/ArchitecturePrincipleRepositoryInterface";
|
|
6
|
+
import type { DemoUserRepositoryInterface } from "@Demo/domain/repositories/DemoUserRepositoryInterface";
|
|
7
|
+
import { HttpDemoUserRepository } from "@Demo/infrastructure/repositories/HttpDemoUserRepository";
|
|
8
|
+
import { StaticArchitecturePrincipleRepository } from "@Demo/infrastructure/repositories/StaticArchitecturePrincipleRepository";
|
|
9
|
+
import { ContainerModule } from "inversify";
|
|
10
|
+
import { DEMO_SERVICES } from "./serviceIdentifiers";
|
|
11
|
+
|
|
12
|
+
export const serviceDemo: ContainerModule = new ContainerModule((options) => {
|
|
13
|
+
options
|
|
14
|
+
.bind<DemoUserRepositoryInterface>(DEMO_SERVICES.DemoUserRepository)
|
|
15
|
+
.toDynamicValue(() => new HttpDemoUserRepository())
|
|
16
|
+
.inSingletonScope();
|
|
17
|
+
|
|
18
|
+
options
|
|
19
|
+
.bind<GetDemoUsersUseCaseInterface>(DEMO_SERVICES.GetDemoUsersUseCase)
|
|
20
|
+
.toDynamicValue(
|
|
21
|
+
(services) =>
|
|
22
|
+
new GetDemoUsersUseCase(services.get<DemoUserRepositoryInterface>(DEMO_SERVICES.DemoUserRepository)),
|
|
23
|
+
)
|
|
24
|
+
.inSingletonScope();
|
|
25
|
+
|
|
26
|
+
options
|
|
27
|
+
.bind<ArchitecturePrincipleRepositoryInterface>(DEMO_SERVICES.ArchitecturePrincipleRepository)
|
|
28
|
+
.toDynamicValue(() => new StaticArchitecturePrincipleRepository())
|
|
29
|
+
.inSingletonScope();
|
|
30
|
+
|
|
31
|
+
options
|
|
32
|
+
.bind<GetArchitecturePrinciplesUseCaseInterface>(DEMO_SERVICES.GetArchitecturePrinciplesUseCase)
|
|
33
|
+
.toDynamicValue(
|
|
34
|
+
(services) =>
|
|
35
|
+
new GetArchitecturePrinciplesUseCase(
|
|
36
|
+
services.get<ArchitecturePrincipleRepositoryInterface>(DEMO_SERVICES.ArchitecturePrincipleRepository),
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
.inSingletonScope();
|
|
40
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const DEMO_SERVICES = Object.freeze({
|
|
2
|
+
DemoUserRepository: Symbol.for("Demo.DemoUserRepository"),
|
|
3
|
+
GetDemoUsersUseCase: Symbol.for("Demo.GetDemoUsersUseCase"),
|
|
4
|
+
ArchitecturePrincipleRepository: Symbol.for("Demo.ArchitecturePrincipleRepository"),
|
|
5
|
+
GetArchitecturePrinciplesUseCase: Symbol.for("Demo.GetArchitecturePrinciplesUseCase"),
|
|
6
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import logo from "@Demo/ui/assets/images/logo.png";
|
|
2
|
+
import { Logo } from "@Demo/ui/components/Logo/Logo";
|
|
3
|
+
import { demoAppRoutes } from "@Demo/ui/routing/demoAppRoutes";
|
|
4
|
+
import { Link } from "react-router";
|
|
5
|
+
|
|
6
|
+
import classes from "./DemoFooter.module.css";
|
|
7
|
+
|
|
8
|
+
export const DemoFooter = () => {
|
|
9
|
+
return (
|
|
10
|
+
<div className={classes.container}>
|
|
11
|
+
<hr />
|
|
12
|
+
<Link to={demoAppRoutes.home}>
|
|
13
|
+
<Logo size="50px" src={logo} alt="logo" />
|
|
14
|
+
</Link>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DemoFooter } from "./DemoFooter";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import logo from "@Demo/ui/assets/images/logo.png";
|
|
2
|
+
import { DemoNavigation } from "@Demo/ui/components/DemoNavigation/DemoNavigation";
|
|
3
|
+
import { Logo } from "@Demo/ui/components/Logo/Logo";
|
|
4
|
+
import { demoAppRoutes } from "@Demo/ui/routing/demoAppRoutes";
|
|
5
|
+
import { Link } from "react-router";
|
|
6
|
+
|
|
7
|
+
import classes from "./DemoHeader.module.css";
|
|
8
|
+
|
|
9
|
+
export const DemoHeader = () => {
|
|
10
|
+
return (
|
|
11
|
+
<div className={classes.container}>
|
|
12
|
+
<Link to={demoAppRoutes.home}>
|
|
13
|
+
<Logo size="50px" src={logo} alt="logo" />
|
|
14
|
+
</Link>
|
|
15
|
+
<DemoNavigation
|
|
16
|
+
links={[
|
|
17
|
+
{ label: "Overview", url: demoAppRoutes.home },
|
|
18
|
+
{ label: "React Query", url: demoAppRoutes.reactQuery },
|
|
19
|
+
{ label: "React Hook Form", url: demoAppRoutes.reactHookForm },
|
|
20
|
+
{ label: "Dependency Injection", url: demoAppRoutes.dependencyInjection },
|
|
21
|
+
]}
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DemoHeader } from "./DemoHeader";
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { NavLink } from
|
|
2
|
-
import { useId } from
|
|
3
|
-
import classes from
|
|
1
|
+
import { NavLink } from "@Demo/ui/components/NavLink/NavLink";
|
|
2
|
+
import { useId } from "react";
|
|
3
|
+
import classes from "./DemoNavigation.module.css";
|
|
4
4
|
|
|
5
|
-
type
|
|
5
|
+
type DemoNavigationProps = {
|
|
6
6
|
links: { label: string; url: string }[];
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
export const
|
|
9
|
+
export const DemoNavigation = ({ links }: DemoNavigationProps) => {
|
|
10
10
|
const id = useId();
|
|
11
11
|
|
|
12
12
|
return (
|
|
13
13
|
<nav>
|
|
14
14
|
<ul className={classes.navList}>
|
|
15
|
-
{links.map(link => (
|
|
15
|
+
{links.map((link) => (
|
|
16
16
|
<li key={`${id} ${link.label} ${link.url}`}>
|
|
17
17
|
<NavLink to={link.url} className={classes.a}>
|
|
18
18
|
{link.label}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DemoNavigation } from "./DemoNavigation";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { forwardRef, InputHTMLAttributes, useId } from
|
|
2
|
-
import classes from
|
|
1
|
+
import { forwardRef, InputHTMLAttributes, useId } from "react";
|
|
2
|
+
import classes from "./Input.module.scss";
|
|
3
3
|
|
|
4
4
|
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
5
5
|
label: string;
|
|
@@ -24,4 +24,4 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(({ label, hasError
|
|
|
24
24
|
);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
Input.displayName =
|
|
27
|
+
Input.displayName = "Input";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Input } from "./Input";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import classes from
|
|
1
|
+
import classes from "./Logo.module.css";
|
|
2
2
|
|
|
3
3
|
type LogoProps = {
|
|
4
4
|
src: string;
|
|
@@ -6,6 +6,6 @@ type LogoProps = {
|
|
|
6
6
|
size?: string;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
export const Logo = ({ src, alt, size =
|
|
9
|
+
export const Logo = ({ src, alt, size = "80px" }: LogoProps) => {
|
|
10
10
|
return <img className={classes.logoStyle} style={{ width: size }} src={src} alt={alt} />;
|
|
11
11
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Logo } from "./Logo";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ComponentProps } from
|
|
2
|
-
import { NavLink as RouterNavLink } from
|
|
1
|
+
import { ComponentProps } from "react";
|
|
2
|
+
import { NavLink as RouterNavLink } from "react-router";
|
|
3
3
|
|
|
4
4
|
type NavLinkProps = ComponentProps<typeof RouterNavLink>;
|
|
5
5
|
|
|
6
6
|
export const NavLink = ({ className, ...props }: NavLinkProps) => (
|
|
7
|
-
<RouterNavLink {...props} className={[className].filter(Boolean).join(
|
|
7
|
+
<RouterNavLink {...props} className={[className].filter(Boolean).join(" ")} />
|
|
8
8
|
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { NavLink } from "./NavLink";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Input } from
|
|
2
|
-
import { InputHTMLAttributes } from
|
|
3
|
-
import { FieldError, useFormContext } from
|
|
1
|
+
import { Input } from "@Demo/ui/components/Input/Input";
|
|
2
|
+
import type { InputHTMLAttributes } from "react";
|
|
3
|
+
import { type FieldError, useFormContext } from "react-hook-form";
|
|
4
4
|
|
|
5
5
|
interface TextInputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
6
6
|
name: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TextInput } from "./TextInput";
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { yupResolver } from
|
|
4
|
-
import { FormProvider, SubmitHandler, useForm } from
|
|
5
|
-
import * as yup from
|
|
6
|
-
import classes from
|
|
1
|
+
import type { ProfileFormValues } from "@Demo/application/dtos/ProfileFormValues";
|
|
2
|
+
import { TextInput } from "@Demo/ui/components/TextInput/TextInput";
|
|
3
|
+
import { yupResolver } from "@hookform/resolvers/yup";
|
|
4
|
+
import { FormProvider, type SubmitHandler, useForm } from "react-hook-form";
|
|
5
|
+
import * as yup from "yup";
|
|
6
|
+
import classes from "./ProfileForm.module.scss";
|
|
7
7
|
|
|
8
8
|
type ProfileFormProps = {
|
|
9
|
-
onSubmit: SubmitHandler<
|
|
9
|
+
onSubmit: SubmitHandler<ProfileFormValues>;
|
|
10
10
|
resetForm: () => void;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
const schema = yup.object().shape({
|
|
14
|
-
mail: yup.string().email(
|
|
15
|
-
firstName: yup.string().required(
|
|
16
|
-
lastName: yup.string().required(
|
|
14
|
+
mail: yup.string().email("Check the mail pattern").required("This field is required"),
|
|
15
|
+
firstName: yup.string().required("This field is required"),
|
|
16
|
+
lastName: yup.string().required("This field is required"),
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
export const ProfileForm = ({ onSubmit, resetForm }: ProfileFormProps) => {
|
|
20
|
-
const methods = useForm<
|
|
20
|
+
const methods = useForm<ProfileFormValues>({
|
|
21
21
|
resolver: yupResolver(schema),
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
25
|
<FormProvider {...methods}>
|
|
26
26
|
<form onSubmit={methods.handleSubmit(onSubmit)}>
|
|
27
|
-
<p>The form
|
|
28
|
-
<TextInput label="E-mail" type="email" name="mail" placeholder="
|
|
27
|
+
<p>The form has been submitted {methods.formState.submitCount} times.</p>
|
|
28
|
+
<TextInput label="E-mail" type="email" name="mail" placeholder="example@gmail.com" />
|
|
29
29
|
|
|
30
30
|
<TextInput label="First Name" name="firstName" placeholder="First Name" />
|
|
31
31
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ProfileForm } from "./ProfileForm";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GetDemoUsersUseCaseInterface } from "@Demo/application/use-cases/GetDemoUsersUseCaseInterface";
|
|
2
|
+
import { DEMO_SERVICES } from "@Demo/serviceIdentifiers";
|
|
3
|
+
import { useService } from "@Shared/ui/hooks/useService/useService";
|
|
4
|
+
import { useQuery } from "@tanstack/react-query";
|
|
5
|
+
|
|
6
|
+
export const useDemoUsersQuery = () => {
|
|
7
|
+
const getDemoUsersUseCase = useService<GetDemoUsersUseCaseInterface>(DEMO_SERVICES.GetDemoUsersUseCase);
|
|
8
|
+
|
|
9
|
+
return useQuery({
|
|
10
|
+
queryKey: ["demo-users"],
|
|
11
|
+
queryFn: () => getDemoUsersUseCase.execute(),
|
|
12
|
+
});
|
|
13
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DemoFooter } from "@Demo/ui/components/DemoFooter/DemoFooter";
|
|
2
|
+
import { DemoHeader } from "@Demo/ui/components/DemoHeader/DemoHeader";
|
|
3
|
+
import { MainTemplate } from "@Demo/ui/layouts/MainTemplate/MainTemplate";
|
|
4
|
+
import { Outlet } from "react-router";
|
|
5
|
+
|
|
6
|
+
export const DemoLayout = () => {
|
|
7
|
+
return (
|
|
8
|
+
<MainTemplate headerProps={{ leftComponents: <DemoHeader /> }} footerProps={{ footerContent: <DemoFooter /> }}>
|
|
9
|
+
<Outlet />
|
|
10
|
+
</MainTemplate>
|
|
11
|
+
);
|
|
12
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
.mainTemplateStyle {
|
|
2
|
+
display: flex;
|
|
3
|
+
min-height: 100%;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.header {
|
|
8
|
+
display: flex;
|
|
9
|
+
padding: 16px;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: space-between;
|
|
12
|
+
gap: 16px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.main {
|
|
16
|
+
flex: 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.footer {
|
|
20
|
+
padding: 16px;
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PropsWithChildren, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import classes from "./MainTemplate.module.css";
|
|
4
|
+
|
|
5
|
+
type MainTemplateProps = {
|
|
6
|
+
headerProps: {
|
|
7
|
+
leftComponents?: ReactNode;
|
|
8
|
+
rightComponents?: ReactNode;
|
|
9
|
+
};
|
|
10
|
+
footerProps: {
|
|
11
|
+
footerContent?: ReactNode;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const MainTemplate = ({ headerProps, footerProps, children }: PropsWithChildren<MainTemplateProps>) => {
|
|
16
|
+
return (
|
|
17
|
+
<div className={classes.mainTemplateStyle}>
|
|
18
|
+
<header className={classes.header}>
|
|
19
|
+
{headerProps.leftComponents}
|
|
20
|
+
{headerProps.rightComponents}
|
|
21
|
+
</header>
|
|
22
|
+
<main className={classes.main}>{children}</main>
|
|
23
|
+
<footer className={classes.footer}>{footerProps.footerContent}</footer>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MainTemplate } from "./MainTemplate";
|