Compare commits

...

2 Commits

Author SHA1 Message Date
42d1de933f WIP invitation layout
All checks were successful
Build Nginx-based docker image / build-static-assets (push) Successful in 1m52s
2025-06-08 09:10:35 +02:00
e23997e39a Define a simple page to display an invitation along with the guests included 2025-06-02 09:12:31 +02:00
3 changed files with 180 additions and 4 deletions

View File

@ -0,0 +1,109 @@
/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/
'use client';
import { AbstractApi } from '@/app/api/abstract-api';
import { Guest } from '@/app/lib/guest';
import { Invitation, InvitationSerializer } from '@/app/lib/invitation';
import { useParams } from 'next/navigation';
import { Dropdown } from 'primereact/dropdown';
import { TabPanel, TabView } from 'primereact/tabview';
import { useEffect, useState } from 'react';
function GuestForm({ guest }: { guest: Guest }) {
const [formResponses, setFormResponses] = useState<Guest['formResponses']>(guest.formResponses || {});
return (
<div className="flex flex-col gap-4">
<div>
<label className="block font-medium">Will you use the bus to get to the venue?</label>
<Dropdown
value={formResponses?.busNeeded ? 'Yes' : formResponses?.busNeeded === false ? 'No' : ''}
options={[
{ label: '', value: '' }, // Blank option as default
{ label: 'Yes', value: 'Yes' },
{ label: 'No', value: 'No' },
]}
onChange={(e) =>
setFormResponses((prev) => ({
...prev,
busNeeded: e.value === 'Yes',
}))
}
placeholder="Select an option"
className="w-full"
/>
</div>
<div>
<label className="block font-medium">Dietary Restrictions</label>
<textarea
value={formResponses?.dietaryRestrictions}
onChange={(e) =>
setFormResponses((prev) => ({
...prev,
dietaryRestrictions: e.target.value,
}))
}
className="w-full p-2 border border-gray-300 rounded-md"
rows={3}
/>
</div>
<div>
<label className="block font-medium">Meal Preference</label>
<Dropdown
value={formResponses?.mealPreference}
options={[
{ label: '', value: '' }, // Blank option as default
{ label: 'Meat', value: 'Meat' },
{ label: 'Fish', value: 'Fish' },
]}
onChange={(e) =>
setFormResponses((prev) => ({
...prev,
mealPreference: e.value,
}))
}
placeholder="Select an option"
className="w-full"
/>
</div>
</div>
);
}
export default function Page() {
const params = useParams<{ slug: string, id: string }>()
const serializer = new InvitationSerializer();
const api = new AbstractApi<Invitation>();
const [invitation, setInvitation] = useState<Invitation | null>(null);
useEffect(() => {
api.get(serializer, params.id, (invitation) => {
setInvitation(invitation);
});
}, [params.slug, params.id]);
return (
<div>
{invitation && (
<div>
<p className="mb-4 font-medium">
{invitation.guests.length} seats have been reserved for this invitation.
</p>
<TabView className="border border-gray-300 rounded-md">
{invitation.guests.map((guest) => (
<TabPanel header={guest.name} >
<GuestForm guest={guest} />
</TabPanel>
))}
</TabView>
</div>
)
}
</div >
);
}

View File

@ -0,0 +1,20 @@
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex flex-col min-h-screen">
{/* Full-width image */}
<div className="w-full">
<img
src="/path-to-your-image.jpg"
alt="Wedding Banner"
className="w-full h-64 object-cover"
/>
</div>
{/* Body */}
<main className="flex-grow p-6 mx-auto" style={{ maxWidth: '60%' }}>
{children}
</main>
</div>
);
}

View File

@ -14,25 +14,72 @@ export class Guest implements Entity {
color?: string;
status?: GuestStatus;
children?: Guest[];
formResponses?: {
busNeeded?: boolean;
dietaryRestrictions?: string;
mealPreference?: 'meat' | 'fish' | undefined;
};
constructor(id?: string, name?: string, group_name?: string, groupId?: string, color?: string, status?: GuestStatus, children?: Guest[]) {
constructor(
id?: string,
name?: string,
groupName?: string,
groupId?: string,
color?: string,
status?: GuestStatus,
children?: Guest[],
formResponses?: {
busNeeded?: boolean;
dietaryRestrictions?: string;
mealPreference?: 'meat' | 'fish' | undefined;
}
) {
this.id = id;
this.name = name;
this.group_name = group_name;
this.group_name = groupName;
this.groupId = groupId;
this.color = color;
this.status = status;
this.children = children;
this.formResponses = formResponses;
}
}
export class GuestSerializer implements Serializable<Guest> {
fromJson(data: any): Guest {
return new Guest(data.id, data.name, data.group?.name, data.group?.id, data.color, data.status, data.children);
return new Guest(
data.id,
data.name,
data.group?.name,
data.group?.id,
data.color,
data.status,
data.children,
data.form_responses
? {
busNeeded: data.form_responses.bus_needed,
dietaryRestrictions: data.form_responses.dietary_restrictions,
mealPreference: data.form_responses.meal_preference,
}
: undefined
);
}
toJson(guest: Guest): string {
return JSON.stringify({ guest: { name: guest.name, status: guest.status, group_id: guest.groupId } });
return JSON.stringify({
guest: {
name: guest.name,
status: guest.status,
group_id: guest.groupId,
form_responses: guest.formResponses
? {
bus_needed: guest.formResponses.busNeeded,
dietary_restrictions: guest.formResponses.dietaryRestrictions,
meal_preference: guest.formResponses.mealPreference,
}
: undefined,
},
});
}
apiPath(): string {