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,42 @@
1
+ import bcrypt from 'bcryptjs';
2
+ import jwt from 'jsonwebtoken';
3
+ import pool from './db';
4
+
5
+ export const hashPassword = async (password) => {
6
+ return await bcrypt.hash(password, 10);
7
+ };
8
+
9
+ export const comparePassword = async (password, hashedPassword) => {
10
+ return await bcrypt.compare(password, hashedPassword);
11
+ };
12
+
13
+ export const generateToken = (userId, email) => {
14
+ return jwt.sign(
15
+ { userId, email },
16
+ process.env.JWT_SECRET,
17
+ { expiresIn: '7d' }
18
+ );
19
+ };
20
+
21
+ export const verifyToken = (token) => {
22
+ try {
23
+ return jwt.verify(token, process.env.JWT_SECRET);
24
+ } catch (error) {
25
+ return null;
26
+ }
27
+ };
28
+
29
+ export const generateResetToken = () => {
30
+ return Math.random().toString(36).substring(2, 15) +
31
+ Math.random().toString(36).substring(2, 15);
32
+ };
33
+
34
+ // For BIGINT timestamps (Unix timestamp in milliseconds)
35
+ export const getExpiryTimestamp = (hours = 1) => {
36
+ return Date.now() + (hours * 60 * 60 * 1000);
37
+ };
38
+
39
+ // Check if token is expired (for BIGINT)
40
+ export const isTokenExpired = (expiryTimestamp) => {
41
+ return Date.now() > expiryTimestamp;
42
+ };
package/src/lib/db.js ADDED
@@ -0,0 +1,7 @@
1
+ import { Pool } from 'pg';
2
+
3
+ const pool = new Pool({
4
+ connectionString: process.env.DATABASE_URL,
5
+ });
6
+
7
+ export default pool;
@@ -0,0 +1,106 @@
1
+ import nodemailer from 'nodemailer';
2
+
3
+ // Create transporter with better configuration
4
+ const createTransporter = () => {
5
+ // For development, use ethereal.email for testing
6
+ if (process.env.NODE_ENV === 'development' && !process.env.EMAIL_USER) {
7
+ console.log('⚠️ Using ethereal.email for testing (no real emails will be sent)');
8
+ return nodemailer.createTransport({
9
+ host: 'smtp.ethereal.email',
10
+ port: 587,
11
+ secure: false,
12
+ auth: {
13
+ user: 'test@ethereal.email', // You can create a test account at ethereal.email
14
+ pass: 'testpassword'
15
+ }
16
+ });
17
+ }
18
+
19
+ // Production configuration
20
+ return nodemailer.createTransport({
21
+ service: 'gmail',
22
+ auth: {
23
+ user: process.env.EMAIL_USER,
24
+ pass: process.env.EMAIL_PASS,
25
+ },
26
+ // For other email services
27
+ // host: process.env.EMAIL_HOST,
28
+ // port: process.env.EMAIL_PORT,
29
+ // secure: true,
30
+ });
31
+ };
32
+
33
+ const transporter = createTransporter();
34
+
35
+ export const sendResetPasswordEmail = async (email, resetToken) => {
36
+ const resetUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/reset-password?token=${resetToken}`;
37
+
38
+ const mailOptions = {
39
+ from: process.env.EMAIL_USER || 'noreply@yourapp.com',
40
+ to: email,
41
+ subject: 'Password Reset Request',
42
+ html: `
43
+ <!DOCTYPE html>
44
+ <html>
45
+ <head>
46
+ <meta charset="utf-8">
47
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
48
+ <title>Password Reset</title>
49
+ <style>
50
+ body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
51
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
52
+ .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0; }
53
+ .content { background: #fff; padding: 30px; border-radius: 0 0 10px 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
54
+ .button { display: inline-block; padding: 12px 24px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; text-decoration: none; border-radius: 5px; margin: 20px 0; }
55
+ .footer { text-align: center; margin-top: 20px; font-size: 12px; color: #666; }
56
+ </style>
57
+ </head>
58
+ <body>
59
+ <div class="container">
60
+ <div class="header">
61
+ <h1 style="color: white; margin: 0;">Password Reset Request</h1>
62
+ </div>
63
+ <div class="content">
64
+ <p>Hello,</p>
65
+ <p>We received a request to reset your password. Click the button below to create a new password:</p>
66
+ <div style="text-align: center;">
67
+ <a href="${resetUrl}" class="button">Reset Password</a>
68
+ </div>
69
+ <p>Or copy and paste this link into your browser:</p>
70
+ <p style="background: #f5f5f5; padding: 10px; border-radius: 5px; word-break: break-all;">${resetUrl}</p>
71
+ <p>This link will expire in 1 hour for security reasons.</p>
72
+ <p>If you didn't request this, please ignore this email.</p>
73
+ <hr>
74
+ <p style="font-size: 14px; color: #666;">Best regards,<br>Your App Team</p>
75
+ </div>
76
+ <div class="footer">
77
+ <p>This is an automated message, please do not reply to this email.</p>
78
+ </div>
79
+ </div>
80
+ </body>
81
+ </html>
82
+ `,
83
+ text: `Reset your password by visiting: ${resetUrl}\n\nThis link expires in 1 hour.`,
84
+ };
85
+
86
+ try {
87
+ const info = await transporter.sendMail(mailOptions);
88
+ console.log('Email sent:', info.messageId);
89
+ return info;
90
+ } catch (error) {
91
+ console.error('Error sending email:', error);
92
+ throw new Error('Failed to send reset email');
93
+ }
94
+ };
95
+
96
+ // Test email configuration
97
+ export const testEmailConfig = async () => {
98
+ try {
99
+ await transporter.verify();
100
+ console.log('✅ Email configuration is valid');
101
+ return true;
102
+ } catch (error) {
103
+ console.error('❌ Email configuration error:', error);
104
+ return false;
105
+ }
106
+ };
@@ -0,0 +1,47 @@
1
+ const { Pool } = require('pg');
2
+ require('dotenv').config({ path: '.env.local' });
3
+
4
+ const pool = new Pool({
5
+ connectionString: process.env.DATABASE_URL,
6
+ });
7
+
8
+ const initDB = async () => {
9
+ const client = await pool.connect();
10
+ try {
11
+ // Create users table
12
+ await client.query(`
13
+ CREATE TABLE IF NOT EXISTS users (
14
+ id SERIAL PRIMARY KEY,
15
+ email VARCHAR(255) UNIQUE NOT NULL,
16
+ password VARCHAR(255) NOT NULL,
17
+ name VARCHAR(255),
18
+ is_verified BOOLEAN DEFAULT FALSE,
19
+ verification_token TEXT,
20
+ reset_password_token TEXT,
21
+ reset_password_expires TIMESTAMP,
22
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
23
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
24
+ );
25
+ `);
26
+
27
+ // Create sessions table (optional - for refresh tokens)
28
+ await client.query(`
29
+ CREATE TABLE IF NOT EXISTS sessions (
30
+ id SERIAL PRIMARY KEY,
31
+ user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
32
+ token TEXT UNIQUE NOT NULL,
33
+ expires_at TIMESTAMP NOT NULL,
34
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
35
+ );
36
+ `);
37
+
38
+ console.log('Database initialized successfully');
39
+ } catch (error) {
40
+ console.error('Error initializing database:', error);
41
+ } finally {
42
+ client.release();
43
+ await pool.end();
44
+ }
45
+ };
46
+
47
+ initDB();
@@ -0,0 +1,10 @@
1
+ # Database
2
+ DATABASE_URL="postgresql://username:password@host.neon.tech/dbname?sslmode=require"
3
+
4
+ # JWT
5
+ JWT_SECRET="your-super-secret-jwt-key-change-this"
6
+
7
+ # Email (optional)
8
+ EMAIL_USER="your-email@gmail.com"
9
+ EMAIL_PASS="your-app-password"
10
+ NEXT_PUBLIC_APP_URL="http://localhost:3000"
@@ -0,0 +1,14 @@
1
+ import { AuthProvider } from '@/context/AuthContext';
2
+ import './globals.css';
3
+
4
+ export default function RootLayout({ children }) {
5
+ return (
6
+ <html lang="en">
7
+ <body>
8
+ <AuthProvider>
9
+ {children}
10
+ </AuthProvider>
11
+ </body>
12
+ </html>
13
+ );
14
+ }
@@ -0,0 +1,289 @@
1
+ 'use client';
2
+
3
+ import { useAuth } from '@/context/AuthContext';
4
+ import Link from 'next/link';
5
+ import { useState, useEffect } from 'react';
6
+ import { ArrowRightIcon, ShieldCheckIcon, BoltIcon, ArrowsRightLeftIcon, UserGroupIcon, KeyIcon, EnvelopeIcon } from '@heroicons/react/24/outline';
7
+
8
+ export default function Home() {
9
+ const { user, logout, isAuthenticated } = useAuth();
10
+ const [mounted, setMounted] = useState(false);
11
+
12
+ useEffect(() => {
13
+ setMounted(true);
14
+ }, []);
15
+
16
+ if (!mounted) return null;
17
+
18
+ return (
19
+ <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-indigo-50">
20
+ {/* Animated Background */}
21
+ <div className="fixed inset-0 -z-10 overflow-hidden">
22
+ <div className="absolute -top-40 -right-32 w-80 h-80 bg-purple-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob"></div>
23
+ <div className="absolute -bottom-40 -left-32 w-80 h-80 bg-blue-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob animation-delay-2000"></div>
24
+ <div className="absolute top-40 left-1/2 w-80 h-80 bg-indigo-300 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob animation-delay-4000"></div>
25
+ </div>
26
+
27
+ {/* Navigation */}
28
+ <nav className="bg-white/90 backdrop-blur-md border-b border-gray-100 shadow-sm sticky top-0 z-50">
29
+ <div className="container mx-auto px-6 py-4">
30
+ <div className="flex justify-between items-center">
31
+ <Link href="/" className="flex items-center space-x-3 group">
32
+ <div className="w-10 h-10 bg-gradient-to-br from-indigo-600 to-purple-600 rounded-xl flex items-center justify-center shadow-lg transform transition-transform group-hover:scale-105">
33
+ <span className="text-white font-bold text-xl">⚡</span>
34
+ </div>
35
+ <div>
36
+ <span className="text-2xl font-bold bg-gradient-to-r from-indigo-600 via-purple-600 to-pink-600 bg-clip-text text-transparent">
37
+ AuthMaster
38
+ </span>
39
+ <span className="text-xs text-gray-500 block -mt-1">Secure Authentication</span>
40
+ </div>
41
+ </Link>
42
+
43
+ <div>
44
+ {isAuthenticated ? (
45
+ <div className="flex items-center gap-4 md:gap-6">
46
+ <div className="flex items-center gap-3 bg-gradient-to-r from-indigo-50 to-purple-50 px-4 py-2 rounded-full">
47
+ <div className="relative">
48
+ <div className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-full flex items-center justify-center text-white font-semibold shadow-md">
49
+ {user?.name ? user.name[0].toUpperCase() : user?.email[0].toUpperCase()}
50
+ </div>
51
+ <div className="absolute -bottom-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-white"></div>
52
+ </div>
53
+ <div className="hidden md:block">
54
+ <p className="text-xs text-gray-500">Welcome back,</p>
55
+ <p className="text-sm font-semibold text-gray-800">{user?.name || user?.email?.split('@')[0]}</p>
56
+ </div>
57
+ </div>
58
+ <button
59
+ onClick={logout}
60
+ className="px-5 py-2.5 bg-gradient-to-r from-red-500 to-rose-600 text-white rounded-xl hover:from-red-600 hover:to-rose-700 transition-all duration-200 shadow-md hover:shadow-lg font-medium flex items-center gap-2"
61
+ >
62
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
63
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
64
+ </svg>
65
+ Logout
66
+ </button>
67
+ </div>
68
+ ) : (
69
+ <div className="flex gap-3">
70
+ <Link
71
+ href="/login"
72
+ className="px-5 py-2.5 text-indigo-600 font-semibold hover:text-indigo-700 transition-colors rounded-xl hover:bg-indigo-50"
73
+ >
74
+ Sign In
75
+ </Link>
76
+ <Link
77
+ href="/register"
78
+ className="px-6 py-2.5 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-xl hover:from-indigo-700 hover:to-purple-700 transition-all duration-200 shadow-md hover:shadow-lg font-semibold"
79
+ >
80
+ Get Started
81
+ <ArrowRightIcon className="w-4 h-4 inline ml-2" />
82
+ </Link>
83
+ </div>
84
+ )}
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </nav>
89
+
90
+ {/* Hero Section */}
91
+ <div className="relative">
92
+ <div className="container mx-auto px-6 py-16 md:py-24">
93
+ <div className="text-center max-w-5xl mx-auto">
94
+ <div className="inline-flex items-center gap-2 px-4 py-2 bg-indigo-50 border border-indigo-100 rounded-full mb-8">
95
+ <ShieldCheckIcon className="w-4 h-4 text-indigo-600" />
96
+ <span className="text-sm font-medium text-indigo-700">Enterprise Grade Security</span>
97
+ </div>
98
+
99
+ <h1 className="text-5xl md:text-7xl font-extrabold mb-6 leading-tight">
100
+ <span className="bg-gradient-to-r from-indigo-600 via-purple-600 to-pink-600 bg-clip-text text-transparent">
101
+ Authentication
102
+ </span>
103
+ <br />
104
+ <span className="bg-gradient-to-r from-slate-700 to-slate-900 bg-clip-text text-transparent">
105
+ Made Simple & Secure
106
+ </span>
107
+ </h1>
108
+
109
+ <p className="text-xl text-gray-600 mb-8 max-w-2xl mx-auto leading-relaxed">
110
+ The complete authentication solution for Next.js applications. Fast, secure, and ready to use in any project.
111
+ </p>
112
+
113
+ {!isAuthenticated && (
114
+ <div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
115
+ <Link
116
+ href="/register"
117
+ className="group px-8 py-4 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-xl hover:from-indigo-700 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 font-semibold text-lg flex items-center gap-2"
118
+ >
119
+ Start Building
120
+ <ArrowRightIcon className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
121
+ </Link>
122
+ <Link
123
+ href="/login"
124
+ className="px-8 py-4 border-2 border-indigo-600 text-indigo-600 rounded-xl hover:bg-indigo-50 transition-all duration-200 font-semibold text-lg"
125
+ >
126
+ Existing Account?
127
+ </Link>
128
+ </div>
129
+ )}
130
+
131
+ {/* Stats */}
132
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-8 mt-16 pt-8 border-t border-gray-200">
133
+ <div>
134
+ <div className="text-3xl font-bold text-indigo-600">5+</div>
135
+ <div className="text-sm text-gray-500 mt-1">Auth Features</div>
136
+ </div>
137
+ <div>
138
+ <div className="text-3xl font-bold text-purple-600">100%</div>
139
+ <div className="text-sm text-gray-500 mt-1">Reusable</div>
140
+ </div>
141
+ <div>
142
+ <div className="text-3xl font-bold text-pink-600">JWT</div>
143
+ <div className="text-sm text-gray-500 mt-1">Token Based</div>
144
+ </div>
145
+ <div>
146
+ <div className="text-3xl font-bold text-indigo-600">SSL</div>
147
+ <div className="text-sm text-gray-500 mt-1">Encrypted</div>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+
154
+ {/* Features Grid */}
155
+ {isAuthenticated ? (
156
+ // Dashboard View for Authenticated Users
157
+ <div className="container mx-auto px-6 py-16">
158
+ <div className="text-center mb-12">
159
+ <h2 className="text-4xl font-bold text-gray-900 mb-4">Welcome to Your Dashboard</h2>
160
+ <p className="text-xl text-gray-600">Manage your account and explore features</p>
161
+ </div>
162
+
163
+ <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
164
+ <div className="group bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 p-8 hover:-translate-y-1">
165
+ <div className="w-14 h-14 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-xl flex items-center justify-center mb-6 shadow-lg group-hover:scale-110 transition-transform">
166
+ <ShieldCheckIcon className="w-7 h-7 text-white" />
167
+ </div>
168
+ <h3 className="text-xl font-bold text-gray-900 mb-3">Protected Route</h3>
169
+ <p className="text-gray-600 leading-relaxed">Your session is securely managed with JWT tokens and automatic refresh.</p>
170
+ </div>
171
+
172
+ <div className="group bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 p-8 hover:-translate-y-1">
173
+ <div className="w-14 h-14 bg-gradient-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center mb-6 shadow-lg group-hover:scale-110 transition-transform">
174
+ <KeyIcon className="w-7 h-7 text-white" />
175
+ </div>
176
+ <h3 className="text-xl font-bold text-gray-900 mb-3">Password Reset</h3>
177
+ <p className="text-gray-600 leading-relaxed">Secure password recovery with email verification and time-limited tokens.</p>
178
+ </div>
179
+
180
+ <div className="group bg-white rounded-2xl shadow-lg hover:shadow-2xl transition-all duration-300 p-8 hover:-translate-y-1">
181
+ <div className="w-14 h-14 bg-gradient-to-br from-pink-500 to-pink-600 rounded-xl flex items-center justify-center mb-6 shadow-lg group-hover:scale-110 transition-transform">
182
+ <EnvelopeIcon className="w-7 h-7 text-white" />
183
+ </div>
184
+ <h3 className="text-xl font-bold text-gray-900 mb-3">Email Notifications</h3>
185
+ <p className="text-gray-600 leading-relaxed">Receive important account notifications and security alerts.</p>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ ) : (
190
+ // Features Section for Non-Authenticated Users
191
+ <div className="container mx-auto px-6 py-16">
192
+ <div className="text-center mb-12">
193
+ <h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
194
+ Everything you need for authentication
195
+ </h2>
196
+ <p className="text-xl text-gray-600 max-w-2xl mx-auto">
197
+ Built with modern best practices and ready to integrate
198
+ </p>
199
+ </div>
200
+
201
+ <div className="grid md:grid-cols-3 gap-8">
202
+ <div className="bg-white rounded-2xl shadow-lg p-8 hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
203
+ <div className="w-12 h-12 bg-indigo-100 rounded-xl flex items-center justify-center mb-5">
204
+ <span className="text-2xl">🔒</span>
205
+ </div>
206
+ <h3 className="text-xl font-bold text-gray-900 mb-3">Secure by Default</h3>
207
+ <p className="text-gray-600 leading-relaxed">Passwords hashed with bcrypt, JWT tokens, and secure session management.</p>
208
+ </div>
209
+
210
+ <div className="bg-white rounded-2xl shadow-lg p-8 hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
211
+ <div className="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center mb-5">
212
+ <span className="text-2xl">⚡</span>
213
+ </div>
214
+ <h3 className="text-xl font-bold text-gray-900 mb-3">Lightning Fast</h3>
215
+ <p className="text-gray-600 leading-relaxed">Optimized database queries and minimal API response times.</p>
216
+ </div>
217
+
218
+ <div className="bg-white rounded-2xl shadow-lg p-8 hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
219
+ <div className="w-12 h-12 bg-pink-100 rounded-xl flex items-center justify-center mb-5">
220
+ <span className="text-2xl">🔄</span>
221
+ </div>
222
+ <h3 className="text-xl font-bold text-gray-900 mb-3">Ready to Reuse</h3>
223
+ <p className="text-gray-600 leading-relaxed">Copy the module to any Next.js project and it works immediately.</p>
224
+ </div>
225
+ </div>
226
+ </div>
227
+ )}
228
+
229
+ {/* CTA Section for Non-Authenticated */}
230
+ {!isAuthenticated && (
231
+ <div className="bg-gradient-to-r from-indigo-600 via-purple-600 to-pink-600 py-16 mt-8">
232
+ <div className="container mx-auto px-6 text-center">
233
+ <h2 className="text-3xl md:text-4xl font-bold text-white mb-4">
234
+ Ready to secure your app?
235
+ </h2>
236
+ <p className="text-indigo-100 text-lg mb-8 max-w-2xl mx-auto">
237
+ Join thousands of developers who use AuthMaster for their authentication needs
238
+ </p>
239
+ <Link
240
+ href="/register"
241
+ className="inline-flex items-center gap-2 px-8 py-4 bg-white text-indigo-600 rounded-xl font-semibold hover:shadow-xl transition-all duration-200 transform hover:-translate-y-0.5"
242
+ >
243
+ Get Started Now
244
+ <ArrowRightIcon className="w-5 h-5" />
245
+ </Link>
246
+ </div>
247
+ </div>
248
+ )}
249
+
250
+ {/* Footer */}
251
+ <footer className="bg-white border-t border-gray-100 py-8 mt-8">
252
+ <div className="container mx-auto px-6">
253
+ <div className="flex flex-col md:flex-row justify-between items-center">
254
+ <div className="flex items-center space-x-2 mb-4 md:mb-0">
255
+ <div className="w-8 h-8 bg-gradient-to-r from-indigo-600 to-purple-600 rounded-lg flex items-center justify-center">
256
+ <span className="text-white font-bold text-sm">⚡</span>
257
+ </div>
258
+ <span className="text-gray-700 font-semibold">AuthMaster</span>
259
+ <span className="text-gray-400 text-sm">© 2026</span>
260
+ </div>
261
+ <div className="flex gap-6 text-sm text-gray-500">
262
+ <Link href="/login" className="hover:text-indigo-600 transition-colors">Sign In</Link>
263
+ <Link href="/register" className="hover:text-indigo-600 transition-colors">Sign Up</Link>
264
+ <Link href="/forgot-password" className="hover:text-indigo-600 transition-colors">Forgot Password?</Link>
265
+ </div>
266
+ </div>
267
+ </div>
268
+ </footer>
269
+
270
+ <style jsx>{`
271
+ @keyframes blob {
272
+ 0% { transform: translate(0px, 0px) scale(1); }
273
+ 33% { transform: translate(30px, -50px) scale(1.1); }
274
+ 66% { transform: translate(-20px, 20px) scale(0.9); }
275
+ 100% { transform: translate(0px, 0px) scale(1); }
276
+ }
277
+ .animate-blob {
278
+ animation: blob 7s infinite;
279
+ }
280
+ .animation-delay-2000 {
281
+ animation-delay: 2s;
282
+ }
283
+ .animation-delay-4000 {
284
+ animation-delay: 4s;
285
+ }
286
+ `}</style>
287
+ </div>
288
+ );
289
+ }