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.
- package/README.md +443 -0
- package/dist/chunk-2H7UOFLK.js +11 -0
- package/dist/chunk-2H7UOFLK.js.map +1 -0
- package/dist/chunk-2ZKONAXC.js +45 -0
- package/dist/chunk-2ZKONAXC.js.map +1 -0
- package/dist/chunk-57VDULE3.mjs +83 -0
- package/dist/chunk-57VDULE3.mjs.map +1 -0
- package/dist/chunk-6K3RXN4Y.mjs +45 -0
- package/dist/chunk-6K3RXN4Y.mjs.map +1 -0
- package/dist/chunk-6KN6UIHT.js +67 -0
- package/dist/chunk-6KN6UIHT.js.map +1 -0
- package/dist/chunk-A5OZK2TO.mjs +56 -0
- package/dist/chunk-A5OZK2TO.mjs.map +1 -0
- package/dist/chunk-ELVFG4US.js +83 -0
- package/dist/chunk-ELVFG4US.js.map +1 -0
- package/dist/chunk-H6XDQJ3N.mjs +11 -0
- package/dist/chunk-H6XDQJ3N.mjs.map +1 -0
- package/dist/chunk-HE3JQ62E.js +56 -0
- package/dist/chunk-HE3JQ62E.js.map +1 -0
- package/dist/chunk-HHXRAV67.mjs +229 -0
- package/dist/chunk-HHXRAV67.mjs.map +1 -0
- package/dist/chunk-J7J2LRG7.js +229 -0
- package/dist/chunk-J7J2LRG7.js.map +1 -0
- package/dist/chunk-LWNHEPTL.mjs +2 -0
- package/dist/chunk-LWNHEPTL.mjs.map +1 -0
- package/dist/chunk-MAVJYD6O.js +2 -0
- package/dist/chunk-MAVJYD6O.js.map +1 -0
- package/dist/chunk-QUV6QXTP.js +363 -0
- package/dist/chunk-QUV6QXTP.js.map +1 -0
- package/dist/chunk-WZBBQLFT.mjs +363 -0
- package/dist/chunk-WZBBQLFT.mjs.map +1 -0
- package/dist/chunk-XMPCR7N3.mjs +67 -0
- package/dist/chunk-XMPCR7N3.mjs.map +1 -0
- package/dist/cli/index.mjs +69 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/client/index.js +11 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/index.mjs +11 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/codegen-J3XOZCQZ.js +14 -0
- package/dist/codegen-J3XOZCQZ.js.map +1 -0
- package/dist/codegen-ZZBQIGUQ.mjs +14 -0
- package/dist/codegen-ZZBQIGUQ.mjs.map +1 -0
- package/dist/dev-server-K5YZAZY2.mjs +14 -0
- package/dist/dev-server-K5YZAZY2.mjs.map +1 -0
- package/dist/dev-server-X453DBCE.js +14 -0
- package/dist/dev-server-X453DBCE.js.map +1 -0
- package/dist/index.js +274 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +274 -0
- package/dist/index.mjs.map +1 -0
- package/dist/nest/index.js +21 -0
- package/dist/nest/index.js.map +1 -0
- package/dist/nest/index.mjs +21 -0
- package/dist/nest/index.mjs.map +1 -0
- package/dist/next/index.js +14 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/index.mjs +14 -0
- package/dist/next/index.mjs.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.mjs +3 -0
- package/dist/types/index.mjs.map +1 -0
- package/package.json +104 -0
- package/templates/nestjs-api/.env.example +4 -0
- package/templates/nestjs-api/README.md +79 -0
- package/templates/nestjs-api/nest-cli.json +7 -0
- package/templates/nestjs-api/package.json +39 -0
- package/templates/nestjs-api/prisma/schema.prisma +29 -0
- package/templates/nestjs-api/src/app.module.ts +17 -0
- package/templates/nestjs-api/src/auth/auth.controller.ts +30 -0
- package/templates/nestjs-api/src/auth/auth.module.ts +26 -0
- package/templates/nestjs-api/src/auth/auth.service.ts +91 -0
- package/templates/nestjs-api/src/auth/dto/auth.dto.ts +22 -0
- package/templates/nestjs-api/src/auth/guards/jwt-auth.guard.ts +5 -0
- package/templates/nestjs-api/src/auth/strategies/jwt.strategy.ts +19 -0
- package/templates/nestjs-api/src/main.ts +32 -0
- package/templates/nestjs-api/src/prisma/prisma.module.ts +9 -0
- package/templates/nestjs-api/src/prisma/prisma.service.ts +14 -0
- package/templates/nestjs-api/src/todos/dto/todo.dto.ts +24 -0
- package/templates/nestjs-api/src/todos/todos.controller.ts +46 -0
- package/templates/nestjs-api/src/todos/todos.module.ts +9 -0
- package/templates/nestjs-api/src/todos/todos.service.ts +53 -0
- package/templates/nestjs-api/src/users/users.controller.ts +17 -0
- package/templates/nestjs-api/src/users/users.module.ts +10 -0
- package/templates/nestjs-api/src/users/users.service.ts +19 -0
- package/templates/nestjs-api/tsconfig.json +21 -0
- package/templates/nextjs-web/.env.local.example +1 -0
- package/templates/nextjs-web/README.md +68 -0
- package/templates/nextjs-web/app/dashboard/page.tsx +175 -0
- package/templates/nextjs-web/app/globals.css +28 -0
- package/templates/nextjs-web/app/layout.tsx +27 -0
- package/templates/nextjs-web/app/login/page.tsx +107 -0
- package/templates/nextjs-web/app/page.tsx +28 -0
- package/templates/nextjs-web/app/register/page.tsx +130 -0
- package/templates/nextjs-web/next.config.mjs +4 -0
- package/templates/nextjs-web/package.json +28 -0
- package/templates/nextjs-web/tailwind.config.ts +15 -0
- 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
|
+
}
|