@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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
import * as admin from "firebase-admin";
|
|
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
|
+
|
|
14
|
+
export async function sendEmailReminder(formData: FormData) {
|
|
15
|
+
const email = formData.get("email") as string;
|
|
16
|
+
const slug = formData.get("slug") as string;
|
|
17
|
+
|
|
18
|
+
console.log("Received email reminder request:", { email, slug });
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const docRef = db.collection("invitation").doc(slug);
|
|
22
|
+
const docSnap = await docRef.get();
|
|
23
|
+
|
|
24
|
+
if (docSnap.exists) {
|
|
25
|
+
const guestData = docSnap.data();
|
|
26
|
+
|
|
27
|
+
await db.collection("email-reminders").add({
|
|
28
|
+
email,
|
|
29
|
+
guestId: slug,
|
|
30
|
+
guestName: guestData?.name || "Unknown",
|
|
31
|
+
familySide: guestData?.familySide || "Unknown",
|
|
32
|
+
invitedFor: guestData?.invitedFor || [],
|
|
33
|
+
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
console.log("Email reminder saved for:", email);
|
|
37
|
+
} else {
|
|
38
|
+
console.log("Invitation not found for slug:", slug);
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error("Error fetching invitation:", error);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// In the future, we will use 'resend' here to send the email.
|
|
45
|
+
// const { Resend } = require("resend");
|
|
46
|
+
// const resend = new Resend(process.env.RESEND_API_KEY);
|
|
47
|
+
|
|
48
|
+
return { success: true };
|
|
49
|
+
}
|
|
@@ -95,7 +95,7 @@ const Auth = ({ userSet }) => {
|
|
|
95
95
|
|
|
96
96
|
return (
|
|
97
97
|
<>
|
|
98
|
-
<section className="flex items-center justify-between p-4 rounded-lg">
|
|
98
|
+
<section className="flex flex-col sm:flex-row items-center justify-between p-4 gap-4 rounded-lg">
|
|
99
99
|
{!user ? (
|
|
100
100
|
<Button color="primary" onPress={() => setModalOpen(true)}>
|
|
101
101
|
Sign In
|
|
@@ -115,6 +115,7 @@ export default function AddGuestModal({
|
|
|
115
115
|
isRequired
|
|
116
116
|
label="Family Side"
|
|
117
117
|
orientation="horizontal"
|
|
118
|
+
classNames={{ wrapper: "flex-wrap gap-4" }}
|
|
118
119
|
value={formData.familySide}
|
|
119
120
|
onValueChange={(val) => handleRadioChange("familySide", val)}
|
|
120
121
|
>
|
|
@@ -126,6 +127,7 @@ export default function AddGuestModal({
|
|
|
126
127
|
isRequired
|
|
127
128
|
label="Relation to Couple"
|
|
128
129
|
orientation="horizontal"
|
|
130
|
+
classNames={{ wrapper: "flex-wrap gap-4" }}
|
|
129
131
|
value={formData.relation}
|
|
130
132
|
onValueChange={(val) => handleRadioChange("relation", val)}
|
|
131
133
|
>
|
|
@@ -138,6 +140,7 @@ export default function AddGuestModal({
|
|
|
138
140
|
<CheckboxGroup
|
|
139
141
|
label="Invited For"
|
|
140
142
|
orientation="horizontal"
|
|
143
|
+
classNames={{ wrapper: "flex-wrap gap-4" }}
|
|
141
144
|
value={formData.invitedFor}
|
|
142
145
|
onValueChange={(val) => handleCheckboxChange("invitedFor", val)}
|
|
143
146
|
isRequired
|
|
@@ -166,6 +169,7 @@ export default function AddGuestModal({
|
|
|
166
169
|
<RadioGroup
|
|
167
170
|
label="RSVP Status"
|
|
168
171
|
orientation="horizontal"
|
|
172
|
+
classNames={{ wrapper: "flex-wrap gap-4" }}
|
|
169
173
|
value={formData.rsvpStatus}
|
|
170
174
|
onValueChange={(val) => handleRadioChange("rsvpStatus", val)}
|
|
171
175
|
>
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
CardHeader,
|
|
11
11
|
CardFooter,
|
|
12
12
|
Link,
|
|
13
|
+
Input,
|
|
13
14
|
} from "@heroui/react";
|
|
14
15
|
import { fontSans, fontMono } from "@/config/fonts";
|
|
15
16
|
import { useState, useMemo } from "react";
|
|
@@ -46,6 +47,7 @@ export default function GuestTable({ guests, onEdit, onDelete, onAdd }) {
|
|
|
46
47
|
relation: "all",
|
|
47
48
|
rsvpStatus: "all",
|
|
48
49
|
});
|
|
50
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
49
51
|
|
|
50
52
|
const handleFilterChange = (key, value) => {
|
|
51
53
|
setFilters((prev) => ({
|
|
@@ -56,6 +58,14 @@ export default function GuestTable({ guests, onEdit, onDelete, onAdd }) {
|
|
|
56
58
|
|
|
57
59
|
const filteredGuests = useMemo(() => {
|
|
58
60
|
return guests.filter((guest) => {
|
|
61
|
+
if (searchTerm) {
|
|
62
|
+
const lowerTerm = searchTerm.toLowerCase();
|
|
63
|
+
const nameMatch = guest.name?.toLowerCase().includes(lowerTerm);
|
|
64
|
+
const contactMatch = guest.contact?.toLowerCase().includes(lowerTerm);
|
|
65
|
+
|
|
66
|
+
if (!nameMatch && !contactMatch) return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
59
69
|
if (
|
|
60
70
|
filters.familySide !== "all" &&
|
|
61
71
|
guest.familySide !== filters.familySide
|
|
@@ -75,18 +85,39 @@ export default function GuestTable({ guests, onEdit, onDelete, onAdd }) {
|
|
|
75
85
|
return false;
|
|
76
86
|
return true;
|
|
77
87
|
});
|
|
78
|
-
}, [guests, filters]);
|
|
88
|
+
}, [guests, filters, searchTerm]);
|
|
79
89
|
|
|
80
90
|
return (
|
|
81
91
|
<div className={`${fontSans.className}`}>
|
|
82
92
|
{/* FILTER SECTION */}
|
|
83
93
|
<Card className="mb-4 w-full shadow-sm bg-transparent">
|
|
84
|
-
<CardBody className="p-
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
<CardBody className="p-4 flex flex-col gap-6">
|
|
95
|
+
<div className="flex flex-col sm:flex-row justify-between items-end gap-4">
|
|
96
|
+
<div className="w-full sm:max-w-md">
|
|
97
|
+
<Input
|
|
98
|
+
label="Search Guests"
|
|
99
|
+
placeholder="Search by name or contact..."
|
|
100
|
+
labelPlacement="outside-top"
|
|
101
|
+
value={searchTerm}
|
|
102
|
+
onValueChange={setSearchTerm}
|
|
103
|
+
variant="bordered"
|
|
104
|
+
size="md"
|
|
105
|
+
classNames={{
|
|
106
|
+
input: "outline-none",
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
<Button
|
|
111
|
+
onPress={onAdd}
|
|
112
|
+
color="primary"
|
|
113
|
+
className="w-full sm:w-auto"
|
|
114
|
+
size="lg"
|
|
115
|
+
>
|
|
116
|
+
Add Guest
|
|
117
|
+
</Button>
|
|
118
|
+
</div>
|
|
88
119
|
|
|
89
|
-
<div className="flex flex-wrap gap-3 w-full
|
|
120
|
+
<div className="flex flex-wrap gap-3 w-full justify-start border-t dark:border-white/10 pt-4">
|
|
90
121
|
<Select
|
|
91
122
|
label="Family Side"
|
|
92
123
|
variant="bordered"
|
|
@@ -166,7 +197,7 @@ export default function GuestTable({ guests, onEdit, onDelete, onAdd }) {
|
|
|
166
197
|
key={index}
|
|
167
198
|
className={`flex text-left flex-col justify-between mb-2 border-t-4 ${rsvpBorderColor[guest.rsvpStatus]} shadow-md h-full`}
|
|
168
199
|
>
|
|
169
|
-
<CardHeader className="flex justify-between items-center px-4 pt-4 pb-2">
|
|
200
|
+
<CardHeader className="flex justify-between items-center px-4 pt-4 pb-2 gap-2">
|
|
170
201
|
<div>
|
|
171
202
|
<p className="font-bold text-base">{guest.name}</p>
|
|
172
203
|
<p className="text-sm capitalize">
|
|
@@ -249,7 +280,7 @@ export default function GuestTable({ guests, onEdit, onDelete, onAdd }) {
|
|
|
249
280
|
{guest.contact && (
|
|
250
281
|
<a
|
|
251
282
|
href={`https://wa.me/${guest.contact.replace(/[^0-9]/g, "")}?text=${encodeURIComponent(
|
|
252
|
-
`Hi! You are invited to Titas & Sukanya's wedding. Here's your invitation link: ${window.location.origin}/${guest.id}`
|
|
283
|
+
`Hi! You are invited to Titas & Sukanya's wedding. Here's your invitation link: ${window.location.origin}/invitation/${guest.id}`
|
|
253
284
|
)}`}
|
|
254
285
|
target="_blank"
|
|
255
286
|
rel="noopener noreferrer"
|
|
@@ -5,7 +5,7 @@ export default function PricingLayout({
|
|
|
5
5
|
}) {
|
|
6
6
|
return (
|
|
7
7
|
<section className="flex flex-col items-center justify-center gap-4 md:px-20 py-8 md:py-10">
|
|
8
|
-
<div className="
|
|
8
|
+
<div className="w-full text-center justify-center">{children}</div>
|
|
9
9
|
</section>
|
|
10
10
|
);
|
|
11
11
|
}
|
|
@@ -133,7 +133,7 @@ const InvitationMaker = () => {
|
|
|
133
133
|
return (
|
|
134
134
|
<>
|
|
135
135
|
<h1
|
|
136
|
-
className={`${fontCursive.className} text-center text-5xl leading-snug bg-cover bg-center bg-no-repeat pt-2 pb-10`}
|
|
136
|
+
className={`${fontCursive.className} text-center text-3xl md:text-5xl leading-snug bg-cover bg-center bg-no-repeat pt-2 pb-10`}
|
|
137
137
|
>
|
|
138
138
|
Guest Management System
|
|
139
139
|
</h1>
|
|
@@ -154,12 +154,14 @@ const InvitationMaker = () => {
|
|
|
154
154
|
)}
|
|
155
155
|
<br />
|
|
156
156
|
{guests && user && (
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
<div className="px-4">
|
|
158
|
+
<GuestTable
|
|
159
|
+
guests={guests}
|
|
160
|
+
onEdit={handleEditClick}
|
|
161
|
+
onDelete={handleDeleteGuest}
|
|
162
|
+
onAdd={handleAddClick}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
163
165
|
)}
|
|
164
166
|
</>
|
|
165
167
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from "react";
|
|
3
|
+
import { useState, useEffect, useMemo } from "react";
|
|
4
4
|
import {
|
|
5
5
|
Table,
|
|
6
6
|
TableHeader,
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
Chip,
|
|
12
12
|
User,
|
|
13
13
|
Tooltip,
|
|
14
|
+
Select,
|
|
15
|
+
SelectItem,
|
|
14
16
|
} from "@heroui/react";
|
|
15
17
|
import {
|
|
16
18
|
getFirestore,
|
|
@@ -27,6 +29,11 @@ const db = getFirestore(firebaseApp());
|
|
|
27
29
|
export default function RSVPViewer({ guests }) {
|
|
28
30
|
const [rsvps, setRsvps] = useState([]);
|
|
29
31
|
const [loading, setLoading] = useState(true);
|
|
32
|
+
|
|
33
|
+
// Filter & Sort States
|
|
34
|
+
const [foodFilter, setFoodFilter] = useState("all");
|
|
35
|
+
const [attendanceFilter, setAttendanceFilter] = useState("all");
|
|
36
|
+
const [sortOption, setSortOption] = useState("newest");
|
|
30
37
|
|
|
31
38
|
useEffect(() => {
|
|
32
39
|
const q = query(collection(db, "rsvps"), orderBy("timestamp", "desc"));
|
|
@@ -40,6 +47,7 @@ export default function RSVPViewer({ guests }) {
|
|
|
40
47
|
...data,
|
|
41
48
|
familySide: masterGuest?.familySide || "Unknown",
|
|
42
49
|
relation: masterGuest?.relation || "Guest",
|
|
50
|
+
timestamp: data.timestamp?.toDate ? data.timestamp.toDate() : new Date(),
|
|
43
51
|
};
|
|
44
52
|
});
|
|
45
53
|
setRsvps(rsvpData);
|
|
@@ -49,6 +57,31 @@ export default function RSVPViewer({ guests }) {
|
|
|
49
57
|
return () => unsubscribe();
|
|
50
58
|
}, [guests]);
|
|
51
59
|
|
|
60
|
+
const processedRsvps = useMemo(() => {
|
|
61
|
+
let result = [...rsvps];
|
|
62
|
+
|
|
63
|
+
// Filter by Food
|
|
64
|
+
if (foodFilter !== "all") {
|
|
65
|
+
result = result.filter((item) => item.food === foodFilter);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Filter by Attendance
|
|
69
|
+
if (attendanceFilter !== "all") {
|
|
70
|
+
result = result.filter((item) => item.attending === attendanceFilter);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Sort
|
|
74
|
+
result.sort((a, b) => {
|
|
75
|
+
if (sortOption === "newest") return b.timestamp - a.timestamp;
|
|
76
|
+
if (sortOption === "oldest") return a.timestamp - b.timestamp;
|
|
77
|
+
if (sortOption === "guests_high") return b.guests - a.guests;
|
|
78
|
+
if (sortOption === "guests_low") return a.guests - b.guests;
|
|
79
|
+
return 0;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return result;
|
|
83
|
+
}, [rsvps, foodFilter, attendanceFilter, sortOption]);
|
|
84
|
+
|
|
52
85
|
const columns = [
|
|
53
86
|
{ name: "GUEST", uid: "guest" },
|
|
54
87
|
{ name: "ATTENDING", uid: "attending" },
|
|
@@ -59,14 +92,62 @@ export default function RSVPViewer({ guests }) {
|
|
|
59
92
|
|
|
60
93
|
return (
|
|
61
94
|
<div className="space-y-4">
|
|
62
|
-
<div className="flex items-center justify-between px-2">
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
95
|
+
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 px-2">
|
|
96
|
+
<div className="flex items-center gap-4">
|
|
97
|
+
<h2 className="text-2xl font-bold text-default-800 dark:text-white">Live RSVP Responses</h2>
|
|
98
|
+
<Chip variant="flat" color="secondary" size="sm" className={fontMono.className}>
|
|
99
|
+
Total: {processedRsvps.length}
|
|
100
|
+
</Chip>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div className="flex flex-wrap gap-3 w-full md:w-auto justify-start md:justify-end">
|
|
104
|
+
<Select
|
|
105
|
+
label="Attendance"
|
|
106
|
+
variant="bordered"
|
|
107
|
+
size="sm"
|
|
108
|
+
labelPlacement="inside"
|
|
109
|
+
className="min-w-[140px] max-w-[160px] flex-1 [&_label]:!pb-1.5 [&_[data-slot=value]]:!pt-1.5"
|
|
110
|
+
selectedKeys={[attendanceFilter]}
|
|
111
|
+
onSelectionChange={(e) => setAttendanceFilter(Array.from(e)[0])}
|
|
112
|
+
>
|
|
113
|
+
<SelectItem key="all">All</SelectItem>
|
|
114
|
+
<SelectItem key="yes">Attending</SelectItem>
|
|
115
|
+
<SelectItem key="no">Declined</SelectItem>
|
|
116
|
+
</Select>
|
|
117
|
+
|
|
118
|
+
<Select
|
|
119
|
+
label="Food"
|
|
120
|
+
variant="bordered"
|
|
121
|
+
size="sm"
|
|
122
|
+
labelPlacement="inside"
|
|
123
|
+
className="min-w-[140px] max-w-[160px] flex-1 [&_label]:!pb-1.5 [&_[data-slot=value]]:!pt-1.5"
|
|
124
|
+
selectedKeys={[foodFilter]}
|
|
125
|
+
onSelectionChange={(e) => setFoodFilter(Array.from(e)[0])}
|
|
126
|
+
>
|
|
127
|
+
<SelectItem key="all">All</SelectItem>
|
|
128
|
+
<SelectItem key="veg">Veg</SelectItem>
|
|
129
|
+
<SelectItem key="non-veg">Non-Veg</SelectItem>
|
|
130
|
+
</Select>
|
|
131
|
+
|
|
132
|
+
<Select
|
|
133
|
+
label="Sort By"
|
|
134
|
+
variant="bordered"
|
|
135
|
+
size="sm"
|
|
136
|
+
labelPlacement="inside"
|
|
137
|
+
className="min-w-[140px] max-w-[160px] flex-1 [&_label]:!pb-1.5 [&_[data-slot=value]]:!pt-1.5"
|
|
138
|
+
selectedKeys={[sortOption]}
|
|
139
|
+
onSelectionChange={(e) => setSortOption(Array.from(e)[0])}
|
|
140
|
+
>
|
|
141
|
+
<SelectItem key="newest">Newest First</SelectItem>
|
|
142
|
+
<SelectItem key="oldest">Oldest First</SelectItem>
|
|
143
|
+
<SelectItem key="guests_high">Most Guests</SelectItem>
|
|
144
|
+
<SelectItem key="guests_low">Fewest Guests</SelectItem>
|
|
145
|
+
</Select>
|
|
146
|
+
</div>
|
|
67
147
|
</div>
|
|
68
148
|
|
|
69
|
-
<
|
|
149
|
+
<div className="overflow-x-auto">
|
|
150
|
+
<Table aria-label="RSVP Response Table" shadow="sm" className="bg-white dark:bg-zinc-900/50 min-w-[600px]">
|
|
70
151
|
<TableHeader columns={columns}>
|
|
71
152
|
{(column) => (
|
|
72
153
|
<TableColumn key={column.uid} className="bg-default-50 dark:bg-zinc-800 font-bold">
|
|
@@ -74,7 +155,7 @@ export default function RSVPViewer({ guests }) {
|
|
|
74
155
|
</TableColumn>
|
|
75
156
|
)}
|
|
76
157
|
</TableHeader>
|
|
77
|
-
<TableBody items={
|
|
158
|
+
<TableBody items={processedRsvps} emptyContent={"No RSVPs match your filters."} isLoading={loading}>
|
|
78
159
|
{(item) => (
|
|
79
160
|
<TableRow key={item.id} className="border-b border-default-100 last:border-none">
|
|
80
161
|
<TableCell>
|
|
@@ -117,6 +198,7 @@ export default function RSVPViewer({ guests }) {
|
|
|
117
198
|
)}
|
|
118
199
|
</TableBody>
|
|
119
200
|
</Table>
|
|
201
|
+
</div>
|
|
120
202
|
</div>
|
|
121
203
|
);
|
|
122
204
|
}
|
package/app/layout.tsx
CHANGED
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
import "@/styles/globals.css";
|
|
2
2
|
import { Metadata, Viewport } from "next";
|
|
3
3
|
import clsx from "clsx";
|
|
4
|
+
import { Analytics } from "@vercel/analytics/next";
|
|
4
5
|
|
|
5
|
-
import
|
|
6
|
+
import Providers from "./providers";
|
|
6
7
|
|
|
7
8
|
import { siteConfig } from "@/config/site";
|
|
8
9
|
import { fontSans, fontMono, fontCursive } from "@/config/fonts"; // include all fonts
|
|
9
10
|
import { Navbar } from "@/components/navbar";
|
|
10
11
|
import { Footer } from "@/components/footer";
|
|
11
|
-
import { Analytics } from "@vercel/analytics/next";
|
|
12
12
|
import ConciergeBot from "@/components/ConciergeBot";
|
|
13
|
+
import { SchemaMarkup } from "@/components/SchemaMarkup";
|
|
13
14
|
|
|
14
15
|
export const metadata: Metadata = {
|
|
15
|
-
metadataBase: new URL("https://www.titas-sukanya-for.life
|
|
16
|
+
metadataBase: new URL("https://www.titas-sukanya-for.life"),
|
|
16
17
|
|
|
17
18
|
title: {
|
|
18
19
|
default: siteConfig.name,
|
|
19
|
-
template: `%s
|
|
20
|
+
template: `%s | ${siteConfig.name}`,
|
|
20
21
|
},
|
|
21
22
|
description: siteConfig.description,
|
|
22
|
-
keywords: [
|
|
23
|
+
keywords: [
|
|
24
|
+
"Wedding",
|
|
25
|
+
"Titas",
|
|
26
|
+
"Sukanya",
|
|
27
|
+
"Marriage",
|
|
28
|
+
"Celebration",
|
|
29
|
+
"Event",
|
|
30
|
+
"Serampore Wedding",
|
|
31
|
+
"Titas and Sukanya Wedding",
|
|
32
|
+
],
|
|
23
33
|
authors: [
|
|
24
34
|
{
|
|
25
35
|
name: "Titas Mallick",
|
|
@@ -29,6 +39,11 @@ export const metadata: Metadata = {
|
|
|
29
39
|
creator: "Titas Mallick",
|
|
30
40
|
icons: {
|
|
31
41
|
icon: "/love-birds.png",
|
|
42
|
+
shortcut: "/love-birds.png",
|
|
43
|
+
apple: "/love-birds.png",
|
|
44
|
+
},
|
|
45
|
+
alternates: {
|
|
46
|
+
canonical: "/",
|
|
32
47
|
},
|
|
33
48
|
openGraph: {
|
|
34
49
|
type: "website",
|
|
@@ -39,10 +54,10 @@ export const metadata: Metadata = {
|
|
|
39
54
|
siteName: siteConfig.name,
|
|
40
55
|
images: [
|
|
41
56
|
{
|
|
42
|
-
url: "/
|
|
57
|
+
url: "https://www.titas-sukanya-for.life/invite.jpeg",
|
|
43
58
|
width: 1200,
|
|
44
59
|
height: 630,
|
|
45
|
-
alt: "Titas & Sukanya Wedding",
|
|
60
|
+
alt: "Titas & Sukanya Wedding Invitation",
|
|
46
61
|
},
|
|
47
62
|
],
|
|
48
63
|
},
|
|
@@ -50,9 +65,20 @@ export const metadata: Metadata = {
|
|
|
50
65
|
card: "summary_large_image",
|
|
51
66
|
title: siteConfig.name,
|
|
52
67
|
description: siteConfig.description,
|
|
53
|
-
images: ["/
|
|
68
|
+
images: ["https://www.titas-sukanya-for.life/invite.jpeg"],
|
|
54
69
|
creator: "@titas",
|
|
55
70
|
},
|
|
71
|
+
robots: {
|
|
72
|
+
index: true,
|
|
73
|
+
follow: true,
|
|
74
|
+
googleBot: {
|
|
75
|
+
index: true,
|
|
76
|
+
follow: true,
|
|
77
|
+
"max-video-preview": -1,
|
|
78
|
+
"max-image-preview": "large",
|
|
79
|
+
"max-snippet": -1,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
56
82
|
};
|
|
57
83
|
|
|
58
84
|
export const viewport: Viewport = {
|
|
@@ -62,11 +88,7 @@ export const viewport: Viewport = {
|
|
|
62
88
|
],
|
|
63
89
|
};
|
|
64
90
|
|
|
65
|
-
|
|
66
|
-
children,
|
|
67
|
-
}: {
|
|
68
|
-
children: React.ReactNode;
|
|
69
|
-
}) {
|
|
91
|
+
function RootLayout({ children }: { children: React.ReactNode }) {
|
|
70
92
|
return (
|
|
71
93
|
<html
|
|
72
94
|
suppressHydrationWarning
|
|
@@ -77,7 +99,9 @@ export default function RootLayout({
|
|
|
77
99
|
)}
|
|
78
100
|
lang="en"
|
|
79
101
|
>
|
|
80
|
-
<head
|
|
102
|
+
<head>
|
|
103
|
+
<SchemaMarkup />
|
|
104
|
+
</head>
|
|
81
105
|
<body
|
|
82
106
|
className={clsx("min-h-screen bg-background font-sans antialiased")}
|
|
83
107
|
>
|
|
@@ -96,3 +120,5 @@ export default function RootLayout({
|
|
|
96
120
|
</html>
|
|
97
121
|
);
|
|
98
122
|
}
|
|
123
|
+
|
|
124
|
+
export default RootLayout;
|
|
@@ -11,7 +11,7 @@ const importantDates = [
|
|
|
11
11
|
{
|
|
12
12
|
title: "The Proposal",
|
|
13
13
|
date: "15th March 2025",
|
|
14
|
-
note: "The day
|
|
14
|
+
note: "The day the proposal happened with a ring.",
|
|
15
15
|
image: "/Images/19.jpg",
|
|
16
16
|
},
|
|
17
17
|
{
|
|
@@ -85,7 +85,13 @@ export default function DatesPage() {
|
|
|
85
85
|
<div className="flex flex-col gap-12 md:gap-24">
|
|
86
86
|
{importantDates.map((item, index) => {
|
|
87
87
|
const eventDate = parseDate(item.date);
|
|
88
|
-
|
|
88
|
+
// Mark as completed only after one full day has passed (24 hours after end of day)
|
|
89
|
+
const completionThreshold = new Date(eventDate);
|
|
90
|
+
|
|
91
|
+
completionThreshold.setDate(completionThreshold.getDate() + 1);
|
|
92
|
+
completionThreshold.setHours(23, 59, 59, 999);
|
|
93
|
+
|
|
94
|
+
const isPast = now ? now > completionThreshold : false;
|
|
89
95
|
const isLeft = index % 2 === 0;
|
|
90
96
|
|
|
91
97
|
return (
|
package/app/page.tsx
CHANGED
package/app/providers.tsx
CHANGED