feat: add event guest management

This commit is contained in:
Levente Orban
2025-09-02 11:16:44 +02:00
parent 1d523c4b88
commit b5df1479dd
2 changed files with 96 additions and 19 deletions

View File

@@ -69,6 +69,7 @@ export const actions: Actions = {
const formData = await request.formData(); const formData = await request.formData();
const name = formData.get('newAttendeeName') as string; const name = formData.get('newAttendeeName') as string;
const numberOfGuests = parseInt(formData.get('numberOfGuests') as string) || 0;
const userId = cookies.get('cactoideUserId'); const userId = cookies.get('cactoideUserId');
if (!name?.trim() || !userId) { if (!name?.trim() || !userId) {
@@ -82,27 +83,48 @@ export const actions: Actions = {
return fail(404, { error: 'Event not found' }); return fail(404, { error: 'Event not found' });
} }
// Get current RSVPs
const currentRSVPs = await database.select().from(rsvps).where(eq(rsvps.eventId, eventId));
// Calculate total attendees (including guests)
const totalAttendees = currentRSVPs.length + numberOfGuests;
// Check if event is full (for limited type events) // Check if event is full (for limited type events)
if (eventData.type === 'limited' && eventData.attendeeLimit) { if (eventData.type === 'limited' && eventData.attendeeLimit) {
const currentRSVPs = await database.select().from(rsvps).where(eq(rsvps.eventId, eventId)); if (totalAttendees > eventData.attendeeLimit) {
if (currentRSVPs.length >= eventData.attendeeLimit) { return fail(400, {
return fail(400, { error: 'Event is full' }); error: `Event capacity exceeded. You're trying to add ${numberOfGuests + 1} attendees (including yourself), but only ${eventData.attendeeLimit - currentRSVPs.length} spots remain.`
});
} }
} }
// Check if name is already in the list // Check if name is already in the list
const existingRSVPs = await database.select().from(rsvps).where(eq(rsvps.eventId, eventId)); if (currentRSVPs.some((rsvp) => rsvp.name.toLowerCase() === name.toLowerCase())) {
if (existingRSVPs.some((rsvp) => rsvp.name.toLowerCase() === name.toLowerCase())) {
return fail(400, { error: 'Name already exists for this event' }); return fail(400, { error: 'Name already exists for this event' });
} }
// Add RSVP to database // Prepare RSVPs to insert
await database.insert(rsvps).values({ const rsvpsToInsert = [
{
eventId: eventId, eventId: eventId,
name: name.trim(), name: name.trim(),
userId: userId, userId: userId,
createdAt: new Date() createdAt: new Date()
}
];
// Add guest entries
for (let i = 1; i <= numberOfGuests; i++) {
rsvpsToInsert.push({
eventId: eventId,
name: `${name.trim()}'s Guest #${i}`,
userId: userId,
createdAt: new Date()
}); });
}
// Insert all RSVPs
await database.insert(rsvps).values(rsvpsToInsert);
return { success: true, type: 'add' }; return { success: true, type: 'add' };
} catch (err) { } catch (err) {

View File

@@ -14,6 +14,8 @@
let isAddingRSVP = false; let isAddingRSVP = false;
let error = ''; let error = '';
let success = ''; let success = '';
let addGuests = false;
let numberOfGuests = 1;
// Use server-side data // Use server-side data
$: event = data.event; $: event = data.event;
@@ -22,7 +24,7 @@
// Handle form errors from server // Handle form errors from server
$: if (form?.error) { $: if (form?.error) {
error = form.error; error = String(form.error);
success = ''; success = '';
} }
@@ -31,6 +33,8 @@
success = 'RSVP added successfully!'; success = 'RSVP added successfully!';
error = ''; error = '';
newAttendeeName = ''; newAttendeeName = '';
addGuests = false;
numberOfGuests = 1;
} }
const eventId = $page.params.id; const eventId = $page.params.id;
@@ -179,7 +183,7 @@
return async ({ result, update }) => { return async ({ result, update }) => {
isAddingRSVP = false; isAddingRSVP = false;
if (result.type === 'failure') { if (result.type === 'failure') {
error = result.data?.error || 'Failed to add RSVP'; error = String(result.data?.error || 'Failed to add RSVP');
} }
update(); update();
}; };
@@ -203,9 +207,48 @@
/> />
</div> </div>
<!-- Add Guests Toggle -->
<div class="flex items-center space-x-3">
<input
id="addGuests"
type="checkbox"
bind:checked={addGuests}
class="h-4 w-4 rounded border-gray-300 text-violet-600 focus:ring-violet-500"
/>
<label for="addGuests" class="text-sm font-medium text-white">
Add guest users
</label>
</div>
<!-- Number of Guests Input -->
{#if addGuests}
<div>
<label for="numberOfGuests" class="mb-2 block text-sm font-semibold">
Number of Guests <span class="text-red-400">*</span>
</label>
<input
id="numberOfGuests"
name="numberOfGuests"
type="number"
bind:value={numberOfGuests}
min="1"
max="10"
class="border-dark-300 w-full rounded-sm border-2 px-4 py-3 text-slate-900 shadow-sm"
placeholder="Enter number of guests"
required
/>
<p class="mt-1 text-xs text-slate-400">
Guests will be added as "{newAttendeeName || 'Your Name'}'s Guest #1", "{newAttendeeName ||
'Your Name'}'s Guest #2", etc.
</p>
</div>
{/if}
<button <button
type="submit" type="submit"
disabled={isAddingRSVP || !newAttendeeName.trim()} disabled={isAddingRSVP ||
!newAttendeeName.trim() ||
(addGuests && numberOfGuests < 1)}
class=" hover:bg-violet-400/70' w-full rounded-sm border-2 border-violet-500 bg-violet-400/20 px-4 py-3 py-4 font-bold font-medium font-semibold text-white shadow-lg transition-all duration-200 hover:scale-105" class=" hover:bg-violet-400/70' w-full rounded-sm border-2 border-violet-500 bg-violet-400/20 px-4 py-3 py-4 font-bold font-medium font-semibold text-white shadow-lg transition-all duration-200 hover:scale-105"
> >
{#if isAddingRSVP} {#if isAddingRSVP}
@@ -215,6 +258,8 @@
></div> ></div>
Adding... Adding...
</div> </div>
{:else if addGuests && numberOfGuests > 0}
Join Event + {numberOfGuests} Guest{numberOfGuests > 1 ? 's' : ''}
{:else} {:else}
Join Event Join Event
{/if} {/if}
@@ -243,12 +288,22 @@
> >
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<div <div
class="flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold" class="flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold {attendee.name.includes(
"'s Guest"
)
? 'text-white-400 bg-violet-500/40'
: 'bg-violet-500/20 text-violet-400'}"
> >
{attendee.name.charAt(0).toUpperCase()} {attendee.name.charAt(0).toUpperCase()}
</div> </div>
<div> <div>
<p class="font-medium text-white">{attendee.name}</p> <p
class="font-medium text-white {attendee.name.includes("'s Guest")
? 'text-amber-300'
: ''}"
>
{attendee.name}
</p>
<p class="text-xs text-violet-400"> <p class="text-xs text-violet-400">
{(() => { {(() => {
const date = new Date(attendee.created_at); const date = new Date(attendee.created_at);
@@ -271,7 +326,7 @@
clearMessages(); clearMessages();
return async ({ result, update }) => { return async ({ result, update }) => {
if (result.type === 'failure') { if (result.type === 'failure') {
error = result.data?.error || 'Failed to remove RSVP'; error = String(result.data?.error || 'Failed to remove RSVP');
} }
update(); update();
}; };
@@ -300,7 +355,7 @@
</div> </div>
{/if} {/if}
</div> </div>
succcess: {success}
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="max-w-2xl"> <div class="max-w-2xl">
<button <button