create-ely 0.1.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/LICENSE +21 -0
- package/README.md +50 -0
- package/package.json +60 -0
- package/src/index.ts +187 -0
- package/templates/monorepo/README.md +75 -0
- package/templates/monorepo/apps/backend/.cursor/mcp.json +8 -0
- package/templates/monorepo/apps/backend/.dockerignore +60 -0
- package/templates/monorepo/apps/backend/.env.example +19 -0
- package/templates/monorepo/apps/backend/.github/workflows/lint.yml +31 -0
- package/templates/monorepo/apps/backend/.github/workflows/tests.yml +36 -0
- package/templates/monorepo/apps/backend/AGENTS.md +79 -0
- package/templates/monorepo/apps/backend/CHANGELOG.md +190 -0
- package/templates/monorepo/apps/backend/CLAUDE.md +149 -0
- package/templates/monorepo/apps/backend/Dockerfile +35 -0
- package/templates/monorepo/apps/backend/LICENSE +21 -0
- package/templates/monorepo/apps/backend/README.md +274 -0
- package/templates/monorepo/apps/backend/biome.json +58 -0
- package/templates/monorepo/apps/backend/bun.lock +303 -0
- package/templates/monorepo/apps/backend/docker-compose.yml +37 -0
- package/templates/monorepo/apps/backend/drizzle.config.ts +14 -0
- package/templates/monorepo/apps/backend/package.json +42 -0
- package/templates/monorepo/apps/backend/src/common/config.ts +29 -0
- package/templates/monorepo/apps/backend/src/common/logger.ts +18 -0
- package/templates/monorepo/apps/backend/src/db/index.ts +31 -0
- package/templates/monorepo/apps/backend/src/db/migrations/20251111132328_curly_spectrum.sql +8 -0
- package/templates/monorepo/apps/backend/src/db/migrations/meta/20251111132328_snapshot.json +70 -0
- package/templates/monorepo/apps/backend/src/db/migrations/meta/_journal.json +13 -0
- package/templates/monorepo/apps/backend/src/db/schema/users.ts +39 -0
- package/templates/monorepo/apps/backend/src/main.ts +67 -0
- package/templates/monorepo/apps/backend/src/middleware/error-handler.ts +36 -0
- package/templates/monorepo/apps/backend/src/modules/users/index.ts +61 -0
- package/templates/monorepo/apps/backend/src/modules/users/model.ts +48 -0
- package/templates/monorepo/apps/backend/src/modules/users/service.ts +46 -0
- package/templates/monorepo/apps/backend/src/tests/users.test.ts +102 -0
- package/templates/monorepo/apps/backend/src/util/graceful-shutdown.ts +37 -0
- package/templates/monorepo/apps/backend/tsconfig.json +35 -0
- package/templates/monorepo/apps/backend-biome.json.template +14 -0
- package/templates/monorepo/apps/frontend/.env.example +1 -0
- package/templates/monorepo/apps/frontend/README.md +59 -0
- package/templates/monorepo/apps/frontend/biome.json +16 -0
- package/templates/monorepo/apps/frontend/components.json +21 -0
- package/templates/monorepo/apps/frontend/index.html +15 -0
- package/templates/monorepo/apps/frontend/package.json +48 -0
- package/templates/monorepo/apps/frontend/public/favicon.ico +0 -0
- package/templates/monorepo/apps/frontend/src/assets/fonts/.gitkeep +0 -0
- package/templates/monorepo/apps/frontend/src/assets/images/.gitkeep +0 -0
- package/templates/monorepo/apps/frontend/src/features/layout/Header.tsx +73 -0
- package/templates/monorepo/apps/frontend/src/main.tsx +36 -0
- package/templates/monorepo/apps/frontend/src/routeTree.gen.ts +95 -0
- package/templates/monorepo/apps/frontend/src/routes/__root.tsx +25 -0
- package/templates/monorepo/apps/frontend/src/routes/index.tsx +34 -0
- package/templates/monorepo/apps/frontend/src/routes/users/index.tsx +79 -0
- package/templates/monorepo/apps/frontend/src/routes/users/new.tsx +148 -0
- package/templates/monorepo/apps/frontend/src/shared/api/client.ts +6 -0
- package/templates/monorepo/apps/frontend/src/shared/components/.gitkeep +0 -0
- package/templates/monorepo/apps/frontend/src/shared/constants/.gitkeep +0 -0
- package/templates/monorepo/apps/frontend/src/shared/hooks/.gitkeep +0 -0
- package/templates/monorepo/apps/frontend/src/shared/types/.gitkeep +0 -0
- package/templates/monorepo/apps/frontend/src/shared/utils/utils.ts +6 -0
- package/templates/monorepo/apps/frontend/src/styles.css +138 -0
- package/templates/monorepo/apps/frontend/src/vite-env.d.ts +13 -0
- package/templates/monorepo/apps/frontend/tsconfig.json +27 -0
- package/templates/monorepo/apps/frontend/vite.config.ts +27 -0
- package/templates/monorepo/biome.json +65 -0
- package/templates/monorepo/bun.lock +1044 -0
- package/templates/monorepo/package.json +13 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" href="/favicon.ico" />
|
|
7
|
+
<meta name="theme-color" content="#000000" />
|
|
8
|
+
<meta name="description" content="ElysiaJS boilerplate with React frontend" />
|
|
9
|
+
<title>ElysiaJS Boilerplate</title>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="app"></div>
|
|
13
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "frontend",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite",
|
|
7
|
+
"build": "vite build && tsc",
|
|
8
|
+
"preview": "vite preview",
|
|
9
|
+
"test": "vitest run",
|
|
10
|
+
"format": "biome format",
|
|
11
|
+
"lint": "biome lint",
|
|
12
|
+
"check": "biome check",
|
|
13
|
+
"routes:generate": "tsr generate",
|
|
14
|
+
"routes:watch": "tsr watch"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@elysiajs/eden": "^1.4.6",
|
|
18
|
+
"@tailwindcss/vite": "^4.0.6",
|
|
19
|
+
"@tanstack/react-devtools": "^0.9.0",
|
|
20
|
+
"@tanstack/react-router": "^1.144.0",
|
|
21
|
+
"@tanstack/react-router-devtools": "^1.144.0",
|
|
22
|
+
"@tanstack/router-plugin": "^1.145.2",
|
|
23
|
+
"class-variance-authority": "^0.7.1",
|
|
24
|
+
"clsx": "^2.1.1",
|
|
25
|
+
"elysia-boilerplate": "workspace:*",
|
|
26
|
+
"lucide-react": "^0.562.0",
|
|
27
|
+
"react": "^19.2.0",
|
|
28
|
+
"react-dom": "^19.2.0",
|
|
29
|
+
"tailwind-merge": "^3.0.2",
|
|
30
|
+
"tailwindcss": "^4.0.6",
|
|
31
|
+
"tw-animate-css": "^1.3.6"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@biomejs/biome": "2.3.10",
|
|
35
|
+
"@tanstack/devtools-vite": "^0.4.0",
|
|
36
|
+
"@tanstack/router-cli": "^1.145.2",
|
|
37
|
+
"@testing-library/dom": "^10.4.0",
|
|
38
|
+
"@testing-library/react": "^16.2.0",
|
|
39
|
+
"@types/node": "^25.0.3",
|
|
40
|
+
"@types/react": "^19.2.0",
|
|
41
|
+
"@types/react-dom": "^19.2.0",
|
|
42
|
+
"@vitejs/plugin-react": "^5.0.4",
|
|
43
|
+
"jsdom": "^27.0.0",
|
|
44
|
+
"typescript": "^5.7.2",
|
|
45
|
+
"vite": "^7.1.7",
|
|
46
|
+
"vitest": "^4.0.16"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Link } from '@tanstack/react-router';
|
|
2
|
+
import { Home, Menu, Users, X } from 'lucide-react';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export default function Header() {
|
|
6
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<header className="p-4 flex items-center bg-gray-800 text-white shadow-lg">
|
|
11
|
+
<button
|
|
12
|
+
onClick={() => setIsOpen(true)}
|
|
13
|
+
className="p-2 hover:bg-gray-700 rounded-lg transition-colors"
|
|
14
|
+
aria-label="Open menu"
|
|
15
|
+
type="button"
|
|
16
|
+
>
|
|
17
|
+
<Menu size={24} />
|
|
18
|
+
</button>
|
|
19
|
+
<h1 className="ml-4 text-xl font-semibold">
|
|
20
|
+
<Link to="/" className="hover:text-cyan-400 transition-colors">
|
|
21
|
+
App
|
|
22
|
+
</Link>
|
|
23
|
+
</h1>
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<aside
|
|
27
|
+
className={`fixed top-0 left-0 h-full w-80 bg-gray-900 text-white shadow-2xl z-50 transform transition-transform duration-300 ease-in-out flex flex-col ${
|
|
28
|
+
isOpen ? 'translate-x-0' : '-translate-x-full'
|
|
29
|
+
}`}
|
|
30
|
+
>
|
|
31
|
+
<div className="flex items-center justify-between p-4 border-b border-gray-700">
|
|
32
|
+
<h2 className="text-xl font-bold">Navigation</h2>
|
|
33
|
+
<button
|
|
34
|
+
onClick={() => setIsOpen(false)}
|
|
35
|
+
className="p-2 hover:bg-gray-800 rounded-lg transition-colors"
|
|
36
|
+
aria-label="Close menu"
|
|
37
|
+
type="button"
|
|
38
|
+
>
|
|
39
|
+
<X size={24} />
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<nav className="flex-1 p-4 overflow-y-auto">
|
|
44
|
+
<Link
|
|
45
|
+
to="/"
|
|
46
|
+
onClick={() => setIsOpen(false)}
|
|
47
|
+
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
|
|
48
|
+
activeProps={{
|
|
49
|
+
className:
|
|
50
|
+
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<Home size={20} />
|
|
54
|
+
<span className="font-medium">Home</span>
|
|
55
|
+
</Link>
|
|
56
|
+
|
|
57
|
+
<Link
|
|
58
|
+
to="/users"
|
|
59
|
+
onClick={() => setIsOpen(false)}
|
|
60
|
+
className="flex items-center gap-3 p-3 rounded-lg hover:bg-gray-800 transition-colors mb-2"
|
|
61
|
+
activeProps={{
|
|
62
|
+
className:
|
|
63
|
+
'flex items-center gap-3 p-3 rounded-lg bg-cyan-600 hover:bg-cyan-700 transition-colors mb-2',
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<Users size={20} />
|
|
67
|
+
<span className="font-medium">Users</span>
|
|
68
|
+
</Link>
|
|
69
|
+
</nav>
|
|
70
|
+
</aside>
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createRouter, RouterProvider } from '@tanstack/react-router';
|
|
2
|
+
import { StrictMode } from 'react';
|
|
3
|
+
import ReactDOM from 'react-dom/client';
|
|
4
|
+
|
|
5
|
+
// Import the generated route tree
|
|
6
|
+
import { routeTree } from './routeTree.gen';
|
|
7
|
+
|
|
8
|
+
import './styles.css';
|
|
9
|
+
|
|
10
|
+
// Create a new router instance
|
|
11
|
+
const router = createRouter({
|
|
12
|
+
routeTree,
|
|
13
|
+
context: {},
|
|
14
|
+
defaultPreload: 'intent',
|
|
15
|
+
scrollRestoration: true,
|
|
16
|
+
defaultStructuralSharing: true,
|
|
17
|
+
defaultPreloadStaleTime: 0,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Register the router instance for type safety
|
|
21
|
+
declare module '@tanstack/react-router' {
|
|
22
|
+
interface Register {
|
|
23
|
+
router: typeof router;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Render the app
|
|
28
|
+
const rootElement = document.getElementById('app');
|
|
29
|
+
if (rootElement && !rootElement.innerHTML) {
|
|
30
|
+
const root = ReactDOM.createRoot(rootElement);
|
|
31
|
+
root.render(
|
|
32
|
+
<StrictMode>
|
|
33
|
+
<RouterProvider router={router} />
|
|
34
|
+
</StrictMode>,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
|
|
5
|
+
// noinspection JSUnusedGlobalSymbols
|
|
6
|
+
|
|
7
|
+
// This file was automatically generated by TanStack Router.
|
|
8
|
+
// You should NOT make any changes in this file as it will be overwritten.
|
|
9
|
+
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
|
10
|
+
|
|
11
|
+
import { Route as rootRouteImport } from './routes/__root'
|
|
12
|
+
import { Route as IndexRouteImport } from './routes/index'
|
|
13
|
+
import { Route as UsersIndexRouteImport } from './routes/users/index'
|
|
14
|
+
import { Route as UsersNewRouteImport } from './routes/users/new'
|
|
15
|
+
|
|
16
|
+
const IndexRoute = IndexRouteImport.update({
|
|
17
|
+
id: '/',
|
|
18
|
+
path: '/',
|
|
19
|
+
getParentRoute: () => rootRouteImport,
|
|
20
|
+
} as any)
|
|
21
|
+
const UsersIndexRoute = UsersIndexRouteImport.update({
|
|
22
|
+
id: '/users/',
|
|
23
|
+
path: '/users/',
|
|
24
|
+
getParentRoute: () => rootRouteImport,
|
|
25
|
+
} as any)
|
|
26
|
+
const UsersNewRoute = UsersNewRouteImport.update({
|
|
27
|
+
id: '/users/new',
|
|
28
|
+
path: '/users/new',
|
|
29
|
+
getParentRoute: () => rootRouteImport,
|
|
30
|
+
} as any)
|
|
31
|
+
|
|
32
|
+
export interface FileRoutesByFullPath {
|
|
33
|
+
'/': typeof IndexRoute
|
|
34
|
+
'/users/new': typeof UsersNewRoute
|
|
35
|
+
'/users': typeof UsersIndexRoute
|
|
36
|
+
}
|
|
37
|
+
export interface FileRoutesByTo {
|
|
38
|
+
'/': typeof IndexRoute
|
|
39
|
+
'/users/new': typeof UsersNewRoute
|
|
40
|
+
'/users': typeof UsersIndexRoute
|
|
41
|
+
}
|
|
42
|
+
export interface FileRoutesById {
|
|
43
|
+
__root__: typeof rootRouteImport
|
|
44
|
+
'/': typeof IndexRoute
|
|
45
|
+
'/users/new': typeof UsersNewRoute
|
|
46
|
+
'/users/': typeof UsersIndexRoute
|
|
47
|
+
}
|
|
48
|
+
export interface FileRouteTypes {
|
|
49
|
+
fileRoutesByFullPath: FileRoutesByFullPath
|
|
50
|
+
fullPaths: '/' | '/users/new' | '/users'
|
|
51
|
+
fileRoutesByTo: FileRoutesByTo
|
|
52
|
+
to: '/' | '/users/new' | '/users'
|
|
53
|
+
id: '__root__' | '/' | '/users/new' | '/users/'
|
|
54
|
+
fileRoutesById: FileRoutesById
|
|
55
|
+
}
|
|
56
|
+
export interface RootRouteChildren {
|
|
57
|
+
IndexRoute: typeof IndexRoute
|
|
58
|
+
UsersNewRoute: typeof UsersNewRoute
|
|
59
|
+
UsersIndexRoute: typeof UsersIndexRoute
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
declare module '@tanstack/react-router' {
|
|
63
|
+
interface FileRoutesByPath {
|
|
64
|
+
'/': {
|
|
65
|
+
id: '/'
|
|
66
|
+
path: '/'
|
|
67
|
+
fullPath: '/'
|
|
68
|
+
preLoaderRoute: typeof IndexRouteImport
|
|
69
|
+
parentRoute: typeof rootRouteImport
|
|
70
|
+
}
|
|
71
|
+
'/users/': {
|
|
72
|
+
id: '/users/'
|
|
73
|
+
path: '/users'
|
|
74
|
+
fullPath: '/users'
|
|
75
|
+
preLoaderRoute: typeof UsersIndexRouteImport
|
|
76
|
+
parentRoute: typeof rootRouteImport
|
|
77
|
+
}
|
|
78
|
+
'/users/new': {
|
|
79
|
+
id: '/users/new'
|
|
80
|
+
path: '/users/new'
|
|
81
|
+
fullPath: '/users/new'
|
|
82
|
+
preLoaderRoute: typeof UsersNewRouteImport
|
|
83
|
+
parentRoute: typeof rootRouteImport
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const rootRouteChildren: RootRouteChildren = {
|
|
89
|
+
IndexRoute: IndexRoute,
|
|
90
|
+
UsersNewRoute: UsersNewRoute,
|
|
91
|
+
UsersIndexRoute: UsersIndexRoute,
|
|
92
|
+
}
|
|
93
|
+
export const routeTree = rootRouteImport
|
|
94
|
+
._addFileChildren(rootRouteChildren)
|
|
95
|
+
._addFileTypes<FileRouteTypes>()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { TanStackDevtools } from '@tanstack/react-devtools';
|
|
2
|
+
import { createRootRoute, Outlet } from '@tanstack/react-router';
|
|
3
|
+
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools';
|
|
4
|
+
|
|
5
|
+
import Header from '@/features/layout/Header';
|
|
6
|
+
|
|
7
|
+
export const Route = createRootRoute({
|
|
8
|
+
component: () => (
|
|
9
|
+
<>
|
|
10
|
+
<Header />
|
|
11
|
+
<Outlet />
|
|
12
|
+
<TanStackDevtools
|
|
13
|
+
config={{
|
|
14
|
+
position: 'bottom-right',
|
|
15
|
+
}}
|
|
16
|
+
plugins={[
|
|
17
|
+
{
|
|
18
|
+
name: 'Tanstack Router',
|
|
19
|
+
render: <TanStackRouterDevtoolsPanel />,
|
|
20
|
+
},
|
|
21
|
+
]}
|
|
22
|
+
/>
|
|
23
|
+
</>
|
|
24
|
+
),
|
|
25
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
2
|
+
|
|
3
|
+
export const Route = createFileRoute('/')({
|
|
4
|
+
component: App,
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
function App() {
|
|
8
|
+
return (
|
|
9
|
+
<div className="text-center">
|
|
10
|
+
<header className="min-h-screen flex flex-col items-center justify-center bg-[#282c34] text-white text-[calc(10px+2vmin)]">
|
|
11
|
+
<h1 className="text-5xl font-bold mb-4">Welcome</h1>
|
|
12
|
+
<p>
|
|
13
|
+
Edit <code>src/routes/index.tsx</code> and save to reload.
|
|
14
|
+
</p>
|
|
15
|
+
<a
|
|
16
|
+
className="text-[#61dafb] hover:underline"
|
|
17
|
+
href="https://reactjs.org"
|
|
18
|
+
target="_blank"
|
|
19
|
+
rel="noopener noreferrer"
|
|
20
|
+
>
|
|
21
|
+
Learn React
|
|
22
|
+
</a>
|
|
23
|
+
<a
|
|
24
|
+
className="text-[#61dafb] hover:underline"
|
|
25
|
+
href="https://tanstack.com"
|
|
26
|
+
target="_blank"
|
|
27
|
+
rel="noopener noreferrer"
|
|
28
|
+
>
|
|
29
|
+
Learn TanStack
|
|
30
|
+
</a>
|
|
31
|
+
</header>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createFileRoute, Link } from '@tanstack/react-router';
|
|
2
|
+
import { UserPlus } from 'lucide-react';
|
|
3
|
+
import { api } from '@/shared/api/client';
|
|
4
|
+
|
|
5
|
+
export const Route = createFileRoute('/users/')({
|
|
6
|
+
component: Users,
|
|
7
|
+
loader: async () => {
|
|
8
|
+
const { data, error } = await api.users.get({
|
|
9
|
+
query: {
|
|
10
|
+
limit: 100,
|
|
11
|
+
offset: 0,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (error) {
|
|
16
|
+
throw new Error(error.value as string);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return { users: data?.users ?? [], total: data?.total ?? 0 };
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function Users() {
|
|
24
|
+
const { users, total } = Route.useLoaderData();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="min-h-screen bg-gray-50 py-8 px-4 sm:px-6 lg:px-8">
|
|
28
|
+
<div className="max-w-4xl mx-auto">
|
|
29
|
+
<div className="flex items-center justify-between mb-6">
|
|
30
|
+
<h1 className="text-3xl font-bold text-gray-900">Users</h1>
|
|
31
|
+
<Link
|
|
32
|
+
to="/users/new"
|
|
33
|
+
className="inline-flex items-center gap-2 px-4 py-2 bg-cyan-600 text-white rounded-md hover:bg-cyan-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 transition-colors"
|
|
34
|
+
>
|
|
35
|
+
<UserPlus size={20} />
|
|
36
|
+
Create New User
|
|
37
|
+
</Link>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div className="bg-white rounded-lg shadow-md overflow-hidden">
|
|
41
|
+
{users.length === 0 ? (
|
|
42
|
+
<div className="p-8 text-center text-gray-500">
|
|
43
|
+
<p className="text-lg mb-2">No users found</p>
|
|
44
|
+
<p className="text-sm">Create your first user to get started.</p>
|
|
45
|
+
</div>
|
|
46
|
+
) : (
|
|
47
|
+
<>
|
|
48
|
+
<div className="px-6 py-4 bg-gray-50 border-b border-gray-200">
|
|
49
|
+
<p className="text-sm text-gray-600">
|
|
50
|
+
Showing {users.length} of {total} users
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
<div className="divide-y divide-gray-200">
|
|
54
|
+
{users.map((user) => (
|
|
55
|
+
<div
|
|
56
|
+
key={user.id}
|
|
57
|
+
className="px-6 py-4 hover:bg-gray-50 transition-colors"
|
|
58
|
+
>
|
|
59
|
+
<div className="flex items-center justify-between">
|
|
60
|
+
<div>
|
|
61
|
+
<h3 className="text-lg font-semibold text-gray-900">
|
|
62
|
+
{user.name} {user.surname}
|
|
63
|
+
</h3>
|
|
64
|
+
<p className="text-sm text-gray-600">{user.email}</p>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="text-xs text-gray-400">
|
|
67
|
+
ID: {user.id.slice(0, 8)}...
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
</>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { api } from '@/shared/api/client';
|
|
4
|
+
|
|
5
|
+
export const Route = createFileRoute('/users/new')({
|
|
6
|
+
component: NewUser,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
function NewUser() {
|
|
10
|
+
const navigate = useNavigate();
|
|
11
|
+
const [formData, setFormData] = useState({
|
|
12
|
+
name: '',
|
|
13
|
+
surname: '',
|
|
14
|
+
email: '',
|
|
15
|
+
});
|
|
16
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
17
|
+
const [error, setError] = useState<string | null>(null);
|
|
18
|
+
|
|
19
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
setIsSubmitting(true);
|
|
22
|
+
setError(null);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const { data, error: apiError } = await api.users.post(formData);
|
|
26
|
+
|
|
27
|
+
if (apiError) {
|
|
28
|
+
setError(apiError.value);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (data) {
|
|
33
|
+
navigate({ to: '/users' });
|
|
34
|
+
}
|
|
35
|
+
} catch (err) {
|
|
36
|
+
setError(err instanceof Error ? err.message : 'Failed to create user');
|
|
37
|
+
} finally {
|
|
38
|
+
setIsSubmitting(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
43
|
+
const { name, value } = e.target;
|
|
44
|
+
setFormData((prev) => ({
|
|
45
|
+
...prev,
|
|
46
|
+
[name]: value,
|
|
47
|
+
}));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="min-h-screen bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
|
52
|
+
<div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-6">
|
|
53
|
+
<h1 className="text-2xl font-bold text-gray-900 mb-6">
|
|
54
|
+
Create New User
|
|
55
|
+
</h1>
|
|
56
|
+
|
|
57
|
+
{error && (
|
|
58
|
+
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-md">
|
|
59
|
+
<p className="text-sm text-red-800">{error}</p>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
64
|
+
<div>
|
|
65
|
+
<label
|
|
66
|
+
htmlFor="name"
|
|
67
|
+
className="block text-sm font-medium text-gray-700 mb-1"
|
|
68
|
+
>
|
|
69
|
+
Name
|
|
70
|
+
</label>
|
|
71
|
+
<input
|
|
72
|
+
type="text"
|
|
73
|
+
id="name"
|
|
74
|
+
name="name"
|
|
75
|
+
value={formData.name}
|
|
76
|
+
onChange={handleChange}
|
|
77
|
+
required
|
|
78
|
+
minLength={1}
|
|
79
|
+
maxLength={255}
|
|
80
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-cyan-500"
|
|
81
|
+
disabled={isSubmitting}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div>
|
|
86
|
+
<label
|
|
87
|
+
htmlFor="surname"
|
|
88
|
+
className="block text-sm font-medium text-gray-700 mb-1"
|
|
89
|
+
>
|
|
90
|
+
Surname
|
|
91
|
+
</label>
|
|
92
|
+
<input
|
|
93
|
+
type="text"
|
|
94
|
+
id="surname"
|
|
95
|
+
name="surname"
|
|
96
|
+
value={formData.surname}
|
|
97
|
+
onChange={handleChange}
|
|
98
|
+
required
|
|
99
|
+
minLength={1}
|
|
100
|
+
maxLength={255}
|
|
101
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-cyan-500"
|
|
102
|
+
disabled={isSubmitting}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div>
|
|
107
|
+
<label
|
|
108
|
+
htmlFor="email"
|
|
109
|
+
className="block text-sm font-medium text-gray-700 mb-1"
|
|
110
|
+
>
|
|
111
|
+
Email
|
|
112
|
+
</label>
|
|
113
|
+
<input
|
|
114
|
+
type="email"
|
|
115
|
+
id="email"
|
|
116
|
+
name="email"
|
|
117
|
+
value={formData.email}
|
|
118
|
+
onChange={handleChange}
|
|
119
|
+
required
|
|
120
|
+
minLength={1}
|
|
121
|
+
maxLength={255}
|
|
122
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-cyan-500"
|
|
123
|
+
disabled={isSubmitting}
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div className="flex gap-3 pt-4">
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
onClick={() => navigate({ to: '/users' })}
|
|
131
|
+
className="flex-1 px-4 py-2 border border-gray-300 rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
132
|
+
disabled={isSubmitting}
|
|
133
|
+
>
|
|
134
|
+
Cancel
|
|
135
|
+
</button>
|
|
136
|
+
<button
|
|
137
|
+
type="submit"
|
|
138
|
+
className="flex-1 px-4 py-2 border border-transparent rounded-md shadow-sm text-white bg-cyan-600 hover:bg-cyan-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
139
|
+
disabled={isSubmitting}
|
|
140
|
+
>
|
|
141
|
+
{isSubmitting ? 'Creating...' : 'Create User'}
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
</form>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|