@titas_mallick/wedding-site-gen 1.0.9 → 2.0.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.
- package/README.md +70 -184
- package/app/api/email-reminders/route.ts +240 -0
- package/app/couple/page.tsx +4 -4
- package/app/game/page.tsx +298 -0
- package/app/guestbook/page.tsx +270 -152
- package/app/invitation/[slug]/layout.tsx +4 -2
- package/app/invitation/[slug]/page.tsx +303 -84
- package/app/invitation/actions.ts +49 -0
- package/app/invitation/maker/auth.js +1 -1
- package/app/invitation/maker/guestAdder.js +4 -0
- package/app/invitation/maker/guestShower.js +39 -8
- package/app/invitation/maker/layout.tsx +1 -1
- package/app/invitation/maker/page.js +9 -7
- package/app/invitation/maker/rsvpViewer.js +90 -8
- package/app/layout.tsx +40 -14
- package/app/mark-the-dates/page.tsx +8 -2
- package/app/page.tsx +7 -1
- package/app/providers.tsx +1 -1
- package/app/sagun/page.tsx +224 -76
- package/app/song-requests/page.tsx +242 -105
- package/app/sukanya/page.tsx +9 -13
- package/app/titas/page.tsx +8 -24
- package/app/travel-guide/page.tsx +361 -120
- package/app/updates/maker/page.js +2 -2
- package/app/updates/overlay/page.tsx +65 -30
- package/app/updates/page.js +3 -3
- package/cli.mjs +49 -21
- package/components/AdminAuth.tsx +145 -0
- package/components/AdminLinks.tsx +120 -0
- package/components/ConciergeBot.tsx +104 -44
- package/components/CountdownTimer.tsx +37 -15
- package/components/Gallery.tsx +1 -1
- package/components/LiveVideos.tsx +27 -15
- package/components/OurStory.tsx +1 -1
- package/components/SchemaMarkup.tsx +74 -0
- package/components/certificate.jsx +287 -300
- package/components/footer.tsx +2 -0
- package/components/hero.tsx +47 -4
- package/components/icons.tsx +45 -0
- package/components/importantNews.js +168 -168
- package/components/navbar.tsx +113 -18
- package/components/updates.tsx +36 -26
- package/config/firebase-admin.js +14 -17
- package/config/firebase.ts +4 -2
- package/config/site.ts +10 -2
- package/firestore.rules +6 -1
- package/next-sitemap.config.js +21 -0
- package/package.json +4 -3
- package/public/corner1-01.svg +0 -0
- package/public/love-birds.png +0 -0
- package/public/next.svg +0 -0
- package/public/pubqr.png +0 -0
- package/public/pw/sample.jpg +0 -0
- package/public/qr.png +0 -0
- package/public/sample.jpg +0 -0
- package/public/vercel.svg +0 -0
- package/vercel.json +1 -0
- package/.recover +0 -9
- package/next-env.d.ts +0 -6
- package/public/DCV.gif +0 -0
- package/public/DCV2.gif +0 -0
- package/public/DCV3.gif +0 -0
- package/public/Images/1.jpg +0 -0
- package/public/Images/11.jpg +0 -0
- package/public/Images/12.jpg +0 -0
- package/public/Images/13.jpg +0 -0
- package/public/Images/14.jpg +0 -0
- package/public/Images/15.jpg +0 -0
- package/public/Images/16.jpg +0 -0
- package/public/Images/17.jpg +0 -0
- package/public/Images/18.jpg +0 -0
- package/public/Images/19.jpg +0 -0
- package/public/Images/2.jpg +0 -0
- package/public/Images/21.jpg +0 -0
- package/public/Images/22.jpg +0 -0
- package/public/Images/3.jpg +0 -0
- package/public/Images/4.jpg +0 -0
- package/public/Images/5.jpg +0 -0
- package/public/Images/6.jpg +0 -0
- package/public/Images/7.jpg +0 -0
- package/public/Images/8.jpg +0 -0
- package/public/Images/9.jpg +0 -0
- package/public/Images/9b.jpg +0 -0
- package/public/Images/Patipatra.jpeg +0 -0
- package/public/audio (1).mp3 +0 -0
- package/public/audio (2).mp3 +0 -0
- package/public/bride.jpg +0 -0
- package/public/groom.jpg +0 -0
- package/public/invite.png +0 -0
- package/public/pw/001.jpg +0 -0
- package/public/pw/002.jpg +0 -0
- package/public/pw/003.jpg +0 -0
- package/public/pw/004.jpg +0 -0
- package/public/pw/005.jpg +0 -0
- package/public/pw/006.jpg +0 -0
- package/public/pw/007.jpg +0 -0
- package/public/pw/008.jpg +0 -0
- package/public/pw/009.jpg +0 -0
- package/public/pw/010.jpg +0 -0
- package/public/pw/011.jpg +0 -0
- package/public/pw/012.jpg +0 -0
- package/public/pw/013.jpg +0 -0
- package/public/pw/014.jpg +0 -0
- package/public/pw/015.jpg +0 -0
- package/public/pw/016.jpg +0 -0
- package/public/pw/017.jpg +0 -0
- package/public/pw/018.jpg +0 -0
- package/public/pw/019.jpg +0 -0
- package/public/pw/020.jpg +0 -0
- package/public/pw/021.jpg +0 -0
- package/public/pw/022.jpg +0 -0
- package/public/pw/023.jpg +0 -0
- package/public/pw/024.jpg +0 -0
- package/public/pw/025.jpg +0 -0
- package/public/pw/026.jpg +0 -0
- package/public/pw/027.jpg +0 -0
- package/public/pw/028.jpg +0 -0
- package/public/pw/029.jpg +0 -0
- package/public/pw/030.jpg +0 -0
- package/public/pw/031.jpg +0 -0
- package/public/pw/032.jpg +0 -0
- package/tsconfig.tsbuildinfo +0 -1
- /package/public/Images/{20.jpg → sample.jpg} +0 -0
|
@@ -68,10 +68,10 @@ const UpdateMaker = () => {
|
|
|
68
68
|
contents: [{
|
|
69
69
|
parts: [{
|
|
70
70
|
text: `Task: Rewrite the following wedding update message to be more elegant, heartwarming, and professional.
|
|
71
|
-
Context: This is for
|
|
71
|
+
Context: This is for a wedding celebration.
|
|
72
72
|
Constraint 1: Output ONLY the refined message in PLAIN TEXT.
|
|
73
73
|
Constraint 2: Do NOT use any Markdown formatting, bolding (**), or symbols.
|
|
74
|
-
Constraint 3: Do not include any introductory text, titles (like "For
|
|
74
|
+
Constraint 3: Do not include any introductory text, titles (like "For the couple:"), or quotes.
|
|
75
75
|
|
|
76
76
|
Message to rewrite: "${text}"`
|
|
77
77
|
}]
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
limit,
|
|
11
11
|
} from "firebase/firestore";
|
|
12
12
|
import { motion, AnimatePresence } from "framer-motion";
|
|
13
|
+
import Image from "next/image";
|
|
13
14
|
|
|
14
15
|
import firebaseApp from "@/config/firebase";
|
|
15
16
|
import { fontCursive, fontSans, fontMono } from "@/config/fonts";
|
|
@@ -28,7 +29,7 @@ export default function UpdateOverlay() {
|
|
|
28
29
|
const q = query(
|
|
29
30
|
collection(db, "updates"),
|
|
30
31
|
orderBy("createdAt", "desc"),
|
|
31
|
-
limit(1)
|
|
32
|
+
limit(1),
|
|
32
33
|
);
|
|
33
34
|
|
|
34
35
|
const unsubscribe = onSnapshot(q, (snapshot) => {
|
|
@@ -49,45 +50,70 @@ export default function UpdateOverlay() {
|
|
|
49
50
|
return (
|
|
50
51
|
<div className="fixed inset-0 flex flex-col justify-between p-16 pointer-events-none">
|
|
51
52
|
{/* Global CSS for OBS Chroma Keying */}
|
|
52
|
-
<style
|
|
53
|
-
nav,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
max-width: none !important;
|
|
57
|
-
margin: 0 !important;
|
|
58
|
-
background: #00ff00 !important;
|
|
53
|
+
<style>{`
|
|
54
|
+
nav,
|
|
55
|
+
footer {
|
|
56
|
+
display: none !important;
|
|
59
57
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
main {
|
|
59
|
+
padding: 0 !important;
|
|
60
|
+
max-width: none !important;
|
|
61
|
+
margin: 0 !important;
|
|
62
|
+
background: #00ff00 !important;
|
|
63
|
+
}
|
|
64
|
+
body {
|
|
65
|
+
background: #00ff00 !important;
|
|
66
|
+
overflow: hidden;
|
|
63
67
|
}
|
|
64
68
|
`}</style>
|
|
65
69
|
|
|
66
70
|
{/* Top Bar: Clock & Brand */}
|
|
67
71
|
<div className="flex justify-between items-start">
|
|
68
|
-
<motion.div
|
|
69
|
-
initial={{ opacity: 0, x: -50 }}
|
|
72
|
+
<motion.div
|
|
70
73
|
animate={{ opacity: 1, x: 0 }}
|
|
71
74
|
className="bg-[#18181b] border-2 border-wedding-pink-500 p-4 rounded-2xl"
|
|
75
|
+
initial={{ opacity: 0, x: -50 }}
|
|
72
76
|
>
|
|
73
|
-
<p
|
|
74
|
-
{
|
|
77
|
+
<p
|
|
78
|
+
className={`${fontMono.className} text-5xl font-black text-white tabular-nums tracking-tighter`}
|
|
79
|
+
>
|
|
80
|
+
{hasMounted
|
|
81
|
+
? time.toLocaleTimeString("en-IN", {
|
|
82
|
+
hour: "2-digit",
|
|
83
|
+
minute: "2-digit",
|
|
84
|
+
second: "2-digit",
|
|
85
|
+
})
|
|
86
|
+
: "--:--:--"}
|
|
75
87
|
</p>
|
|
76
|
-
<p
|
|
88
|
+
<p
|
|
89
|
+
className={`${fontMono.className} text-wedding-gold-400 text-xs uppercase tracking-[0.3em] mt-1 font-bold`}
|
|
90
|
+
>
|
|
77
91
|
Event Time
|
|
78
92
|
</p>
|
|
79
93
|
</motion.div>
|
|
80
94
|
|
|
81
|
-
<motion.div
|
|
82
|
-
initial={{ opacity: 0, x: 50 }}
|
|
95
|
+
<motion.div
|
|
83
96
|
animate={{ opacity: 1, x: 0 }}
|
|
84
97
|
className="bg-white p-3 rounded-2xl flex items-center gap-4 border-2 border-wedding-pink-500"
|
|
98
|
+
initial={{ opacity: 0, x: 50 }}
|
|
85
99
|
>
|
|
86
100
|
<div className="text-right">
|
|
87
|
-
<p className={`${fontCursive.className} text-2xl text-zinc-900`}>
|
|
88
|
-
|
|
101
|
+
<p className={`${fontCursive.className} text-2xl text-zinc-900`}>
|
|
102
|
+
Follow Live
|
|
103
|
+
</p>
|
|
104
|
+
<p
|
|
105
|
+
className={`${fontSans.className} text-[9px] font-black text-wedding-pink-600 uppercase tracking-widest leading-none`}
|
|
106
|
+
>
|
|
107
|
+
Scan for Updates
|
|
108
|
+
</p>
|
|
89
109
|
</div>
|
|
90
|
-
<
|
|
110
|
+
<Image
|
|
111
|
+
alt="QR Code"
|
|
112
|
+
className="w-16 h-16 rounded-lg"
|
|
113
|
+
height={64}
|
|
114
|
+
src="/pubqr.png"
|
|
115
|
+
width={64}
|
|
116
|
+
/>
|
|
91
117
|
</motion.div>
|
|
92
118
|
</div>
|
|
93
119
|
|
|
@@ -96,41 +122,50 @@ export default function UpdateOverlay() {
|
|
|
96
122
|
{latestUpdate && (
|
|
97
123
|
<motion.div
|
|
98
124
|
key={latestUpdate.id}
|
|
99
|
-
initial={{ opacity: 0, y: 150 }}
|
|
100
125
|
animate={{ opacity: 1, y: 0 }}
|
|
126
|
+
className="w-full flex justify-center"
|
|
101
127
|
exit={{ opacity: 0, y: 100 }}
|
|
128
|
+
initial={{ opacity: 0, y: 150 }}
|
|
102
129
|
transition={{ type: "spring", damping: 25, stiffness: 120 }}
|
|
103
|
-
className="w-full flex justify-center"
|
|
104
130
|
>
|
|
105
131
|
<div className="relative w-full max-w-5xl">
|
|
106
132
|
{/* Solid outline instead of glow */}
|
|
107
133
|
<div className="absolute -inset-1 border border-wedding-pink-500/20 rounded-[30px]" />
|
|
108
|
-
|
|
134
|
+
|
|
109
135
|
<div className="relative bg-[#09090b] border-4 border-wedding-pink-500 rounded-[28px] p-6 md:p-8 flex items-center gap-8">
|
|
110
|
-
|
|
111
136
|
<div className="flex-shrink-0 flex flex-col items-center gap-2">
|
|
112
137
|
<div className="bg-wedding-pink-500 p-4 rounded-full">
|
|
113
138
|
<HeartFilledIcon className="w-8 h-8 text-white" />
|
|
114
139
|
</div>
|
|
115
|
-
<span
|
|
140
|
+
<span
|
|
141
|
+
className={`${fontMono.className} text-[10px] uppercase tracking-[0.4em] text-wedding-gold-400 font-black`}
|
|
142
|
+
>
|
|
116
143
|
LIVE
|
|
117
144
|
</span>
|
|
118
145
|
</div>
|
|
119
146
|
|
|
120
147
|
<div className="flex-1 space-y-2">
|
|
121
|
-
<p
|
|
148
|
+
<p
|
|
149
|
+
className={`${fontCursive.className} text-4xl text-wedding-pink-400`}
|
|
150
|
+
>
|
|
122
151
|
Wedding News
|
|
123
152
|
</p>
|
|
124
|
-
<p
|
|
153
|
+
<p
|
|
154
|
+
className={`${fontSans.className} text-3xl md:text-4xl font-black text-white leading-tight tracking-tight`}
|
|
155
|
+
>
|
|
125
156
|
{latestUpdate.text}
|
|
126
157
|
</p>
|
|
127
158
|
</div>
|
|
128
159
|
|
|
129
160
|
<div className="hidden lg:flex flex-col items-end justify-center border-l-2 border-zinc-800 pl-8 space-y-1">
|
|
130
|
-
<p
|
|
161
|
+
<p
|
|
162
|
+
className={`${fontCursive.className} text-4xl text-wedding-gold-200`}
|
|
163
|
+
>
|
|
131
164
|
Titas & Sukanya
|
|
132
165
|
</p>
|
|
133
|
-
<p
|
|
166
|
+
<p
|
|
167
|
+
className={`${fontMono.className} text-[10px] uppercase tracking-[0.5em] text-zinc-500 font-bold`}
|
|
168
|
+
>
|
|
134
169
|
Jan 2026
|
|
135
170
|
</p>
|
|
136
171
|
</div>
|
package/app/updates/page.js
CHANGED
|
@@ -158,13 +158,13 @@ const UpdateFeed = () => {
|
|
|
158
158
|
color={index === 0 ? "warning" : "danger"}
|
|
159
159
|
radius="full"
|
|
160
160
|
size="md"
|
|
161
|
-
src={update.email ===
|
|
162
|
-
name={update.email ===
|
|
161
|
+
src={update.email === process.env.NEXT_PUBLIC_ADMIN_EMAIL ? "/groom.jpg" : undefined}
|
|
162
|
+
name={update.email === process.env.NEXT_PUBLIC_ADMIN_EMAIL ? "T" : update.email?.charAt(0).toUpperCase()}
|
|
163
163
|
/>
|
|
164
164
|
<div className="flex flex-col flex-1">
|
|
165
165
|
<div className="flex items-center justify-between">
|
|
166
166
|
<p className="text-md font-bold text-default-800 dark:text-white">
|
|
167
|
-
{update.email ===
|
|
167
|
+
{update.email === process.env.NEXT_PUBLIC_ADMIN_EMAIL ? "Titas Mallick" : update.email}
|
|
168
168
|
</p>
|
|
169
169
|
{index === 0 && (
|
|
170
170
|
<Chip
|
package/cli.mjs
CHANGED
|
@@ -17,7 +17,7 @@ const rl = readline.createInterface({
|
|
|
17
17
|
const question = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
18
18
|
|
|
19
19
|
async function main() {
|
|
20
|
-
console.log("Welcome to the Wedding Website Generator! 💍");
|
|
20
|
+
console.log("Welcome to the Wedding Website Generator v2.0! 💍");
|
|
21
21
|
console.log("This tool will help you scaffold a personalized wedding website.\n");
|
|
22
22
|
|
|
23
23
|
const config = {};
|
|
@@ -86,21 +86,23 @@ async function main() {
|
|
|
86
86
|
];
|
|
87
87
|
|
|
88
88
|
const replacements = [
|
|
89
|
-
{ search: /
|
|
90
|
-
{ search: /
|
|
91
|
-
{ search: /
|
|
92
|
-
{ search: /
|
|
89
|
+
{ search: /Groom & Bride/g, replace: `${config.groomName} & ${config.brideName}` },
|
|
90
|
+
{ search: /Groom and Bride/g, replace: `${config.groomName} and ${config.brideName}` },
|
|
91
|
+
{ search: /Groom Name/g, replace: config.groomFullName },
|
|
92
|
+
{ search: /Bride Name/g, replace: config.brideFullName },
|
|
93
|
+
{ search: /the couple/g, replace: `${config.groomName} and ${config.brideName}` },
|
|
94
|
+
{ search: /the groom/g, replace: config.groomName },
|
|
93
95
|
{ search: /#TitasWedsSukanya/g, replace: config.hashtag },
|
|
94
|
-
{ search: /
|
|
96
|
+
{ search: /your-wedding-site.com/g, replace: config.siteUrl.replace(/^https?:\/\//, '') },
|
|
95
97
|
{ search: /January 23, 2026/g, replace: config.weddingDate },
|
|
96
98
|
{ search: /2026-01-23/g, replace: config.weddingDateISO },
|
|
97
|
-
{ search: /
|
|
98
|
-
{ search: /
|
|
99
|
-
//
|
|
100
|
-
{ search:
|
|
101
|
-
{ search:
|
|
102
|
-
{ search:
|
|
103
|
-
{ search:
|
|
99
|
+
{ search: /admin@example.com/g, replace: config.adminEmail },
|
|
100
|
+
{ search: /your-upi-id@upi/g, replace: config.upiId },
|
|
101
|
+
// Code-level replacements for exported components/functions
|
|
102
|
+
{ search: /TitasLayout/g, replace: `${config.groomName}Layout` },
|
|
103
|
+
{ search: /SukanyaLayout/g, replace: `${config.brideName}Layout` },
|
|
104
|
+
{ search: /TitasPage/g, replace: `${config.groomName}Page` },
|
|
105
|
+
{ search: /SukanyaPage/g, replace: `${config.brideName}Page` },
|
|
104
106
|
];
|
|
105
107
|
|
|
106
108
|
async function copyDir(src, dest) {
|
|
@@ -123,7 +125,7 @@ const adminCred = {
|
|
|
123
125
|
type: "service_account",
|
|
124
126
|
project_id: process.env.FIREBASE_ADMIN_PROJECT_ID,
|
|
125
127
|
private_key_id: process.env.FIREBASE_ADMIN_PRIVATE_KEY_ID,
|
|
126
|
-
private_key: process.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(
|
|
128
|
+
private_key: process.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\n/g, '\n'),
|
|
127
129
|
client_email: process.env.FIREBASE_ADMIN_CLIENT_EMAIL,
|
|
128
130
|
client_id: process.env.FIREBASE_ADMIN_CLIENT_ID,
|
|
129
131
|
auth_uri: "https://accounts.google.com/o/oauth2/auth",
|
|
@@ -196,12 +198,21 @@ FIREBASE_ADMIN_CLIENT_EMAIL=
|
|
|
196
198
|
FIREBASE_ADMIN_CLIENT_ID=
|
|
197
199
|
FIREBASE_ADMIN_CLIENT_X509_CERT_URL=
|
|
198
200
|
|
|
201
|
+
# Admin Controls
|
|
202
|
+
NEXT_PUBLIC_ADMIN_EMAIL=${config.adminEmail}
|
|
203
|
+
|
|
199
204
|
# Google Gemini AI
|
|
200
205
|
NEXT_PUBLIC_GEMINI_API_KEY=
|
|
201
206
|
|
|
202
|
-
# Cloudinary Config
|
|
207
|
+
# Cloudinary Config (for guestbook uploads)
|
|
203
208
|
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=
|
|
204
209
|
NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET=wedding
|
|
210
|
+
|
|
211
|
+
# Resend API (for email reminders)
|
|
212
|
+
RESEND_API_KEY=
|
|
213
|
+
|
|
214
|
+
# Cron Security (for reminders endpoint)
|
|
215
|
+
CRON_SECRET=
|
|
205
216
|
`.trim();
|
|
206
217
|
|
|
207
218
|
await fs.writeFile(path.join(absoluteTargetDir, '.env.local'), envContent);
|
|
@@ -209,16 +220,33 @@ NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET=wedding
|
|
|
209
220
|
// Handle folder renaming for titas/sukanya
|
|
210
221
|
const appDir = path.join(absoluteTargetDir, 'app');
|
|
211
222
|
|
|
212
|
-
const
|
|
213
|
-
|
|
223
|
+
const groomFolderName = config.groomName.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
224
|
+
let brideFolderName = config.brideName.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
214
225
|
|
|
215
|
-
|
|
216
|
-
|
|
226
|
+
// Avoid collision if names are identical
|
|
227
|
+
if (groomFolderName === brideFolderName) {
|
|
228
|
+
brideFolderName = brideFolderName + "_partner";
|
|
217
229
|
}
|
|
218
|
-
|
|
219
|
-
|
|
230
|
+
|
|
231
|
+
const groomDir = path.join(appDir, groomFolderName);
|
|
232
|
+
const brideDir = path.join(appDir, brideFolderName);
|
|
233
|
+
|
|
234
|
+
// Helper for robust rename (Windows fix)
|
|
235
|
+
async function safeRename(oldPath, newPath) {
|
|
236
|
+
if (await fs.stat(oldPath).catch(() => null)) {
|
|
237
|
+
// Check if destination exists
|
|
238
|
+
if (await fs.stat(newPath).catch(() => null)) {
|
|
239
|
+
await fs.rm(newPath, { recursive: true, force: true });
|
|
240
|
+
}
|
|
241
|
+
// Small delay to let OS release handles
|
|
242
|
+
await new Promise(r => setTimeout(r, 100));
|
|
243
|
+
await fs.rename(oldPath, newPath);
|
|
244
|
+
}
|
|
220
245
|
}
|
|
221
246
|
|
|
247
|
+
await safeRename(path.join(appDir, 'titas'), groomDir);
|
|
248
|
+
await safeRename(path.join(appDir, 'sukanya'), brideDir);
|
|
249
|
+
|
|
222
250
|
console.log(`\nSuccess! Your wedding website is ready in ${targetDir}.`);
|
|
223
251
|
console.log("\nNext steps:");
|
|
224
252
|
console.log(`1. cd ${targetDir}`);
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Modal,
|
|
6
|
+
ModalContent,
|
|
7
|
+
ModalHeader,
|
|
8
|
+
ModalBody,
|
|
9
|
+
ModalFooter,
|
|
10
|
+
Button,
|
|
11
|
+
Input,
|
|
12
|
+
addToast,
|
|
13
|
+
} from "@heroui/react";
|
|
14
|
+
import { getAuth, signInWithEmailAndPassword, signOut, onAuthStateChanged, User } from "firebase/auth";
|
|
15
|
+
import firebaseApp from "@/config/firebase";
|
|
16
|
+
|
|
17
|
+
export const AdminAuth = () => {
|
|
18
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
19
|
+
const [user, setUser] = useState<User | null>(null);
|
|
20
|
+
const [email, setEmail] = useState("");
|
|
21
|
+
const [password, setPassword] = useState("");
|
|
22
|
+
const [loading, setLoading] = useState(false);
|
|
23
|
+
|
|
24
|
+
const auth = getAuth(firebaseApp());
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
|
|
28
|
+
setUser(currentUser);
|
|
29
|
+
});
|
|
30
|
+
return () => unsubscribe();
|
|
31
|
+
}, [auth]);
|
|
32
|
+
|
|
33
|
+
const handleLogin = async (e: React.FormEvent) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
setLoading(true);
|
|
36
|
+
try {
|
|
37
|
+
await signInWithEmailAndPassword(auth, email, password);
|
|
38
|
+
addToast({
|
|
39
|
+
title: "Welcome Titas",
|
|
40
|
+
description: "Admin access granted.",
|
|
41
|
+
color: "success",
|
|
42
|
+
});
|
|
43
|
+
setIsOpen(false);
|
|
44
|
+
setEmail("");
|
|
45
|
+
setPassword("");
|
|
46
|
+
} catch (error) {
|
|
47
|
+
addToast({
|
|
48
|
+
title: "Access Denied",
|
|
49
|
+
description: "Invalid credentials.",
|
|
50
|
+
color: "danger",
|
|
51
|
+
});
|
|
52
|
+
} finally {
|
|
53
|
+
setLoading(false);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleLogout = async () => {
|
|
58
|
+
try {
|
|
59
|
+
await signOut(auth);
|
|
60
|
+
addToast({
|
|
61
|
+
title: "Signed Out",
|
|
62
|
+
description: "Admin session ended.",
|
|
63
|
+
color: "warning",
|
|
64
|
+
});
|
|
65
|
+
setIsOpen(false);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error("Logout error", error);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<>
|
|
73
|
+
<button
|
|
74
|
+
onClick={() => setIsOpen(true)}
|
|
75
|
+
className="text-[10px] text-default-300 dark:text-default-700 hover:text-wedding-pink-500 transition-colors opacity-30 hover:opacity-100 focus:outline-none"
|
|
76
|
+
aria-label="Admin Access"
|
|
77
|
+
>
|
|
78
|
+
●
|
|
79
|
+
</button>
|
|
80
|
+
|
|
81
|
+
<Modal isOpen={isOpen} onOpenChange={setIsOpen} backdrop="blur" placement="center">
|
|
82
|
+
<ModalContent>
|
|
83
|
+
{(onClose) => (
|
|
84
|
+
<>
|
|
85
|
+
<ModalHeader className="flex flex-col gap-1">
|
|
86
|
+
{user ? "Admin Profile" : "Secure Login"}
|
|
87
|
+
</ModalHeader>
|
|
88
|
+
<ModalBody>
|
|
89
|
+
{user ? (
|
|
90
|
+
<div className="py-4 text-center">
|
|
91
|
+
<p className="text-default-500 mb-2">Logged in as</p>
|
|
92
|
+
<p className="font-bold text-lg text-wedding-pink-600">{user.email}</p>
|
|
93
|
+
</div>
|
|
94
|
+
) : (
|
|
95
|
+
<form onSubmit={handleLogin} className="space-y-4">
|
|
96
|
+
<Input
|
|
97
|
+
label="Admin Email"
|
|
98
|
+
labelPlacement="outside-top"
|
|
99
|
+
placeholder="Enter admin email"
|
|
100
|
+
variant="bordered"
|
|
101
|
+
value={email}
|
|
102
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
103
|
+
isRequired
|
|
104
|
+
classNames={{ input: "outline-none" }}
|
|
105
|
+
/>
|
|
106
|
+
<Input
|
|
107
|
+
label="Password"
|
|
108
|
+
labelPlacement="outside-top"
|
|
109
|
+
placeholder="Enter password"
|
|
110
|
+
type="password"
|
|
111
|
+
variant="bordered"
|
|
112
|
+
value={password}
|
|
113
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
114
|
+
isRequired
|
|
115
|
+
classNames={{ input: "outline-none" }}
|
|
116
|
+
/>
|
|
117
|
+
<Button
|
|
118
|
+
type="submit"
|
|
119
|
+
color="primary"
|
|
120
|
+
className="w-full bg-wedding-pink-500"
|
|
121
|
+
isLoading={loading}
|
|
122
|
+
>
|
|
123
|
+
Login
|
|
124
|
+
</Button>
|
|
125
|
+
</form>
|
|
126
|
+
)}
|
|
127
|
+
</ModalBody>
|
|
128
|
+
<ModalFooter>
|
|
129
|
+
{user ? (
|
|
130
|
+
<Button color="danger" variant="flat" onClick={handleLogout} className="w-full">
|
|
131
|
+
Logout
|
|
132
|
+
</Button>
|
|
133
|
+
) : (
|
|
134
|
+
<Button variant="light" onClick={onClose} className="w-full">
|
|
135
|
+
Cancel
|
|
136
|
+
</Button>
|
|
137
|
+
)}
|
|
138
|
+
</ModalFooter>
|
|
139
|
+
</>
|
|
140
|
+
)}
|
|
141
|
+
</ModalContent>
|
|
142
|
+
</Modal>
|
|
143
|
+
</>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { getAuth, onAuthStateChanged, User } from "firebase/auth";
|
|
5
|
+
import { Button } from "@heroui/button";
|
|
6
|
+
import { Link } from "@heroui/link";
|
|
7
|
+
import { motion } from "framer-motion";
|
|
8
|
+
|
|
9
|
+
import firebaseApp from "@/config/firebase";
|
|
10
|
+
import { title } from "@/components/primitives";
|
|
11
|
+
|
|
12
|
+
const ADMIN_EMAIL = process.env.NEXT_PUBLIC_ADMIN_EMAIL;
|
|
13
|
+
|
|
14
|
+
export const AdminLinks = () => {
|
|
15
|
+
const [user, setUser] = useState<User | null>(null);
|
|
16
|
+
const [loading, setLoading] = useState(true);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const auth = getAuth(firebaseApp());
|
|
20
|
+
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
|
|
21
|
+
setUser(currentUser);
|
|
22
|
+
setLoading(false);
|
|
23
|
+
});
|
|
24
|
+
return () => unsubscribe();
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
if (loading || !user || user.email !== ADMIN_EMAIL) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const adminTools = [
|
|
32
|
+
{
|
|
33
|
+
name: "Invitation Maker",
|
|
34
|
+
description: "Create and manage guest invitations",
|
|
35
|
+
href: "/invitation/maker",
|
|
36
|
+
icon: "✉️",
|
|
37
|
+
color: "gold",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "Updates Maker",
|
|
41
|
+
description: "Post new announcements for guests",
|
|
42
|
+
href: "/updates/maker",
|
|
43
|
+
icon: "📢",
|
|
44
|
+
color: "pink",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "Guestbook",
|
|
48
|
+
description: "Review wishes and messages",
|
|
49
|
+
href: "/guestbook",
|
|
50
|
+
icon: "📖",
|
|
51
|
+
color: "blue",
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<section className="container mx-auto px-4 pt-12">
|
|
57
|
+
<motion.div
|
|
58
|
+
initial={{ opacity: 0, y: -20 }}
|
|
59
|
+
animate={{ opacity: 1, y: 0 }}
|
|
60
|
+
className="relative overflow-hidden rounded-[2.5rem] bg-white/50 dark:bg-zinc-900/50 backdrop-blur-2xl border border-white dark:border-white/10 shadow-2xl"
|
|
61
|
+
>
|
|
62
|
+
{/* Decorative background elements */}
|
|
63
|
+
<div className="absolute top-0 right-0 w-64 h-64 bg-wedding-gold-200/20 rounded-full blur-3xl -mr-20 -mt-20 pointer-events-none" />
|
|
64
|
+
<div className="absolute bottom-0 left-0 w-64 h-64 bg-wedding-pink-200/20 rounded-full blur-3xl -ml-20 -mb-20 pointer-events-none" />
|
|
65
|
+
|
|
66
|
+
<div className="relative z-10 p-8 md:p-12">
|
|
67
|
+
<div className="flex flex-col md:flex-row md:items-end justify-between mb-10 gap-4">
|
|
68
|
+
<div>
|
|
69
|
+
<div className="flex items-center gap-3 mb-2">
|
|
70
|
+
<span className="flex h-3 w-3 rounded-full bg-green-500 animate-pulse" />
|
|
71
|
+
<span className="text-xs font-black uppercase tracking-[0.3em] text-default-400">
|
|
72
|
+
Secure Admin Access
|
|
73
|
+
</span>
|
|
74
|
+
</div>
|
|
75
|
+
<h2 className={title({ size: "sm", color: "foreground" })}>
|
|
76
|
+
Control Center
|
|
77
|
+
</h2>
|
|
78
|
+
<p className="text-default-500 mt-2 font-medium">
|
|
79
|
+
Welcome, <span className="text-wedding-pink-500">{user.email}</span>. What would you like to manage today?
|
|
80
|
+
</p>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="bg-default-100 dark:bg-white/5 px-4 py-2 rounded-2xl border border-default-200 dark:border-white/10">
|
|
83
|
+
<p className="text-[10px] uppercase tracking-wider text-default-400 font-bold mb-1">Last Sync</p>
|
|
84
|
+
<p className="text-sm font-mono">{new Date().toLocaleTimeString()}</p>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
89
|
+
{adminTools.map((tool, index) => (
|
|
90
|
+
<motion.div
|
|
91
|
+
key={tool.name}
|
|
92
|
+
initial={{ opacity: 0, y: 20 }}
|
|
93
|
+
animate={{ opacity: 1, y: 0 }}
|
|
94
|
+
transition={{ delay: index * 0.1 }}
|
|
95
|
+
whileHover={{ y: -5 }}
|
|
96
|
+
>
|
|
97
|
+
<Link href={tool.href} className="w-full block">
|
|
98
|
+
<div className="group p-6 rounded-[2rem] bg-white dark:bg-zinc-800/50 border border-default-200 dark:border-white/5 hover:border-wedding-pink-300 dark:hover:border-wedding-pink-900 transition-all duration-300 shadow-sm hover:shadow-xl">
|
|
99
|
+
<div className="flex items-start justify-between mb-4">
|
|
100
|
+
<div className="text-4xl">{tool.icon}</div>
|
|
101
|
+
<div className="p-2 rounded-full bg-default-50 dark:bg-white/5 group-hover:bg-wedding-pink-500 group-hover:text-white transition-colors">
|
|
102
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
103
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
|
104
|
+
</svg>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
<h4 className="text-xl font-bold text-default-900 mb-1">{tool.name}</h4>
|
|
108
|
+
<p className="text-sm text-default-500 leading-relaxed">
|
|
109
|
+
{tool.description}
|
|
110
|
+
</p>
|
|
111
|
+
</div>
|
|
112
|
+
</Link>
|
|
113
|
+
</motion.div>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</motion.div>
|
|
118
|
+
</section>
|
|
119
|
+
);
|
|
120
|
+
};
|