@titas_mallick/wedding-site-gen 1.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.
Files changed (141) hide show
  1. package/.eslintignore +20 -0
  2. package/.eslintrc.json +93 -0
  3. package/.recover +9 -0
  4. package/.vscode/settings.json +3 -0
  5. package/LICENSE +21 -0
  6. package/README.md +83 -0
  7. package/app/Neo-Lucentism/layout.tsx +7 -0
  8. package/app/Neo-Lucentism/page.tsx +259 -0
  9. package/app/couple/layout.tsx +7 -0
  10. package/app/couple/page.tsx +164 -0
  11. package/app/error.tsx +31 -0
  12. package/app/guestbook/page.tsx +470 -0
  13. package/app/invitation/[slug]/layout.tsx +36 -0
  14. package/app/invitation/[slug]/page.tsx +462 -0
  15. package/app/invitation/maker/auth.js +165 -0
  16. package/app/invitation/maker/dashboard.js +81 -0
  17. package/app/invitation/maker/guestAdder.js +204 -0
  18. package/app/invitation/maker/guestShower.js +287 -0
  19. package/app/invitation/maker/layout.tsx +11 -0
  20. package/app/invitation/maker/page.js +168 -0
  21. package/app/invitation/maker/rsvpViewer.js +122 -0
  22. package/app/layout.tsx +98 -0
  23. package/app/mark-the-dates/layout.tsx +7 -0
  24. package/app/mark-the-dates/page.tsx +196 -0
  25. package/app/memories/layout.tsx +7 -0
  26. package/app/memories/page.tsx +29 -0
  27. package/app/page.tsx +5 -0
  28. package/app/providers.tsx +33 -0
  29. package/app/sagun/layout.tsx +7 -0
  30. package/app/sagun/page.tsx +348 -0
  31. package/app/song-requests/page.tsx +354 -0
  32. package/app/sukanya/layout.tsx +7 -0
  33. package/app/sukanya/page.tsx +167 -0
  34. package/app/titas/layout.tsx +7 -0
  35. package/app/titas/page.tsx +175 -0
  36. package/app/travel-guide/page.tsx +400 -0
  37. package/app/updates/maker/page.js +323 -0
  38. package/app/updates/overlay/page.tsx +144 -0
  39. package/app/updates/page.js +207 -0
  40. package/cli.mjs +196 -0
  41. package/components/ConciergeBot.tsx +203 -0
  42. package/components/CountdownTimer.tsx +137 -0
  43. package/components/Gallery.tsx +372 -0
  44. package/components/LiveVideos.tsx +173 -0
  45. package/components/OurStory.tsx +160 -0
  46. package/components/certificate.jsx +300 -0
  47. package/components/counter.tsx +14 -0
  48. package/components/footer.tsx +89 -0
  49. package/components/hero.tsx +136 -0
  50. package/components/icons.tsx +283 -0
  51. package/components/importantNews.js +168 -0
  52. package/components/navbar.tsx +106 -0
  53. package/components/primitives.ts +53 -0
  54. package/components/sagun.js +22 -0
  55. package/components/theme-switch.tsx +81 -0
  56. package/components/updates.tsx +118 -0
  57. package/components/weddingcard.js +68 -0
  58. package/components/weddingcard2.js +58 -0
  59. package/config/firebase-admin.js +17 -0
  60. package/config/firebase.ts +36 -0
  61. package/config/fonts.ts +21 -0
  62. package/config/site.ts +74 -0
  63. package/next-env.d.ts +6 -0
  64. package/next.config.js +4 -0
  65. package/package.json +64 -0
  66. package/postcss.config.js +6 -0
  67. package/public/DCV.gif +0 -0
  68. package/public/DCV2.gif +0 -0
  69. package/public/DCV3.gif +0 -0
  70. package/public/Images/1.jpg +0 -0
  71. package/public/Images/11.jpg +0 -0
  72. package/public/Images/12.jpg +0 -0
  73. package/public/Images/13.jpg +0 -0
  74. package/public/Images/14.jpg +0 -0
  75. package/public/Images/15.jpg +0 -0
  76. package/public/Images/16.jpg +0 -0
  77. package/public/Images/17.jpg +0 -0
  78. package/public/Images/18.jpg +0 -0
  79. package/public/Images/19.jpg +0 -0
  80. package/public/Images/2.jpg +0 -0
  81. package/public/Images/20.jpg +0 -0
  82. package/public/Images/21.jpg +0 -0
  83. package/public/Images/22.jpg +0 -0
  84. package/public/Images/3.jpg +0 -0
  85. package/public/Images/4.jpg +0 -0
  86. package/public/Images/5.jpg +0 -0
  87. package/public/Images/6.jpg +0 -0
  88. package/public/Images/7.jpg +0 -0
  89. package/public/Images/8.jpg +0 -0
  90. package/public/Images/9.jpg +0 -0
  91. package/public/Images/9b.jpg +0 -0
  92. package/public/Images/Patipatra.jpeg +0 -0
  93. package/public/audio (1).mp3 +0 -0
  94. package/public/audio (2).mp3 +0 -0
  95. package/public/bride.jpg +0 -0
  96. package/public/corner1-01.svg +1 -0
  97. package/public/favicon.ico +0 -0
  98. package/public/groom.jpg +0 -0
  99. package/public/invite.png +0 -0
  100. package/public/love-birds.png +0 -0
  101. package/public/next.svg +1 -0
  102. package/public/pubqr.png +0 -0
  103. package/public/pw/001.jpg +0 -0
  104. package/public/pw/002.jpg +0 -0
  105. package/public/pw/003.jpg +0 -0
  106. package/public/pw/004.jpg +0 -0
  107. package/public/pw/005.jpg +0 -0
  108. package/public/pw/006.jpg +0 -0
  109. package/public/pw/007.jpg +0 -0
  110. package/public/pw/008.jpg +0 -0
  111. package/public/pw/009.jpg +0 -0
  112. package/public/pw/010.jpg +0 -0
  113. package/public/pw/011.jpg +0 -0
  114. package/public/pw/012.jpg +0 -0
  115. package/public/pw/013.jpg +0 -0
  116. package/public/pw/014.jpg +0 -0
  117. package/public/pw/015.jpg +0 -0
  118. package/public/pw/016.jpg +0 -0
  119. package/public/pw/017.jpg +0 -0
  120. package/public/pw/018.jpg +0 -0
  121. package/public/pw/019.jpg +0 -0
  122. package/public/pw/020.jpg +0 -0
  123. package/public/pw/021.jpg +0 -0
  124. package/public/pw/022.jpg +0 -0
  125. package/public/pw/023.jpg +0 -0
  126. package/public/pw/024.jpg +0 -0
  127. package/public/pw/025.jpg +0 -0
  128. package/public/pw/026.jpg +0 -0
  129. package/public/pw/027.jpg +0 -0
  130. package/public/pw/028.jpg +0 -0
  131. package/public/pw/029.jpg +0 -0
  132. package/public/pw/030.jpg +0 -0
  133. package/public/pw/031.jpg +0 -0
  134. package/public/pw/032.jpg +0 -0
  135. package/public/qr.png +0 -0
  136. package/public/vercel.svg +1 -0
  137. package/styles/globals.css +3 -0
  138. package/tailwind.config.js +51 -0
  139. package/tsconfig.json +45 -0
  140. package/tsconfig.tsbuildinfo +1 -0
  141. package/types/index.ts +5 -0
package/cli.mjs ADDED
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import readline from 'readline';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ // Helper for prompting
12
+ const rl = readline.createInterface({
13
+ input: process.stdin,
14
+ output: process.stdout
15
+ });
16
+
17
+ const question = (query) => new Promise((resolve) => rl.question(query, resolve));
18
+
19
+ async function main() {
20
+ console.log("Welcome to the Wedding Website Generator! 💍");
21
+ console.log("This tool will help you scaffold a personalized wedding website.\n");
22
+
23
+ const config = {};
24
+ config.groomName = await question("Groom's First Name (e.g., Titas): ") || "Titas";
25
+ config.groomFullName = await question(`Groom's Full Name (default: ${config.groomName} Mallick): `) || `${config.groomName} Mallick`;
26
+
27
+ config.brideName = await question("Bride's First Name (e.g., Sukanya): ") || "Sukanya";
28
+ config.brideFullName = await question(`Bride's Full Name (default: ${config.brideName} Saha): `) || `${config.brideName} Saha`;
29
+
30
+ config.weddingDate = await question("Wedding Date (e.g., January 23, 2026): ") || "January 23, 2026";
31
+ config.weddingDateISO = await question("Wedding Date ISO (YYYY-MM-DD): ") || "2026-01-23";
32
+
33
+ config.adminEmail = await question("Admin Email (for management): ") || "admin@wedding.com";
34
+ config.upiId = await question("UPI ID for gifts (e.g., name@upi): ") || "gifts@upi";
35
+ config.siteUrl = await question("Website URL (e.g., https://ourwedding.com): ") || "https://ourwedding.com";
36
+ config.hashtag = await question("Wedding Hashtag (e.g., #TitasWedsSukanya): ") || `#${config.groomName}Weds${config.brideName}`;
37
+
38
+ const targetDir = await question("Target directory name: ") || "my-wedding-website";
39
+
40
+ rl.close();
41
+
42
+ console.log(`\nGenerating website in ${targetDir}...`);
43
+
44
+ const sourceDir = __dirname;
45
+ const absoluteTargetDir = path.resolve(process.cwd(), targetDir);
46
+
47
+ await fs.mkdir(absoluteTargetDir, { recursive: true });
48
+
49
+ const ignoreList = [
50
+ '.git',
51
+ '.next',
52
+ 'node_modules',
53
+ 'cli.mjs',
54
+ 'dist',
55
+ '.env.local',
56
+ '.recover',
57
+ 'package-lock.json',
58
+ ];
59
+
60
+ const replacements = [
61
+ { search: /Titas & Sukanya/g, replace: `${config.groomName} & ${config.brideName}` },
62
+ { search: /Titas and Sukanya/g, replace: `${config.groomName} and ${config.brideName}` },
63
+ { search: /Titas Mallick/g, replace: config.groomFullName },
64
+ { search: /Sukanya Saha/g, replace: config.brideFullName },
65
+ { search: /#TitasWedsSukanya/g, replace: config.hashtag },
66
+ { search: /titas-sukanya-for.life/g, replace: config.siteUrl.replace(/^https?:\/\//, '') },
67
+ { search: /January 23, 2026/g, replace: config.weddingDate },
68
+ { search: /2026-01-23/g, replace: config.weddingDateISO },
69
+ { search: /titas@titas.titas/g, replace: config.adminEmail },
70
+ { search: /eugenics@ybl/g, replace: config.upiId },
71
+ // Generic replacements for names
72
+ { search: /\bTitas\b/g, replace: config.groomName },
73
+ { search: /\btitas\b/g, replace: config.groomName.toLowerCase() },
74
+ { search: /\bSukanya\b/g, replace: config.brideName },
75
+ { search: /\bsukanya\b/g, replace: config.brideName.toLowerCase() },
76
+ ];
77
+
78
+ async function copyDir(src, dest) {
79
+ const entries = await fs.readdir(src, { withFileTypes: true });
80
+
81
+ for (const entry of entries) {
82
+ const srcPath = path.join(src, entry.name);
83
+ const destPath = path.join(dest, entry.name);
84
+
85
+ if (ignoreList.includes(entry.name)) continue;
86
+
87
+ if (entry.isDirectory()) {
88
+ await fs.mkdir(destPath, { recursive: true });
89
+ await copyDir(srcPath, destPath);
90
+ } else {
91
+ // Special case for secrets
92
+ if (entry.name === 'firebase-admin.js') {
93
+ const adminTemplate = `
94
+ const adminCred = {
95
+ type: "service_account",
96
+ project_id: process.env.FIREBASE_ADMIN_PROJECT_ID,
97
+ private_key_id: process.env.FIREBASE_ADMIN_PRIVATE_KEY_ID,
98
+ private_key: process.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\\\n/g, '\\n'),
99
+ client_email: process.env.FIREBASE_ADMIN_CLIENT_EMAIL,
100
+ client_id: process.env.FIREBASE_ADMIN_CLIENT_ID,
101
+ auth_uri: "https://accounts.google.com/o/oauth2/auth",
102
+ token_uri: "https://oauth2.googleapis.com/token",
103
+ auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
104
+ client_x509_cert_url: process.env.FIREBASE_ADMIN_CLIENT_X509_CERT_URL,
105
+ universe_domain: "googleapis.com",
106
+ };
107
+ export default adminCred;
108
+ `.trim();
109
+ await fs.writeFile(destPath, adminTemplate, 'utf8');
110
+ continue;
111
+ }
112
+
113
+ // Only process text files for replacements
114
+ const ext = path.extname(srcPath).toLowerCase();
115
+ const textExtensions = ['.ts', '.tsx', '.js', '.jsx', '.json', '.md', '.css', '.html'];
116
+
117
+ if (textExtensions.includes(ext)) {
118
+ let content = await fs.readFile(srcPath, 'utf8');
119
+ for (const r of replacements) {
120
+ content = content.replace(r.search, r.replace);
121
+ }
122
+
123
+ if (entry.name === 'package.json') {
124
+ const pkg = JSON.parse(content);
125
+ pkg.name = targetDir;
126
+ delete pkg.bin; // Remove the CLI bin from the generated project
127
+ content = JSON.stringify(pkg, null, 2);
128
+ }
129
+
130
+ await fs.writeFile(destPath, content, 'utf8');
131
+ } else {
132
+ await fs.copyFile(srcPath, destPath);
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ try {
139
+ await copyDir(sourceDir, absoluteTargetDir);
140
+
141
+ // Create a basic .env.local template
142
+ const envContent = `
143
+ # Firebase Client Config
144
+ NEXT_PUBLIC_APIKEY=
145
+ NEXT_PUBLIC_AUTHDOMAIN=
146
+ NEXT_PUBLIC_PROJECTID=
147
+ NEXT_PUBLIC_STORAGEBUCKET=
148
+ NEXT_PUBLIC_SENDERID=
149
+ NEXT_PUBLIC_APPID=
150
+ NEXT_PUBLIC_MEASUREMENTID=
151
+ NEXT_PUBLIC_RECAPTCHA_SITE_KEY=
152
+
153
+ # Firebase Admin Config (Keep these secret!)
154
+ FIREBASE_ADMIN_PROJECT_ID=
155
+ FIREBASE_ADMIN_PRIVATE_KEY_ID=
156
+ FIREBASE_ADMIN_PRIVATE_KEY=
157
+ FIREBASE_ADMIN_CLIENT_EMAIL=
158
+ FIREBASE_ADMIN_CLIENT_ID=
159
+ FIREBASE_ADMIN_CLIENT_X509_CERT_URL=
160
+
161
+ # Google Gemini AI
162
+ NEXT_PUBLIC_GEMINI_API_KEY=
163
+
164
+ # Cloudinary Config
165
+ NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=
166
+ NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET=wedding
167
+ `.trim();
168
+
169
+ await fs.writeFile(path.join(absoluteTargetDir, '.env.local'), envContent);
170
+
171
+ // Handle folder renaming for titas/sukanya
172
+ const appDir = path.join(absoluteTargetDir, 'app');
173
+
174
+ const groomDir = path.join(appDir, config.groomName.toLowerCase());
175
+ const brideDir = path.join(appDir, config.brideName.toLowerCase());
176
+
177
+ if (await fs.stat(path.join(appDir, 'titas')).catch(() => null)) {
178
+ await fs.rename(path.join(appDir, 'titas'), groomDir);
179
+ }
180
+ if (await fs.stat(path.join(appDir, 'sukanya')).catch(() => null)) {
181
+ await fs.rename(path.join(appDir, 'sukanya'), brideDir);
182
+ }
183
+
184
+ console.log(`\nSuccess! Your wedding website is ready in ${targetDir}.`);
185
+ console.log("\nNext steps:");
186
+ console.log(`1. cd ${targetDir}`);
187
+ console.log("2. npm install");
188
+ console.log("3. Fill in your secrets in .env.local");
189
+ console.log("4. npm run dev");
190
+
191
+ } catch (err) {
192
+ console.error("Error generating website:", err);
193
+ }
194
+ }
195
+
196
+ main();
@@ -0,0 +1,203 @@
1
+ "use client";
2
+
3
+ import React, { useState, useEffect, useRef } from "react";
4
+ import {
5
+ Button,
6
+ Card,
7
+ CardHeader,
8
+ CardBody,
9
+ CardFooter,
10
+ Input,
11
+ Avatar,
12
+ ScrollShadow,
13
+ Badge,
14
+ } from "@heroui/react";
15
+ import { motion, AnimatePresence } from "framer-motion";
16
+ import { HeartFilledIcon } from "./icons";
17
+ import { fontCursive, fontMono } from "@/config/fonts";
18
+
19
+ interface Message {
20
+ role: "user" | "bot";
21
+ text: string;
22
+ }
23
+
24
+ const ConciergeBot = () => {
25
+ const [isOpen, setIsOpen] = useState(false);
26
+ const [input, setInput] = useState("");
27
+ const [messages, setMessages] = useState<Message[]>([
28
+ {
29
+ role: "bot",
30
+ text: "Namaste! I am your Wedding Concierge. How can I help you today?",
31
+ },
32
+ ]);
33
+ const [isTyping, setIsTyping] = useState(false);
34
+ const scrollRef = useRef<HTMLDivElement>(null);
35
+
36
+ useEffect(() => {
37
+ if (scrollRef.current) {
38
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
39
+ }
40
+ }, [messages, isTyping]);
41
+
42
+ const handleSend = async () => {
43
+ if (!input.trim()) return;
44
+
45
+ const userMsg = input.trim();
46
+ setMessages((prev) => [...prev, { role: "user", text: userMsg }]);
47
+ setInput("");
48
+ setIsTyping(true);
49
+
50
+ try {
51
+ const apiKey = process.env.NEXT_PUBLIC_GEMINI_API_KEY;
52
+ if (!apiKey) throw new Error("API Key missing");
53
+
54
+ const systemInstruction = `
55
+ You are an elegant and helpful Wedding Concierge for Titas and Sukanya's wedding.
56
+ Your tone is warm, respectful, and slightly formal (Indian hospitality style).
57
+
58
+ Key Info:
59
+ - Couple: Titas Mallick & Sukanya (married after 10 years of love).
60
+ - Engagement: 23rd Nov 2025 at Mohan Jyoti Banquet Hall, Serampore (10:00 AM).
61
+ - Wedding: 23rd Jan 2026 at Anandamayee Bhawan, Serampore (06:00 PM).
62
+ - Reception: 25th Jan 2026 at Friends Union Club, Konnagar (06:00 PM).
63
+ - Story: They were college classmates who became soulmates. They love the mountains.
64
+
65
+ Instructions:
66
+ - Keep answers concise and polite.
67
+ - If you don't know something, suggest they check the specific pages on the site.
68
+ - Use "Namaste" or "Greetings" occasionally.
69
+ - IMPORTANT: Provide responses in PLAIN TEXT ONLY. Do not use Markdown, bolding (**), or any other special formatting symbols.
70
+ `;
71
+
72
+ const response = await fetch(
73
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`,
74
+ {
75
+ method: "POST",
76
+ headers: { "Content-Type": "application/json" },
77
+ body: JSON.stringify({
78
+ contents: [
79
+ { role: "user", parts: [{ text: systemInstruction }] },
80
+ ...messages.map(m => ({
81
+ role: m.role === "user" ? "user" : "model",
82
+ parts: [{ text: m.text }]
83
+ })),
84
+ { role: "user", parts: [{ text: userMsg }] }
85
+ ],
86
+ generationConfig: { maxOutputTokens: 400, temperature: 0.7 }
87
+ })
88
+ }
89
+ );
90
+
91
+ const data = await response.json();
92
+
93
+ if (data.error) {
94
+ console.error("Gemini API Error Detail:", data.error);
95
+ throw new Error(data.error.message);
96
+ }
97
+
98
+ const botResponse = data.candidates?.[0]?.content?.parts?.[0]?.text || "I apologize, I'm having trouble connecting. Please try again soon!";
99
+
100
+ setMessages((prev) => [...prev, { role: "bot", text: botResponse }]);
101
+ } catch (error: any) {
102
+ console.error("Chatbot Error:", error);
103
+ setMessages((prev) => [...prev, { role: "bot", text: `I'm having a small technical issue: ${error.message || "Please try again later."}` }]);
104
+ } finally {
105
+ setIsTyping(false);
106
+ }
107
+ };
108
+
109
+ return (
110
+ <div className="fixed bottom-4 right-4 md:bottom-6 md:right-6 z-[100] flex flex-col items-end max-w-full">
111
+ <AnimatePresence>
112
+ {isOpen && (
113
+ <motion.div
114
+ initial={{ opacity: 0, scale: 0.8, y: 20 }}
115
+ animate={{ opacity: 1, scale: 1, y: 0 }}
116
+ exit={{ opacity: 0, scale: 0.8, y: 20 }}
117
+ className="mb-3 w-[calc(100vw-32px)] sm:w-[350px] md:w-[380px] shadow-2xl overflow-hidden"
118
+ >
119
+ <Card className="border-none bg-white/90 dark:bg-zinc-900/95 backdrop-blur-xl h-[400px] md:h-[500px] max-h-[70vh] flex flex-col">
120
+ <CardHeader className="bg-gradient-to-r from-wedding-pink-500 to-wedding-gold-500 p-4 flex justify-between items-center text-white">
121
+ <div className="flex items-center gap-3">
122
+ <Avatar src="/love-birds.png" className="bg-white/20" size="sm" />
123
+ <div>
124
+ <p className={`${fontCursive.className} text-xl leading-none`}>Wedding Assistant</p>
125
+ <p className="text-[10px] uppercase tracking-widest opacity-80">Online & Ready</p>
126
+ </div>
127
+ </div>
128
+ <Button isIconOnly variant="light" size="sm" onPress={() => setIsOpen(false)} className="text-white min-w-0">
129
+
130
+ </Button>
131
+ </CardHeader>
132
+
133
+ <CardBody className="p-0">
134
+ <ScrollShadow ref={scrollRef} className="h-full p-4 space-y-4">
135
+ {messages.map((msg, i) => (
136
+ <div key={i} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
137
+ <div className={`max-w-[85%] p-3 rounded-2xl text-sm ${
138
+ msg.role === "user"
139
+ ? "bg-wedding-pink-500 text-white rounded-tr-none"
140
+ : "bg-default-100 dark:bg-zinc-800 text-default-800 dark:text-zinc-200 rounded-tl-none"
141
+ }`}>
142
+ {msg.text}
143
+ </div>
144
+ </div>
145
+ ))}
146
+ {isTyping && (
147
+ <div className="flex justify-start">
148
+ <div className="bg-default-100 dark:bg-zinc-800 p-3 rounded-2xl rounded-tl-none">
149
+ <span className="flex gap-1">
150
+ <span className="w-1.5 h-1.5 bg-default-400 rounded-full animate-bounce" />
151
+ <span className="w-1.5 h-1.5 bg-default-400 rounded-full animate-bounce [animation-delay:0.2s]" />
152
+ <span className="w-1.5 h-1.5 bg-default-400 rounded-full animate-bounce [animation-delay:0.4s]" />
153
+ </span>
154
+ </div>
155
+ </div>
156
+ )}
157
+ </ScrollShadow>
158
+ </CardBody>
159
+
160
+ <CardFooter className="p-4 pt-2">
161
+ <form className="flex w-full gap-2" onSubmit={(e) => { e.preventDefault(); handleSend(); }}>
162
+ <Input
163
+ placeholder="Ask me anything..."
164
+ value={input}
165
+ onChange={(e) => setInput(e.target.value)}
166
+ variant="bordered"
167
+ size="sm"
168
+ className="flex-1"
169
+ classNames={{ inputWrapper: "border-wedding-pink-100 focus-within:!border-wedding-pink-500", input: "outline-none" }}
170
+ />
171
+ <Button
172
+ isIconOnly
173
+ color="danger"
174
+ size="sm"
175
+ onPress={handleSend}
176
+ className="bg-wedding-pink-500"
177
+ >
178
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
179
+ <line x1="22" y1="2" x2="11" y2="13"></line>
180
+ <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
181
+ </svg>
182
+ </Button>
183
+ </form>
184
+ </CardFooter>
185
+ </Card>
186
+ </motion.div>
187
+ )}
188
+ </AnimatePresence>
189
+
190
+ <Button
191
+ isIconOnly
192
+ className="w-10 h-10 md:w-14 md:h-14 rounded-full bg-gradient-to-tr from-wedding-pink-500 to-wedding-gold-500 shadow-wedding-pink-500/30 shadow-2xl group transition-transform hover:scale-110 active:scale-95"
193
+ onPress={() => setIsOpen(!isOpen)}
194
+ >
195
+ <Badge content="" color="success" shape="circle" placement="top-right" className="border-2 border-white dark:border-zinc-900 min-w-0 h-2.5 w-2.5">
196
+ <HeartFilledIcon className={`text-white w-4 h-4 md:w-6 md:h-6 transition-transform duration-500 ${isOpen ? "rotate-180" : ""}`} />
197
+ </Badge>
198
+ </Button>
199
+ </div>
200
+ );
201
+ };
202
+
203
+ export default ConciergeBot;
@@ -0,0 +1,137 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { motion, AnimatePresence } from "framer-motion";
5
+
6
+ import { fontMono, fontCursive } from "@/config/fonts";
7
+ import { HeartFilledIcon } from "./icons";
8
+
9
+ interface TimeLeft {
10
+ days: number;
11
+ hours: number;
12
+ minutes: number;
13
+ seconds: number;
14
+ }
15
+
16
+ type TimerPhase = "BEFORE_WEDDING" | "WEDDING_DAY" | "BEFORE_RECEPTION" | "RECEPTION_DAY" | "POST_EVENT";
17
+
18
+ export default function CountdownTimer() {
19
+ const [timeLeft, setTimeLeft] = useState<TimeLeft | null>(null);
20
+ const [phase, setPhase] = useState<TimerPhase>("BEFORE_WEDDING");
21
+
22
+ useEffect(() => {
23
+ const weddingStart = new Date("2026-01-23T18:00:00").getTime();
24
+ const weddingEnd = new Date("2026-01-24T06:00:00").getTime();
25
+ const receptionStart = new Date("2026-01-25T18:00:00").getTime();
26
+ const receptionEnd = new Date("2026-01-26T06:00:00").getTime();
27
+
28
+ const calculateTimeLeft = () => {
29
+ const now = new Date().getTime();
30
+
31
+ let currentPhase: TimerPhase = "BEFORE_WEDDING";
32
+ let targetDate = weddingStart;
33
+
34
+ if (now < weddingStart) {
35
+ currentPhase = "BEFORE_WEDDING";
36
+ targetDate = weddingStart;
37
+ } else if (now >= weddingStart && now < weddingEnd) {
38
+ currentPhase = "WEDDING_DAY";
39
+ } else if (now >= weddingEnd && now < receptionStart) {
40
+ currentPhase = "BEFORE_RECEPTION";
41
+ targetDate = receptionStart;
42
+ } else if (now >= receptionStart && now < receptionEnd) {
43
+ currentPhase = "RECEPTION_DAY";
44
+ } else {
45
+ currentPhase = "POST_EVENT";
46
+ }
47
+
48
+ setPhase(currentPhase);
49
+
50
+ if (currentPhase === "BEFORE_WEDDING" || currentPhase === "BEFORE_RECEPTION") {
51
+ const difference = targetDate - now;
52
+
53
+ setTimeLeft({
54
+ days: Math.floor(difference / (1000 * 60 * 60 * 24)),
55
+ hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
56
+ minutes: Math.floor((difference / 1000 / 60) % 60),
57
+ seconds: Math.floor((difference / 1000) % 60),
58
+ });
59
+ } else {
60
+ setTimeLeft(null);
61
+ }
62
+ };
63
+
64
+ calculateTimeLeft();
65
+ const timer = setInterval(calculateTimeLeft, 1000);
66
+
67
+ return () => clearInterval(timer);
68
+ }, []);
69
+
70
+ const TimeUnit = ({ value, label }: { value: number; label: string }) => (
71
+ <div className="flex flex-col items-center">
72
+ <div className="relative h-[48px] md:h-[72px] overflow-hidden flex items-center justify-center min-w-[60px] md:min-w-[80px]">
73
+ <AnimatePresence mode="popLayout">
74
+ <motion.span
75
+ key={value}
76
+ animate={{ y: 0, opacity: 1 }}
77
+ className={`${fontMono.className} text-4xl md:text-6xl font-black text-default-800 dark:text-white tabular-nums`}
78
+ exit={{ y: -20, opacity: 0 }}
79
+ initial={{ y: 20, opacity: 0 }}
80
+ transition={{ duration: 0.4, ease: "easeOut" }}
81
+ >
82
+ {String(value).padStart(2, "0")}
83
+ </motion.span>
84
+ </AnimatePresence>
85
+ </div>
86
+ <span className="text-[10px] md:text-xs uppercase tracking-[0.2em] text-wedding-pink-500 font-bold mt-1">
87
+ {label}
88
+ </span>
89
+ </div>
90
+ );
91
+
92
+ return (
93
+ <div className="flex flex-col items-center gap-6 py-8">
94
+ <AnimatePresence mode="wait">
95
+ {(phase === "BEFORE_WEDDING" || phase === "BEFORE_RECEPTION") && timeLeft ? (
96
+ <motion.div
97
+ key="countdown"
98
+ animate={{ opacity: 1, scale: 1 }}
99
+ className="flex items-center gap-4 md:gap-8 bg-white/30 dark:bg-black/20 backdrop-blur-md px-8 py-6 rounded-[40px] border border-wedding-gold-200/50 dark:border-wedding-gold-800/50 shadow-xl"
100
+ exit={{ opacity: 0, scale: 0.9 }}
101
+ initial={{ opacity: 0, scale: 0.9 }}
102
+ >
103
+ <TimeUnit label="Days" value={timeLeft.days} />
104
+ <div className="text-wedding-gold-400 text-3xl md:text-5xl font-light mb-6">:</div>
105
+ <TimeUnit label="Hours" value={timeLeft.hours} />
106
+ <div className="text-wedding-gold-400 text-3xl md:text-5xl font-light mb-6">:</div>
107
+ <TimeUnit label="Mins" value={timeLeft.minutes} />
108
+ <div className="text-wedding-gold-400 text-3xl md:text-5xl font-light mb-6">:</div>
109
+ <TimeUnit label="Secs" value={timeLeft.seconds} />
110
+ </motion.div>
111
+ ) : (
112
+ <motion.div
113
+ key="celebration"
114
+ animate={{ opacity: 1, y: 0 }}
115
+ className="flex flex-col items-center gap-4 bg-gradient-to-br from-wedding-pink-500 to-wedding-gold-500 p-8 rounded-[40px] shadow-2xl text-white"
116
+ exit={{ opacity: 0, y: 20 }}
117
+ initial={{ opacity: 0, y: 20 }}
118
+ >
119
+ <HeartFilledIcon className="w-12 h-12 animate-beat text-white" />
120
+ <h2 className={`${fontCursive.className} text-4xl md:text-5xl text-center`}>
121
+ {phase === "WEDDING_DAY" && "Today is our Wedding Day!"}
122
+ {phase === "RECEPTION_DAY" && "Today is our Reception!"}
123
+ {phase === "POST_EVENT" && "Just Married!"}
124
+ </h2>
125
+ </motion.div>
126
+ )}
127
+ </AnimatePresence>
128
+
129
+ <p className={`${fontCursive.className} text-2xl text-wedding-gold-600 dark:text-wedding-gold-400`}>
130
+ {phase === "BEFORE_WEDDING" && "Until we say \"I Do\""}
131
+ {phase === "BEFORE_RECEPTION" && "Until the Grand Reception"}
132
+ {phase === "POST_EVENT" && "Happily Ever After"}
133
+ {(phase === "WEDDING_DAY" || phase === "RECEPTION_DAY") && "The Celebration Continues..."}
134
+ </p>
135
+ </div>
136
+ );
137
+ }