@t1ep1l0t/create-fsd 2.0.2 → 3.0.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 (42) hide show
  1. package/README.md +105 -49
  2. package/bin/cli.js +36 -12
  3. package/package.json +7 -3
  4. package/templates/react-ts/.editorconfig +12 -0
  5. package/templates/react-ts/.env.example +1 -0
  6. package/templates/react-ts/.prettierignore +7 -0
  7. package/templates/react-ts/.prettierrc +10 -0
  8. package/templates/react-ts/.vscode/extensions.json +7 -0
  9. package/templates/react-ts/README.md +70 -0
  10. package/templates/react-ts/eslint.config.js +143 -0
  11. package/templates/react-ts/index.html +13 -0
  12. package/templates/react-ts/package.json +55 -0
  13. package/templates/react-ts/public/locales/en/basic.json +9 -0
  14. package/templates/react-ts/public/locales/ru/basic.json +9 -0
  15. package/templates/react-ts/src/app/App.tsx +11 -0
  16. package/templates/react-ts/src/app/providers/i18n/index.ts +37 -0
  17. package/templates/react-ts/src/app/providers/router/index.tsx +25 -0
  18. package/templates/react-ts/src/app/styles/global.css +15 -0
  19. package/templates/react-ts/src/app/styles/index.css +9 -0
  20. package/templates/react-ts/src/entities/.gitkeep +0 -0
  21. package/templates/react-ts/src/features/.gitkeep +0 -0
  22. package/templates/react-ts/src/main.tsx +14 -0
  23. package/templates/react-ts/src/pages/about/AboutPage.tsx +91 -0
  24. package/templates/react-ts/src/pages/about/index.ts +1 -0
  25. package/templates/react-ts/src/pages/home/HomePage.tsx +94 -0
  26. package/templates/react-ts/src/pages/home/index.ts +1 -0
  27. package/templates/react-ts/src/shared/api/client.ts +32 -0
  28. package/templates/react-ts/src/shared/api/query-client.ts +11 -0
  29. package/templates/react-ts/src/shared/store/counter.ts +15 -0
  30. package/templates/react-ts/src/shared/ui/Button/Button.tsx +31 -0
  31. package/templates/react-ts/src/shared/ui/Button/index.ts +1 -0
  32. package/templates/react-ts/src/shared/ui/Card/Card.tsx +16 -0
  33. package/templates/react-ts/src/shared/ui/Card/index.ts +1 -0
  34. package/templates/react-ts/src/vite-env.d.ts +10 -0
  35. package/templates/react-ts/src/widgets/Header/Header.tsx +45 -0
  36. package/templates/react-ts/src/widgets/Header/index.ts +1 -0
  37. package/templates/react-ts/src/widgets/layouts/BaseLayout/BaseLayout.tsx +13 -0
  38. package/templates/react-ts/src/widgets/layouts/BaseLayout/index.ts +1 -0
  39. package/templates/react-ts/tsconfig.app.json +39 -0
  40. package/templates/react-ts/tsconfig.json +37 -0
  41. package/templates/react-ts/tsconfig.node.json +11 -0
  42. package/templates/react-ts/vite.config.ts +23 -0
@@ -0,0 +1,143 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import react from 'eslint-plugin-react'
4
+ import reactHooks from 'eslint-plugin-react-hooks'
5
+ import reactRefresh from 'eslint-plugin-react-refresh'
6
+ import reactQuery from '@tanstack/eslint-plugin-query'
7
+ import importPlugin from 'eslint-plugin-import'
8
+ import jsxA11y from 'eslint-plugin-jsx-a11y'
9
+ import prettier from 'eslint-plugin-prettier'
10
+ import prettierConfig from 'eslint-config-prettier'
11
+ import tseslint from 'typescript-eslint'
12
+
13
+ export default tseslint.config(
14
+ { ignores: ['dist', 'node_modules', '.vite'] },
15
+ {
16
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
17
+ files: ['**/*.{ts,tsx}'],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ parserOptions: {
22
+ ecmaVersion: 'latest',
23
+ ecmaFeatures: { jsx: true },
24
+ sourceType: 'module',
25
+ },
26
+ },
27
+ settings: {
28
+ react: { version: '18.3' },
29
+ 'import/resolver': {
30
+ node: {
31
+ extensions: ['.ts', '.tsx'],
32
+ },
33
+ },
34
+ },
35
+ plugins: {
36
+ react,
37
+ 'react-hooks': reactHooks,
38
+ 'react-refresh': reactRefresh,
39
+ '@tanstack/query': reactQuery,
40
+ import: importPlugin,
41
+ 'jsx-a11y': jsxA11y,
42
+ prettier,
43
+ },
44
+ rules: {
45
+ ...react.configs.recommended.rules,
46
+ ...react.configs['jsx-runtime'].rules,
47
+ ...reactHooks.configs.recommended.rules,
48
+ ...jsxA11y.configs.recommended.rules,
49
+ ...prettierConfig.rules,
50
+
51
+ // React rules
52
+ 'react/jsx-no-target-blank': 'off',
53
+ 'react/prop-types': 'off',
54
+
55
+ // TypeScript rules
56
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
57
+ '@typescript-eslint/no-explicit-any': 'warn',
58
+ 'react-refresh/only-export-components': [
59
+ 'warn',
60
+ { allowConstantExport: true },
61
+ ],
62
+
63
+ // React Query rules
64
+ '@tanstack/query/exhaustive-deps': 'error',
65
+ '@tanstack/query/no-rest-destructuring': 'warn',
66
+ '@tanstack/query/stable-query-client': 'error',
67
+
68
+ // Import rules
69
+ 'import/order': [
70
+ 'error',
71
+ {
72
+ groups: [
73
+ 'builtin',
74
+ 'external',
75
+ 'internal',
76
+ ['parent', 'sibling'],
77
+ 'index',
78
+ 'object',
79
+ 'type',
80
+ ],
81
+ 'newlines-between': 'always',
82
+ alphabetize: {
83
+ order: 'asc',
84
+ caseInsensitive: true,
85
+ },
86
+ pathGroups: [
87
+ {
88
+ pattern: 'react',
89
+ group: 'external',
90
+ position: 'before',
91
+ },
92
+ {
93
+ pattern: '@app/**',
94
+ group: 'internal',
95
+ position: 'before',
96
+ },
97
+ {
98
+ pattern: '@pages/**',
99
+ group: 'internal',
100
+ position: 'before',
101
+ },
102
+ {
103
+ pattern: '@widgets/**',
104
+ group: 'internal',
105
+ position: 'before',
106
+ },
107
+ {
108
+ pattern: '@features/**',
109
+ group: 'internal',
110
+ position: 'before',
111
+ },
112
+ {
113
+ pattern: '@entities/**',
114
+ group: 'internal',
115
+ position: 'before',
116
+ },
117
+ {
118
+ pattern: '@shared/**',
119
+ group: 'internal',
120
+ position: 'before',
121
+ },
122
+ ],
123
+ pathGroupsExcludedImportTypes: ['react'],
124
+ },
125
+ ],
126
+ 'import/no-unresolved': 'off',
127
+ 'import/no-duplicates': 'error',
128
+ 'import/newline-after-import': 'error',
129
+
130
+ // Accessibility rules
131
+ 'jsx-a11y/anchor-is-valid': [
132
+ 'error',
133
+ {
134
+ components: ['Link'],
135
+ specialLink: ['to'],
136
+ },
137
+ ],
138
+
139
+ // Prettier integration
140
+ 'prettier/prettier': 'error',
141
+ },
142
+ },
143
+ )
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>FSD React App</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "fsd-react-app",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "lint:fix": "eslint . --fix",
11
+ "format": "prettier --write \"src/**/*.{ts,tsx,json,css}\"",
12
+ "format:check": "prettier --check \"src/**/*.{ts,tsx,json,css}\"",
13
+ "preview": "vite preview",
14
+ "type-check": "tsc --noEmit"
15
+ },
16
+ "dependencies": {
17
+ "react": "^18.3.1",
18
+ "react-dom": "^18.3.1",
19
+ "react-router-dom": "^6.22.0",
20
+ "zustand": "^4.5.0",
21
+ "axios": "^1.6.7",
22
+ "i18next": "^23.8.2",
23
+ "i18next-browser-languagedetector": "^8.2.0",
24
+ "i18next-http-backend": "^3.0.2",
25
+ "react-i18next": "^14.0.5",
26
+ "@tanstack/react-query": "^5.22.2",
27
+ "classnames": "^2.5.1",
28
+ "react-hook-form": "^7.65.0",
29
+ "@hookform/resolvers": "^5.2.2"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^22.10.5",
33
+ "@types/react": "^18.3.12",
34
+ "@types/react-dom": "^18.3.1",
35
+ "@vitejs/plugin-react": "^4.3.4",
36
+ "vite": "^6.0.3",
37
+ "typescript": "^5.6.3",
38
+ "@eslint/js": "^9.17.0",
39
+ "eslint": "^9.17.0",
40
+ "globals": "^15.14.0",
41
+ "eslint-plugin-react": "^7.37.2",
42
+ "eslint-plugin-react-hooks": "^5.0.0",
43
+ "eslint-plugin-react-refresh": "^0.4.16",
44
+ "@tanstack/eslint-plugin-query": "^5.62.3",
45
+ "eslint-plugin-import": "^2.31.0",
46
+ "eslint-plugin-jsx-a11y": "^6.10.2",
47
+ "eslint-config-prettier": "^9.1.0",
48
+ "eslint-plugin-prettier": "^5.2.1",
49
+ "prettier": "^3.4.2",
50
+ "prettier-plugin-tailwindcss": "^0.6.9",
51
+ "@tailwindcss/vite": "^4.0.0",
52
+ "tailwindcss": "^4.0.0",
53
+ "typescript-eslint": "^8.18.2"
54
+ }
55
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "welcome": "Welcome to FSD React App",
3
+ "home": "Home",
4
+ "about": "About",
5
+ "counter": "Counter",
6
+ "increment": "Increment",
7
+ "decrement": "Decrement",
8
+ "reset": "Reset"
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "welcome": "Добро пожаловать в FSD React приложение",
3
+ "home": "Главная",
4
+ "about": "О нас",
5
+ "counter": "Счетчик",
6
+ "increment": "Увеличить",
7
+ "decrement": "Уменьшить",
8
+ "reset": "Сбросить"
9
+ }
@@ -0,0 +1,11 @@
1
+ import { QueryClientProvider } from '@tanstack/react-query';
2
+ import { RouterProvider } from '@app/providers/router';
3
+ import { queryClient } from '@shared/api/query-client';
4
+
5
+ export function App() {
6
+ return (
7
+ <QueryClientProvider client={queryClient}>
8
+ <RouterProvider />
9
+ </QueryClientProvider>
10
+ );
11
+ }
@@ -0,0 +1,37 @@
1
+ import i18n from 'i18next';
2
+ import { initReactI18next } from 'react-i18next';
3
+ import LanguageDetector from 'i18next-browser-languagedetector';
4
+ import Backend from 'i18next-http-backend';
5
+
6
+ i18n
7
+ .use(Backend)
8
+ .use(LanguageDetector)
9
+ .use(initReactI18next)
10
+ .init({
11
+ lng: 'en',
12
+ fallbackLng: 'en',
13
+ interpolation: {
14
+ escapeValue: false,
15
+ },
16
+ detection: {
17
+ order: ['path', 'cookie', 'localStorage'],
18
+ caches: ['cookie', 'localStorage'],
19
+ },
20
+ fallbackNS: 'basic',
21
+ ns: ['basic'],
22
+ defaultNS: 'basic',
23
+ backend: {
24
+ loadPath: '/locales/{{lng}}/{{ns}}.json',
25
+ },
26
+ missingKeyHandler: (lng, ns, key) => {
27
+ console.warn(`Missing translation: ${lng}:${ns}:${key}`);
28
+ },
29
+ });
30
+
31
+ document.documentElement.lang = i18n.language;
32
+
33
+ i18n.on('languageChanged', (lng) => {
34
+ document.documentElement.lang = lng;
35
+ });
36
+
37
+ export default i18n;
@@ -0,0 +1,25 @@
1
+ import { createBrowserRouter, RouterProvider as RRDRouterProvider } from 'react-router-dom';
2
+ import { HomePage } from '@pages/home';
3
+ import { AboutPage } from '@pages/about';
4
+ import { BaseLayout } from '@widgets/layouts/BaseLayout';
5
+
6
+ const router = createBrowserRouter([
7
+ {
8
+ path: '/',
9
+ element: <BaseLayout />,
10
+ children: [
11
+ {
12
+ index: true,
13
+ element: <HomePage />,
14
+ },
15
+ {
16
+ path: 'about',
17
+ element: <AboutPage />,
18
+ },
19
+ ],
20
+ },
21
+ ]);
22
+
23
+ export function RouterProvider() {
24
+ return <RRDRouterProvider router={router} />;
25
+ }
@@ -0,0 +1,15 @@
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ html {
6
+ font-size: 14px;
7
+ }
8
+
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
11
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
12
+ sans-serif;
13
+ -webkit-font-smoothing: antialiased;
14
+ -moz-osx-font-smoothing: grayscale;
15
+ }
@@ -0,0 +1,9 @@
1
+ @import "tailwindcss";
2
+
3
+ @theme {
4
+ --color-primary: #3b82f6;
5
+ --color-secondary: #8b5cf6;
6
+ --color-success: #10b981;
7
+ --color-danger: #ef4444;
8
+ --color-warning: #f59e0b;
9
+ }
File without changes
File without changes
@@ -0,0 +1,14 @@
1
+ import React from 'react'
2
+
3
+ import ReactDOM from 'react-dom/client'
4
+
5
+ import { App } from '@app/App'
6
+ import '@app/styles/index.css'
7
+ import '@app/styles/global.css'
8
+ import '@app/providers/i18n'
9
+
10
+ ReactDOM.createRoot(document.getElementById('root')!).render(
11
+ <React.StrictMode>
12
+ <App />
13
+ </React.StrictMode>
14
+ )
@@ -0,0 +1,91 @@
1
+ import { Card } from '@shared/ui/Card';
2
+ import { useTranslation } from 'react-i18next';
3
+
4
+ export function AboutPage() {
5
+ const { t } = useTranslation();
6
+
7
+ return (
8
+ <div className="max-w-3xl mx-auto space-y-6">
9
+ <h1 className="text-4xl font-bold text-gray-900">{t('about')}</h1>
10
+
11
+ <Card title="FSD Architecture">
12
+ <p className="text-gray-700 leading-relaxed mb-4">
13
+ This project follows Feature-Sliced Design (FSD) architecture, which provides
14
+ a clear and scalable folder structure for your React applications.
15
+ </p>
16
+ <div className="space-y-2">
17
+ <div className="flex gap-2">
18
+ <span className="font-mono bg-gray-100 px-2 py-1 rounded">app/</span>
19
+ <span className="text-gray-600">- Application initialization and providers</span>
20
+ </div>
21
+ <div className="flex gap-2">
22
+ <span className="font-mono bg-gray-100 px-2 py-1 rounded">pages/</span>
23
+ <span className="text-gray-600">- Application pages</span>
24
+ </div>
25
+ <div className="flex gap-2">
26
+ <span className="font-mono bg-gray-100 px-2 py-1 rounded">widgets/</span>
27
+ <span className="text-gray-600">- Complex UI components</span>
28
+ </div>
29
+ <div className="flex gap-2">
30
+ <span className="font-mono bg-gray-100 px-2 py-1 rounded">features/</span>
31
+ <span className="text-gray-600">- Business features</span>
32
+ </div>
33
+ <div className="flex gap-2">
34
+ <span className="font-mono bg-gray-100 px-2 py-1 rounded">entities/</span>
35
+ <span className="text-gray-600">- Business entities</span>
36
+ </div>
37
+ <div className="flex gap-2">
38
+ <span className="font-mono bg-gray-100 px-2 py-1 rounded">shared/</span>
39
+ <span className="text-gray-600">- Reusable code and UI components</span>
40
+ </div>
41
+ </div>
42
+ </Card>
43
+
44
+ <Card title="Configured Libraries">
45
+ <ul className="space-y-3">
46
+ <li>
47
+ <strong className="text-blue-600">React Router DOM</strong>
48
+ <p className="text-gray-600 text-sm">Client-side routing with nested routes</p>
49
+ </li>
50
+ <li>
51
+ <strong className="text-blue-600">Zustand</strong>
52
+ <p className="text-gray-600 text-sm">Lightweight state management</p>
53
+ </li>
54
+ <li>
55
+ <strong className="text-blue-600">Axios</strong>
56
+ <p className="text-gray-600 text-sm">HTTP client with interceptors configured</p>
57
+ </li>
58
+ <li>
59
+ <strong className="text-blue-600">React Query</strong>
60
+ <p className="text-gray-600 text-sm">Server state management and caching</p>
61
+ </li>
62
+ <li>
63
+ <strong className="text-blue-600">i18next</strong>
64
+ <p className="text-gray-600 text-sm">Internationalization (EN/RU)</p>
65
+ </li>
66
+ <li>
67
+ <strong className="text-blue-600">TailwindCSS v4</strong>
68
+ <p className="text-gray-600 text-sm">Utility-first CSS framework</p>
69
+ </li>
70
+ </ul>
71
+ </Card>
72
+
73
+ <Card title="Get Started">
74
+ <div className="space-y-4">
75
+ <div>
76
+ <h4 className="font-semibold mb-2">Development</h4>
77
+ <code className="block bg-gray-900 text-green-400 p-3 rounded">
78
+ npm run dev
79
+ </code>
80
+ </div>
81
+ <div>
82
+ <h4 className="font-semibold mb-2">Build for production</h4>
83
+ <code className="block bg-gray-900 text-green-400 p-3 rounded">
84
+ npm run build
85
+ </code>
86
+ </div>
87
+ </div>
88
+ </Card>
89
+ </div>
90
+ );
91
+ }
@@ -0,0 +1 @@
1
+ export { AboutPage } from './AboutPage';
@@ -0,0 +1,94 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ import { Card } from '@shared/ui/Card';
3
+ import { Button } from '@shared/ui/Button';
4
+ import { useCounterStore } from '@shared/store/counter';
5
+ import { useQuery } from '@tanstack/react-query';
6
+ import { apiClient } from '@shared/api/client';
7
+
8
+ interface Post {
9
+ id: number;
10
+ title: string;
11
+ body: string;
12
+ userId: number;
13
+ }
14
+
15
+ export function HomePage() {
16
+ const { t } = useTranslation();
17
+ const { count, increment, decrement, reset } = useCounterStore();
18
+
19
+ const { data: posts, isLoading } = useQuery<Post[]>({
20
+ queryKey: ['posts'],
21
+ queryFn: async () => {
22
+ const response = await apiClient.get<Post[]>('/posts?_limit=5');
23
+ return response.data;
24
+ },
25
+ });
26
+
27
+ return (
28
+ <div className="space-y-8">
29
+ <div className="text-center">
30
+ <h1 className="text-4xl font-bold text-gray-900 mb-4">{t('welcome')}</h1>
31
+ <p className="text-lg text-gray-600">
32
+ React + Vite + FSD Architecture + TailwindCSS v4
33
+ </p>
34
+ </div>
35
+
36
+ <div className="grid md:grid-cols-2 gap-6">
37
+ <Card title={t('counter')}>
38
+ <div className="text-center space-y-4">
39
+ <div className="text-6xl font-bold text-blue-600">{count}</div>
40
+ <div className="flex gap-2 justify-center">
41
+ <Button onClick={decrement} variant="danger">
42
+ {t('decrement')}
43
+ </Button>
44
+ <Button onClick={reset} variant="secondary">
45
+ {t('reset')}
46
+ </Button>
47
+ <Button onClick={increment} variant="success">
48
+ {t('increment')}
49
+ </Button>
50
+ </div>
51
+ <p className="text-sm text-gray-500">Powered by Zustand</p>
52
+ </div>
53
+ </Card>
54
+
55
+ <Card title="React Query Example">
56
+ {isLoading ? (
57
+ <div className="text-center text-gray-500">Loading posts...</div>
58
+ ) : (
59
+ <ul className="space-y-2">
60
+ {posts?.map((post) => (
61
+ <li key={post.id} className="p-2 bg-gray-50 rounded">
62
+ <div className="font-medium text-sm">{post.title}</div>
63
+ </li>
64
+ ))}
65
+ </ul>
66
+ )}
67
+ <p className="text-sm text-gray-500 mt-4">Powered by React Query + Axios</p>
68
+ </Card>
69
+ </div>
70
+
71
+ <Card title="Technologies Used">
72
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
73
+ {[
74
+ 'React Router',
75
+ 'Zustand',
76
+ 'Axios',
77
+ 'i18next',
78
+ 'React Query',
79
+ 'TailwindCSS v4',
80
+ 'Vite',
81
+ 'FSD Architecture',
82
+ ].map((tech) => (
83
+ <div
84
+ key={tech}
85
+ className="p-3 bg-blue-50 text-blue-700 rounded-lg text-center font-medium"
86
+ >
87
+ {tech}
88
+ </div>
89
+ ))}
90
+ </div>
91
+ </Card>
92
+ </div>
93
+ );
94
+ }
@@ -0,0 +1 @@
1
+ export { HomePage } from './HomePage';
@@ -0,0 +1,32 @@
1
+ import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
2
+
3
+ export const apiClient = axios.create({
4
+ baseURL: import.meta.env.VITE_API_URL || 'https://jsonplaceholder.typicode.com',
5
+ timeout: 10000,
6
+ headers: {
7
+ 'Content-Type': 'application/json',
8
+ },
9
+ });
10
+
11
+ apiClient.interceptors.request.use(
12
+ (config: InternalAxiosRequestConfig) => {
13
+ const token = localStorage.getItem('token');
14
+ if (token) {
15
+ config.headers.Authorization = `Bearer ${token}`;
16
+ }
17
+ return config;
18
+ },
19
+ (error: AxiosError) => {
20
+ return Promise.reject(error);
21
+ }
22
+ );
23
+
24
+ apiClient.interceptors.response.use(
25
+ (response) => response,
26
+ (error: AxiosError) => {
27
+ if (error.response?.status === 401) {
28
+ localStorage.removeItem('token');
29
+ }
30
+ return Promise.reject(error);
31
+ }
32
+ );
@@ -0,0 +1,11 @@
1
+ import { QueryClient } from '@tanstack/react-query';
2
+
3
+ export const queryClient = new QueryClient({
4
+ defaultOptions: {
5
+ queries: {
6
+ retry: 1,
7
+ refetchOnWindowFocus: false,
8
+ staleTime: 5 * 60 * 1000, // 5 minutes
9
+ },
10
+ },
11
+ });
@@ -0,0 +1,15 @@
1
+ import { create } from 'zustand';
2
+
3
+ interface CounterState {
4
+ count: number;
5
+ increment: () => void;
6
+ decrement: () => void;
7
+ reset: () => void;
8
+ }
9
+
10
+ export const useCounterStore = create<CounterState>((set) => ({
11
+ count: 0,
12
+ increment: () => set((state) => ({ count: state.count + 1 })),
13
+ decrement: () => set((state) => ({ count: state.count - 1 })),
14
+ reset: () => set({ count: 0 }),
15
+ }));
@@ -0,0 +1,31 @@
1
+ import { ButtonHTMLAttributes, ReactNode } from 'react';
2
+
3
+ type ButtonVariant = 'primary' | 'secondary' | 'success' | 'danger' | 'outline';
4
+
5
+ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
6
+ children: ReactNode;
7
+ variant?: ButtonVariant;
8
+ className?: string;
9
+ }
10
+
11
+ export function Button({ children, variant = 'primary', onClick, className = '', ...props }: ButtonProps) {
12
+ const baseStyles = 'px-4 py-2 rounded-lg font-medium transition-colors duration-200';
13
+
14
+ const variantStyles: Record<ButtonVariant, string> = {
15
+ primary: 'bg-blue-600 hover:bg-blue-700 text-white',
16
+ secondary: 'bg-gray-600 hover:bg-gray-700 text-white',
17
+ success: 'bg-green-600 hover:bg-green-700 text-white',
18
+ danger: 'bg-red-600 hover:bg-red-700 text-white',
19
+ outline: 'border-2 border-blue-600 text-blue-600 hover:bg-blue-50',
20
+ };
21
+
22
+ return (
23
+ <button
24
+ className={`${baseStyles} ${variantStyles[variant]} ${className}`}
25
+ onClick={onClick}
26
+ {...props}
27
+ >
28
+ {children}
29
+ </button>
30
+ );
31
+ }
@@ -0,0 +1 @@
1
+ export { Button } from './Button';
@@ -0,0 +1,16 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ interface CardProps {
4
+ children: ReactNode;
5
+ className?: string;
6
+ title?: string;
7
+ }
8
+
9
+ export function Card({ children, className = '', title }: CardProps) {
10
+ return (
11
+ <div className={`bg-white rounded-lg shadow-md p-6 ${className}`}>
12
+ {title && <h3 className="text-xl font-bold mb-4">{title}</h3>}
13
+ {children}
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1 @@
1
+ export { Card } from './Card';