nextjs-auth-module 1.1.0

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.
@@ -0,0 +1,79 @@
1
+ import { NextResponse } from 'next/server';
2
+ import pool from '@/lib/db';
3
+ import { sendResetPasswordEmail } from '@/lib/email';
4
+
5
+ function generateResetToken() {
6
+ return Math.random().toString(36).substring(2, 15) +
7
+ Math.random().toString(36).substring(2, 15);
8
+ }
9
+
10
+ export async function POST(request) {
11
+ console.log('=== FORGOT PASSWORD API CALLED ===');
12
+
13
+ try {
14
+ const { email } = await request.json();
15
+
16
+ if (!email) {
17
+ return NextResponse.json(
18
+ { error: 'Email is required' },
19
+ { status: 400 }
20
+ );
21
+ }
22
+
23
+ console.log('Processing email:', email);
24
+
25
+ // Check if user exists
26
+ const result = await pool.query(
27
+ 'SELECT id, email FROM users WHERE email = $1',
28
+ [email.toLowerCase()]
29
+ );
30
+
31
+ if (result.rows.length > 0) {
32
+ const resetToken = generateResetToken();
33
+ const expiresAt = new Date(Date.now() + 3600000); // 1 hour
34
+
35
+ console.log('Generated reset token:', resetToken);
36
+ console.log('Expires at:', expiresAt);
37
+
38
+ // āœ… CORRECT: Using your actual column names
39
+ await pool.query(
40
+ `UPDATE users
41
+ SET reset_password_token = $1,
42
+ reset_password_expires = $2
43
+ WHERE email = $3`,
44
+ [resetToken, expiresAt, email.toLowerCase()]
45
+ );
46
+ console.log('User updated with reset token');
47
+
48
+ // Log the reset link for development
49
+ const resetUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/reset-password?token=${resetToken}`;
50
+ console.log('\nšŸ” ========== PASSWORD RESET LINK ==========');
51
+ console.log(`Reset URL: ${resetUrl}`);
52
+ console.log(`Token: ${resetToken}`);
53
+ console.log('==========================================\n');
54
+
55
+ // Try to send email
56
+ try {
57
+ await sendResetPasswordEmail(email, resetToken);
58
+ console.log('Email sent successfully');
59
+ } catch (emailError) {
60
+ console.error('Email sending failed:', emailError.message);
61
+ }
62
+ } else {
63
+ console.log('No user found with email:', email);
64
+ }
65
+
66
+ return NextResponse.json({
67
+ message: 'If an account exists with this email, you will receive password reset instructions.'
68
+ });
69
+
70
+ } catch (error) {
71
+ console.error('=== FORGOT PASSWORD ERROR ===');
72
+ console.error('Error:', error.message);
73
+
74
+ return NextResponse.json(
75
+ { error: 'Unable to process request. Please try again.' },
76
+ { status: 500 }
77
+ );
78
+ }
79
+ }
@@ -0,0 +1,53 @@
1
+ import { NextResponse } from 'next/server';
2
+ import pool from '@/lib/db';
3
+ import { comparePassword, generateToken } from '@/lib/auth';
4
+
5
+ export async function POST(request) {
6
+ try {
7
+ const { email, password } = await request.json();
8
+
9
+ if (!email || !password) {
10
+ return NextResponse.json(
11
+ { error: 'Email and password are required' },
12
+ { status: 400 }
13
+ );
14
+ }
15
+
16
+ // Find user
17
+ const result = await pool.query(
18
+ 'SELECT id, email, name, password FROM users WHERE email = $1',
19
+ [email]
20
+ );
21
+
22
+ if (result.rows.length === 0) {
23
+ return NextResponse.json(
24
+ { error: 'Invalid credentials' },
25
+ { status: 401 }
26
+ );
27
+ }
28
+
29
+ const user = result.rows[0];
30
+ const isValidPassword = await comparePassword(password, user.password);
31
+
32
+ if (!isValidPassword) {
33
+ return NextResponse.json(
34
+ { error: 'Invalid credentials' },
35
+ { status: 401 }
36
+ );
37
+ }
38
+
39
+ const token = generateToken(user.id, user.email);
40
+
41
+ return NextResponse.json({
42
+ user: { id: user.id, email: user.email, name: user.name },
43
+ token,
44
+ });
45
+
46
+ } catch (error) {
47
+ console.error('Login error:', error);
48
+ return NextResponse.json(
49
+ { error: 'Internal server error' },
50
+ { status: 500 }
51
+ );
52
+ }
53
+ }
@@ -0,0 +1,52 @@
1
+ import { NextResponse } from 'next/server';
2
+ import pool from '@/lib/db';
3
+ import { hashPassword, generateToken } from '@/lib/auth';
4
+
5
+ export async function POST(request) {
6
+ try {
7
+ const { name, email, password } = await request.json();
8
+
9
+ // Validation
10
+ if (!email || !password) {
11
+ return NextResponse.json(
12
+ { error: 'Email and password are required' },
13
+ { status: 400 }
14
+ );
15
+ }
16
+
17
+ // Check if user exists
18
+ const existingUser = await pool.query(
19
+ 'SELECT id FROM users WHERE email = $1',
20
+ [email]
21
+ );
22
+
23
+ if (existingUser.rows.length > 0) {
24
+ return NextResponse.json(
25
+ { error: 'User already exists' },
26
+ { status: 400 }
27
+ );
28
+ }
29
+
30
+ // Hash password and create user
31
+ const hashedPassword = await hashPassword(password);
32
+ const result = await pool.query(
33
+ 'INSERT INTO users (name, email, password) VALUES ($1, $2, $3) RETURNING id, email, name',
34
+ [name || null, email, hashedPassword]
35
+ );
36
+
37
+ const user = result.rows[0];
38
+ const token = generateToken(user.id, user.email);
39
+
40
+ return NextResponse.json({
41
+ user: { id: user.id, email: user.email, name: user.name },
42
+ token,
43
+ }, { status: 201 });
44
+
45
+ } catch (error) {
46
+ console.error('Registration error:', error);
47
+ return NextResponse.json(
48
+ { error: 'Internal server error' },
49
+ { status: 500 }
50
+ );
51
+ }
52
+ }
@@ -0,0 +1,91 @@
1
+ import { NextResponse } from 'next/server';
2
+ import pool from '@/lib/db';
3
+ import bcrypt from 'bcryptjs';
4
+
5
+ async function hashPassword(password) {
6
+ return await bcrypt.hash(password, 10);
7
+ }
8
+
9
+ export async function POST(request) {
10
+ console.log('=== RESET PASSWORD API CALLED ===');
11
+
12
+ try {
13
+ const { token, newPassword } = await request.json();
14
+
15
+ console.log('Received token:', token);
16
+
17
+ if (!token || !newPassword) {
18
+ return NextResponse.json(
19
+ { error: 'Token and new password are required' },
20
+ { status: 400 }
21
+ );
22
+ }
23
+
24
+ if (newPassword.length < 6) {
25
+ return NextResponse.json(
26
+ { error: 'Password must be at least 6 characters' },
27
+ { status: 400 }
28
+ );
29
+ }
30
+
31
+ // āœ… CORRECT: Using your actual column names
32
+ const result = await pool.query(
33
+ `SELECT id, email, reset_password_expires
34
+ FROM users
35
+ WHERE reset_password_token = $1`,
36
+ [token]
37
+ );
38
+
39
+ console.log('Query result rows:', result.rows.length);
40
+
41
+ if (result.rows.length === 0) {
42
+ return NextResponse.json(
43
+ { error: 'Invalid reset token' },
44
+ { status: 400 }
45
+ );
46
+ }
47
+
48
+ const user = result.rows[0];
49
+ console.log('User found:', user.id);
50
+ console.log('Token expires at:', user.reset_password_expires);
51
+ console.log('Current time:', new Date());
52
+
53
+ // Check if token is expired
54
+ if (new Date() > new Date(user.reset_password_expires)) {
55
+ console.log('Token has expired');
56
+ return NextResponse.json(
57
+ { error: 'Reset token has expired. Please request a new password reset.' },
58
+ { status: 400 }
59
+ );
60
+ }
61
+
62
+ // Hash new password
63
+ const hashedPassword = await hashPassword(newPassword);
64
+ console.log('Password hashed successfully');
65
+
66
+ // āœ… CORRECT: Using your actual column names
67
+ await pool.query(
68
+ `UPDATE users
69
+ SET password = $1,
70
+ reset_password_token = NULL,
71
+ reset_password_expires = NULL
72
+ WHERE id = $2`,
73
+ [hashedPassword, user.id]
74
+ );
75
+
76
+ console.log('Password updated successfully for user:', user.id);
77
+
78
+ return NextResponse.json({
79
+ message: 'Password reset successful! You can now login with your new password.'
80
+ });
81
+
82
+ } catch (error) {
83
+ console.error('=== RESET PASSWORD ERROR ===');
84
+ console.error('Error:', error.message);
85
+
86
+ return NextResponse.json(
87
+ { error: 'Unable to reset password: ' + error.message },
88
+ { status: 500 }
89
+ );
90
+ }
91
+ }
@@ -0,0 +1,126 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import Link from 'next/link';
5
+ import { ArrowLeftIcon } from '@heroicons/react/24/outline';
6
+ import { useAuth } from '@/context/AuthContext';
7
+
8
+ export default function ForgotPasswordPage() {
9
+ const [email, setEmail] = useState('');
10
+ const [message, setMessage] = useState('');
11
+ const [error, setError] = useState('');
12
+ const [loading, setLoading] = useState(false);
13
+ const { forgotPassword } = useAuth();
14
+
15
+ const handleSubmit = async (e) => {
16
+ e.preventDefault();
17
+ setError('');
18
+ setMessage('');
19
+ setLoading(true);
20
+
21
+ try {
22
+ const result = await forgotPassword(email);
23
+ console.log('API Response:', result);
24
+ setMessage(result.message || 'Password reset instructions sent!');
25
+ setEmail(''); // Clear email after success
26
+ } catch (err) {
27
+ console.error('Error:', err);
28
+ setError(err.message || 'Something went wrong. Please try again.');
29
+ } finally {
30
+ setLoading(false);
31
+ }
32
+ };
33
+
34
+ return (
35
+ <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50 flex items-center justify-center px-6">
36
+ <div className="max-w-md w-full">
37
+ <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl p-8">
38
+ <div className="mb-6">
39
+ <Link href="/login" className="inline-flex items-center text-blue-600 hover:text-blue-700">
40
+ <ArrowLeftIcon className="w-4 h-4 mr-2" />
41
+ Back to Login
42
+ </Link>
43
+ </div>
44
+
45
+ <div className="text-center mb-8">
46
+ <div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-blue-600 to-purple-600 rounded-2xl mb-4 shadow-lg">
47
+ <span className="text-white text-2xl">šŸ”</span>
48
+ </div>
49
+ <h2 className="text-3xl font-bold text-gray-900 mb-2">Forgot Password?</h2>
50
+ <p className="text-gray-700">Enter your email and we'll help you reset your password</p>
51
+ </div>
52
+
53
+ <form onSubmit={handleSubmit} className="space-y-6">
54
+ {error && (
55
+ <div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-lg text-sm">
56
+ {error}
57
+ </div>
58
+ )}
59
+
60
+ {message && (
61
+ <div className="bg-green-50 border border-green-200 text-green-600 px-4 py-3 rounded-lg text-sm">
62
+ {message}
63
+ {message.includes('instructions') && (
64
+ <div className="mt-2 text-xs">
65
+ Check your email or look at the terminal for the reset link.
66
+ </div>
67
+ )}
68
+ </div>
69
+ )}
70
+
71
+ <div>
72
+ <label className="block text-sm font-medium text-gray-900 mb-2">
73
+ Email Address
74
+ </label>
75
+ <input
76
+ type="email"
77
+ value={email}
78
+ onChange={(e) => setEmail(e.target.value)}
79
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all text-gray-700"
80
+ placeholder="email@gmail.com"
81
+ required
82
+ disabled={loading}
83
+ />
84
+ </div>
85
+
86
+ <button
87
+ type="submit"
88
+ disabled={loading}
89
+ className="w-full py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-lg font-medium hover:from-blue-700 hover:to-purple-700 transition-all duration-200 shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
90
+ >
91
+ {loading ? (
92
+ <span className="flex items-center justify-center">
93
+ <svg className="animate-spin h-5 w-5 mr-2" viewBox="0 0 24 24">
94
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"/>
95
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
96
+ </svg>
97
+ Sending...
98
+ </span>
99
+ ) : (
100
+ 'Send Reset Instructions'
101
+ )}
102
+ </button>
103
+ </form>
104
+
105
+ <div className="mt-6 text-center">
106
+ <p className="text-sm text-gray-600">
107
+ Remember your password?{' '}
108
+ <Link href="/login" className="text-blue-600 hover:text-blue-700 font-medium">
109
+ Back to Login
110
+ </Link>
111
+ </p>
112
+ </div>
113
+
114
+ {/* Development helper */}
115
+ {process.env.NODE_ENV === 'development' && (
116
+ <div className="mt-6 p-3 bg-blue-50 border border-blue-200 rounded-lg">
117
+ <p className="text-xs text-blue-800">
118
+ šŸ’” Development Tip: Check your terminal console for the password reset link!
119
+ </p>
120
+ </div>
121
+ )}
122
+ </div>
123
+ </div>
124
+ </div>
125
+ );
126
+ }
@@ -0,0 +1,255 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { useAuth } from '@/context/AuthContext';
5
+ import Link from 'next/link';
6
+ import { useRouter } from 'next/navigation';
7
+ import { EyeIcon, EyeSlashIcon, EnvelopeIcon, LockClosedIcon, ArrowRightIcon, FingerPrintIcon } from '@heroicons/react/24/outline';
8
+ import { CheckCircleIcon } from '@heroicons/react/24/solid';
9
+
10
+ export default function LoginPage() {
11
+ const [email, setEmail] = useState('');
12
+ const [password, setPassword] = useState('');
13
+ const [showPassword, setShowPassword] = useState(false);
14
+ const [rememberMe, setRememberMe] = useState(false);
15
+ const [error, setError] = useState('');
16
+ const [loading, setLoading] = useState(false);
17
+ const { login } = useAuth();
18
+ const router = useRouter();
19
+
20
+ const handleSubmit = async (e) => {
21
+ e.preventDefault();
22
+ setError('');
23
+ setLoading(true);
24
+
25
+ try {
26
+ await login(email, password);
27
+ if (rememberMe) {
28
+ localStorage.setItem('rememberedEmail', email);
29
+ } else {
30
+ localStorage.removeItem('rememberedEmail');
31
+ }
32
+ router.push('/');
33
+ } catch (err) {
34
+ setError(err.message);
35
+ } finally {
36
+ setLoading(false);
37
+ }
38
+ };
39
+
40
+ // Load remembered email on component mount
41
+ useState(() => {
42
+ const rememberedEmail = localStorage.getItem('rememberedEmail');
43
+ if (rememberedEmail) {
44
+ setEmail(rememberedEmail);
45
+ setRememberMe(true);
46
+ }
47
+ }, []);
48
+
49
+ // Demo credentials for testing
50
+ const fillDemoCredentials = () => {
51
+ setEmail('muler@gmail.com');
52
+ setPassword('12345678');
53
+ setError('');
54
+ };
55
+
56
+ return (
57
+ <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-indigo-50 flex items-center justify-center px-4 py-8 relative overflow-hidden">
58
+ {/* Animated Background */}
59
+ <div className="absolute inset-0 overflow-hidden">
60
+ <div className="absolute -top-40 -right-32 w-80 h-80 bg-indigo-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob"></div>
61
+ <div className="absolute -bottom-40 -left-32 w-80 h-80 bg-purple-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob animation-delay-2000"></div>
62
+ <div className="absolute top-40 left-1/2 w-80 h-80 bg-pink-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob animation-delay-4000"></div>
63
+ </div>
64
+
65
+ <div className="max-w-md w-full relative z-10">
66
+ {/* Logo/Brand */}
67
+ <div className="text-center mb-8">
68
+ {/* <div className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-indigo-600 via-purple-600 to-pink-600 rounded-2xl mb-5 shadow-2xl transform hover:scale-105 transition-transform duration-300">
69
+ <span className="text-white text-3xl font-bold">⚔</span>
70
+ </div> */}
71
+ <h2 className="text-4xl font-bold text-gray-900 mb-3 bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
72
+ Welcome Back
73
+ </h2>
74
+ <p className="text-gray-600 text-lg">Sign in to continue your journey</p>
75
+ </div>
76
+
77
+ {/* Login Form */}
78
+ <div className="bg-white/90 backdrop-blur-sm rounded-3xl shadow-2xl p-8 border border-gray-100">
79
+ <form onSubmit={handleSubmit} className="space-y-6">
80
+ {error && (
81
+ <div className="bg-red-50 border-l-4 border-red-500 text-red-700 px-4 py-3 rounded-lg text-sm flex items-start gap-3 animate-shake">
82
+ <div className="text-red-500 text-lg">āš ļø</div>
83
+ <div>{error}</div>
84
+ </div>
85
+ )}
86
+
87
+ {/* Email Field */}
88
+ <div className="group">
89
+ <label className="block text-sm font-semibold text-gray-700 mb-2">
90
+ Email Address
91
+ </label>
92
+ <div className="relative">
93
+ <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
94
+ <EnvelopeIcon className="h-5 w-5 text-gray-400 group-focus-within:text-indigo-500 transition-colors" />
95
+ </div>
96
+ <input
97
+ type="email"
98
+ value={email}
99
+ onChange={(e) => setEmail(e.target.value)}
100
+ className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all text-gray-700 placeholder-gray-400"
101
+ placeholder="you@example.com"
102
+ required
103
+ />
104
+ </div>
105
+ </div>
106
+
107
+ {/* Password Field */}
108
+ <div className="group">
109
+ <label className="block text-sm font-semibold text-gray-700 mb-2">
110
+ Password
111
+ </label>
112
+ <div className="relative">
113
+ <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
114
+ <LockClosedIcon className="h-5 w-5 text-gray-400 group-focus-within:text-indigo-500 transition-colors" />
115
+ </div>
116
+ <input
117
+ type={showPassword ? 'text' : 'password'}
118
+ value={password}
119
+ onChange={(e) => setPassword(e.target.value)}
120
+ className="w-full pl-10 pr-12 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all text-gray-700 placeholder-gray-400"
121
+ placeholder="Enter your password"
122
+ required
123
+ />
124
+ <button
125
+ type="button"
126
+ onClick={() => setShowPassword(!showPassword)}
127
+ className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-indigo-600 transition-colors"
128
+ >
129
+ {showPassword ? <EyeSlashIcon className="w-5 h-5" /> : <EyeIcon className="w-5 h-5" />}
130
+ </button>
131
+ </div>
132
+ </div>
133
+
134
+ {/* Remember Me & Forgot Password */}
135
+ <div className="flex items-center justify-between">
136
+ <label className="flex items-center cursor-pointer group">
137
+ <div className="relative">
138
+ <input
139
+ type="checkbox"
140
+ checked={rememberMe}
141
+ onChange={(e) => setRememberMe(e.target.checked)}
142
+ className="sr-only"
143
+ />
144
+ <div className={`w-5 h-5 border-2 rounded transition-all duration-200 flex items-center justify-center ${
145
+ rememberMe
146
+ ? 'bg-indigo-600 border-indigo-600'
147
+ : 'border-gray-300 bg-white group-hover:border-indigo-400'
148
+ }`}>
149
+ {rememberMe && <CheckCircleIcon className="w-4 h-4 text-white" />}
150
+ </div>
151
+ </div>
152
+ <span className="ml-2 text-sm text-gray-700 group-hover:text-gray-900 transition-colors">
153
+ Remember me
154
+ </span>
155
+ </label>
156
+ <Link
157
+ href="/forgot-password"
158
+ className="text-sm text-indigo-600 hover:text-indigo-700 font-medium hover:underline transition-all"
159
+ >
160
+ Forgot password?
161
+ </Link>
162
+ </div>
163
+
164
+ {/* Login Button */}
165
+ <button
166
+ type="submit"
167
+ disabled={loading}
168
+ className="group relative w-full py-3.5 bg-gradient-to-r from-indigo-600 via-purple-600 to-pink-600 text-white rounded-xl font-semibold hover:from-indigo-700 hover:via-purple-700 hover:to-pink-700 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed overflow-hidden"
169
+ >
170
+ <span className="relative z-10 flex items-center justify-center gap-2">
171
+ {loading ? (
172
+ <>
173
+ <svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
174
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
175
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
176
+ </svg>
177
+ Signing in...
178
+ </>
179
+ ) : (
180
+ <>
181
+ Sign In
182
+ <ArrowRightIcon className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
183
+ </>
184
+ )}
185
+ </span>
186
+ </button>
187
+ </form>
188
+
189
+ {/* Divider */}
190
+ <div className="relative my-6">
191
+ <div className="absolute inset-0 flex items-center">
192
+ <div className="w-full border-t border-gray-200"></div>
193
+ </div>
194
+ <div className="relative flex justify-center text-sm">
195
+ <span className="px-4 bg-white text-gray-500">Or </span>
196
+ </div>
197
+ </div>
198
+
199
+
200
+ {/* Sign Up Link */}
201
+ <div className="text-center">
202
+ <p className="text-sm text-gray-600">
203
+ Don't have an account?{' '}
204
+ <Link
205
+ href="/register"
206
+ className="text-indigo-600 hover:text-indigo-700 font-semibold hover:underline transition-all"
207
+ >
208
+ Create account
209
+ </Link>
210
+ </p>
211
+ </div>
212
+
213
+ {/* Demo Credentials Button */}
214
+ <div className="mt-4">
215
+ <button
216
+ type="button"
217
+ onClick={fillDemoCredentials}
218
+ className="w-full text-center text-xs text-gray-500 hover:text-indigo-600 transition-colors py-2 flex items-center justify-center gap-2"
219
+ >
220
+ <FingerPrintIcon className="w-4 h-4" />
221
+ Use Demo Credentials
222
+ </button>
223
+
224
+ </div>
225
+ </div>
226
+ </div>
227
+
228
+ <style jsx>{`
229
+ @keyframes blob {
230
+ 0% { transform: translate(0px, 0px) scale(1); }
231
+ 33% { transform: translate(30px, -50px) scale(1.1); }
232
+ 66% { transform: translate(-20px, 20px) scale(0.9); }
233
+ 100% { transform: translate(0px, 0px) scale(1); }
234
+ }
235
+ @keyframes shake {
236
+ 0%, 100% { transform: translateX(0); }
237
+ 25% { transform: translateX(-5px); }
238
+ 75% { transform: translateX(5px); }
239
+ }
240
+ .animate-blob {
241
+ animation: blob 7s infinite;
242
+ }
243
+ .animate-shake {
244
+ animation: shake 0.3s ease-in-out;
245
+ }
246
+ .animation-delay-2000 {
247
+ animation-delay: 2s;
248
+ }
249
+ .animation-delay-4000 {
250
+ animation-delay: 4s;
251
+ }
252
+ `}</style>
253
+ </div>
254
+ );
255
+ }