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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/package.json +60 -0
  4. package/src/index.ts +187 -0
  5. package/templates/monorepo/README.md +75 -0
  6. package/templates/monorepo/apps/backend/.cursor/mcp.json +8 -0
  7. package/templates/monorepo/apps/backend/.dockerignore +60 -0
  8. package/templates/monorepo/apps/backend/.env.example +19 -0
  9. package/templates/monorepo/apps/backend/.github/workflows/lint.yml +31 -0
  10. package/templates/monorepo/apps/backend/.github/workflows/tests.yml +36 -0
  11. package/templates/monorepo/apps/backend/AGENTS.md +79 -0
  12. package/templates/monorepo/apps/backend/CHANGELOG.md +190 -0
  13. package/templates/monorepo/apps/backend/CLAUDE.md +149 -0
  14. package/templates/monorepo/apps/backend/Dockerfile +35 -0
  15. package/templates/monorepo/apps/backend/LICENSE +21 -0
  16. package/templates/monorepo/apps/backend/README.md +274 -0
  17. package/templates/monorepo/apps/backend/biome.json +58 -0
  18. package/templates/monorepo/apps/backend/bun.lock +303 -0
  19. package/templates/monorepo/apps/backend/docker-compose.yml +37 -0
  20. package/templates/monorepo/apps/backend/drizzle.config.ts +14 -0
  21. package/templates/monorepo/apps/backend/package.json +42 -0
  22. package/templates/monorepo/apps/backend/src/common/config.ts +29 -0
  23. package/templates/monorepo/apps/backend/src/common/logger.ts +18 -0
  24. package/templates/monorepo/apps/backend/src/db/index.ts +31 -0
  25. package/templates/monorepo/apps/backend/src/db/migrations/20251111132328_curly_spectrum.sql +8 -0
  26. package/templates/monorepo/apps/backend/src/db/migrations/meta/20251111132328_snapshot.json +70 -0
  27. package/templates/monorepo/apps/backend/src/db/migrations/meta/_journal.json +13 -0
  28. package/templates/monorepo/apps/backend/src/db/schema/users.ts +39 -0
  29. package/templates/monorepo/apps/backend/src/main.ts +67 -0
  30. package/templates/monorepo/apps/backend/src/middleware/error-handler.ts +36 -0
  31. package/templates/monorepo/apps/backend/src/modules/users/index.ts +61 -0
  32. package/templates/monorepo/apps/backend/src/modules/users/model.ts +48 -0
  33. package/templates/monorepo/apps/backend/src/modules/users/service.ts +46 -0
  34. package/templates/monorepo/apps/backend/src/tests/users.test.ts +102 -0
  35. package/templates/monorepo/apps/backend/src/util/graceful-shutdown.ts +37 -0
  36. package/templates/monorepo/apps/backend/tsconfig.json +35 -0
  37. package/templates/monorepo/apps/backend-biome.json.template +14 -0
  38. package/templates/monorepo/apps/frontend/.env.example +1 -0
  39. package/templates/monorepo/apps/frontend/README.md +59 -0
  40. package/templates/monorepo/apps/frontend/biome.json +16 -0
  41. package/templates/monorepo/apps/frontend/components.json +21 -0
  42. package/templates/monorepo/apps/frontend/index.html +15 -0
  43. package/templates/monorepo/apps/frontend/package.json +48 -0
  44. package/templates/monorepo/apps/frontend/public/favicon.ico +0 -0
  45. package/templates/monorepo/apps/frontend/src/assets/fonts/.gitkeep +0 -0
  46. package/templates/monorepo/apps/frontend/src/assets/images/.gitkeep +0 -0
  47. package/templates/monorepo/apps/frontend/src/features/layout/Header.tsx +73 -0
  48. package/templates/monorepo/apps/frontend/src/main.tsx +36 -0
  49. package/templates/monorepo/apps/frontend/src/routeTree.gen.ts +95 -0
  50. package/templates/monorepo/apps/frontend/src/routes/__root.tsx +25 -0
  51. package/templates/monorepo/apps/frontend/src/routes/index.tsx +34 -0
  52. package/templates/monorepo/apps/frontend/src/routes/users/index.tsx +79 -0
  53. package/templates/monorepo/apps/frontend/src/routes/users/new.tsx +148 -0
  54. package/templates/monorepo/apps/frontend/src/shared/api/client.ts +6 -0
  55. package/templates/monorepo/apps/frontend/src/shared/components/.gitkeep +0 -0
  56. package/templates/monorepo/apps/frontend/src/shared/constants/.gitkeep +0 -0
  57. package/templates/monorepo/apps/frontend/src/shared/hooks/.gitkeep +0 -0
  58. package/templates/monorepo/apps/frontend/src/shared/types/.gitkeep +0 -0
  59. package/templates/monorepo/apps/frontend/src/shared/utils/utils.ts +6 -0
  60. package/templates/monorepo/apps/frontend/src/styles.css +138 -0
  61. package/templates/monorepo/apps/frontend/src/vite-env.d.ts +13 -0
  62. package/templates/monorepo/apps/frontend/tsconfig.json +27 -0
  63. package/templates/monorepo/apps/frontend/vite.config.ts +27 -0
  64. package/templates/monorepo/biome.json +65 -0
  65. package/templates/monorepo/bun.lock +1044 -0
  66. 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
+ }
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ import { treaty } from '@elysiajs/eden';
2
+ import type { App } from 'elysia-boilerplate';
3
+
4
+ export const api = treaty<App>(
5
+ import.meta.env.VITE_API_URL || 'http://localhost:3000',
6
+ );
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }