Compare commits

..

8 Commits

Author SHA1 Message Date
Renovate Bot
c5bd36d230 Update dependency primereact to v10.9.6
All checks were successful
Add copyright notice / copyright_notice (pull_request) Successful in 2m36s
Check usage of free licenses / build-static-assets (pull_request) Successful in 2m33s
Playwright Tests / test (pull_request) Successful in 46m21s
Build Nginx-based docker image / build-static-assets (push) Successful in 48m54s
2025-06-13 02:05:56 +00:00
a74ef0c8b6 Merge pull request 'Add a link to the invitation in the invitation card' (#262) from invitation-link into main
All checks were successful
Check usage of free licenses / build-static-assets (push) Successful in 59s
Playwright Tests / test (push) Successful in 6m35s
Build Nginx-based docker image / build-static-assets (push) Successful in 7m18s
Reviewed-on: #262
2025-06-12 21:08:05 +00:00
87cf2d3bb2 Merge pull request 'Display a basic invitation form on invites page' (#279) from invitation-form into main
Some checks failed
Check usage of free licenses / build-static-assets (push) Successful in 1m57s
Playwright Tests / test (push) Has been cancelled
Build Nginx-based docker image / build-static-assets (push) Has been cancelled
Reviewed-on: #279
2025-06-12 21:04:34 +00:00
5c1f8b0dbe Update the path of the invitation
All checks were successful
Playwright Tests / test (pull_request) Has been skipped
Check usage of free licenses / build-static-assets (pull_request) Successful in 57s
Add copyright notice / copyright_notice (pull_request) Successful in 1m15s
Build Nginx-based docker image / build-static-assets (push) Successful in 11m23s
2025-06-12 22:56:37 +02:00
24e60231d0 Display a basic invitation form on invites page
All checks were successful
Check usage of free licenses / build-static-assets (pull_request) Successful in 22s
Add copyright notice / copyright_notice (pull_request) Successful in 26s
Build Nginx-based docker image / build-static-assets (push) Successful in 4m22s
Playwright Tests / test (pull_request) Successful in 9m39s
2025-06-12 22:54:25 +02:00
943da9c7e8 Merge pull request 'Store the current slug when accessing the website directly' (#278) from fix/slug-website into main
All checks were successful
Check usage of free licenses / build-static-assets (push) Successful in 37s
Playwright Tests / test (push) Successful in 4m2s
Build Nginx-based docker image / build-static-assets (push) Successful in 4m23s
Reviewed-on: #278
2025-06-12 17:11:22 +00:00
cc698102f2 Store the current slug when accessing the website directly
All checks were successful
Check usage of free licenses / build-static-assets (pull_request) Successful in 30s
Add copyright notice / copyright_notice (pull_request) Successful in 58s
Build Nginx-based docker image / build-static-assets (push) Successful in 2m58s
Playwright Tests / test (pull_request) Successful in 4m25s
2025-06-12 19:06:23 +02:00
87bd3fac4b Add a link to the invitation in the invitation card
All checks were successful
Playwright Tests / test (pull_request) Has been skipped
Check usage of free licenses / build-static-assets (pull_request) Successful in 38s
Add copyright notice / copyright_notice (pull_request) Successful in 39s
Build Nginx-based docker image / build-static-assets (push) Successful in 2m20s
2025-06-02 00:08:42 +02:00
5 changed files with 142 additions and 3 deletions

View 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>
);
}

View File

@ -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}

View File

@ -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>("");

View File

@ -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(),
}
})

View File

@ -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)