Compare commits
8 Commits
0da3eeb0d1
...
1bea961002
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1bea961002 | ||
a74ef0c8b6 | |||
87cf2d3bb2 | |||
5c1f8b0dbe | |||
24e60231d0 | |||
943da9c7e8 | |||
cc698102f2 | |||
87bd3fac4b |
119
app/[slug]/site/invitation/[id]/page.tsx
Normal file
119
app/[slug]/site/invitation/[id]/page.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { AbstractApi } from "@/app/api/abstract-api";
|
||||
import { Invitation, InvitationSerializer } from "@/app/lib/invitation";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FloatLabel } from "primereact/floatlabel";
|
||||
import { Dropdown } from "primereact/dropdown";
|
||||
import { Guest, GuestSerializer, GuestStatus } from "@/app/lib/guest";
|
||||
import { Button } from "primereact/button";
|
||||
|
||||
type FormResponse = {
|
||||
attendance: GuestStatus;
|
||||
};
|
||||
|
||||
function GuestForm({ guest, idx }: { guest: Guest, idx: number }) {
|
||||
const [response, setResponse] = useState<FormResponse>({
|
||||
attendance: guest.status!
|
||||
});
|
||||
|
||||
const [pendingChanges, setPendingChanges] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
|
||||
console.log('GuestForm response', response.attendance);
|
||||
|
||||
const attendanceOptions: { name: string, code: GuestStatus }[] = [
|
||||
{
|
||||
name: 'Attending',
|
||||
code: 'confirmed'
|
||||
},
|
||||
{
|
||||
name: 'Declined',
|
||||
code: 'declined'
|
||||
},
|
||||
{
|
||||
name: 'Tentative',
|
||||
code: 'tentative'
|
||||
}
|
||||
];
|
||||
|
||||
const api = new AbstractApi<Guest>();
|
||||
const serializer = new GuestSerializer();
|
||||
|
||||
const submitForm = () => {
|
||||
setSending(true);
|
||||
setPendingChanges(false);
|
||||
api.update(serializer, {
|
||||
id: guest.id!,
|
||||
status: response.attendance,
|
||||
}, () => setSending(false));
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={guest.id}
|
||||
className={`px-2 py-6 flex flex-col items-center ${idx !== 0 ? 'border-t border-gray-300' : ''}`}
|
||||
>
|
||||
<h2 className="m-2 text-xl font-semibold">{guest.name}</h2>
|
||||
|
||||
<Dropdown
|
||||
value={response.attendance}
|
||||
options={attendanceOptions}
|
||||
optionLabel="name"
|
||||
optionValue="code"
|
||||
className="rounded-md w-full max-w-xs border border-gray-300"
|
||||
checkmark={true}
|
||||
highlightOnSelect={false}
|
||||
onChange={(e) => {
|
||||
setPendingChanges(true);
|
||||
setResponse({ ...response, attendance: e.value })
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
label="Save"
|
||||
icon="pi pi-save"
|
||||
loading={sending}
|
||||
onClick={submitForm}
|
||||
disabled={!pendingChanges || sending}
|
||||
className="mt-4 max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const params = useParams<{ slug: string, id: string }>()
|
||||
|
||||
const [invitation, setInvitation] = useState<Invitation>();
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('slug', params.slug);
|
||||
|
||||
const api = new AbstractApi<Invitation>();
|
||||
const serializer = new InvitationSerializer();
|
||||
|
||||
api.get(serializer, params.id, (invitation: Invitation) => {
|
||||
setInvitation(invitation);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<h1 className="text-2xl font-bold mb-4">Invitation</h1>
|
||||
{invitation ? (
|
||||
<div>
|
||||
<p>We have reserved {invitation.guests.length} seats in your honor. Please, confirm attendance submitting the following form:</p>
|
||||
{invitation.guests.map((guest, idx) => (
|
||||
<GuestForm key={guest.id} guest={guest} idx={idx} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p>Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -29,7 +29,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
|
||||
<div className="max-w-4xl w-full h-full bg-[#f9f9f7] shadow-lg">
|
||||
<div
|
||||
className="max-w-4xl lg:m-6 m-3 lg:px-6 px-3 py-24 border-2 border-[#d3d3d1] rounded-xl text-[#958971] flex justify-center"
|
||||
className="max-w-4xl lg:m-6 m-3 lg:px-6 px-3 lg:py-24 py-2 border-2 border-[#d3d3d1] rounded-xl text-[#958971] flex justify-center"
|
||||
style={{ height: 'calc(100% - 3rem)' }}
|
||||
>
|
||||
{children}
|
||||
|
@ -6,8 +6,16 @@ import { AbstractApi } from '@/app/api/abstract-api';
|
||||
import { Website, WebsiteSerializer } from '@/app/lib/website';
|
||||
import { useState, useEffect } from 'react';
|
||||
import DOMPurify from "dompurify";
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
export default function Page() {
|
||||
const params = useParams<{ slug: string }>()
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('slug', params.slug);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [websiteContent, setWebsiteContent] = useState<string>("");
|
||||
|
||||
|
@ -49,6 +49,7 @@ export class AbstractApi<T extends Entity> implements Api<T> {
|
||||
body: serializable.toJson(object),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': getCsrfToken(),
|
||||
}
|
||||
}).then(callback)
|
||||
@ -61,6 +62,7 @@ export class AbstractApi<T extends Entity> implements Api<T> {
|
||||
body: serializable.toJson(object),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': getCsrfToken(),
|
||||
}
|
||||
})
|
||||
|
@ -5,8 +5,9 @@
|
||||
import { AbstractApi } from "@/app/api/abstract-api";
|
||||
import { Guest } from "@/app/lib/guest";
|
||||
import { Invitation, InvitationSerializer } from "@/app/lib/invitation";
|
||||
import { getSlug } from "@/app/lib/utils";
|
||||
import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { LinkIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
@ -24,6 +25,8 @@ function InvitationCard({ invitation, allGuests, onGuestAdded, onDestroy }: {
|
||||
const api = new AbstractApi<Invitation>();
|
||||
const serializer = new InvitationSerializer();
|
||||
|
||||
const iconClassName = "w-5 h-5 text-white absolute top-2 opacity-0 group-hover:opacity-100 cursor-pointer";
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
return dropTargetForElements({
|
||||
@ -53,8 +56,14 @@ function InvitationCard({ invitation, allGuests, onGuestAdded, onDestroy }: {
|
||||
className="relative flex items-center justify-center w-full bg-green-800 border border-green-900 group"
|
||||
style={{ aspectRatio: "1.618 / 1" }}
|
||||
>
|
||||
<LinkIcon
|
||||
className={`${iconClassName} right-8`}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(`https://${window.location.host}/${getSlug()}/site/invitation/${invitation.id}`);
|
||||
}}
|
||||
/>
|
||||
<TrashIcon
|
||||
className="w-5 h-5 text-white absolute top-2 right-2 opacity-0 group-hover:opacity-100 cursor-pointer"
|
||||
className={`${iconClassName} right-2`}
|
||||
onClick={() => {
|
||||
if (window.confirm("Are you sure you want to delete this invitation?")) {
|
||||
api.destroy(serializer, invitation, () => {
|
||||
@ -64,6 +73,7 @@ function InvitationCard({ invitation, allGuests, onGuestAdded, onDestroy }: {
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
{guests.length === 0 ? (
|
||||
<p className="text-center text-yellow-500 text-lg italic">
|
||||
(empty invitation)
|
||||
|
36
pnpm-lock.yaml
generated
36
pnpm-lock.yaml
generated
@ -40,10 +40,10 @@ importers:
|
||||
version: 3.2.6
|
||||
next:
|
||||
specifier: 15.3.3
|
||||
version: 15.3.3(@playwright/test@1.52.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704)
|
||||
version: 15.3.3(@playwright/test@1.53.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704)
|
||||
next-auth:
|
||||
specifier: 5.0.0-beta.28
|
||||
version: 5.0.0-beta.28(next@15.3.3(@playwright/test@1.52.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704)
|
||||
version: 5.0.0-beta.28(next@15.3.3(@playwright/test@1.53.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704)
|
||||
postcss:
|
||||
specifier: 8.5.4
|
||||
version: 8.5.4
|
||||
@ -77,7 +77,7 @@ importers:
|
||||
devDependencies:
|
||||
'@playwright/test':
|
||||
specifier: ^1.52.0
|
||||
version: 1.52.0
|
||||
version: 1.53.0
|
||||
'@types/bcrypt':
|
||||
specifier: ^5.0.2
|
||||
version: 5.0.2
|
||||
@ -341,8 +341,8 @@ packages:
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@playwright/test@1.52.0':
|
||||
resolution: {integrity: sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==}
|
||||
'@playwright/test@1.53.0':
|
||||
resolution: {integrity: sha512-15hjKreZDcp7t6TL/7jkAo6Df5STZN09jGiv5dbP9A6vMVncXRqE7/B2SncsyOwrkZRBH2i6/TPOL8BVmm3c7w==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
@ -1158,13 +1158,13 @@ packages:
|
||||
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
playwright-core@1.52.0:
|
||||
resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==}
|
||||
playwright-core@1.53.0:
|
||||
resolution: {integrity: sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.52.0:
|
||||
resolution: {integrity: sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==}
|
||||
playwright@1.53.0:
|
||||
resolution: {integrity: sha512-ghGNnIEYZC4E+YtclRn4/p6oYbdPiASELBIYkBXfaTVKreQUYbMUYQDwS12a8F0/HtIjr/CkGjtwABeFPGcS4Q==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
@ -1762,9 +1762,9 @@ snapshots:
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@playwright/test@1.52.0':
|
||||
'@playwright/test@1.53.0':
|
||||
dependencies:
|
||||
playwright: 1.52.0
|
||||
playwright: 1.53.0
|
||||
|
||||
'@popperjs/core@2.11.8': {}
|
||||
|
||||
@ -2461,13 +2461,13 @@ snapshots:
|
||||
|
||||
nanoid@3.3.8: {}
|
||||
|
||||
next-auth@5.0.0-beta.28(next@15.3.3(@playwright/test@1.52.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704):
|
||||
next-auth@5.0.0-beta.28(next@15.3.3(@playwright/test@1.53.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704):
|
||||
dependencies:
|
||||
'@auth/core': 0.39.1
|
||||
next: 15.3.3(@playwright/test@1.52.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704)
|
||||
next: 15.3.3(@playwright/test@1.53.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704)
|
||||
react: 19.0.0-rc-f38c22b244-20240704
|
||||
|
||||
next@15.3.3(@playwright/test@1.52.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704):
|
||||
next@15.3.3(@playwright/test@1.53.0)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704):
|
||||
dependencies:
|
||||
'@next/env': 15.3.3
|
||||
'@swc/counter': 0.1.3
|
||||
@ -2487,7 +2487,7 @@ snapshots:
|
||||
'@next/swc-linux-x64-musl': 15.3.3
|
||||
'@next/swc-win32-arm64-msvc': 15.3.3
|
||||
'@next/swc-win32-x64-msvc': 15.3.3
|
||||
'@playwright/test': 1.52.0
|
||||
'@playwright/test': 1.53.0
|
||||
sharp: 0.34.1
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
@ -2547,11 +2547,11 @@ snapshots:
|
||||
|
||||
pirates@4.0.6: {}
|
||||
|
||||
playwright-core@1.52.0: {}
|
||||
playwright-core@1.53.0: {}
|
||||
|
||||
playwright@1.52.0:
|
||||
playwright@1.53.0:
|
||||
dependencies:
|
||||
playwright-core: 1.52.0
|
||||
playwright-core: 1.53.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user