@titas_mallick/wedding-site-gen 1.1.0 → 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 +26 -15
- 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
package/README.md
CHANGED
|
@@ -1,212 +1,98 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 💍 Wedding Website Generator v2.0
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Empower your wedding with AI.** Transform your special day into a digital masterpiece with an interactive invitation, AI-powered concierge, and real-time guest engagement.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## 🚀 The A-B-C Quick Start
|
|
8
8
|
|
|
9
|
+
### Step A: Generate Your Codebase
|
|
10
|
+
Open your terminal and run the following command to scaffold your project:
|
|
9
11
|
```bash
|
|
10
12
|
npx @titas_mallick/wedding-site-gen
|
|
11
13
|
```
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
3. **
|
|
14
|
+
*The CLI will guide you through couple names, wedding dates, and theme selection.*
|
|
15
|
+
|
|
16
|
+
### Step B: Infrastructure & Environment
|
|
17
|
+
1. **Navigate & Install**:
|
|
18
|
+
```bash
|
|
19
|
+
cd your-wedding-folder
|
|
20
|
+
npm install
|
|
21
|
+
```
|
|
22
|
+
2. **Firebase Setup**:
|
|
23
|
+
- Create a project at [Firebase Console](https://console.firebase.google.com/).
|
|
24
|
+
- Enable **Firestore** and **Authentication** (Email/Password).
|
|
25
|
+
- Copy the `firestore.rules` file content from your project into the Firebase console.
|
|
26
|
+
3. **Configuration**:
|
|
27
|
+
- Rename `.env.example` to `.env.local` (or create a new one).
|
|
28
|
+
- Fill in your Firebase keys, Gemini API key, and Cloudinary credentials.
|
|
29
|
+
|
|
30
|
+
### Step C: Asset Personalization
|
|
31
|
+
This is where the magic happens. Replace the placeholders in the `public/` directory with your own files.
|
|
25
32
|
|
|
26
33
|
---
|
|
27
34
|
|
|
28
|
-
##
|
|
29
|
-
|
|
30
|
-
### 🤖 AI-Powered Intelligence
|
|
31
|
-
- **Wedding Concierge (Gemini 2.5-flash)**: A sitewide floating chatbot that answers guest queries about venues, schedules, and the couple's 10-year journey.
|
|
32
|
-
- **Smart Bengali Translation**: Instantly translates invitation details into elegant, formal Bengali script for guests.
|
|
33
|
-
- **AI Content Refinement**: An admin tool to polish rough updates into poetic, wedding-appropriate announcements.
|
|
34
|
-
- **Sentiment Wall**: Automatically summarizes guest wishes into a beautiful "Collective Blessing" using AI analysis.
|
|
35
|
-
|
|
36
|
-
### 📸 Interactive Guest Experience
|
|
37
|
-
- **Digital Guestbook**: Allows guests to upload photos directly from their devices. Features client-side compression/resizing and an immersive lightbox gallery.
|
|
38
|
-
- **Reception Playlist**: A real-time queue where guests can request songs for the reception party.
|
|
39
|
-
- **Live RSVP System**: Secure confirm-attendance form with food preferences and guest counts.
|
|
40
|
-
- **Travel & Stay Guide**: Dynamic, venue-aware guide with heritage highlights, hotel recommendations, and "Bengali 101."
|
|
41
|
-
|
|
42
|
-
### 🎥 Broadcast & Display
|
|
43
|
-
- **OBS Broadcast Overlay**: A dedicated `/updates/overlay` route designed for venue video walls. Features a real-time clock, QR codes, and a chroma-key ready green background.
|
|
44
|
-
- **Live Countdown**: High-impact, animated timer counting down to the wedding on January 23, 2026.
|
|
45
|
-
|
|
46
|
-
## 🚀 Tech Stack
|
|
47
|
-
|
|
48
|
-
- **Framework**: Next.js (App Router)
|
|
49
|
-
- **UI Components**: HeroUI (formerly NextUI)
|
|
50
|
-
- **Animations**: Framer Motion
|
|
51
|
-
- **Database & Auth**: Firebase (Firestore & Authentication)
|
|
52
|
-
- **AI Model**: Google Gemini 2.5-flash
|
|
53
|
-
- **Image Hosting**: Cloudinary (Unsigned Uploads)
|
|
54
|
-
- **Analytics**: Vercel Analytics
|
|
55
|
-
|
|
56
|
-
## 📁 Project Architecture & Codebase Map
|
|
57
|
-
|
|
58
|
-
### 1. Route Structure (`/app`)
|
|
59
|
-
- **`/invitation/[slug]`**: The core engine for personalized digital invites. It reads the `slug` (guest name) and displays a customized experience.
|
|
60
|
-
- **`/[groom]` & `/[bride]`**: (Dynamically renamed by CLI) Individual bio pages for the couple.
|
|
61
|
-
- *Modification Point*: Edit these files to change personal stories or education/work history.
|
|
62
|
-
- **`/guestbook`**: Handles real-time photo uploads and gallery display.
|
|
63
|
-
- *Tech Detail*: Uses Cloudinary for storage and Firestore to track image metadata.
|
|
64
|
-
- **`/song-requests`**: A dual-purpose route. Guests see a request form; Admins see a management queue.
|
|
65
|
-
- **`/updates/overlay`**: A specialized green-screen route for OBS.
|
|
66
|
-
- *Usage*: Add this as a Browser Source in OBS with a Chroma Key filter.
|
|
67
|
-
|
|
68
|
-
### 2. Configuration (`/config`)
|
|
69
|
-
- **`site.ts`**: The central source of truth for navigation links, site name, and SEO metadata.
|
|
70
|
-
- **Modify here** if you want to add/remove menu items or change the site description.
|
|
71
|
-
- **`firebase.ts`**: Client-side Firebase initialization.
|
|
72
|
-
- **`firebase-admin.js`**: Server-side Admin SDK configuration for secure operations (RSVP management, etc.).
|
|
73
|
-
|
|
74
|
-
### 3. Key Components (`/components`)
|
|
75
|
-
- **`ConciergeBot.tsx`**: The Gemini-powered AI assistant.
|
|
76
|
-
- *Customization*: Edit the `systemInstruction` in this file to change the bot's personality or the facts it knows about your wedding.
|
|
77
|
-
- **`CountdownTimer.tsx`**: The main landing page countdown.
|
|
78
|
-
- *Logic*: Uses a `setInterval` to calculate time until the `weddingStart` date.
|
|
79
|
-
- **`OurStory.tsx`**: The timeline component showing the couple's history.
|
|
80
|
-
|
|
81
|
-
## 🧠 Deep-Dive: How it Works
|
|
82
|
-
|
|
83
|
-
### 1. AI Wedding Concierge (`ConciergeBot.tsx`)
|
|
84
|
-
The floating assistant uses **Google Gemini 2.5-flash**.
|
|
85
|
-
- **Personality**: Defined in the `systemInstruction` constant. It's programmed to be a warm, respectful Indian wedding host.
|
|
86
|
-
- **Customization**: To update facts (like venue changes), simply edit the `Key Info` section inside the `systemInstruction` in `components/ConciergeBot.tsx`.
|
|
87
|
-
|
|
88
|
-
### 2. Personalized Invitations (`/app/invitation/[slug]`)
|
|
89
|
-
This route handles dynamic guest experiences.
|
|
90
|
-
- **Dynamic Content**: Based on the `slug` (guest ID), it fetches personalized data from Firestore.
|
|
91
|
-
- **Smart Bengali Translation**: Uses Gemini to translate invitation details into formal Bengali when the user clicks "বাংলায় দেখুন".
|
|
92
|
-
|
|
93
|
-
### 3. Digital Guestbook (`/app/guestbook`)
|
|
94
|
-
- **Image Processing**: Before uploading, photos are client-side resized and compressed (JPEG 70% quality) to ensure fast uploads even on mobile networks.
|
|
95
|
-
- **Storage**: Images go to Cloudinary, and the resulting URL is saved to Firestore.
|
|
96
|
-
|
|
97
|
-
### 4. Sentiment Wall (`/app/sagun`)
|
|
98
|
-
- **Collective Blessing**: When guests leave wishes, Gemini periodically summarizes the latest messages into a single "poetic paragraph" that updates live on the "Sentiment Wall."
|
|
35
|
+
## 🎨 Asset Mapping (Photo & Media Guide)
|
|
99
36
|
|
|
100
|
-
|
|
37
|
+
To ensure the site looks perfect, replace these files but **keep the filenames exactly the same**.
|
|
101
38
|
|
|
102
|
-
|
|
39
|
+
### 📸 Core Portraits (`/public/`)
|
|
40
|
+
| Filename | Purpose | Recommended Aspect Ratio |
|
|
41
|
+
| :--- | :--- | :--- |
|
|
42
|
+
| `bride.jpg` | Main profile photo of the Bride. | 3:4 (Portrait) |
|
|
43
|
+
| `groom.jpg` | Main profile photo of the Groom. | 3:4 (Portrait) |
|
|
44
|
+
| `love-birds.png` | Avatar for the AI Chatbot. | 1:1 (Square) |
|
|
45
|
+
| `qr.png` | UPI Payment QR code for gifts. | 1:1 (Square) |
|
|
103
46
|
|
|
104
|
-
|
|
47
|
+
### 🗓️ Milestone Timeline (`/public/Images/`)
|
|
48
|
+
These images appear on the **"Mark the Dates"** timeline.
|
|
49
|
+
- `19.jpg`: The Proposal Milestone.
|
|
50
|
+
- `Patipatra.jpeg`: The Date-Fixing/Traditional Ceremony.
|
|
51
|
+
- `22.jpg`: The Engagement Milestone.
|
|
52
|
+
- `21.jpg`: The Wedding Day Milestone.
|
|
53
|
+
- `20.jpg`: The Reception Milestone.
|
|
105
54
|
|
|
106
|
-
###
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"name": "Rahul & Priya",
|
|
111
|
-
"invitedGuests": 2,
|
|
112
|
-
"relation": "Close Friend",
|
|
113
|
-
"familySide": "groom",
|
|
114
|
-
"invitedFor": ["wedding", "reception"]
|
|
115
|
-
}
|
|
116
|
-
```
|
|
55
|
+
### 🏔️ Pre-Wedding & Memories Gallery
|
|
56
|
+
The **"Memories"** page is designed to showcase your journey.
|
|
57
|
+
- **Bulk Uploads**: Place all your pre-wedding shoot photos in `/public/Images/`.
|
|
58
|
+
- **Naming**: Use descriptive names or simply number them. The gallery will automatically generate a beautiful masonry layout for all images found in this directory.
|
|
117
59
|
|
|
118
|
-
|
|
119
|
-
Automatically populated when guests confirm attendance.
|
|
120
|
-
```json
|
|
121
|
-
{
|
|
122
|
-
"guestId": "rahul-wedding",
|
|
123
|
-
"attending": "yes",
|
|
124
|
-
"guests": 2,
|
|
125
|
-
"food": "non-veg",
|
|
126
|
-
"note": "Looking forward to it!",
|
|
127
|
-
"timestamp": "serverTime"
|
|
128
|
-
}
|
|
129
|
-
```
|
|
60
|
+
---
|
|
130
61
|
|
|
131
|
-
|
|
132
|
-
Store guest memories and text blessings respectively.
|
|
133
|
-
|
|
134
|
-
### 🔒 Recommended Firestore Rules
|
|
135
|
-
Paste these into your Firebase Console to secure your data (also available in `firestore.rules`):
|
|
136
|
-
```javascript
|
|
137
|
-
rules_version = '2';
|
|
138
|
-
service cloud.firestore {
|
|
139
|
-
match /databases/{database}/documents {
|
|
140
|
-
match /wishes/{wishId} { allow read, write: if true; }
|
|
141
|
-
match /song_requests/{requestId} { allow read, write: if true; }
|
|
142
|
-
match /guestbook/{entryId} { allow read, write: if true; }
|
|
143
|
-
match /rsvps/{rsvpId} { allow read, write: if true; }
|
|
144
|
-
match /{document=**} {
|
|
145
|
-
allow read: if true;
|
|
146
|
-
allow write: if request.auth != null;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
```
|
|
62
|
+
## 🤖 Advanced AI Features
|
|
151
63
|
|
|
152
|
-
|
|
64
|
+
### 1. The Wedding Concierge (Gemini 2.5)
|
|
65
|
+
Guests can chat with an AI assistant trained on your wedding data.
|
|
66
|
+
- **Setup**: Provide your `NEXT_PUBLIC_GEMINI_API_KEY` in `.env.local`.
|
|
67
|
+
- **Knowledge**: The bot automatically knows your venues, dates, and story based on the info you provided during Step A.
|
|
153
68
|
|
|
154
|
-
|
|
69
|
+
### 2. Automated Email Reminders
|
|
70
|
+
Send beautiful, automated reminders to your guest list.
|
|
71
|
+
- **Endpoint**: `YOUR_SITE_URL/api/email-reminders`
|
|
72
|
+
- **Auth**: Requires `Authorization: Bearer [YOUR_CRON_SECRET]`.
|
|
73
|
+
- **Cron Suggestion**: `0 10 10-26 1 *` (Runs daily at 10 AM during the wedding month).
|
|
155
74
|
|
|
156
|
-
|
|
157
|
-
This project uses **Tailwind CSS** and **HeroUI**.
|
|
158
|
-
- To change the primary "Wedding Gold" or "Wedding Pink" colors, edit `tailwind.config.js`.
|
|
159
|
-
- Search for the `extend.colors` section to update the hex codes.
|
|
75
|
+
---
|
|
160
76
|
|
|
161
|
-
|
|
162
|
-
- **KEEP**: The `/app/invitation` logic and `/components/providers.tsx` as they handle the core framework setup.
|
|
163
|
-
- **REPLACE**:
|
|
164
|
-
- All images in `/public` (groom.jpg, bride.jpg, and gallery photos).
|
|
165
|
-
- The text content in `app/[groom]/page.tsx` and `app/[bride]/page.tsx`.
|
|
166
|
-
- The venue details in `app/mark-the-dates/page.tsx`.
|
|
77
|
+
## ⚙️ Environment Variables Checklist
|
|
167
78
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
79
|
+
| Key | Description |
|
|
80
|
+
| :--- | :--- |
|
|
81
|
+
| `NEXT_PUBLIC_ADMIN_EMAIL` | The email that has "Super User" powers (delete photos/songs). |
|
|
82
|
+
| `NEXT_PUBLIC_GEMINI_API_KEY` | Powers the Concierge and the AI Wish Summarizer. |
|
|
83
|
+
| `RESEND_API_KEY` | Used to send the actual emails for reminders. |
|
|
84
|
+
| `NEXT_PUBLIC_CLOUDINARY_URL` | Enables guests to upload photos to your live guestbook. |
|
|
173
85
|
|
|
174
86
|
---
|
|
175
87
|
|
|
176
|
-
##
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
4. Go to **Project Settings > General** and scroll down to "Your apps" to create a Web App.
|
|
183
|
-
5. Copy the `firebaseConfig` values into your `.env.local`:
|
|
184
|
-
- `NEXT_PUBLIC_APIKEY`, `NEXT_PUBLIC_AUTHDOMAIN`, `NEXT_PUBLIC_PROJECTID`, etc.
|
|
185
|
-
6. **Admin SDK**: Go to **Project Settings > Service accounts**, click "Generate new private key", and use those values for the `FIREBASE_ADMIN_*` variables in `.env.local`.
|
|
186
|
-
7. **Security Rules**: Go to the **Rules** tab in Firestore and paste the contents of `firestore.rules` found in this project.
|
|
187
|
-
|
|
188
|
-
### 2. Google Gemini (AI Concierge)
|
|
189
|
-
1. Go to the [Google AI Studio](https://aistudio.google.com/).
|
|
190
|
-
2. Click **"Get API key"**.
|
|
191
|
-
3. Copy the key and paste it as `NEXT_PUBLIC_GEMINI_API_KEY` in your `.env.local`.
|
|
192
|
-
|
|
193
|
-
### 3. Cloudinary (Guestbook Photo Uploads)
|
|
194
|
-
1. Create a free account at [Cloudinary](https://cloudinary.com/).
|
|
195
|
-
2. Go to **Dashboard** and copy your **Cloud Name** into `NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME`.
|
|
196
|
-
3. Go to **Settings > Upload** and scroll down to **Upload presets**.
|
|
197
|
-
4. Click "Add upload preset".
|
|
198
|
-
5. Set **Signing Mode** to **Unsigned**.
|
|
199
|
-
6. Set the name to `wedding` (or match whatever you put in `NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET`).
|
|
200
|
-
7. Save and you're ready!
|
|
88
|
+
## 📁 Key Project Routes
|
|
89
|
+
- `/` - The Home Hero & Countdown.
|
|
90
|
+
- `/couple` - Your love story & individual bios.
|
|
91
|
+
- `/invitation/[guest-id]` - Personalized landing pages for guests.
|
|
92
|
+
- `/guestbook` - The real-time masonry photo wall.
|
|
93
|
+
- `/song-requests` - Interactive guest playlist.
|
|
201
94
|
|
|
202
95
|
---
|
|
203
96
|
|
|
204
|
-
## 📁 Project Structure Highlights
|
|
205
|
-
- `/app/invitation/[slug]`: The core personalized guest experience.
|
|
206
|
-
- `/app/updates/maker`: Admin dashboard for news and announcements.
|
|
207
|
-
- `/app/song-requests`: Consolidated guest form and admin queue.
|
|
208
|
-
- `/app/guestbook`: Real-time masonry gallery with Cloudinary integration.
|
|
209
|
-
- `/components/ConciergeBot.tsx`: The sitewide AI assistant component.
|
|
210
|
-
|
|
211
97
|
## 📜 License
|
|
212
|
-
|
|
98
|
+
MIT License. Built with ❤️ for the open-source wedding community.
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import * as admin from "firebase-admin";
|
|
3
|
+
import { Resend } from "resend";
|
|
4
|
+
import adminCred from "@/config/firebase-admin";
|
|
5
|
+
|
|
6
|
+
if (!admin.apps.length) {
|
|
7
|
+
admin.initializeApp({
|
|
8
|
+
credential: admin.credential.cert(adminCred as admin.ServiceAccount),
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const db = admin.firestore();
|
|
13
|
+
const resend = new Resend(process.env.RESEND_API_KEY);
|
|
14
|
+
|
|
15
|
+
const EVENTS: Record<string, { title: string; date: string; venue: string; isoDate: string; details: string; startTime: string; endTime: string; location: string }> = {
|
|
16
|
+
registration: {
|
|
17
|
+
title: "Engagement Ceremony",
|
|
18
|
+
date: "23rd November 2025",
|
|
19
|
+
venue: "Venue City",
|
|
20
|
+
isoDate: "2025-11-23",
|
|
21
|
+
details: "Join us for the Engagement Ceremony of the couple",
|
|
22
|
+
startTime: "20251123T043000Z",
|
|
23
|
+
endTime: "20251123T083000Z",
|
|
24
|
+
location: "Venue Name, City"
|
|
25
|
+
},
|
|
26
|
+
wedding: {
|
|
27
|
+
title: "Wedding Ceremony",
|
|
28
|
+
date: "23rd January 2026",
|
|
29
|
+
venue: "Venue City",
|
|
30
|
+
isoDate: "2026-01-23",
|
|
31
|
+
details: "The Wedding Ceremony",
|
|
32
|
+
startTime: "20260123T123000Z",
|
|
33
|
+
endTime: "20260123T163000Z",
|
|
34
|
+
location: "Venue Name, City"
|
|
35
|
+
},
|
|
36
|
+
reception: {
|
|
37
|
+
title: "Reception Celebration",
|
|
38
|
+
date: "25th January 2026",
|
|
39
|
+
venue: "Venue City",
|
|
40
|
+
isoDate: "2026-01-25",
|
|
41
|
+
details: "Reception Party for the couple",
|
|
42
|
+
startTime: "20260125T123000Z",
|
|
43
|
+
endTime: "20260125T163000Z",
|
|
44
|
+
location: "Venue Name, City"
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const getCalendarLink = (event: any) => {
|
|
49
|
+
return `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${encodeURIComponent(event.title)}&dates=${event.startTime}/${event.endTime}&details=${encodeURIComponent(event.details)}&location=${encodeURIComponent(event.location)}`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getDaysToGo = (isoDate: string) => {
|
|
53
|
+
const eventDate = new Date(isoDate);
|
|
54
|
+
// Get current time in IST
|
|
55
|
+
const nowIST = new Date(new Date().toLocaleString("en-US", { timeZone: "Asia/Kolkata" }));
|
|
56
|
+
|
|
57
|
+
eventDate.setHours(0, 0, 0, 0);
|
|
58
|
+
nowIST.setHours(0, 0, 0, 0);
|
|
59
|
+
|
|
60
|
+
const diffTime = eventDate.getTime() - nowIST.getTime();
|
|
61
|
+
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
62
|
+
|
|
63
|
+
if (diffDays === 0) return "TODAY! 🎉";
|
|
64
|
+
if (diffDays === 1) return "TOMORROW! ⏳";
|
|
65
|
+
if (diffDays < 0) return "Completed ✔️";
|
|
66
|
+
return `${diffDays} days to go`;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const emailTemplate = (guestName: string, invitedFor: string[] = []) => {
|
|
70
|
+
const eventDetailsHtml = invitedFor
|
|
71
|
+
.map((eventKey) => {
|
|
72
|
+
const event = EVENTS[eventKey.toLowerCase()];
|
|
73
|
+
if (!event) return "";
|
|
74
|
+
|
|
75
|
+
const daysMessage = getDaysToGo(event.isoDate);
|
|
76
|
+
const calendarLink = getCalendarLink(event);
|
|
77
|
+
|
|
78
|
+
return `
|
|
79
|
+
<div class="detail-item">
|
|
80
|
+
<strong>${event.title}</strong>
|
|
81
|
+
<span class="days-badge" style="background-color: #fce7f3; color: #db2777; padding: 2px 8px; border-radius: 10px; font-size: 12px; font-weight: bold; margin-left: 8px;">${daysMessage}</span>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="detail-item">📅 ${event.date}</div>
|
|
84
|
+
<div class="detail-item">📍 ${event.venue}</div>
|
|
85
|
+
<div class="detail-item" style="margin-top: 8px;">
|
|
86
|
+
<a href="${calendarLink}" style="color: #ec4899; text-decoration: underline; font-size: 14px;">📅 Add to Calendar</a>
|
|
87
|
+
</div>
|
|
88
|
+
<br>
|
|
89
|
+
`;
|
|
90
|
+
})
|
|
91
|
+
.join("");
|
|
92
|
+
|
|
93
|
+
return `
|
|
94
|
+
<!DOCTYPE html>
|
|
95
|
+
<html>
|
|
96
|
+
<head>
|
|
97
|
+
<style>
|
|
98
|
+
body { font-family: 'Helvetica', 'Arial', sans-serif; background-color: #fdf2f8; color: #4a4a4a; margin: 0; padding: 0; }
|
|
99
|
+
.container { max-width: 600px; margin: 20px auto; background-color: #ffffff; padding: 30px; border-radius: 16px; border: 1px solid #fbcfe8; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
|
|
100
|
+
.header { text-align: center; color: #ec4899; font-family: 'Brush Script MT', cursive; font-size: 36px; margin-bottom: 24px; border-bottom: 2px solid #fce7f3; padding-bottom: 16px; }
|
|
101
|
+
.content { font-size: 16px; line-height: 1.6; color: #374151; }
|
|
102
|
+
.details { background-color: #fffbeb; padding: 20px; border-radius: 12px; margin: 24px 0; border: 1px solid #edd5a3; }
|
|
103
|
+
.detail-item { margin-bottom: 8px; }
|
|
104
|
+
.footer { text-align: center; font-size: 12px; color: #9ca3af; margin-top: 32px; border-top: 1px solid #f3f4f6; padding-top: 16px; }
|
|
105
|
+
.btn { display: inline-block; background-color: #ec4899; color: #ffffff !important; padding: 12px 32px; text-decoration: none; border-radius: 50px; font-weight: bold; margin-top: 16px; }
|
|
106
|
+
</style>
|
|
107
|
+
</head>
|
|
108
|
+
<body>
|
|
109
|
+
<div class="container">
|
|
110
|
+
<div class="header">Groom & Bride</div>
|
|
111
|
+
<div class="content">
|
|
112
|
+
<p>Dear <strong>${guestName}</strong>,</p>
|
|
113
|
+
<p>We are counting down the days and are so excited to celebrate our union with you! This is a gentle reminder that our big day is just around the corner.</p>
|
|
114
|
+
${eventDetailsHtml ? `<div class="details">${eventDetailsHtml}</div>` : ''}
|
|
115
|
+
<p>Please visit our wedding website for the full schedule, maps, and travel guide.</p>
|
|
116
|
+
<div style="text-align: center;">
|
|
117
|
+
<a href="https://www.your-wedding-site.com" class="btn">View Wedding Details</a>
|
|
118
|
+
</div>
|
|
119
|
+
<p style="text-align: center; margin-top: 30px;">With Love,<br>Groom & Bride</p>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="footer">
|
|
122
|
+
<p>You received this email because you signed up for reminders on our wedding website.</p>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</body>
|
|
126
|
+
</html>
|
|
127
|
+
`;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export async function GET(request: NextRequest) {
|
|
131
|
+
const authHeader = request.headers.get('Authorization');
|
|
132
|
+
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
133
|
+
return new Response('Unauthorized', { status: 401 });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const snapshot = await db.collection("email-reminders").get();
|
|
138
|
+
|
|
139
|
+
if (snapshot.empty) {
|
|
140
|
+
return NextResponse.json({ message: "No email reminders found." });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Deduplicate by email
|
|
144
|
+
const uniqueRecipients = new Map<string, any>();
|
|
145
|
+
snapshot.docs.forEach(doc => {
|
|
146
|
+
const data = doc.data();
|
|
147
|
+
if (data.email) {
|
|
148
|
+
uniqueRecipients.set(data.email, data);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const results = [];
|
|
153
|
+
const recipients = Array.from(uniqueRecipients.values());
|
|
154
|
+
|
|
155
|
+
// Get current time in IST
|
|
156
|
+
const nowIST = new Date(new Date().toLocaleString("en-US", { timeZone: "Asia/Kolkata" }));
|
|
157
|
+
|
|
158
|
+
const isFutureOrToday = (isoDateStr: string) => {
|
|
159
|
+
const eventDate = new Date(isoDateStr);
|
|
160
|
+
// Set event date to end of day in IST
|
|
161
|
+
eventDate.setHours(23, 59, 59, 999);
|
|
162
|
+
return eventDate >= nowIST;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
for (const recipient of recipients) {
|
|
166
|
+
const { email, guestName, invitedFor } = recipient;
|
|
167
|
+
|
|
168
|
+
const upcomingEvents = (invitedFor || []).filter((eventKey: string) => {
|
|
169
|
+
const evt = EVENTS[eventKey.toLowerCase()];
|
|
170
|
+
return evt && isFutureOrToday(evt.isoDate);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (upcomingEvents.length === 0) {
|
|
174
|
+
results.push({ email, status: 'skipped', reason: 'No upcoming events' });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const { data: emailData, error } = await resend.emails.send({
|
|
180
|
+
from: 'Groom & Bride <wedding@your-wedding-site.com>',
|
|
181
|
+
to: [email],
|
|
182
|
+
subject: "Reminder: Wedding Celebration! 🎉",
|
|
183
|
+
html: emailTemplate(guestName || "Guest", upcomingEvents),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (error) {
|
|
187
|
+
console.error(`Failed to send to ${email}:`, error);
|
|
188
|
+
results.push({ email, status: 'failed', error });
|
|
189
|
+
} else {
|
|
190
|
+
results.push({ email, status: 'sent', id: emailData?.id });
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error(`Exception sending to ${email}:`, err);
|
|
194
|
+
results.push({ email, status: 'failed', error: err });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const sentCount = results.filter(r => r?.status === 'sent').length;
|
|
199
|
+
const failedCount = results.length - sentCount;
|
|
200
|
+
|
|
201
|
+
// Send Report to Admin
|
|
202
|
+
const reportHtml = `
|
|
203
|
+
<h1>Email Reminder Report</h1>
|
|
204
|
+
<p><strong>Total Unique Recipients:</strong> ${results.length}</p>
|
|
205
|
+
<p><strong>Successfully Sent:</strong> ${sentCount}</p>
|
|
206
|
+
<p><strong>Failed:</strong> ${failedCount}</p>
|
|
207
|
+
<hr/>
|
|
208
|
+
<h3>Details:</h3>
|
|
209
|
+
<ul>
|
|
210
|
+
${results.map(r => `<li>${r.email}: <strong>${r.status}</strong> ${r.error ? `(${JSON.stringify(r.error)})` : ''}</li>`).join('')}
|
|
211
|
+
</ul>
|
|
212
|
+
`;
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
await resend.emails.send({
|
|
216
|
+
from: 'Wedding Bot <bot@your-wedding-site.com>',
|
|
217
|
+
to: [process.env.ADMIN_EMAIL || 'admin@example.com'],
|
|
218
|
+
subject: `Wedding Reminder Report: ${sentCount}/${results.length} Sent`,
|
|
219
|
+
html: reportHtml,
|
|
220
|
+
});
|
|
221
|
+
} catch (reportError) {
|
|
222
|
+
console.error("Failed to send admin report:", reportError);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return NextResponse.json({
|
|
226
|
+
success: true,
|
|
227
|
+
total: results.length,
|
|
228
|
+
sent: sentCount,
|
|
229
|
+
failed: failedCount,
|
|
230
|
+
results
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error("Error processing email reminders:", error);
|
|
235
|
+
return NextResponse.json(
|
|
236
|
+
{ error: "Internal Server Error" },
|
|
237
|
+
{ status: 500 }
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
package/app/couple/page.tsx
CHANGED
|
@@ -48,13 +48,13 @@ export default function CouplePage() {
|
|
|
48
48
|
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-black/60 z-10 opacity-80 group-hover:opacity-100 transition-opacity" />
|
|
49
49
|
<Image
|
|
50
50
|
removeWrapper
|
|
51
|
-
alt="
|
|
51
|
+
alt="Bride"
|
|
52
52
|
className="z-0 w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-700"
|
|
53
53
|
src="/bride.jpg"
|
|
54
54
|
/>
|
|
55
55
|
<div className="absolute bottom-0 left-0 right-0 p-6 z-20 text-white">
|
|
56
56
|
<h3 className={`${fontCursive.className} text-4xl mb-1`}>
|
|
57
|
-
|
|
57
|
+
Bride
|
|
58
58
|
</h3>
|
|
59
59
|
<p className="text-white/80 font-medium">The Bride</p>
|
|
60
60
|
</div>
|
|
@@ -95,13 +95,13 @@ export default function CouplePage() {
|
|
|
95
95
|
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-black/60 z-10 opacity-80 group-hover:opacity-100 transition-opacity" />
|
|
96
96
|
<Image
|
|
97
97
|
removeWrapper
|
|
98
|
-
alt="
|
|
98
|
+
alt="Groom"
|
|
99
99
|
className="z-0 w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-700"
|
|
100
100
|
src="/groom.jpg"
|
|
101
101
|
/>
|
|
102
102
|
<div className="absolute bottom-0 left-0 right-0 p-6 z-20 text-white">
|
|
103
103
|
<h3 className={`${fontCursive.className} text-4xl mb-1`}>
|
|
104
|
-
|
|
104
|
+
Groom
|
|
105
105
|
</h3>
|
|
106
106
|
<p className="text-white/80 font-medium">The Groom</p>
|
|
107
107
|
</div>
|