wexts 1.0.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 (99) hide show
  1. package/README.md +443 -0
  2. package/dist/chunk-2H7UOFLK.js +11 -0
  3. package/dist/chunk-2H7UOFLK.js.map +1 -0
  4. package/dist/chunk-2ZKONAXC.js +45 -0
  5. package/dist/chunk-2ZKONAXC.js.map +1 -0
  6. package/dist/chunk-57VDULE3.mjs +83 -0
  7. package/dist/chunk-57VDULE3.mjs.map +1 -0
  8. package/dist/chunk-6K3RXN4Y.mjs +45 -0
  9. package/dist/chunk-6K3RXN4Y.mjs.map +1 -0
  10. package/dist/chunk-6KN6UIHT.js +67 -0
  11. package/dist/chunk-6KN6UIHT.js.map +1 -0
  12. package/dist/chunk-A5OZK2TO.mjs +56 -0
  13. package/dist/chunk-A5OZK2TO.mjs.map +1 -0
  14. package/dist/chunk-ELVFG4US.js +83 -0
  15. package/dist/chunk-ELVFG4US.js.map +1 -0
  16. package/dist/chunk-H6XDQJ3N.mjs +11 -0
  17. package/dist/chunk-H6XDQJ3N.mjs.map +1 -0
  18. package/dist/chunk-HE3JQ62E.js +56 -0
  19. package/dist/chunk-HE3JQ62E.js.map +1 -0
  20. package/dist/chunk-HHXRAV67.mjs +229 -0
  21. package/dist/chunk-HHXRAV67.mjs.map +1 -0
  22. package/dist/chunk-J7J2LRG7.js +229 -0
  23. package/dist/chunk-J7J2LRG7.js.map +1 -0
  24. package/dist/chunk-LWNHEPTL.mjs +2 -0
  25. package/dist/chunk-LWNHEPTL.mjs.map +1 -0
  26. package/dist/chunk-MAVJYD6O.js +2 -0
  27. package/dist/chunk-MAVJYD6O.js.map +1 -0
  28. package/dist/chunk-QUV6QXTP.js +363 -0
  29. package/dist/chunk-QUV6QXTP.js.map +1 -0
  30. package/dist/chunk-WZBBQLFT.mjs +363 -0
  31. package/dist/chunk-WZBBQLFT.mjs.map +1 -0
  32. package/dist/chunk-XMPCR7N3.mjs +67 -0
  33. package/dist/chunk-XMPCR7N3.mjs.map +1 -0
  34. package/dist/cli/index.mjs +69 -0
  35. package/dist/cli/index.mjs.map +1 -0
  36. package/dist/client/index.js +11 -0
  37. package/dist/client/index.js.map +1 -0
  38. package/dist/client/index.mjs +11 -0
  39. package/dist/client/index.mjs.map +1 -0
  40. package/dist/codegen-J3XOZCQZ.js +14 -0
  41. package/dist/codegen-J3XOZCQZ.js.map +1 -0
  42. package/dist/codegen-ZZBQIGUQ.mjs +14 -0
  43. package/dist/codegen-ZZBQIGUQ.mjs.map +1 -0
  44. package/dist/dev-server-K5YZAZY2.mjs +14 -0
  45. package/dist/dev-server-K5YZAZY2.mjs.map +1 -0
  46. package/dist/dev-server-X453DBCE.js +14 -0
  47. package/dist/dev-server-X453DBCE.js.map +1 -0
  48. package/dist/index.js +274 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/index.mjs +274 -0
  51. package/dist/index.mjs.map +1 -0
  52. package/dist/nest/index.js +21 -0
  53. package/dist/nest/index.js.map +1 -0
  54. package/dist/nest/index.mjs +21 -0
  55. package/dist/nest/index.mjs.map +1 -0
  56. package/dist/next/index.js +14 -0
  57. package/dist/next/index.js.map +1 -0
  58. package/dist/next/index.mjs +14 -0
  59. package/dist/next/index.mjs.map +1 -0
  60. package/dist/types/index.js +3 -0
  61. package/dist/types/index.js.map +1 -0
  62. package/dist/types/index.mjs +3 -0
  63. package/dist/types/index.mjs.map +1 -0
  64. package/package.json +104 -0
  65. package/templates/nestjs-api/.env.example +4 -0
  66. package/templates/nestjs-api/README.md +79 -0
  67. package/templates/nestjs-api/nest-cli.json +7 -0
  68. package/templates/nestjs-api/package.json +39 -0
  69. package/templates/nestjs-api/prisma/schema.prisma +29 -0
  70. package/templates/nestjs-api/src/app.module.ts +17 -0
  71. package/templates/nestjs-api/src/auth/auth.controller.ts +30 -0
  72. package/templates/nestjs-api/src/auth/auth.module.ts +26 -0
  73. package/templates/nestjs-api/src/auth/auth.service.ts +91 -0
  74. package/templates/nestjs-api/src/auth/dto/auth.dto.ts +22 -0
  75. package/templates/nestjs-api/src/auth/guards/jwt-auth.guard.ts +5 -0
  76. package/templates/nestjs-api/src/auth/strategies/jwt.strategy.ts +19 -0
  77. package/templates/nestjs-api/src/main.ts +32 -0
  78. package/templates/nestjs-api/src/prisma/prisma.module.ts +9 -0
  79. package/templates/nestjs-api/src/prisma/prisma.service.ts +14 -0
  80. package/templates/nestjs-api/src/todos/dto/todo.dto.ts +24 -0
  81. package/templates/nestjs-api/src/todos/todos.controller.ts +46 -0
  82. package/templates/nestjs-api/src/todos/todos.module.ts +9 -0
  83. package/templates/nestjs-api/src/todos/todos.service.ts +53 -0
  84. package/templates/nestjs-api/src/users/users.controller.ts +17 -0
  85. package/templates/nestjs-api/src/users/users.module.ts +10 -0
  86. package/templates/nestjs-api/src/users/users.service.ts +19 -0
  87. package/templates/nestjs-api/tsconfig.json +21 -0
  88. package/templates/nextjs-web/.env.local.example +1 -0
  89. package/templates/nextjs-web/README.md +68 -0
  90. package/templates/nextjs-web/app/dashboard/page.tsx +175 -0
  91. package/templates/nextjs-web/app/globals.css +28 -0
  92. package/templates/nextjs-web/app/layout.tsx +27 -0
  93. package/templates/nextjs-web/app/login/page.tsx +107 -0
  94. package/templates/nextjs-web/app/page.tsx +28 -0
  95. package/templates/nextjs-web/app/register/page.tsx +130 -0
  96. package/templates/nextjs-web/next.config.mjs +4 -0
  97. package/templates/nextjs-web/package.json +28 -0
  98. package/templates/nextjs-web/tailwind.config.ts +15 -0
  99. package/templates/nextjs-web/tsconfig.json +39 -0
@@ -0,0 +1,53 @@
1
+ import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
2
+ import { PrismaService } from '../prisma/prisma.service';
3
+ import { CreateTodoDto, UpdateTodoDto } from './dto/todo.dto';
4
+
5
+ @Injectable()
6
+ export class TodosService {
7
+ constructor(private prisma: PrismaService) { }
8
+
9
+ async findAll(userId: string) {
10
+ return this.prisma.todo.findMany({
11
+ where: { userId },
12
+ orderBy: { createdAt: 'desc' },
13
+ });
14
+ }
15
+
16
+ async findOne(id: string, userId: string) {
17
+ const todo = await this.prisma.todo.findUnique({ where: { id } });
18
+
19
+ if (!todo) {
20
+ throw new NotFoundException('Todo not found');
21
+ }
22
+
23
+ if (todo.userId !== userId) {
24
+ throw new ForbiddenException('Access denied');
25
+ }
26
+
27
+ return todo;
28
+ }
29
+
30
+ async create(dto: CreateTodoDto, userId: string) {
31
+ return this.prisma.todo.create({
32
+ data: {
33
+ ...dto,
34
+ userId,
35
+ },
36
+ });
37
+ }
38
+
39
+ async update(id: string, dto: UpdateTodoDto, userId: string) {
40
+ await this.findOne(id, userId); // Check ownership
41
+
42
+ return this.prisma.todo.update({
43
+ where: { id },
44
+ data: dto,
45
+ });
46
+ }
47
+
48
+ async remove(id: string, userId: string) {
49
+ await this.findOne(id, userId); // Check ownership
50
+
51
+ return this.prisma.todo.delete({ where: { id } });
52
+ }
53
+ }
@@ -0,0 +1,17 @@
1
+ import { Controller, Get, UseGuards, Request } from '@nestjs/common';
2
+ import { FusionController, FusionGet } from 'wexts/nest';
3
+ import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
4
+ import { UsersService } from './users.service';
5
+
6
+ @FusionController('users')
7
+ @Controller('users')
8
+ @UseGuards(JwtAuthGuard)
9
+ export class UsersController {
10
+ constructor(private usersService: UsersService) { }
11
+
12
+ @FusionGet()
13
+ @Get('me')
14
+ async getMe(@Request() req) {
15
+ return this.usersService.findById(req.user.userId);
16
+ }
17
+ }
@@ -0,0 +1,10 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { UsersController } from './users.controller';
3
+ import { UsersService } from './users.service';
4
+
5
+ @Module({
6
+ controllers: [UsersController],
7
+ providers: [UsersService],
8
+ exports: [UsersService],
9
+ })
10
+ export class UsersModule { }
@@ -0,0 +1,19 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { PrismaService } from '../prisma/prisma.service';
3
+
4
+ @Injectable()
5
+ export class UsersService {
6
+ constructor(private prisma: PrismaService) { }
7
+
8
+ async findById(id: string) {
9
+ return this.prisma.user.findUnique({
10
+ where: { id },
11
+ select: {
12
+ id: true,
13
+ email: true,
14
+ name: true,
15
+ createdAt: true,
16
+ },
17
+ });
18
+ }
19
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "declaration": true,
5
+ "removeComments": true,
6
+ "emitDecoratorMetadata": true,
7
+ "experimentalDecorators": true,
8
+ "allowSyntheticDefaultImports": true,
9
+ "target": "ES2021",
10
+ "sourceMap": true,
11
+ "outDir": "./dist",
12
+ "baseUrl": "./",
13
+ "incremental": true,
14
+ "skipLibCheck": true,
15
+ "strictNullChecks": false,
16
+ "noImplicitAny": false,
17
+ "strictBindCallApply": false,
18
+ "forceConsistentCasingInFileNames": false,
19
+ "noFallthroughCasesInSwitch": false
20
+ }
21
+ }
@@ -0,0 +1 @@
1
+ NEXT_PUBLIC_API_URL=http://localhost:5050
@@ -0,0 +1,68 @@
1
+ # Fusion Next.js Web
2
+
3
+ Modern Next.js 16 frontend with wexts integration.
4
+
5
+ ## Features
6
+
7
+ - ✅ Next.js 16 (App Router)
8
+ - ✅ React 19
9
+ - ✅ Tailwind CSS v4
10
+ - ✅ TypeScript
11
+ - ✅ wexts Provider & Hooks
12
+ - ✅ Authentication Flow
13
+ - ✅ Todo Management Dashboard
14
+
15
+ ## Setup
16
+
17
+ ```bash
18
+ # Install dependencies
19
+ npm install
20
+
21
+ # Copy environment variables
22
+ cp .env.local.example .env.local
23
+
24
+ # Update NEXT_PUBLIC_API_URL in .env.local to point to your API
25
+
26
+ # Start development server
27
+ npm run dev
28
+ ```
29
+
30
+ ## Project Structure
31
+
32
+ ```
33
+ app/
34
+ ├── layout.tsx # Root layout with FusionProvider
35
+ ├── page.tsx # Homepage with auth redirect
36
+ ├── globals.css # Global styles
37
+ ├── login/
38
+ │ └── page.tsx # Login page
39
+ ├── register/
40
+ │ └── page.tsx # Registration page
41
+ └── dashboard/
42
+ └── page.tsx # Protected dashboard with todos
43
+ ```
44
+
45
+ ## Using Fusion Hooks
46
+
47
+ ```tsx
48
+ import { useFusion, useAuth } from 'wexts/next';
49
+
50
+ function MyComponent() {
51
+ const { client } = useFusion();
52
+ const { user, isAuthenticated } = useAuth();
53
+
54
+ // Make API calls
55
+ const data = await client.get('/endpoint');
56
+ }
57
+ ```
58
+
59
+ ## Building for Production
60
+
61
+ ```bash
62
+ npm run build
63
+ npm start
64
+ ```
65
+
66
+ ## Environment Variables
67
+
68
+ - `NEXT_PUBLIC_API_URL` - Backend API URL (default: http://localhost:5050)
@@ -0,0 +1,175 @@
1
+ 'use client';
2
+
3
+ import { useAuth, useFusion } from 'wexts/next';
4
+ import { useRouter } from 'next/navigation';
5
+ import { useEffect, useState } from 'react';
6
+
7
+ interface Todo {
8
+ id: string;
9
+ title: string;
10
+ description?: string;
11
+ completed: boolean;
12
+ }
13
+
14
+ export default function DashboardPage() {
15
+ const { user, isAuthenticated, loading: authLoading, logout } = useAuth();
16
+ const { client } = useFusion();
17
+ const router = useRouter();
18
+
19
+ const [todos, setTodos] = useState<Todo[]>([]);
20
+ const [newTodoTitle, setNewTodoTitle] = useState('');
21
+ const [loading, setLoading] = useState(false);
22
+
23
+ useEffect(() => {
24
+ if (!authLoading && !isAuthenticated) {
25
+ router.push('/login');
26
+ }
27
+ }, [isAuthenticated, authLoading, router]);
28
+
29
+ useEffect(() => {
30
+ if (isAuthenticated) {
31
+ loadTodos();
32
+ }
33
+ }, [isAuthenticated]);
34
+
35
+ const loadTodos = async () => {
36
+ try {
37
+ const data = await client.get<Todo[]>('/todos');
38
+ setTodos(data);
39
+ } catch (err) {
40
+ console.error('Failed to load todos:', err);
41
+ }
42
+ };
43
+
44
+ const handleAddTodo = async (e: React.FormEvent) => {
45
+ e.preventDefault();
46
+ if (!newTodoTitle.trim()) return;
47
+
48
+ setLoading(true);
49
+ try {
50
+ await client.post('/todos', { title: newTodoTitle });
51
+ setNewTodoTitle('');
52
+ await loadTodos();
53
+ } catch (err) {
54
+ console.error('Failed to add todo:', err);
55
+ } finally {
56
+ setLoading(false);
57
+ }
58
+ };
59
+
60
+ const handleToggleTodo = async (id: string, completed: boolean) => {
61
+ try {
62
+ await client.put(`/todos/${id}`, { completed: !completed });
63
+ await loadTodos();
64
+ } catch (err) {
65
+ console.error('Failed to update todo:', err);
66
+ }
67
+ };
68
+
69
+ const handleDeleteTodo = async (id: string) => {
70
+ try {
71
+ await client.delete(`/todos/${id}`);
72
+ await loadTodos();
73
+ } catch (err) {
74
+ console.error('Failed to delete todo:', err);
75
+ }
76
+ };
77
+
78
+ const handleLogout = async () => {
79
+ await logout();
80
+ router.push('/login');
81
+ };
82
+
83
+ if (authLoading) {
84
+ return (
85
+ <div className="min-h-screen flex items-center justify-center">
86
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-violet-600"></div>
87
+ </div>
88
+ );
89
+ }
90
+
91
+ if (!isAuthenticated) return null;
92
+
93
+ return (
94
+ <div className="min-h-screen bg-gray-50 dark:bg-gray-900">
95
+ <nav className="bg-white dark:bg-gray-800 shadow">
96
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
97
+ <div className="flex justify-between h-16">
98
+ <div className="flex items-center">
99
+ <h1 className="text-2xl font-bold text-violet-600">Fusion Dashboard</h1>
100
+ </div>
101
+ <div className="flex items-center gap-4">
102
+ <span className="text-sm text-gray-700 dark:text-gray-300">
103
+ {user?.email}
104
+ </span>
105
+ <button
106
+ onClick={handleLogout}
107
+ className="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700 transition"
108
+ >
109
+ Logout
110
+ </button>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </nav>
115
+
116
+ <main className="max-w-4xl mx-auto py-12 px-4">
117
+ <div className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8">
118
+ <h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-8">
119
+ My Todos
120
+ </h2>
121
+
122
+ <form onSubmit={handleAddTodo} className="mb-8">
123
+ <div className="flex gap-3">
124
+ <input
125
+ type="text"
126
+ value={newTodoTitle}
127
+ onChange={(e) => setNewTodoTitle(e.target.value)}
128
+ placeholder="Add a new todo..."
129
+ className="flex-1 px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-violet-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
130
+ />
131
+ <button
132
+ type="submit"
133
+ disabled={loading}
134
+ className="px-6 py-3 bg-violet-600 text-white rounded-lg hover:bg-violet-700 font-medium disabled:opacity-50 transition"
135
+ >
136
+ Add
137
+ </button>
138
+ </div>
139
+ </form>
140
+
141
+ <div className="space-y-3">
142
+ {todos.length === 0 ? (
143
+ <p className="text-center text-gray-500 dark:text-gray-400 py-8">
144
+ No todos yet. Create one above!
145
+ </p>
146
+ ) : (
147
+ todos.map((todo) => (
148
+ <div
149
+ key={todo.id}
150
+ className="flex items-center gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition"
151
+ >
152
+ <input
153
+ type="checkbox"
154
+ checked={todo.completed}
155
+ onChange={() => handleToggleTodo(todo.id, todo.completed)}
156
+ className="w-5 h-5 text-violet-600 rounded focus:ring-2 focus:ring-violet-500"
157
+ />
158
+ <span className={`flex-1 text-gray-900 dark:text-white ${todo.completed ? 'line-through opacity-50' : ''}`}>
159
+ {todo.title}
160
+ </span>
161
+ <button
162
+ onClick={() => handleDeleteTodo(todo.id)}
163
+ className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 text-sm font-medium"
164
+ >
165
+ Delete
166
+ </button>
167
+ </div>
168
+ ))
169
+ )}
170
+ </div>
171
+ </div>
172
+ </main>
173
+ </div>
174
+ );
175
+ }
@@ -0,0 +1,28 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ --background: 0 0% 100%;
7
+ --foreground: 240 10% 3.9%;
8
+ --primary: 262 83% 58%;
9
+ --primary-foreground: 0 0% 100%;
10
+ }
11
+
12
+ @media (prefers-color-scheme: dark) {
13
+ :root {
14
+ --background: 240 10% 3.9%;
15
+ --foreground: 0 0% 98%;
16
+ }
17
+ }
18
+
19
+ body {
20
+ background-color: hsl(var(--background));
21
+ color: hsl(var(--foreground));
22
+ }
23
+
24
+ @layer utilities {
25
+ .text-balance {
26
+ text-wrap: balance;
27
+ }
28
+ }
@@ -0,0 +1,27 @@
1
+ import type { Metadata } from 'next';
2
+ import { Inter } from 'next/font/google';
3
+ import { FusionProvider } from 'wexts/next';
4
+ import './globals.css';
5
+
6
+ const inter = Inter({ subsets: ['latin'] });
7
+
8
+ export const metadata: Metadata = {
9
+ title: 'Fusion App',
10
+ description: 'Built with wexts',
11
+ };
12
+
13
+ export default function RootLayout({
14
+ children,
15
+ }: {
16
+ children: React.ReactNode;
17
+ }) {
18
+ return (
19
+ <html lang="en">
20
+ <body className={inter.className}>
21
+ <FusionProvider baseUrl={process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5050'}>
22
+ {children}
23
+ </FusionProvider>
24
+ </body>
25
+ </html>
26
+ );
27
+ }
@@ -0,0 +1,107 @@
1
+ 'use client';
2
+
3
+ import { useState, FormEvent } from 'react';
4
+ import { useAuth } from 'wexts/next';
5
+ import { useRouter } from 'next/navigation';
6
+ import Link from 'next/link';
7
+
8
+ export default function LoginPage() {
9
+ const [email, setEmail] = useState('');
10
+ const [password, setPassword] = useState('');
11
+ const [error, setError] = useState('');
12
+ const [loading, setLoading] = useState(false);
13
+
14
+ const { login } = useAuth();
15
+ const router = useRouter();
16
+
17
+ const handleSubmit = async (e: FormEvent) => {
18
+ e.preventDefault();
19
+ setError('');
20
+ setLoading(true);
21
+
22
+ try {
23
+ await login(email, password);
24
+ router.push('/dashboard');
25
+ } catch (err: any) {
26
+ setError(err.message || 'Login failed');
27
+ } finally {
28
+ setLoading(false);
29
+ }
30
+ };
31
+
32
+ return (
33
+ <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-violet-50 to-purple-100 dark:from-gray-900 dark:to-gray-800 px-4">
34
+ <div className="max-w-md w-full space-y-8 bg-white dark:bg-gray-800 p-10 rounded-2xl shadow-xl">
35
+ <div>
36
+ <h2 className="text-center text-4xl font-bold text-gray-900 dark:text-white">
37
+ Welcome Back
38
+ </h2>
39
+ <p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
40
+ Sign in to your account
41
+ </p>
42
+ </div>
43
+
44
+ <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
45
+ {error && (
46
+ <div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-sm text-red-600 dark:text-red-400">
47
+ {error}
48
+ </div>
49
+ )}
50
+
51
+ <div className="space-y-4">
52
+ <div>
53
+ <label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
54
+ Email
55
+ </label>
56
+ <input
57
+ id="email"
58
+ name="email"
59
+ type="email"
60
+ autoComplete="email"
61
+ required
62
+ value={email}
63
+ onChange={(e) => setEmail(e.target.value)}
64
+ className="appearance-none relative block w-full px-4 py-3 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-transparent bg-white dark:bg-gray-700 transition"
65
+ placeholder="you@example.com"
66
+ />
67
+ </div>
68
+
69
+ <div>
70
+ <label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
71
+ Password
72
+ </label>
73
+ <input
74
+ id="password"
75
+ name="password"
76
+ type="password"
77
+ autoComplete="current-password"
78
+ required
79
+ value={password}
80
+ onChange={(e) => setPassword(e.target.value)}
81
+ className="appearance-none relative block w-full px-4 py-3 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-transparent bg-white dark:bg-gray-700 transition"
82
+ placeholder="••••••••"
83
+ />
84
+ </div>
85
+ </div>
86
+
87
+ <div>
88
+ <button
89
+ type="submit"
90
+ disabled={loading}
91
+ className="group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-semibold rounded-lg text-white bg-violet-600 hover:bg-violet-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-violet-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
92
+ >
93
+ {loading ? 'Signing in...' : 'Sign in'}
94
+ </button>
95
+ </div>
96
+
97
+ <div className="text-center text-sm">
98
+ <span className="text-gray-600 dark:text-gray-400">Don't have an account? </span>
99
+ <Link href="/register" className="font-medium text-violet-600 hover:text-violet-500 dark:text-violet-400">
100
+ Sign up
101
+ </Link>
102
+ </div>
103
+ </form>
104
+ </div>
105
+ </div>
106
+ );
107
+ }
@@ -0,0 +1,28 @@
1
+ 'use client';
2
+
3
+ import { useAuth } from 'wexts/next';
4
+ import { useRouter } from 'next/navigation';
5
+ import { useEffect } from 'react';
6
+
7
+ export default function Home() {
8
+ const { isAuthenticated, user, loading } = useAuth();
9
+ const router = useRouter();
10
+
11
+ useEffect(() => {
12
+ if (!loading && !isAuthenticated) {
13
+ router.push('/login');
14
+ } else if (!loading && isAuthenticated) {
15
+ router.push('/dashboard');
16
+ }
17
+ }, [isAuthenticated, loading, router]);
18
+
19
+ if (loading) {
20
+ return (
21
+ <div className="min-h-screen flex items-center justify-center">
22
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-violet-600"></div>
23
+ </div>
24
+ );
25
+ }
26
+
27
+ return null;
28
+ }