From 6ea31e8ac00010a1e83a1189d33fe22e7e4f90a0 Mon Sep 17 00:00:00 2001 From: jeanGaston Date: Wed, 29 Jul 2026 20:45:24 +0200 Subject: [PATCH] feat: allow a single activity proposition and add optional location field Senders no longer need to fill in two activities to publish; one is now enough (still capped at 5). Each activity can also specify a place, which flows into the generated Google Calendar link and .ics file when the recipient picks a slot. --- prisma/schema.prisma | 1 + src/app/create/CreateClient.tsx | 1 + src/app/manage/[token]/ManageClient.tsx | 15 ++++++++++++--- src/app/p/[slug]/ProposalPageClient.tsx | 22 ++++++++++++++++++---- src/components/CalendarButtons.tsx | 7 ++++--- src/components/ProposalForm.tsx | 15 +++++++++++---- src/lib/calendar.ts | 8 +++++--- src/lib/proposal-create.ts | 1 + src/lib/proposal-schema.ts | 3 ++- 9 files changed, 55 insertions(+), 18 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3fa6011..6d86a76 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -35,6 +35,7 @@ model ActivityOption { proposal Proposal @relation(fields: [proposalId], references: [id], onDelete: Cascade) title String description String? + location String? emoji String @default("🎉") order Int @default(0) slots TimeSlot[] diff --git a/src/app/create/CreateClient.tsx b/src/app/create/CreateClient.tsx index 96ad597..7ffef27 100644 --- a/src/app/create/CreateClient.tsx +++ b/src/app/create/CreateClient.tsx @@ -23,6 +23,7 @@ async function encryptPayload(key: string, data: ProposalFormData) { data.activities.map(async (a) => ({ title: await encryptField(key, a.title), description: await encryptOptional(key, a.description), + location: await encryptOptional(key, a.location), emoji: a.emoji, slots: await Promise.all( a.slots.map(async (s) => ({ diff --git a/src/app/manage/[token]/ManageClient.tsx b/src/app/manage/[token]/ManageClient.tsx index 14643d6..91e7405 100644 --- a/src/app/manage/[token]/ManageClient.tsx +++ b/src/app/manage/[token]/ManageClient.tsx @@ -33,7 +33,7 @@ type FullProposal = Proposal & { }; interface DecryptedSlot { id: string; label: string; startsAt: string | null } -interface DecryptedActivity { id: string; emoji: string; title: string; description: string | null; slots: DecryptedSlot[] } +interface DecryptedActivity { id: string; emoji: string; title: string; description: string | null; location: string | null; slots: DecryptedSlot[] } interface DecryptedManage { title: string; recipientName: string; @@ -90,9 +90,10 @@ export default function ManageClient({ decryptField(k, proposal.recipientName), ]); const activities = await Promise.all(proposal.activities.map(async (a) => { - const [aTitle, aDesc] = await Promise.all([ + const [aTitle, aDesc, aLocation] = await Promise.all([ decryptField(k, a.title), decryptOptional(k, a.description), + decryptOptional(k, a.location), ]); const slots = await Promise.all(a.slots.map(async (s) => { const [label, startsAt] = await Promise.all([ @@ -101,7 +102,7 @@ export default function ManageClient({ ]); return { id: s.id, label, startsAt: startsAt ?? null }; })); - return { id: a.id, emoji: a.emoji, title: aTitle, description: aDesc ?? null, slots }; + return { id: a.id, emoji: a.emoji, title: aTitle, description: aDesc ?? null, location: aLocation ?? null, slots }; })); let response: DecryptedManage['response'] = null; if (proposal.response) { @@ -159,6 +160,7 @@ export default function ManageClient({ ? { startsAt: new Date(selectedSlot.startsAt), title: `${selectedActivity?.title ?? 'Date'} with ${decrypted.recipientName}`, + location: selectedActivity?.location ?? undefined, } : null; @@ -237,6 +239,11 @@ export default function ManageClient({ 🗓 {formatSlotDate(selectedSlot.startsAt, selectedSlot.label)}

)} + {selectedActivity.location && ( +

+ 📍 {selectedActivity.location} +

+ )} )} {decrypted.response.note && ( @@ -250,6 +257,7 @@ export default function ManageClient({ title={calendarSlot.title} startsAt={calendarSlot.startsAt} description={`Arranged via lovelope.app: ${decrypted.title}`} + location={calendarSlot.location} /> )} @@ -327,6 +335,7 @@ export default function ManageClient({

{a.title}

{a.description &&

{a.description}

} + {a.location &&

📍 {a.location}

} {a.slots.length > 0 && (
{a.slots.map((s) => ( diff --git a/src/app/p/[slug]/ProposalPageClient.tsx b/src/app/p/[slug]/ProposalPageClient.tsx index ee83626..45f24a0 100644 --- a/src/app/p/[slug]/ProposalPageClient.tsx +++ b/src/app/p/[slug]/ProposalPageClient.tsx @@ -21,7 +21,7 @@ interface Props { } interface DecryptedSlot { id: string; label: string; startsAt: string | null } -interface DecryptedActivity { id: string; emoji: string; title: string; description: string | null; slots: DecryptedSlot[] } +interface DecryptedActivity { id: string; emoji: string; title: string; description: string | null; location: string | null; slots: DecryptedSlot[] } interface Decrypted { key: string; senderName: string; @@ -117,9 +117,10 @@ export default function ProposalPageClient({ decryptOptional(k, existingAnswer ?? undefined), ]); const activities = await Promise.all(proposal.activities.map(async (a) => { - const [aTitle, aDesc] = await Promise.all([ + const [aTitle, aDesc, aLocation] = await Promise.all([ decryptField(k, a.title), decryptOptional(k, a.description), + decryptOptional(k, a.location), ]); const slots = await Promise.all(a.slots.map(async (s) => { const [label, startsAt] = await Promise.all([ @@ -128,7 +129,7 @@ export default function ProposalPageClient({ ]); return { id: s.id, label, startsAt: startsAt ?? null }; })); - return { id: a.id, emoji: a.emoji, title: aTitle, description: aDesc ?? null, slots }; + return { id: a.id, emoji: a.emoji, title: aTitle, description: aDesc ?? null, location: aLocation ?? null, slots }; })); if (!cancelled) { setDecrypted({ key: k, senderName, recipientName, title, message, gifUrl: gifUrl ?? null, activities }); @@ -228,7 +229,11 @@ export default function ProposalPageClient({ const isMaybe = submittedAnswer === 'maybe'; const calendarSlot = isYes && submitted && selectedSlot?.startsAt - ? { startsAt: new Date(selectedSlot.startsAt), title: `${selectedActivity?.title ?? 'Date'} with ${decrypted.recipientName}` } + ? { + startsAt: new Date(selectedSlot.startsAt), + title: `${selectedActivity?.title ?? 'Date'} with ${decrypted.recipientName}`, + location: selectedActivity?.location ?? undefined, + } : null; return ( @@ -267,6 +272,11 @@ export default function ProposalPageClient({ 🗓 {formatSlotDate(selectedSlot.startsAt, selectedSlot.label)}

)} + {selectedActivity.location && ( +

+ 📍 {selectedActivity.location} +

+ )}
)} {calendarSlot && ( @@ -274,6 +284,7 @@ export default function ProposalPageClient({ title={calendarSlot.title} startsAt={calendarSlot.startsAt} description={`Arranged via lovelope.app: ${decrypted.title}`} + location={calendarSlot.location} /> )} @@ -371,6 +382,9 @@ export default function ProposalPageClient({ {activity.description && (

{activity.description}

)} + {activity.location && ( +

📍 {activity.location}

+ )}
@@ -388,7 +388,7 @@ export default function ProposalForm({
Activity {ai + 1} - {activities.length > 2 && ( + {activities.length > 1 && (