@yousxlfs/next-arch 0.1.1 → 0.2.1
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/dist/index.js +580 -84
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/app/package.json +25 -4
- package/templates/packages/better-auth/examples/src/features/_examples/with-better-auth/lib/auth-placeholder.ts +14 -0
- package/templates/packages/env/core/src/shared/config/env.ts +22 -0
- package/templates/packages/jotai/core/src/shared/providers/JotaiProvider.tsx +19 -0
- package/templates/packages/jotai/examples/src/features/_examples/with-jotai/README.md +3 -0
- package/templates/packages/jotai/examples/src/features/_examples/with-jotai/model/example.atoms.ts +19 -0
- package/templates/packages/motion/core/src/shared/lib/motion.ts +17 -0
- package/templates/packages/motion/examples/src/features/_examples/with-motion/components/ExampleMotionCard.tsx +19 -0
- package/templates/packages/next-intl/core/src/shared/config/i18n.ts +10 -0
- package/templates/packages/nuqs/examples/src/features/_examples/with-nuqs/README.md +3 -0
- package/templates/packages/nuqs/examples/src/features/_examples/with-nuqs/hooks/use-example-params.ts +19 -0
- package/templates/packages/react-hook-form/examples/src/features/_examples/with-react-hook-form/README.md +1 -0
- package/templates/packages/react-hook-form/examples/src/features/_examples/with-react-hook-form/components/ExampleRhfForm.tsx +40 -0
- package/templates/packages/redux/core/src/app/providers/redux-store.ts +15 -0
- package/templates/packages/redux/core/src/shared/providers/ReduxProvider.tsx +24 -0
- package/templates/packages/redux/examples/src/app/providers/redux-store.ts +18 -0
- package/templates/packages/redux/examples/src/features/_examples/with-redux/README.md +4 -0
- package/templates/packages/redux/examples/src/features/_examples/with-redux/model/example.slice.ts +36 -0
- package/templates/packages/sentry/core/src/shared/config/sentry.ts +9 -0
- package/templates/packages/sonner/examples/src/features/_examples/with-sonner/lib/toast.ts +7 -0
- package/templates/packages/sonner-provider/core/src/shared/providers/SonnerToaster.tsx +13 -0
- package/templates/packages/tanstack-form/examples/src/features/_examples/with-tanstack-form/README.md +3 -0
- package/templates/packages/tanstack-form/examples/src/features/_examples/with-tanstack-form/components/ExampleForm.tsx +60 -0
- package/templates/packages/tanstack-query/core/src/shared/lib/query-client.ts +24 -0
- package/templates/packages/tanstack-query/core/src/shared/providers/QueryProvider.tsx +30 -0
- package/templates/packages/tanstack-query/examples/src/features/_examples/with-tanstack-query/README.md +34 -0
- package/templates/packages/tanstack-query/examples/src/features/_examples/with-tanstack-query/actions/example.action.ts +34 -0
- package/templates/packages/tanstack-query/examples/src/features/_examples/with-tanstack-query/queries/use-example.query.ts +49 -0
- package/templates/packages/tanstack-table/examples/src/features/_examples/with-tanstack-table/README.md +3 -0
- package/templates/packages/tanstack-table/examples/src/features/_examples/with-tanstack-table/components/ExampleTable.tsx +66 -0
- package/templates/packages/trpc/core/src/app/api/trpc/router.ts +19 -0
- package/templates/packages/trpc/examples/src/app/providers/trpc-client.ts +13 -0
- package/templates/packages/uploadthing/core/src/app/api/uploadthing/route.ts +10 -0
- package/templates/packages/zustand/core/src/shared/lib/store.ts +13 -0
- package/templates/packages/zustand/examples/src/features/_examples/with-zustand/README.md +13 -0
- package/templates/packages/zustand/examples/src/features/_examples/with-zustand/model/example.store.ts +28 -0
- package/templates/pages/auth/src/app/({{name}})/layout.tsx +7 -0
- package/templates/pages/auth/src/app/({{name}})/login/page.tsx +5 -0
- package/templates/pages/auth/src/app/({{name}})/register/page.tsx +5 -0
- package/templates/pages/auth/src/entities/user/index.ts +2 -0
- package/templates/pages/auth/src/entities/user/lib/user-schema.ts +9 -0
- package/templates/pages/auth/src/entities/user/types/user.types.ts +5 -0
- package/templates/pages/auth/src/features/{{name}}/actions/login.action.ts +7 -0
- package/templates/pages/auth/src/features/{{name}}/actions/logout.action.ts +5 -0
- package/templates/pages/auth/src/features/{{name}}/actions/register.action.ts +7 -0
- package/templates/pages/auth/src/features/{{name}}/components/AuthGuard.tsx +14 -0
- package/templates/pages/auth/src/features/{{name}}/components/LoginForm.tsx +36 -0
- package/templates/pages/auth/src/features/{{name}}/components/RegisterForm.tsx +43 -0
- package/templates/pages/auth/src/features/{{name}}/hooks/use-session.ts +14 -0
- package/templates/pages/auth/src/features/{{name}}/index.ts +9 -0
- package/templates/pages/auth/src/features/{{name}}/lib/auth-helpers.ts +3 -0
- package/templates/pages/auth/src/features/{{name}}/queries/use-user.query.ts +16 -0
- package/templates/pages/auth/src/features/{{name}}/types/auth.types.ts +13 -0
- package/templates/pages/auth/src/views/{{name}}/LoginView.tsx +10 -0
- package/templates/pages/auth/src/views/{{name}}/RegisterView.tsx +10 -0
- package/templates/pages/auth/src/views/{{name}}/index.ts +2 -0
- package/templates/pages/blank/src/app/{{name}}/page.tsx +5 -0
- package/templates/pages/blank/src/features/{{name}}/index.ts +3 -0
- package/templates/pages/blank/src/views/{{name}}/index.ts +1 -0
- package/templates/pages/blank/src/views/{{name}}/{{Name}}View.tsx +8 -0
- package/templates/pages/crud/src/app/{{name}}/[id]/page.tsx +5 -0
- package/templates/pages/crud/src/app/{{name}}/new/page.tsx +5 -0
- package/templates/pages/crud/src/app/{{name}}/page.tsx +5 -0
- package/templates/pages/crud/src/entities/{{name}}/index.ts +2 -0
- package/templates/pages/crud/src/entities/{{name}}/lib/{{name}}-schema.ts +6 -0
- package/templates/pages/crud/src/entities/{{name}}/types/{{name}}.types.ts +4 -0
- package/templates/pages/crud/src/features/{{name}}/actions/create-{{name}}.action.ts +5 -0
- package/templates/pages/crud/src/features/{{name}}/actions/delete-{{name}}.action.ts +5 -0
- package/templates/pages/crud/src/features/{{name}}/actions/update-{{name}}.action.ts +5 -0
- package/templates/pages/crud/src/features/{{name}}/components/ProductCard.tsx +3 -0
- package/templates/pages/crud/src/features/{{name}}/components/ProductForm.tsx +5 -0
- package/templates/pages/crud/src/features/{{name}}/components/ProductsList.tsx +15 -0
- package/templates/pages/crud/src/features/{{name}}/index.ts +8 -0
- package/templates/pages/crud/src/features/{{name}}/queries/use-{{name}}.query.ts +10 -0
- package/templates/pages/crud/src/features/{{name}}/queries/use-{{name}}s.query.ts +10 -0
- package/templates/pages/crud/src/views/{{name}}/index.ts +1 -0
- package/templates/pages/crud/src/views/{{name}}/{{Name}}ListView.tsx +26 -0
- package/templates/pages/dashboard/src/app/{{name}}/layout.tsx +8 -0
- package/templates/pages/dashboard/src/app/{{name}}/page.tsx +5 -0
- package/templates/pages/dashboard/src/features/{{name}}/components/AnalyticsCard.tsx +8 -0
- package/templates/pages/dashboard/src/features/{{name}}/index.ts +1 -0
- package/templates/pages/dashboard/src/views/{{name}}/DashboardView.tsx +10 -0
- package/templates/pages/dashboard/src/views/{{name}}/index.ts +1 -0
- package/templates/pages/profile/src/app/{{name}}/page.tsx +5 -0
- package/templates/pages/profile/src/features/{{name}}/components/ProfileCard.tsx +8 -0
- package/templates/pages/profile/src/features/{{name}}/index.ts +1 -0
- package/templates/pages/profile/src/views/{{name}}/ProfileView.tsx +9 -0
- package/templates/pages/profile/src/views/{{name}}/index.ts +1 -0
- package/templates/pages/settings/src/app/{{name}}/page.tsx +5 -0
- package/templates/pages/settings/src/features/{{name}}/components/SettingsTabs.tsx +18 -0
- package/templates/pages/settings/src/features/{{name}}/index.ts +1 -0
- package/templates/pages/settings/src/views/{{name}}/SettingsView.tsx +10 -0
- package/templates/pages/settings/src/views/{{name}}/index.ts +1 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ПРИМЕР: глобальный QueryClient для TanStack Query
|
|
3
|
+
*
|
|
4
|
+
* Где живёт: shared/lib/
|
|
5
|
+
* Почему здесь: QueryClient — инфраструктура, не бизнес-логика.
|
|
6
|
+
* Один экземпляр на всё приложение, переиспользуется всеми фичами.
|
|
7
|
+
*
|
|
8
|
+
* Правило: фичи импортируют queryClient отсюда, но queries/mutations
|
|
9
|
+
* объявляют внутри своей фичи (features/<name>/queries/).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
13
|
+
|
|
14
|
+
export const queryClient = new QueryClient({
|
|
15
|
+
defaultOptions: {
|
|
16
|
+
queries: {
|
|
17
|
+
staleTime: 60 * 1000,
|
|
18
|
+
refetchOnWindowFocus: false,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Куда это идёт в архитектуре:
|
|
24
|
+
// shared/lib/ — слой shared, доступен всем верхним слоям (entities → features → views → app).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ПРИМЕР: провайдер TanStack Query для Next.js App Router
|
|
5
|
+
*
|
|
6
|
+
* Где живёт: shared/providers/
|
|
7
|
+
* Почему здесь: провайдер — инфраструктурная обёртка, не привязана к фиче.
|
|
8
|
+
* Подключается один раз в app/layout.tsx.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
12
|
+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|
13
|
+
import type { ReactNode } from 'react';
|
|
14
|
+
import { queryClient } from '@/shared/lib/query-client';
|
|
15
|
+
|
|
16
|
+
interface QueryProviderProps {
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function QueryProvider({ children }: QueryProviderProps) {
|
|
21
|
+
return (
|
|
22
|
+
<QueryClientProvider client={queryClient}>
|
|
23
|
+
{children}
|
|
24
|
+
<ReactQueryDevtools initialIsOpen={false} />
|
|
25
|
+
</QueryClientProvider>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Куда это идёт в архитектуре:
|
|
30
|
+
// shared/providers/ → импортируется в app/layout.tsx (слой app, только композиция).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Пример: TanStack Query
|
|
2
|
+
|
|
3
|
+
Папку `_examples` можно удалить после изучения.
|
|
4
|
+
|
|
5
|
+
## Структура
|
|
6
|
+
|
|
7
|
+
- `queries/` — клиентские хуки с кешем (`useQuery`, `useMutation`)
|
|
8
|
+
- `actions/` — Server Actions, источник данных на сервере
|
|
9
|
+
|
|
10
|
+
## Как перенести в свою фичу
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx @yousxlfs/next-arch g feature billing
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Скопируй паттерн в `src/features/billing/queries/` и `actions/`.
|
|
17
|
+
|
|
18
|
+
## Подключение провайдера
|
|
19
|
+
|
|
20
|
+
В `src/app/layout.tsx`:
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { QueryProvider } from '@/shared/providers/QueryProvider';
|
|
24
|
+
|
|
25
|
+
export default function RootLayout({ children }) {
|
|
26
|
+
return (
|
|
27
|
+
<html>
|
|
28
|
+
<body>
|
|
29
|
+
<QueryProvider>{children}</QueryProvider>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ПРИМЕР: Server Action как источник данных для TanStack Query
|
|
5
|
+
*
|
|
6
|
+
* Где живёт: features/<name>/actions/
|
|
7
|
+
* Почему здесь: actions выполняются на сервере (мутации, fetch с секретами).
|
|
8
|
+
* Query-хук в queries/ вызывает action и кеширует результат на клиенте.
|
|
9
|
+
*
|
|
10
|
+
* Правило: не импортируй server actions напрямую в UI-компоненты для чтения —
|
|
11
|
+
* оборачивай в useQuery/useMutation.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface ExampleData {
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function fetchExampleAction(): Promise<
|
|
20
|
+
{ ok: true; data: ExampleData } | { ok: false; error: string }
|
|
21
|
+
> {
|
|
22
|
+
// В реальном проекте здесь будет запрос к БД или внешнему API
|
|
23
|
+
return {
|
|
24
|
+
ok: true,
|
|
25
|
+
data: {
|
|
26
|
+
id: '1',
|
|
27
|
+
title: 'Пример данных с сервера',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Куда это идёт в архитектуре:
|
|
33
|
+
// features/<name>/actions/ — серверный слой фичи.
|
|
34
|
+
// Вызывается из queries/ той же фичи, не из views/ напрямую.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ПРИМЕР: TanStack Query в архитектуре next-arch
|
|
5
|
+
*
|
|
6
|
+
* Где живёт: features/<name>/queries/
|
|
7
|
+
* Почему здесь: queries — клиентский кеш серверных данных.
|
|
8
|
+
* Они принадлежат конкретной фиче и не выносятся в shared/.
|
|
9
|
+
*
|
|
10
|
+
* Правило: Server Actions делают мутации (actions/),
|
|
11
|
+
* TanStack Query кеширует и синхронизирует данные (queries/).
|
|
12
|
+
* Никогда не смешивай их ответственности.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
16
|
+
import { fetchExampleAction } from '../actions/example.action';
|
|
17
|
+
|
|
18
|
+
const EXAMPLE_QUERY_KEY = ['example'] as const;
|
|
19
|
+
|
|
20
|
+
export function useExampleQuery() {
|
|
21
|
+
return useQuery({
|
|
22
|
+
queryKey: EXAMPLE_QUERY_KEY,
|
|
23
|
+
queryFn: async () => {
|
|
24
|
+
const result = await fetchExampleAction();
|
|
25
|
+
if (!result.ok) {
|
|
26
|
+
throw new Error(result.error);
|
|
27
|
+
}
|
|
28
|
+
return result.data;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useExampleMutation() {
|
|
34
|
+
const queryClient = useQueryClient();
|
|
35
|
+
|
|
36
|
+
return useMutation({
|
|
37
|
+
mutationFn: async (message: string) => {
|
|
38
|
+
return { message, updatedAt: new Date().toISOString() };
|
|
39
|
+
},
|
|
40
|
+
onSuccess: () => {
|
|
41
|
+
void queryClient.invalidateQueries({ queryKey: EXAMPLE_QUERY_KEY });
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Куда это идёт в архитектуре:
|
|
47
|
+
// features/_examples/with-tanstack-query/queries/ — клиентский слой фичи.
|
|
48
|
+
// Импортирует action из ../actions/ (та же фича, относительный путь — OK).
|
|
49
|
+
// В реальном проекте замени _examples на свою фичу, например features/billing/queries/.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ПРИМЕР: TanStack Table (headless)
|
|
5
|
+
*
|
|
6
|
+
* Где живёт: features/<name>/components/
|
|
7
|
+
* Таблица — UI фичи, данные приходят из queries/ или props от view.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createColumnHelper,
|
|
12
|
+
flexRender,
|
|
13
|
+
getCoreRowModel,
|
|
14
|
+
useReactTable,
|
|
15
|
+
} from '@tanstack/react-table';
|
|
16
|
+
|
|
17
|
+
interface Row {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const columnHelper = createColumnHelper<Row>();
|
|
23
|
+
|
|
24
|
+
const columns = [
|
|
25
|
+
columnHelper.accessor('id', { header: 'ID' }),
|
|
26
|
+
columnHelper.accessor('name', { header: 'Name' }),
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const data: Row[] = [
|
|
30
|
+
{ id: '1', name: 'Пример' },
|
|
31
|
+
{ id: '2', name: 'Строка' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export function ExampleTable() {
|
|
35
|
+
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() });
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<table className="w-full border-collapse text-sm">
|
|
39
|
+
<thead>
|
|
40
|
+
{table.getHeaderGroups().map((hg) => (
|
|
41
|
+
<tr key={hg.id}>
|
|
42
|
+
{hg.headers.map((header) => (
|
|
43
|
+
<th key={header.id} className="border px-3 py-2 text-left">
|
|
44
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
45
|
+
</th>
|
|
46
|
+
))}
|
|
47
|
+
</tr>
|
|
48
|
+
))}
|
|
49
|
+
</thead>
|
|
50
|
+
<tbody>
|
|
51
|
+
{table.getRowModel().rows.map((row) => (
|
|
52
|
+
<tr key={row.id}>
|
|
53
|
+
{row.getVisibleCells().map((cell) => (
|
|
54
|
+
<td key={cell.id} className="border px-3 py-2">
|
|
55
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
56
|
+
</td>
|
|
57
|
+
))}
|
|
58
|
+
</tr>
|
|
59
|
+
))}
|
|
60
|
+
</tbody>
|
|
61
|
+
</table>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Куда это идёт в архитектуре:
|
|
66
|
+
// features/<name>/components/ — не в shared/, таблица привязана к домену фичи.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ПРИМЕР: tRPC router (заглушка)
|
|
3
|
+
*
|
|
4
|
+
* Где живёт: app/api/trpc/ или отдельный backend.
|
|
5
|
+
* В FSD клиентские хуки tRPC — в features/<name>/queries/.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { initTRPC } from '@trpc/server';
|
|
9
|
+
|
|
10
|
+
const t = initTRPC.create();
|
|
11
|
+
|
|
12
|
+
export const appRouter = t.router({
|
|
13
|
+
hello: t.procedure.query(() => ({ message: 'Привет из tRPC' })),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type AppRouter = typeof appRouter;
|
|
17
|
+
|
|
18
|
+
// Куда это идёт в архитектуре:
|
|
19
|
+
// Серверный роутер — app/api или packages/server. Не в shared/.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ПРИМЕР: tRPC React client
|
|
5
|
+
*
|
|
6
|
+
* Где живёт: app/providers/
|
|
7
|
+
* app — верхний слой, может импортировать типы роутера из app/api/.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createTRPCReact } from '@trpc/react-query';
|
|
11
|
+
import type { AppRouter } from '@/app/api/trpc/router';
|
|
12
|
+
|
|
13
|
+
export const trpc = createTRPCReact<AppRouter>();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ПРИМЕР: Uploadthing — route handler
|
|
3
|
+
*
|
|
4
|
+
* Где живёт: app/api/uploadthing/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// import { createRouteHandler } from 'uploadthing/next';
|
|
8
|
+
// export const { GET, POST } = createRouteHandler({ router: uploadRouter });
|
|
9
|
+
|
|
10
|
+
export const UPLOADTHING_PLACEHOLDER = 'Создай uploadRouter в features/<name>/lib/';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ПРИМЕР: где держать Zustand
|
|
3
|
+
*
|
|
4
|
+
* Бизнес-стейт → features/<name>/model/
|
|
5
|
+
* Глобальный UI (тема, sidebar) → можно здесь в shared/lib/, если не привязан к фиче.
|
|
6
|
+
*
|
|
7
|
+
* См. пример store в src/features/_examples/with-zustand/model/example.store.ts
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export {};
|
|
11
|
+
|
|
12
|
+
// Куда это идёт в архитектуре:
|
|
13
|
+
// shared/lib/ — только инфраструктура без бизнес-логики фич.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Пример: Zustand
|
|
2
|
+
|
|
3
|
+
Store живёт в `model/` внутри фичи, не в `shared/`.
|
|
4
|
+
|
|
5
|
+
## Публичный API
|
|
6
|
+
|
|
7
|
+
Добавь в `features/<name>/index.ts`:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
export { useUiStore } from './model/example.store';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Views импортируют только через `@/features/<name>`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ПРИМЕР: Zustand store внутри фичи
|
|
5
|
+
*
|
|
6
|
+
* Где живёт: features/<name>/model/
|
|
7
|
+
* Почему здесь: стейт принадлежит фиче. Другие фичи не импортируют этот store
|
|
8
|
+
* напрямую — используй shared/ или прокидывай данные через props из view.
|
|
9
|
+
*
|
|
10
|
+
* Правило next-arch/no-cross-feature-imports: features/cart не импортирует
|
|
11
|
+
* features/auth/model/*.store.ts. Вынеси общее в entities/ или shared/.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { create } from 'zustand';
|
|
15
|
+
|
|
16
|
+
interface UiState {
|
|
17
|
+
sidebarOpen: boolean;
|
|
18
|
+
toggleSidebar: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const useUiStore = create<UiState>((set) => ({
|
|
22
|
+
sidebarOpen: true,
|
|
23
|
+
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
// Куда это идёт в архитектуре:
|
|
27
|
+
// features/<name>/model/ — клиентский стейт конкретной фичи.
|
|
28
|
+
// Экспортируй через features/<name>/index.ts для views/.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { useSession } from '../hooks/use-session';
|
|
5
|
+
|
|
6
|
+
interface AuthGuardProps {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
fallback?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function AuthGuard({ children, fallback = <p>Требуется авторизация</p> }: AuthGuardProps) {
|
|
12
|
+
const { isAuthenticated } = useSession();
|
|
13
|
+
return isAuthenticated ? children : fallback;
|
|
14
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { loginAction } from '../actions/login.action';
|
|
5
|
+
|
|
6
|
+
export function LoginForm() {
|
|
7
|
+
const [email, setEmail] = useState('');
|
|
8
|
+
const [password, setPassword] = useState('');
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<form
|
|
12
|
+
className="flex flex-col gap-3"
|
|
13
|
+
onSubmit={(e) => {
|
|
14
|
+
e.preventDefault();
|
|
15
|
+
void loginAction({ email, password });
|
|
16
|
+
}}
|
|
17
|
+
>
|
|
18
|
+
<input
|
|
19
|
+
className="rounded border px-3 py-2"
|
|
20
|
+
placeholder="Email"
|
|
21
|
+
value={email}
|
|
22
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
23
|
+
/>
|
|
24
|
+
<input
|
|
25
|
+
className="rounded border px-3 py-2"
|
|
26
|
+
type="password"
|
|
27
|
+
placeholder="Пароль"
|
|
28
|
+
value={password}
|
|
29
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
30
|
+
/>
|
|
31
|
+
<button type="submit" className="rounded bg-primary px-4 py-2 text-primary-foreground">
|
|
32
|
+
Войти
|
|
33
|
+
</button>
|
|
34
|
+
</form>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { registerAction } from '../actions/register.action';
|
|
5
|
+
|
|
6
|
+
export function RegisterForm() {
|
|
7
|
+
const [name, setName] = useState('');
|
|
8
|
+
const [email, setEmail] = useState('');
|
|
9
|
+
const [password, setPassword] = useState('');
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<form
|
|
13
|
+
className="flex flex-col gap-3"
|
|
14
|
+
onSubmit={(e) => {
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
void registerAction({ name, email, password });
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
<input
|
|
20
|
+
className="rounded border px-3 py-2"
|
|
21
|
+
placeholder="Имя"
|
|
22
|
+
value={name}
|
|
23
|
+
onChange={(e) => setName(e.target.value)}
|
|
24
|
+
/>
|
|
25
|
+
<input
|
|
26
|
+
className="rounded border px-3 py-2"
|
|
27
|
+
placeholder="Email"
|
|
28
|
+
value={email}
|
|
29
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
30
|
+
/>
|
|
31
|
+
<input
|
|
32
|
+
className="rounded border px-3 py-2"
|
|
33
|
+
type="password"
|
|
34
|
+
placeholder="Пароль"
|
|
35
|
+
value={password}
|
|
36
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
37
|
+
/>
|
|
38
|
+
<button type="submit" className="rounded bg-primary px-4 py-2 text-primary-foreground">
|
|
39
|
+
Зарегистрироваться
|
|
40
|
+
</button>
|
|
41
|
+
</form>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from 'react';
|
|
4
|
+
import type { Session } from '../types/auth.types';
|
|
5
|
+
|
|
6
|
+
export function useSession() {
|
|
7
|
+
const [session, setSession] = useState<Session | null>(null);
|
|
8
|
+
|
|
9
|
+
const setDemoSession = useCallback((value: Session | null) => {
|
|
10
|
+
setSession(value);
|
|
11
|
+
}, []);
|
|
12
|
+
|
|
13
|
+
return { session, setDemoSession, isAuthenticated: Boolean(session) };
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { LoginForm } from './components/LoginForm';
|
|
2
|
+
export { RegisterForm } from './components/RegisterForm';
|
|
3
|
+
export { AuthGuard } from './components/AuthGuard';
|
|
4
|
+
export { useSession } from './hooks/use-session';
|
|
5
|
+
export { useUserQuery } from './queries/use-user.query';
|
|
6
|
+
export { loginAction } from './actions/login.action';
|
|
7
|
+
export { registerAction } from './actions/register.action';
|
|
8
|
+
export { logoutAction } from './actions/logout.action';
|
|
9
|
+
export type { LoginInput, RegisterInput, Session } from './types/auth.types';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useQuery } from '@tanstack/react-query';
|
|
4
|
+
import { loginAction } from '../actions/login.action';
|
|
5
|
+
|
|
6
|
+
export function useUserQuery(email: string | null) {
|
|
7
|
+
return useQuery({
|
|
8
|
+
queryKey: ['user', email],
|
|
9
|
+
enabled: Boolean(email),
|
|
10
|
+
queryFn: async () => {
|
|
11
|
+
if (!email) return null;
|
|
12
|
+
const result = await loginAction({ email, password: 'demo' });
|
|
13
|
+
return result.ok ? result : null;
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { LoginForm } from '@/features/{{name}}';
|
|
2
|
+
|
|
3
|
+
export function LoginView() {
|
|
4
|
+
return (
|
|
5
|
+
<main className="w-full max-w-md space-y-4 rounded-xl border bg-card p-8 shadow-sm">
|
|
6
|
+
<h1 className="text-2xl font-semibold">Вход</h1>
|
|
7
|
+
<LoginForm />
|
|
8
|
+
</main>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { RegisterForm } from '@/features/{{name}}';
|
|
2
|
+
|
|
3
|
+
export function RegisterView() {
|
|
4
|
+
return (
|
|
5
|
+
<main className="w-full max-w-md space-y-4 rounded-xl border bg-card p-8 shadow-sm">
|
|
6
|
+
<h1 className="text-2xl font-semibold">Регистрация</h1>
|
|
7
|
+
<RegisterForm />
|
|
8
|
+
</main>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { {{Name}}View } from './{{Name}}View';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function {{Name}}View() {
|
|
2
|
+
return (
|
|
3
|
+
<main className="mx-auto flex max-w-3xl flex-col gap-4 p-8">
|
|
4
|
+
<h1 className="text-3xl font-semibold">{{Name}}</h1>
|
|
5
|
+
<p className="text-muted-foreground">Blank page — добавь фичи и собери UI здесь.</p>
|
|
6
|
+
</main>
|
|
7
|
+
);
|
|
8
|
+
}
|