wexts 2.0.7 → 2.0.9
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 +123 -237
- package/dist/chunk-63MTCWU2.mjs +361 -0
- package/dist/chunk-63MTCWU2.mjs.map +1 -0
- package/dist/chunk-67IJ6H4J.mjs +44 -0
- package/dist/chunk-67IJ6H4J.mjs.map +1 -0
- package/dist/chunk-7NSRDJ5C.mjs +1 -0
- package/dist/chunk-7NSRDJ5C.mjs.map +1 -0
- package/dist/chunk-ASDXAK6G.js +44 -0
- package/dist/chunk-ASDXAK6G.js.map +1 -0
- package/dist/chunk-CKZ4VSCB.mjs +18 -0
- package/dist/chunk-CKZ4VSCB.mjs.map +1 -0
- package/dist/chunk-DW6GOKMF.js +57 -0
- package/dist/chunk-DW6GOKMF.js.map +1 -0
- package/dist/chunk-GKVPGKAH.js +66 -0
- package/dist/chunk-GKVPGKAH.js.map +1 -0
- package/dist/chunk-HSFLZUJN.mjs +57 -0
- package/dist/chunk-HSFLZUJN.mjs.map +1 -0
- package/dist/chunk-HU63F22V.js +361 -0
- package/dist/chunk-HU63F22V.js.map +1 -0
- package/dist/chunk-JMBD6DOP.js +225 -0
- package/dist/chunk-JMBD6DOP.js.map +1 -0
- package/dist/chunk-K7EIJSYQ.js +1 -0
- package/dist/chunk-K7EIJSYQ.js.map +1 -0
- package/dist/chunk-OTBYRUBE.mjs +225 -0
- package/dist/chunk-OTBYRUBE.mjs.map +1 -0
- package/dist/chunk-WMHVXEYQ.mjs +66 -0
- package/dist/chunk-WMHVXEYQ.mjs.map +1 -0
- package/dist/cli/index.js +156 -25
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +140 -7
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/index.js +2 -2
- package/dist/client/index.mjs +2 -2
- package/dist/codegen/index.d.mts +1 -0
- package/dist/codegen/index.d.ts +1 -0
- package/dist/codegen/index.js +13 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/codegen/index.mjs +13 -0
- package/dist/codegen/index.mjs.map +1 -0
- package/dist/dev-server/index.d.mts +1 -0
- package/dist/dev-server/index.d.ts +1 -0
- package/dist/dev-server/index.js +13 -0
- package/dist/dev-server/index.js.map +1 -0
- package/dist/dev-server/index.mjs +13 -0
- package/dist/dev-server/index.mjs.map +1 -0
- package/dist/index-SjUaHgFr.d.mts +75 -0
- package/dist/index-SjUaHgFr.d.ts +75 -0
- package/dist/index-tFGPFVfQ.d.mts +67 -0
- package/dist/index-tFGPFVfQ.d.ts +67 -0
- package/dist/index.d.mts +83 -164
- package/dist/index.d.ts +83 -164
- package/dist/index.js +89 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +76 -9
- package/dist/index.mjs.map +1 -1
- package/dist/nest/index.js +2 -2
- package/dist/nest/index.mjs +2 -2
- package/dist/next/index.d.mts +61 -3
- package/dist/next/index.d.ts +61 -3
- package/dist/next/index.js +140 -7
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +102 -7
- package/dist/next/index.mjs.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/index.mjs +2 -1
- package/package.json +2 -2
- package/templates/nestjs-api/.env +4 -0
- package/templates/nestjs-api/.env.example +1 -2
- package/templates/nestjs-api/package-lock.json +5623 -0
- package/templates/nestjs-api/package.json +21 -19
- package/templates/nestjs-api/prisma/dev.db +0 -0
- package/templates/nestjs-api/prisma/migrations/20251123205437_init/migration.sql +24 -0
- package/templates/nestjs-api/prisma/migrations/migration_lock.toml +3 -0
- package/templates/nestjs-api/src/auth/auth.controller.ts +5 -5
- package/templates/nestjs-api/src/main.ts +1 -1
- package/templates/nestjs-api/src/todos/todos.controller.ts +7 -7
- package/templates/nestjs-api/src/users/users.controller.ts +3 -3
- package/templates/nestjs-api/tsconfig.json +20 -1
- package/templates/nextjs-web/app/actions/auth.ts +79 -0
- package/templates/nextjs-web/app/dashboard/error.tsx +39 -0
- package/templates/nextjs-web/app/dashboard/loading.tsx +14 -0
- package/templates/nextjs-web/app/dashboard/page.tsx +2 -172
- package/templates/nextjs-web/app/globals.css +80 -15
- package/templates/nextjs-web/app/layout.tsx +7 -5
- package/templates/nextjs-web/app/login/page.tsx +2 -104
- package/templates/nextjs-web/app/page.tsx +1 -1
- package/templates/nextjs-web/app/register/page.tsx +2 -127
- package/templates/nextjs-web/components/ui/button.tsx +56 -0
- package/templates/nextjs-web/components/ui/card.tsx +79 -0
- package/templates/nextjs-web/components/ui/input.tsx +25 -0
- package/templates/nextjs-web/components/ui/label.tsx +24 -0
- package/templates/nextjs-web/features/auth/LoginForm.tsx +140 -0
- package/templates/nextjs-web/features/auth/RegisterForm.tsx +159 -0
- package/templates/nextjs-web/features/auth/api.ts +35 -0
- package/templates/nextjs-web/features/auth/index.ts +3 -0
- package/templates/nextjs-web/features/dashboard/DashboardView.tsx +204 -0
- package/templates/nextjs-web/features/dashboard/api.ts +9 -0
- package/templates/nextjs-web/features/dashboard/components.tsx +74 -0
- package/templates/nextjs-web/features/dashboard/index.ts +3 -0
- package/templates/nextjs-web/hooks/index.ts +4 -0
- package/templates/nextjs-web/lib/api-client.ts +89 -0
- package/templates/nextjs-web/lib/axios-global-config.ts +17 -0
- package/templates/nextjs-web/lib/utils.ts +6 -0
- package/templates/nextjs-web/lib/wexts-client.ts +4 -0
- package/templates/nextjs-web/next-env.d.ts +6 -0
- package/templates/nextjs-web/next.config.ts +20 -0
- package/templates/nextjs-web/package-lock.json +3254 -0
- package/templates/nextjs-web/package.json +23 -14
- package/templates/nextjs-web/postcss.config.js +6 -0
- package/templates/nextjs-web/tailwind.config.ts +55 -1
- package/templates/nextjs-web/tsconfig.json +41 -39
- package/templates/nextjs-web/next.config.mjs +0 -4
- /package/templates/nextjs-web/{.env.local.example → .env} +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
3
|
-
"version": "0.0
|
|
2
|
+
"name": "wexts-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "wexts NestJS API",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "nest build",
|
|
7
|
+
"dev": "nest start --watch",
|
|
7
8
|
"start": "nest start",
|
|
8
9
|
"start:dev": "nest start --watch",
|
|
9
10
|
"start:prod": "node dist/main",
|
|
@@ -12,28 +13,29 @@
|
|
|
12
13
|
"prisma:studio": "prisma studio"
|
|
13
14
|
},
|
|
14
15
|
"dependencies": {
|
|
15
|
-
"@nestjs/common": "^
|
|
16
|
-
"@nestjs/
|
|
17
|
-
"@nestjs/
|
|
18
|
-
"@nestjs/jwt": "^
|
|
19
|
-
"@nestjs/passport": "^
|
|
20
|
-
"@
|
|
21
|
-
"
|
|
22
|
-
"passport-jwt": "^4.0.1",
|
|
16
|
+
"@nestjs/common": "^11.0.0",
|
|
17
|
+
"@nestjs/config": "^4.0.2",
|
|
18
|
+
"@nestjs/core": "^11.0.0",
|
|
19
|
+
"@nestjs/jwt": "^11.0.0",
|
|
20
|
+
"@nestjs/passport": "^11.0.0",
|
|
21
|
+
"@nestjs/platform-fastify": "^11.0.0",
|
|
22
|
+
"@prisma/client": "^6.0.0",
|
|
23
23
|
"bcrypt": "^5.1.1",
|
|
24
|
-
"class-validator": "^0.14.1",
|
|
25
24
|
"class-transformer": "^0.5.1",
|
|
26
|
-
"
|
|
25
|
+
"class-validator": "^0.14.1",
|
|
26
|
+
"passport": "^0.7.0",
|
|
27
|
+
"passport-jwt": "^4.0.1",
|
|
28
|
+
"reflect-metadata": "^0.2.2",
|
|
27
29
|
"rxjs": "^7.8.1",
|
|
28
|
-
"wexts": "^
|
|
30
|
+
"wexts": "^2.0.0"
|
|
29
31
|
},
|
|
30
32
|
"devDependencies": {
|
|
31
|
-
"@nestjs/cli": "^
|
|
32
|
-
"@nestjs/schematics": "^
|
|
33
|
-
"@types/node": "^20.11.0",
|
|
34
|
-
"@types/passport-jwt": "^4.0.1",
|
|
33
|
+
"@nestjs/cli": "^11.0.0",
|
|
34
|
+
"@nestjs/schematics": "^11.0.0",
|
|
35
35
|
"@types/bcrypt": "^5.0.2",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"@types/passport-jwt": "^4.0.1",
|
|
38
|
+
"prisma": "^6.0.0",
|
|
39
|
+
"typescript": "^5.9.3"
|
|
38
40
|
}
|
|
39
41
|
}
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- CreateTable
|
|
2
|
+
CREATE TABLE "User" (
|
|
3
|
+
"id" TEXT NOT NULL PRIMARY KEY,
|
|
4
|
+
"email" TEXT NOT NULL,
|
|
5
|
+
"name" TEXT,
|
|
6
|
+
"password" TEXT NOT NULL,
|
|
7
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
8
|
+
"updatedAt" DATETIME NOT NULL
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
-- CreateTable
|
|
12
|
+
CREATE TABLE "Todo" (
|
|
13
|
+
"id" TEXT NOT NULL PRIMARY KEY,
|
|
14
|
+
"title" TEXT NOT NULL,
|
|
15
|
+
"description" TEXT,
|
|
16
|
+
"completed" BOOLEAN NOT NULL DEFAULT false,
|
|
17
|
+
"userId" TEXT NOT NULL,
|
|
18
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
19
|
+
"updatedAt" DATETIME NOT NULL,
|
|
20
|
+
CONSTRAINT "Todo_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
-- CreateIndex
|
|
24
|
+
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import { Controller, Post, Get, Body, UseGuards, Request } from '@nestjs/common';
|
|
2
|
-
import { FusionController, FusionPost, FusionGet } from 'wexts/nest';
|
|
2
|
+
import { FusionController as WextsController, FusionPost as WextsPost, FusionGet as WextsGet } from 'wexts/nest';
|
|
3
3
|
import { AuthService } from './auth.service';
|
|
4
4
|
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
|
5
5
|
import { RegisterDto, LoginDto } from './dto/auth.dto';
|
|
6
6
|
|
|
7
|
-
@
|
|
7
|
+
@WextsController('auth')
|
|
8
8
|
@Controller('auth')
|
|
9
9
|
export class AuthController {
|
|
10
10
|
constructor(private authService: AuthService) { }
|
|
11
11
|
|
|
12
|
-
@
|
|
12
|
+
@WextsPost()
|
|
13
13
|
@Post('register')
|
|
14
14
|
async register(@Body() dto: RegisterDto) {
|
|
15
15
|
return this.authService.register(dto);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
@
|
|
18
|
+
@WextsPost()
|
|
19
19
|
@Post('login')
|
|
20
20
|
async login(@Body() dto: LoginDto) {
|
|
21
21
|
return this.authService.login(dto);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
@
|
|
24
|
+
@WextsGet()
|
|
25
25
|
@Get('me')
|
|
26
26
|
@UseGuards(JwtAuthGuard)
|
|
27
27
|
async getMe(@Request() req) {
|
|
@@ -26,7 +26,7 @@ async function bootstrap() {
|
|
|
26
26
|
const port = process.env.PORT || 5050;
|
|
27
27
|
await app.listen(port, '0.0.0.0');
|
|
28
28
|
|
|
29
|
-
console.log(`🚀
|
|
29
|
+
console.log(`🚀 wexts API running on http://localhost:${port}`);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
bootstrap();
|
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
import { Controller, Get, Post, Put, Delete, Body, Param, UseGuards, Request } from '@nestjs/common';
|
|
2
|
-
import { FusionController, FusionGet, FusionPost, FusionPut, FusionDelete } from 'wexts/nest';
|
|
2
|
+
import { FusionController as WextsController, FusionGet as WextsGet, FusionPost as WextsPost, FusionPut as WextsPut, FusionDelete as WextsDelete } from 'wexts/nest';
|
|
3
3
|
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
|
4
4
|
import { TodosService } from './todos.service';
|
|
5
5
|
import { CreateTodoDto, UpdateTodoDto } from './dto/todo.dto';
|
|
6
6
|
|
|
7
|
-
@
|
|
7
|
+
@WextsController('todos')
|
|
8
8
|
@Controller('todos')
|
|
9
9
|
@UseGuards(JwtAuthGuard)
|
|
10
10
|
export class TodosController {
|
|
11
11
|
constructor(private todosService: TodosService) { }
|
|
12
12
|
|
|
13
|
-
@
|
|
13
|
+
@WextsGet()
|
|
14
14
|
@Get()
|
|
15
15
|
async findAll(@Request() req) {
|
|
16
16
|
return this.todosService.findAll(req.user.userId);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
@
|
|
19
|
+
@WextsGet()
|
|
20
20
|
@Get(':id')
|
|
21
21
|
async findOne(@Param('id') id: string, @Request() req) {
|
|
22
22
|
return this.todosService.findOne(id, req.user.userId);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
@
|
|
25
|
+
@WextsPost()
|
|
26
26
|
@Post()
|
|
27
27
|
async create(@Body() dto: CreateTodoDto, @Request() req) {
|
|
28
28
|
return this.todosService.create(dto, req.user.userId);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
@
|
|
31
|
+
@WextsPut()
|
|
32
32
|
@Put(':id')
|
|
33
33
|
async update(
|
|
34
34
|
@Param('id') id: string,
|
|
@@ -38,7 +38,7 @@ export class TodosController {
|
|
|
38
38
|
return this.todosService.update(id, dto, req.user.userId);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
@
|
|
41
|
+
@WextsDelete()
|
|
42
42
|
@Delete(':id')
|
|
43
43
|
async remove(@Param('id') id: string, @Request() req) {
|
|
44
44
|
return this.todosService.remove(id, req.user.userId);
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { Controller, Get, UseGuards, Request } from '@nestjs/common';
|
|
2
|
-
import { FusionController, FusionGet } from 'wexts/nest';
|
|
2
|
+
import { FusionController as WextsController, FusionGet as WextsGet } from 'wexts/nest';
|
|
3
3
|
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
|
4
4
|
import { UsersService } from './users.service';
|
|
5
5
|
|
|
6
|
-
@
|
|
6
|
+
@WextsController('users')
|
|
7
7
|
@Controller('users')
|
|
8
8
|
@UseGuards(JwtAuthGuard)
|
|
9
9
|
export class UsersController {
|
|
10
10
|
constructor(private usersService: UsersService) { }
|
|
11
11
|
|
|
12
|
-
@
|
|
12
|
+
@WextsGet()
|
|
13
13
|
@Get('me')
|
|
14
14
|
async getMe(@Request() req) {
|
|
15
15
|
return this.usersService.findById(req.user.userId);
|
|
@@ -16,6 +16,25 @@
|
|
|
16
16
|
"noImplicitAny": false,
|
|
17
17
|
"strictBindCallApply": false,
|
|
18
18
|
"forceConsistentCasingInFileNames": false,
|
|
19
|
-
"noFallthroughCasesInSwitch": false
|
|
19
|
+
"noFallthroughCasesInSwitch": false,
|
|
20
|
+
"moduleResolution": "node",
|
|
21
|
+
"esModuleInterop": true,
|
|
22
|
+
"paths": {
|
|
23
|
+
"wexts/nest": [
|
|
24
|
+
"node_modules/wexts/dist/nest/index"
|
|
25
|
+
],
|
|
26
|
+
"wexts/next": [
|
|
27
|
+
"node_modules/wexts/dist/next/index"
|
|
28
|
+
],
|
|
29
|
+
"wexts/client": [
|
|
30
|
+
"node_modules/wexts/dist/client/index"
|
|
31
|
+
],
|
|
32
|
+
"wexts/types": [
|
|
33
|
+
"node_modules/wexts/dist/types/index"
|
|
34
|
+
],
|
|
35
|
+
"wexts": [
|
|
36
|
+
"node_modules/wexts/dist/index"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
20
39
|
}
|
|
21
40
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { cookies } from 'next/headers';
|
|
4
|
+
import { redirect } from 'next/navigation';
|
|
5
|
+
|
|
6
|
+
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5050';
|
|
7
|
+
|
|
8
|
+
export type ActionState = {
|
|
9
|
+
message?: string;
|
|
10
|
+
errors?: {
|
|
11
|
+
email?: string[];
|
|
12
|
+
password?: string[];
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export async function loginAction(prevState: ActionState, formData: FormData): Promise<ActionState> {
|
|
17
|
+
const email = formData.get('email') as string;
|
|
18
|
+
const password = formData.get('password') as string;
|
|
19
|
+
|
|
20
|
+
const errors: { email?: string[]; password?: string[] } = {};
|
|
21
|
+
|
|
22
|
+
if (!email || !email.includes('@')) {
|
|
23
|
+
errors.email = ['Invalid email address'];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!password || password.length < 6) {
|
|
27
|
+
errors.password = ['Password must be at least 6 characters'];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (Object.keys(errors).length > 0) {
|
|
31
|
+
return { errors };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(`${API_URL}/auth/login`, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: {
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({ email, password }),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const data = await response.json();
|
|
44
|
+
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
return {
|
|
47
|
+
message: data.message || 'Invalid credentials',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Assuming the API returns { access_token: string }
|
|
52
|
+
if (data.access_token) {
|
|
53
|
+
const cookieStore = await cookies();
|
|
54
|
+
cookieStore.set('wexts_token', data.access_token, {
|
|
55
|
+
httpOnly: true,
|
|
56
|
+
secure: process.env.NODE_ENV === 'production',
|
|
57
|
+
maxAge: 60 * 60 * 24 * 7, // 1 week
|
|
58
|
+
path: '/',
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
return {
|
|
62
|
+
message: 'Login failed: No token received',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('Login error:', error);
|
|
67
|
+
return {
|
|
68
|
+
message: 'Something went wrong. Please try again.',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
redirect('/dashboard');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function logoutAction() {
|
|
76
|
+
const cookieStore = await cookies();
|
|
77
|
+
cookieStore.delete('wexts_token');
|
|
78
|
+
redirect('/login');
|
|
79
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
6
|
+
import { AlertTriangle } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
export default function DashboardError({
|
|
9
|
+
error,
|
|
10
|
+
reset,
|
|
11
|
+
}: {
|
|
12
|
+
error: Error & { digest?: string };
|
|
13
|
+
reset: () => void;
|
|
14
|
+
}) {
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
console.error(error);
|
|
17
|
+
}, [error]);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="min-h-screen bg-background flex items-center justify-center p-8">
|
|
21
|
+
<Card className="glass border-destructive/20 max-w-md w-full">
|
|
22
|
+
<CardHeader>
|
|
23
|
+
<CardTitle className="text-destructive flex items-center gap-2">
|
|
24
|
+
<AlertTriangle className="w-5 h-5" />
|
|
25
|
+
Something went wrong
|
|
26
|
+
</CardTitle>
|
|
27
|
+
</CardHeader>
|
|
28
|
+
<CardContent className="space-y-4">
|
|
29
|
+
<p className="text-muted-foreground">
|
|
30
|
+
We encountered an error while loading your dashboard. Please try again.
|
|
31
|
+
</p>
|
|
32
|
+
<Button onClick={reset} className="w-full">
|
|
33
|
+
Try again
|
|
34
|
+
</Button>
|
|
35
|
+
</CardContent>
|
|
36
|
+
</Card>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Loader2 } from 'lucide-react';
|
|
2
|
+
|
|
3
|
+
export default function DashboardLoading() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="min-h-screen bg-background flex items-center justify-center">
|
|
6
|
+
<div className="text-center space-y-4">
|
|
7
|
+
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary/10 text-primary animate-pulse">
|
|
8
|
+
<Loader2 className="w-8 h-8 animate-spin" />
|
|
9
|
+
</div>
|
|
10
|
+
<p className="text-muted-foreground animate-pulse">Loading dashboard...</p>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -1,175 +1,5 @@
|
|
|
1
|
-
|
|
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
|
-
}
|
|
1
|
+
import { DashboardView } from '@/features/dashboard';
|
|
13
2
|
|
|
14
3
|
export default function DashboardPage() {
|
|
15
|
-
|
|
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
|
-
);
|
|
4
|
+
return <DashboardView />;
|
|
175
5
|
}
|
|
@@ -2,27 +2,92 @@
|
|
|
2
2
|
@tailwind components;
|
|
3
3
|
@tailwind utilities;
|
|
4
4
|
|
|
5
|
-
|
|
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) {
|
|
5
|
+
@layer base {
|
|
13
6
|
:root {
|
|
14
|
-
|
|
15
|
-
--
|
|
7
|
+
/* Brand Colors - Deep Violet & Electric Purple */
|
|
8
|
+
--background: 260 30% 98%;
|
|
9
|
+
--foreground: 260 40% 10%;
|
|
10
|
+
|
|
11
|
+
--primary: 265 89% 66%;
|
|
12
|
+
--primary-foreground: 210 40% 98%;
|
|
13
|
+
|
|
14
|
+
--secondary: 260 20% 90%;
|
|
15
|
+
--secondary-foreground: 260 40% 10%;
|
|
16
|
+
|
|
17
|
+
--muted: 260 20% 96%;
|
|
18
|
+
--muted-foreground: 260 20% 40%;
|
|
19
|
+
|
|
20
|
+
--accent: 265 89% 96%;
|
|
21
|
+
--accent-foreground: 265 89% 66%;
|
|
22
|
+
|
|
23
|
+
--destructive: 0 84% 60%;
|
|
24
|
+
--destructive-foreground: 210 40% 98%;
|
|
25
|
+
|
|
26
|
+
--border: 260 20% 90%;
|
|
27
|
+
--input: 260 20% 90%;
|
|
28
|
+
--ring: 265 89% 66%;
|
|
29
|
+
|
|
30
|
+
--radius: 1rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.dark {
|
|
34
|
+
--background: 260 40% 5%;
|
|
35
|
+
--foreground: 210 40% 98%;
|
|
36
|
+
|
|
37
|
+
--primary: 265 89% 66%;
|
|
38
|
+
--primary-foreground: 210 40% 98%;
|
|
39
|
+
|
|
40
|
+
--secondary: 260 30% 15%;
|
|
41
|
+
--secondary-foreground: 210 40% 98%;
|
|
42
|
+
|
|
43
|
+
--muted: 260 30% 15%;
|
|
44
|
+
--muted-foreground: 215 20% 65%;
|
|
45
|
+
|
|
46
|
+
--accent: 260 30% 15%;
|
|
47
|
+
--accent-foreground: 210 40% 98%;
|
|
48
|
+
|
|
49
|
+
--destructive: 0 62% 30%;
|
|
50
|
+
--destructive-foreground: 210 40% 98%;
|
|
51
|
+
|
|
52
|
+
--border: 260 30% 15%;
|
|
53
|
+
--input: 260 30% 15%;
|
|
54
|
+
--ring: 265 89% 66%;
|
|
16
55
|
}
|
|
17
56
|
}
|
|
18
57
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
58
|
+
@layer base {
|
|
59
|
+
* {
|
|
60
|
+
@apply border-border;
|
|
61
|
+
}
|
|
62
|
+
body {
|
|
63
|
+
@apply bg-background text-foreground antialiased;
|
|
64
|
+
font-feature-settings: "rlig" 1, "calt" 1;
|
|
65
|
+
}
|
|
22
66
|
}
|
|
23
67
|
|
|
24
68
|
@layer utilities {
|
|
25
|
-
.
|
|
26
|
-
|
|
69
|
+
.glass {
|
|
70
|
+
@apply bg-white/10 backdrop-blur-lg border border-white/20 shadow-xl;
|
|
27
71
|
}
|
|
72
|
+
.glass-dark {
|
|
73
|
+
@apply bg-black/30 backdrop-blur-lg border border-white/10 shadow-xl;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.text-gradient {
|
|
77
|
+
@apply bg-clip-text text-transparent bg-gradient-to-r from-violet-600 to-indigo-600 dark:from-violet-400 dark:to-indigo-400;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.animate-float {
|
|
81
|
+
animation: float 6s ease-in-out infinite;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.animate-pulse-slow {
|
|
85
|
+
animation: pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@keyframes float {
|
|
90
|
+
0% { transform: translateY(0px); }
|
|
91
|
+
50% { transform: translateY(-20px); }
|
|
92
|
+
100% { transform: translateY(0px); }
|
|
28
93
|
}
|