@yousxlfs/next-arch 0.1.1 → 0.2.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.
Files changed (96) hide show
  1. package/dist/index.js +581 -84
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/app/package.json +25 -4
  5. package/templates/packages/better-auth/examples/src/features/_examples/with-better-auth/lib/auth-placeholder.ts +14 -0
  6. package/templates/packages/env/core/src/shared/config/env.ts +22 -0
  7. package/templates/packages/jotai/core/src/shared/providers/JotaiProvider.tsx +19 -0
  8. package/templates/packages/jotai/examples/src/features/_examples/with-jotai/README.md +3 -0
  9. package/templates/packages/jotai/examples/src/features/_examples/with-jotai/model/example.atoms.ts +19 -0
  10. package/templates/packages/motion/core/src/shared/lib/motion.ts +17 -0
  11. package/templates/packages/motion/examples/src/features/_examples/with-motion/components/ExampleMotionCard.tsx +19 -0
  12. package/templates/packages/next-intl/core/src/shared/config/i18n.ts +10 -0
  13. package/templates/packages/nuqs/examples/src/features/_examples/with-nuqs/README.md +3 -0
  14. package/templates/packages/nuqs/examples/src/features/_examples/with-nuqs/hooks/use-example-params.ts +19 -0
  15. package/templates/packages/react-hook-form/examples/src/features/_examples/with-react-hook-form/README.md +1 -0
  16. package/templates/packages/react-hook-form/examples/src/features/_examples/with-react-hook-form/components/ExampleRhfForm.tsx +40 -0
  17. package/templates/packages/redux/core/src/app/providers/redux-store.ts +15 -0
  18. package/templates/packages/redux/core/src/shared/providers/ReduxProvider.tsx +24 -0
  19. package/templates/packages/redux/examples/src/app/providers/redux-store.ts +18 -0
  20. package/templates/packages/redux/examples/src/features/_examples/with-redux/README.md +4 -0
  21. package/templates/packages/redux/examples/src/features/_examples/with-redux/model/example.slice.ts +36 -0
  22. package/templates/packages/sentry/core/src/shared/config/sentry.ts +9 -0
  23. package/templates/packages/sonner/examples/src/features/_examples/with-sonner/lib/toast.ts +7 -0
  24. package/templates/packages/sonner-provider/core/src/shared/providers/SonnerToaster.tsx +13 -0
  25. package/templates/packages/tanstack-form/examples/src/features/_examples/with-tanstack-form/README.md +3 -0
  26. package/templates/packages/tanstack-form/examples/src/features/_examples/with-tanstack-form/components/ExampleForm.tsx +60 -0
  27. package/templates/packages/tanstack-query/core/src/shared/lib/query-client.ts +24 -0
  28. package/templates/packages/tanstack-query/core/src/shared/providers/QueryProvider.tsx +30 -0
  29. package/templates/packages/tanstack-query/examples/src/features/_examples/with-tanstack-query/README.md +34 -0
  30. package/templates/packages/tanstack-query/examples/src/features/_examples/with-tanstack-query/actions/example.action.ts +34 -0
  31. package/templates/packages/tanstack-query/examples/src/features/_examples/with-tanstack-query/queries/use-example.query.ts +49 -0
  32. package/templates/packages/tanstack-table/examples/src/features/_examples/with-tanstack-table/README.md +3 -0
  33. package/templates/packages/tanstack-table/examples/src/features/_examples/with-tanstack-table/components/ExampleTable.tsx +66 -0
  34. package/templates/packages/trpc/core/src/app/api/trpc/router.ts +19 -0
  35. package/templates/packages/trpc/examples/src/app/providers/trpc-client.ts +13 -0
  36. package/templates/packages/uploadthing/core/src/app/api/uploadthing/route.ts +10 -0
  37. package/templates/packages/zustand/core/src/shared/lib/store.ts +13 -0
  38. package/templates/packages/zustand/examples/src/features/_examples/with-zustand/README.md +13 -0
  39. package/templates/packages/zustand/examples/src/features/_examples/with-zustand/model/example.store.ts +28 -0
  40. package/templates/pages/auth/src/app/({{name}})/layout.tsx +7 -0
  41. package/templates/pages/auth/src/app/({{name}})/login/page.tsx +5 -0
  42. package/templates/pages/auth/src/app/({{name}})/register/page.tsx +5 -0
  43. package/templates/pages/auth/src/entities/user/index.ts +2 -0
  44. package/templates/pages/auth/src/entities/user/lib/user-schema.ts +9 -0
  45. package/templates/pages/auth/src/entities/user/types/user.types.ts +5 -0
  46. package/templates/pages/auth/src/features/{{name}}/actions/login.action.ts +7 -0
  47. package/templates/pages/auth/src/features/{{name}}/actions/logout.action.ts +5 -0
  48. package/templates/pages/auth/src/features/{{name}}/actions/register.action.ts +7 -0
  49. package/templates/pages/auth/src/features/{{name}}/components/AuthGuard.tsx +14 -0
  50. package/templates/pages/auth/src/features/{{name}}/components/LoginForm.tsx +36 -0
  51. package/templates/pages/auth/src/features/{{name}}/components/RegisterForm.tsx +43 -0
  52. package/templates/pages/auth/src/features/{{name}}/hooks/use-session.ts +14 -0
  53. package/templates/pages/auth/src/features/{{name}}/index.ts +9 -0
  54. package/templates/pages/auth/src/features/{{name}}/lib/auth-helpers.ts +3 -0
  55. package/templates/pages/auth/src/features/{{name}}/queries/use-user.query.ts +16 -0
  56. package/templates/pages/auth/src/features/{{name}}/types/auth.types.ts +13 -0
  57. package/templates/pages/auth/src/views/{{name}}/LoginView.tsx +10 -0
  58. package/templates/pages/auth/src/views/{{name}}/RegisterView.tsx +10 -0
  59. package/templates/pages/auth/src/views/{{name}}/index.ts +2 -0
  60. package/templates/pages/blank/src/app/{{name}}/page.tsx +5 -0
  61. package/templates/pages/blank/src/features/{{name}}/index.ts +3 -0
  62. package/templates/pages/blank/src/views/{{name}}/index.ts +1 -0
  63. package/templates/pages/blank/src/views/{{name}}/{{Name}}View.tsx +8 -0
  64. package/templates/pages/crud/src/app/{{name}}/[id]/page.tsx +5 -0
  65. package/templates/pages/crud/src/app/{{name}}/new/page.tsx +5 -0
  66. package/templates/pages/crud/src/app/{{name}}/page.tsx +5 -0
  67. package/templates/pages/crud/src/entities/{{name}}/index.ts +2 -0
  68. package/templates/pages/crud/src/entities/{{name}}/lib/{{name}}-schema.ts +6 -0
  69. package/templates/pages/crud/src/entities/{{name}}/types/{{name}}.types.ts +4 -0
  70. package/templates/pages/crud/src/features/{{name}}/actions/create-{{name}}.action.ts +5 -0
  71. package/templates/pages/crud/src/features/{{name}}/actions/delete-{{name}}.action.ts +5 -0
  72. package/templates/pages/crud/src/features/{{name}}/actions/update-{{name}}.action.ts +5 -0
  73. package/templates/pages/crud/src/features/{{name}}/components/ProductCard.tsx +3 -0
  74. package/templates/pages/crud/src/features/{{name}}/components/ProductForm.tsx +5 -0
  75. package/templates/pages/crud/src/features/{{name}}/components/ProductsList.tsx +15 -0
  76. package/templates/pages/crud/src/features/{{name}}/index.ts +8 -0
  77. package/templates/pages/crud/src/features/{{name}}/queries/use-{{name}}.query.ts +10 -0
  78. package/templates/pages/crud/src/features/{{name}}/queries/use-{{name}}s.query.ts +10 -0
  79. package/templates/pages/crud/src/views/{{name}}/index.ts +1 -0
  80. package/templates/pages/crud/src/views/{{name}}/{{Name}}ListView.tsx +26 -0
  81. package/templates/pages/dashboard/src/app/{{name}}/layout.tsx +8 -0
  82. package/templates/pages/dashboard/src/app/{{name}}/page.tsx +5 -0
  83. package/templates/pages/dashboard/src/features/{{name}}/components/AnalyticsCard.tsx +8 -0
  84. package/templates/pages/dashboard/src/features/{{name}}/index.ts +1 -0
  85. package/templates/pages/dashboard/src/views/{{name}}/DashboardView.tsx +10 -0
  86. package/templates/pages/dashboard/src/views/{{name}}/index.ts +1 -0
  87. package/templates/pages/profile/src/app/{{name}}/page.tsx +5 -0
  88. package/templates/pages/profile/src/features/{{name}}/components/ProfileCard.tsx +8 -0
  89. package/templates/pages/profile/src/features/{{name}}/index.ts +1 -0
  90. package/templates/pages/profile/src/views/{{name}}/ProfileView.tsx +9 -0
  91. package/templates/pages/profile/src/views/{{name}}/index.ts +1 -0
  92. package/templates/pages/settings/src/app/{{name}}/page.tsx +5 -0
  93. package/templates/pages/settings/src/features/{{name}}/components/SettingsTabs.tsx +18 -0
  94. package/templates/pages/settings/src/features/{{name}}/index.ts +1 -0
  95. package/templates/pages/settings/src/views/{{name}}/SettingsView.tsx +10 -0
  96. package/templates/pages/settings/src/views/{{name}}/index.ts +1 -0
@@ -0,0 +1,60 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * ПРИМЕР: TanStack Form + Zod в фиче
5
+ *
6
+ * Где живёт: features/<name>/components/ (или ui/)
7
+ * Форма — UI с логикой ввода, принадлежит фиче.
8
+ * Схема валидации — entities/<entity>/lib/ или types/ фичи.
9
+ */
10
+
11
+ import { useForm } from '@tanstack/react-form';
12
+ import { z } from 'zod';
13
+
14
+ const exampleSchema = z.object({
15
+ email: z.string().email('Некорректный email'),
16
+ password: z.string().min(8, 'Минимум 8 символов'),
17
+ });
18
+
19
+ type ExampleFormValues = z.infer<typeof exampleSchema>;
20
+
21
+ export function ExampleForm() {
22
+ const form = useForm({
23
+ defaultValues: { email: '', password: '' } satisfies ExampleFormValues,
24
+ onSubmit: async ({ value }) => {
25
+ const parsed = exampleSchema.safeParse(value);
26
+ if (!parsed.success) return;
27
+ // Вызов Server Action из features/<name>/actions/
28
+ console.log(parsed.data);
29
+ },
30
+ });
31
+
32
+ return (
33
+ <form
34
+ onSubmit={(e) => {
35
+ e.preventDefault();
36
+ void form.handleSubmit();
37
+ }}
38
+ className="flex flex-col gap-4 max-w-sm"
39
+ >
40
+ <form.Field name="email">
41
+ {(field) => (
42
+ <label className="flex flex-col gap-1 text-sm">
43
+ Email
44
+ <input
45
+ className="rounded border px-3 py-2"
46
+ value={field.state.value}
47
+ onChange={(e) => field.handleChange(e.target.value)}
48
+ />
49
+ </label>
50
+ )}
51
+ </form.Field>
52
+ <button type="submit" className="rounded bg-primary px-4 py-2 text-primary-foreground">
53
+ Отправить
54
+ </button>
55
+ </form>
56
+ );
57
+ }
58
+
59
+ // Куда это идёт в архитектуре:
60
+ // features/<name>/components/ — UI фичи. View импортирует форму через @/features/<name>.
@@ -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,3 @@
1
+ # Пример: TanStack Table
2
+
3
+ Headless UI — стилизуй через Tailwind в components/ фичи.
@@ -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,7 @@
1
+ export default function {{Name}}Layout({ children }: { children: React.ReactNode }) {
2
+ return (
3
+ <div className="flex min-h-screen items-center justify-center bg-muted/30 p-4">
4
+ {children}
5
+ </div>
6
+ );
7
+ }
@@ -0,0 +1,5 @@
1
+ import { LoginView } from '@/views/{{name}}';
2
+
3
+ export default function LoginPage() {
4
+ return <LoginView />;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { RegisterView } from '@/views/{{name}}';
2
+
3
+ export default function RegisterPage() {
4
+ return <RegisterView />;
5
+ }
@@ -0,0 +1,2 @@
1
+ export type { User } from './types/user.types';
2
+ export { userSchema, type UserSchema } from './lib/user-schema';
@@ -0,0 +1,9 @@
1
+ import { z } from 'zod';
2
+
3
+ export const userSchema = z.object({
4
+ id: z.string(),
5
+ email: z.string().email(),
6
+ name: z.string().min(1),
7
+ });
8
+
9
+ export type UserSchema = z.infer<typeof userSchema>;
@@ -0,0 +1,5 @@
1
+ export interface User {
2
+ id: string;
3
+ email: string;
4
+ name: string;
5
+ }
@@ -0,0 +1,7 @@
1
+ 'use server';
2
+
3
+ import type { LoginInput } from '../types/auth.types';
4
+
5
+ export async function loginAction(input: LoginInput) {
6
+ return { ok: true as const, userId: 'demo', email: input.email };
7
+ }
@@ -0,0 +1,5 @@
1
+ 'use server';
2
+
3
+ export async function logoutAction() {
4
+ return { ok: true as const };
5
+ }
@@ -0,0 +1,7 @@
1
+ 'use server';
2
+
3
+ import type { RegisterInput } from '../types/auth.types';
4
+
5
+ export async function registerAction(input: RegisterInput) {
6
+ return { ok: true as const, userId: 'demo', email: input.email };
7
+ }
@@ -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,3 @@
1
+ export function isValidEmail(email: string): boolean {
2
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
3
+ }
@@ -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,13 @@
1
+ export interface LoginInput {
2
+ email: string;
3
+ password: string;
4
+ }
5
+
6
+ export interface RegisterInput extends LoginInput {
7
+ name: string;
8
+ }
9
+
10
+ export interface Session {
11
+ userId: string;
12
+ email: string;
13
+ }
@@ -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
+ }