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,13 +1,14 @@
|
|
|
1
1
|
import type { Metadata } from 'next';
|
|
2
2
|
import { Inter } from 'next/font/google';
|
|
3
|
-
import {
|
|
3
|
+
import { WextsProvider } from '@/lib/wexts-client';
|
|
4
|
+
import { Toaster } from 'react-hot-toast';
|
|
4
5
|
import './globals.css';
|
|
5
6
|
|
|
6
7
|
const inter = Inter({ subsets: ['latin'] });
|
|
7
8
|
|
|
8
9
|
export const metadata: Metadata = {
|
|
9
|
-
title: '
|
|
10
|
-
description: '
|
|
10
|
+
title: 'wexts Web App',
|
|
11
|
+
description: 'Modern web application built with wexts',
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export default function RootLayout({
|
|
@@ -18,9 +19,10 @@ export default function RootLayout({
|
|
|
18
19
|
return (
|
|
19
20
|
<html lang="en">
|
|
20
21
|
<body className={inter.className}>
|
|
21
|
-
<
|
|
22
|
+
<WextsProvider baseUrl={process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5050'}>
|
|
22
23
|
{children}
|
|
23
|
-
|
|
24
|
+
<Toaster position="top-center" />
|
|
25
|
+
</WextsProvider>
|
|
24
26
|
</body>
|
|
25
27
|
</html>
|
|
26
28
|
);
|
|
@@ -1,107 +1,5 @@
|
|
|
1
|
-
|
|
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';
|
|
1
|
+
import { LoginForm } from '@/features/auth';
|
|
7
2
|
|
|
8
3
|
export default function LoginPage() {
|
|
9
|
-
|
|
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
|
-
);
|
|
4
|
+
return <LoginForm />;
|
|
107
5
|
}
|
|
@@ -1,130 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { useState, FormEvent } from 'react';
|
|
4
|
-
import { useFusion } from 'wexts/next';
|
|
5
|
-
import { useRouter } from 'next/navigation';
|
|
6
|
-
import Link from 'next/link';
|
|
1
|
+
import { RegisterForm } from '@/features/auth';
|
|
7
2
|
|
|
8
3
|
export default function RegisterPage() {
|
|
9
|
-
|
|
10
|
-
const [password, setPassword] = useState('');
|
|
11
|
-
const [name, setName] = useState('');
|
|
12
|
-
const [error, setError] = useState('');
|
|
13
|
-
const [loading, setLoading] = useState(false);
|
|
14
|
-
|
|
15
|
-
const { client } = useFusion();
|
|
16
|
-
const router = useRouter();
|
|
17
|
-
|
|
18
|
-
const handleSubmit = async (e: FormEvent) => {
|
|
19
|
-
e.preventDefault();
|
|
20
|
-
setError('');
|
|
21
|
-
setLoading(true);
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
const response = await client.post<{ token: string; user: any }>('/auth/register', {
|
|
25
|
-
email,
|
|
26
|
-
password,
|
|
27
|
-
name,
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
localStorage.setItem('fusion_token', response.token);
|
|
31
|
-
router.push('/dashboard');
|
|
32
|
-
} catch (err: any) {
|
|
33
|
-
setError(err.message || 'Registration failed');
|
|
34
|
-
} finally {
|
|
35
|
-
setLoading(false);
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<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">
|
|
41
|
-
<div className="max-w-md w-full space-y-8 bg-white dark:bg-gray-800 p-10 rounded-2xl shadow-xl">
|
|
42
|
-
<div>
|
|
43
|
-
<h2 className="text-center text-4xl font-bold text-gray-900 dark:text-white">
|
|
44
|
-
Create Account
|
|
45
|
-
</h2>
|
|
46
|
-
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
|
|
47
|
-
Get started with Fusion
|
|
48
|
-
</p>
|
|
49
|
-
</div>
|
|
50
|
-
|
|
51
|
-
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
|
52
|
-
{error && (
|
|
53
|
-
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-sm text-red-600 dark:text-red-400">
|
|
54
|
-
{error}
|
|
55
|
-
</div>
|
|
56
|
-
)}
|
|
57
|
-
|
|
58
|
-
<div className="space-y-4">
|
|
59
|
-
<div>
|
|
60
|
-
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
61
|
-
Name (optional)
|
|
62
|
-
</label>
|
|
63
|
-
<input
|
|
64
|
-
id="name"
|
|
65
|
-
name="name"
|
|
66
|
-
type="text"
|
|
67
|
-
value={name}
|
|
68
|
-
onChange={(e) => setName(e.target.value)}
|
|
69
|
-
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"
|
|
70
|
-
placeholder="John Doe"
|
|
71
|
-
/>
|
|
72
|
-
</div>
|
|
73
|
-
|
|
74
|
-
<div>
|
|
75
|
-
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
76
|
-
Email
|
|
77
|
-
</label>
|
|
78
|
-
<input
|
|
79
|
-
id="email"
|
|
80
|
-
name="email"
|
|
81
|
-
type="email"
|
|
82
|
-
autoComplete="email"
|
|
83
|
-
required
|
|
84
|
-
value={email}
|
|
85
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
86
|
-
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"
|
|
87
|
-
placeholder="you@example.com"
|
|
88
|
-
/>
|
|
89
|
-
</div>
|
|
90
|
-
|
|
91
|
-
<div>
|
|
92
|
-
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
93
|
-
Password
|
|
94
|
-
</label>
|
|
95
|
-
<input
|
|
96
|
-
id="password"
|
|
97
|
-
name="password"
|
|
98
|
-
type="password"
|
|
99
|
-
autoComplete="new-password"
|
|
100
|
-
required
|
|
101
|
-
value={password}
|
|
102
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
103
|
-
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"
|
|
104
|
-
placeholder="••••••••"
|
|
105
|
-
/>
|
|
106
|
-
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">Minimum 6 characters</p>
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
109
|
-
|
|
110
|
-
<div>
|
|
111
|
-
<button
|
|
112
|
-
type="submit"
|
|
113
|
-
disabled={loading}
|
|
114
|
-
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"
|
|
115
|
-
>
|
|
116
|
-
{loading ? 'Creating account...' : 'Sign up'}
|
|
117
|
-
</button>
|
|
118
|
-
</div>
|
|
119
|
-
|
|
120
|
-
<div className="text-center text-sm">
|
|
121
|
-
<span className="text-gray-600 dark:text-gray-400">Already have an account? </span>
|
|
122
|
-
<Link href="/login" className="font-medium text-violet-600 hover:text-violet-500 dark:text-violet-400">
|
|
123
|
-
Sign in
|
|
124
|
-
</Link>
|
|
125
|
-
</div>
|
|
126
|
-
</form>
|
|
127
|
-
</div>
|
|
128
|
-
</div>
|
|
129
|
-
);
|
|
4
|
+
return <RegisterForm />;
|
|
130
5
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
13
|
+
destructive:
|
|
14
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
15
|
+
outline:
|
|
16
|
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
17
|
+
secondary:
|
|
18
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
19
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
20
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
default: "h-10 px-4 py-2",
|
|
24
|
+
sm: "h-9 rounded-md px-3",
|
|
25
|
+
lg: "h-11 rounded-md px-8",
|
|
26
|
+
icon: "h-10 w-10",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
variant: "default",
|
|
31
|
+
size: "default",
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
export interface ButtonProps
|
|
37
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
38
|
+
VariantProps<typeof buttonVariants> {
|
|
39
|
+
asChild?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
43
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
44
|
+
const Comp = asChild ? Slot : "button"
|
|
45
|
+
return (
|
|
46
|
+
<Comp
|
|
47
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
48
|
+
ref={ref}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
Button.displayName = "Button"
|
|
55
|
+
|
|
56
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
const Card = React.forwardRef<
|
|
6
|
+
HTMLDivElement,
|
|
7
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
8
|
+
>(({ className, ...props }, ref) => (
|
|
9
|
+
<div
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn(
|
|
12
|
+
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
))
|
|
18
|
+
Card.displayName = "Card"
|
|
19
|
+
|
|
20
|
+
const CardHeader = React.forwardRef<
|
|
21
|
+
HTMLDivElement,
|
|
22
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
23
|
+
>(({ className, ...props }, ref) => (
|
|
24
|
+
<div
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
))
|
|
30
|
+
CardHeader.displayName = "CardHeader"
|
|
31
|
+
|
|
32
|
+
const CardTitle = React.forwardRef<
|
|
33
|
+
HTMLParagraphElement,
|
|
34
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
35
|
+
>(({ className, ...props }, ref) => (
|
|
36
|
+
<h3
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn(
|
|
39
|
+
"text-2xl font-semibold leading-none tracking-tight",
|
|
40
|
+
className
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
))
|
|
45
|
+
CardTitle.displayName = "CardTitle"
|
|
46
|
+
|
|
47
|
+
const CardDescription = React.forwardRef<
|
|
48
|
+
HTMLParagraphElement,
|
|
49
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
50
|
+
>(({ className, ...props }, ref) => (
|
|
51
|
+
<p
|
|
52
|
+
ref={ref}
|
|
53
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
))
|
|
57
|
+
CardDescription.displayName = "CardDescription"
|
|
58
|
+
|
|
59
|
+
const CardContent = React.forwardRef<
|
|
60
|
+
HTMLDivElement,
|
|
61
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
62
|
+
>(({ className, ...props }, ref) => (
|
|
63
|
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
64
|
+
))
|
|
65
|
+
CardContent.displayName = "CardContent"
|
|
66
|
+
|
|
67
|
+
const CardFooter = React.forwardRef<
|
|
68
|
+
HTMLDivElement,
|
|
69
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
70
|
+
>(({ className, ...props }, ref) => (
|
|
71
|
+
<div
|
|
72
|
+
ref={ref}
|
|
73
|
+
className={cn("flex items-center p-6 pt-0", className)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
))
|
|
77
|
+
CardFooter.displayName = "CardFooter"
|
|
78
|
+
|
|
79
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
export interface InputProps
|
|
6
|
+
extends React.InputHTMLAttributes<HTMLInputElement> { }
|
|
7
|
+
|
|
8
|
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
9
|
+
({ className, type, ...props }, ref) => {
|
|
10
|
+
return (
|
|
11
|
+
<input
|
|
12
|
+
type={type}
|
|
13
|
+
className={cn(
|
|
14
|
+
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
15
|
+
className
|
|
16
|
+
)}
|
|
17
|
+
ref={ref}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
Input.displayName = "Input"
|
|
24
|
+
|
|
25
|
+
export { Input }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const labelVariants = cva(
|
|
8
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
const Label = React.forwardRef<
|
|
12
|
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
13
|
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
|
14
|
+
VariantProps<typeof labelVariants>
|
|
15
|
+
>(({ className, ...props }, ref) => (
|
|
16
|
+
<LabelPrimitive.Root
|
|
17
|
+
ref={ref}
|
|
18
|
+
className={cn(labelVariants(), className)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
))
|
|
22
|
+
Label.displayName = LabelPrimitive.Root.displayName
|
|
23
|
+
|
|
24
|
+
export { Label }
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { login } from './api';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { Input } from '@/components/ui/input';
|
|
8
|
+
import { Label } from '@/components/ui/label';
|
|
9
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
|
10
|
+
import Link from 'next/link';
|
|
11
|
+
import { Zap, Mail, Lock, ArrowRight, Loader2 } from 'lucide-react';
|
|
12
|
+
|
|
13
|
+
export function LoginForm() {
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
const [loading, setLoading] = useState(false);
|
|
16
|
+
const [error, setError] = useState('');
|
|
17
|
+
|
|
18
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
setLoading(true);
|
|
21
|
+
setError('');
|
|
22
|
+
|
|
23
|
+
const formData = new FormData(e.currentTarget);
|
|
24
|
+
const data = Object.fromEntries(formData.entries());
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await login(data);
|
|
28
|
+
router.push('/dashboard');
|
|
29
|
+
} catch (err: any) {
|
|
30
|
+
console.error(err);
|
|
31
|
+
setError(err.response?.data?.message || 'Login failed. Please check your credentials.');
|
|
32
|
+
} finally {
|
|
33
|
+
setLoading(false);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="min-h-screen flex items-center justify-center relative overflow-hidden bg-background">
|
|
39
|
+
{/* Animated Background Elements */}
|
|
40
|
+
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
|
41
|
+
<div className="absolute -top-[20%] -left-[10%] w-[50%] h-[50%] rounded-full bg-primary/20 blur-[100px] animate-float" />
|
|
42
|
+
<div className="absolute top-[40%] -right-[10%] w-[40%] h-[40%] rounded-full bg-indigo-500/20 blur-[100px] animate-float" style={{ animationDelay: '2s' }} />
|
|
43
|
+
<div className="absolute -bottom-[10%] left-[20%] w-[30%] h-[30%] rounded-full bg-violet-500/20 blur-[100px] animate-float" style={{ animationDelay: '4s' }} />
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div className="w-full max-w-md p-8 relative z-10">
|
|
47
|
+
<Card className="glass border-white/20 shadow-2xl backdrop-blur-xl">
|
|
48
|
+
<CardHeader className="text-center">
|
|
49
|
+
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary/10 mb-6 mx-auto text-primary">
|
|
50
|
+
<Zap className="w-8 h-8" />
|
|
51
|
+
</div>
|
|
52
|
+
<CardTitle className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-indigo-600">
|
|
53
|
+
Welcome Back
|
|
54
|
+
</CardTitle>
|
|
55
|
+
<CardDescription>
|
|
56
|
+
Sign in to continue to wexts
|
|
57
|
+
</CardDescription>
|
|
58
|
+
</CardHeader>
|
|
59
|
+
<CardContent>
|
|
60
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
61
|
+
{error && (
|
|
62
|
+
<div className="p-4 rounded-xl bg-destructive/10 text-destructive text-sm font-medium border border-destructive/20 animate-pulse-slow flex items-center gap-2">
|
|
63
|
+
<span className="text-lg">⚠️</span>
|
|
64
|
+
{error}
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
<div className="space-y-4">
|
|
69
|
+
<div className="space-y-2">
|
|
70
|
+
<Label htmlFor="email">Email Address</Label>
|
|
71
|
+
<div className="relative">
|
|
72
|
+
<Mail className="absolute left-3 top-3 w-4 h-4 text-muted-foreground" />
|
|
73
|
+
<Input
|
|
74
|
+
id="email"
|
|
75
|
+
name="email"
|
|
76
|
+
type="email"
|
|
77
|
+
placeholder="name@example.com"
|
|
78
|
+
required
|
|
79
|
+
className="pl-10 bg-secondary/50 border-transparent focus:border-primary/50 focus:bg-background transition-all"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div className="space-y-2">
|
|
85
|
+
<div className="flex items-center justify-between">
|
|
86
|
+
<Label htmlFor="password">Password</Label>
|
|
87
|
+
<Link href="#" className="text-xs font-medium text-primary hover:text-primary/80 transition-colors">
|
|
88
|
+
Forgot password?
|
|
89
|
+
</Link>
|
|
90
|
+
</div>
|
|
91
|
+
<div className="relative">
|
|
92
|
+
<Lock className="absolute left-3 top-3 w-4 h-4 text-muted-foreground" />
|
|
93
|
+
<Input
|
|
94
|
+
id="password"
|
|
95
|
+
name="password"
|
|
96
|
+
type="password"
|
|
97
|
+
placeholder="••••••••"
|
|
98
|
+
required
|
|
99
|
+
className="pl-10 bg-secondary/50 border-transparent focus:border-primary/50 focus:bg-background transition-all"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<Button
|
|
106
|
+
type="submit"
|
|
107
|
+
disabled={loading}
|
|
108
|
+
className="w-full bg-gradient-to-r from-primary to-indigo-600 hover:from-primary/90 hover:to-indigo-600/90 shadow-lg shadow-primary/25 group"
|
|
109
|
+
>
|
|
110
|
+
{loading ? (
|
|
111
|
+
<>
|
|
112
|
+
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
113
|
+
Signing in...
|
|
114
|
+
</>
|
|
115
|
+
) : (
|
|
116
|
+
<>
|
|
117
|
+
Sign in
|
|
118
|
+
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
|
|
119
|
+
</>
|
|
120
|
+
)}
|
|
121
|
+
</Button>
|
|
122
|
+
</form>
|
|
123
|
+
</CardContent>
|
|
124
|
+
<CardFooter className="justify-center">
|
|
125
|
+
<div className="text-center text-sm text-muted-foreground">
|
|
126
|
+
Don't have an account?{' '}
|
|
127
|
+
<Link href="/register" className="font-semibold text-primary hover:text-primary/80 transition-colors">
|
|
128
|
+
Create account
|
|
129
|
+
</Link>
|
|
130
|
+
</div>
|
|
131
|
+
</CardFooter>
|
|
132
|
+
</Card>
|
|
133
|
+
|
|
134
|
+
<p className="text-center mt-8 text-xs text-muted-foreground/60">
|
|
135
|
+
© 2025 wexts Inc. All rights reserved.
|
|
136
|
+
</p>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|