Compare commits

..

43 Commits

Author SHA1 Message Date
Renovate Bot
8543c7bed6 Update dependency bcrypt to v6
Some checks failed
Add copyright notice / copyright_notice (pull_request) Successful in 7m35s
Check usage of free licenses / build-static-assets (pull_request) Successful in 6m10s
Playwright Tests / test (pull_request) Failing after 3m39s
Build Nginx-based docker image / build-static-assets (push) Successful in 54m22s
2025-06-09 02:06:00 +00:00
0dc182f712 Merge pull request 'Render the website inside an iframe to preview the changes being applied' (#273) from website-preview-iframe into main
All checks were successful
Check usage of free licenses / build-static-assets (push) Successful in 1m7s
Playwright Tests / test (push) Successful in 6m17s
Build Nginx-based docker image / build-static-assets (push) Successful in 6m42s
Reviewed-on: #273
2025-06-08 21:43:46 +00:00
0d35668efe Render the website inside an iframe to preview the changes being applied
All checks were successful
Check usage of free licenses / build-static-assets (pull_request) Successful in 1m39s
Add copyright notice / copyright_notice (pull_request) Successful in 2m15s
Build Nginx-based docker image / build-static-assets (push) Successful in 6m45s
Playwright Tests / test (pull_request) Successful in 6m46s
2025-06-08 23:36:32 +02:00
a60523b97a Merge pull request 'Implement WYSIWYG to edit the website content' (#272) from website-editor into main
All checks were successful
Check usage of free licenses / build-static-assets (push) Successful in 43s
Playwright Tests / test (push) Successful in 4m13s
Build Nginx-based docker image / build-static-assets (push) Successful in 6m14s
Reviewed-on: #272
2025-06-08 19:10:41 +00:00
55a2703577 Fix interface of API
All checks were successful
Check usage of free licenses / build-static-assets (pull_request) Successful in 2m4s
Add copyright notice / copyright_notice (pull_request) Successful in 2m37s
Playwright Tests / test (pull_request) Successful in 9m15s
Build Nginx-based docker image / build-static-assets (push) Successful in 10m53s
2025-06-08 20:59:45 +02:00
981f5079e3 Use dompurify to sanitize content before rendering
Some checks failed
Check usage of free licenses / build-static-assets (pull_request) Successful in 54s
Playwright Tests / test (pull_request) Failing after 1m9s
Add copyright notice / copyright_notice (pull_request) Successful in 1m14s
Build Nginx-based docker image / build-static-assets (push) Has been cancelled
2025-06-08 20:57:22 +02:00
0b8a444b39 Implement debouncing
Some checks failed
Check usage of free licenses / build-static-assets (pull_request) Successful in 50s
Playwright Tests / test (pull_request) Failing after 54s
Add copyright notice / copyright_notice (pull_request) Successful in 1m5s
Build Nginx-based docker image / build-static-assets (push) Failing after 2m51s
2025-06-08 20:52:16 +02:00
847666bfc8 Configure API to send website updates on change 2025-06-08 20:52:16 +02:00
97d47b339a Add copyright notice
Some checks failed
Check usage of free licenses / build-static-assets (pull_request) Successful in 48s
Add copyright notice / copyright_notice (pull_request) Successful in 1m56s
Build Nginx-based docker image / build-static-assets (push) Successful in 7m30s
Playwright Tests / test (pull_request) Failing after 10m28s
2025-06-08 18:04:42 +00:00
82f9cf8e4b Initial steps to create a WYSIWYG for the website
Some checks failed
Build Nginx-based docker image / build-static-assets (push) Has been cancelled
Check usage of free licenses / build-static-assets (pull_request) Successful in 1m20s
Add copyright notice / copyright_notice (pull_request) Successful in 1m28s
Playwright Tests / test (pull_request) Successful in 6m58s
2025-06-08 20:03:23 +02:00
2e7043bb3c Merge pull request 'Make website design responsive' (#271) from website-responsive into main
All checks were successful
Check usage of free licenses / build-static-assets (push) Successful in 31s
Build Nginx-based docker image / build-static-assets (push) Successful in 4m3s
Playwright Tests / test (push) Successful in 5m5s
Reviewed-on: #271
2025-06-08 17:26:00 +00:00
965b6ccd22 Make website design responsive
All checks were successful
Check usage of free licenses / build-static-assets (pull_request) Successful in 42s
Add copyright notice / copyright_notice (pull_request) Successful in 53s
Build Nginx-based docker image / build-static-assets (push) Successful in 3m51s
Playwright Tests / test (pull_request) Successful in 4m46s
2025-06-08 19:20:57 +02:00
1a5424fc8a Merge pull request 'Create the layout of the public website' (#270) from website into main
All checks were successful
Check usage of free licenses / build-static-assets (push) Successful in 42s
Playwright Tests / test (push) Successful in 3m26s
Build Nginx-based docker image / build-static-assets (push) Successful in 4m5s
Reviewed-on: #270
2025-06-08 16:53:59 +00:00
9d59485ad6 Create the layout of the public website
All checks were successful
Check usage of free licenses / build-static-assets (pull_request) Successful in 49s
Add copyright notice / copyright_notice (pull_request) Successful in 1m3s
Build Nginx-based docker image / build-static-assets (push) Successful in 3m23s
Playwright Tests / test (pull_request) Successful in 4m6s
2025-06-08 18:49:37 +02:00
171de9f1aa Merge pull request 'Create new guest without reloading guests list' (#269) from create-guest-without-reload into main
All checks were successful
Check usage of free licenses / build-static-assets (push) Successful in 43s
Playwright Tests / test (push) Successful in 4m29s
Build Nginx-based docker image / build-static-assets (push) Successful in 4m46s
Reviewed-on: #269
2025-06-08 15:48:35 +00:00
ee2e33ee51 Create new guest without reloading guests list
All checks were successful
Check usage of free licenses / build-static-assets (pull_request) Successful in 33s
Add copyright notice / copyright_notice (pull_request) Successful in 53s
Playwright Tests / test (pull_request) Successful in 5m42s
Build Nginx-based docker image / build-static-assets (push) Successful in 6m11s
2025-06-08 17:42:19 +02:00
7f68756d16 Merge pull request 'Update pnpm to v10.11.1' (#267) from renovate/pnpm-10.x into main
All checks were successful
Check usage of free licenses / build-static-assets (push) Successful in 49s
Playwright Tests / test (push) Successful in 4m35s
Build Nginx-based docker image / build-static-assets (push) Successful in 6m37s
Reviewed-on: #267
2025-06-08 11:50:33 +00:00
671ae8cb6e Merge branch 'main' into renovate/pnpm-10.x
All checks were successful
Check usage of free licenses / build-static-assets (pull_request) Successful in 1m23s
Add copyright notice / copyright_notice (pull_request) Successful in 1m37s
Playwright Tests / test (pull_request) Successful in 11m1s
Build Nginx-based docker image / build-static-assets (push) Successful in 12m13s
2025-06-08 11:38:16 +00:00
730d3ad1a1 Merge pull request 'Restore execution of playwright tests' (#130) from restore-playwright into main
Some checks failed
Check usage of free licenses / build-static-assets (push) Successful in 50s
Playwright Tests / test (push) Successful in 10m58s
Build Nginx-based docker image / build-static-assets (push) Has been cancelled
Reviewed-on: #130
2025-06-08 11:37:29 +00:00
f51b646430 Copy static and build files int the standalone directory
All checks were successful
Check usage of free licenses / build-static-assets (pull_request) Successful in 50s
Add copyright notice / copyright_notice (pull_request) Successful in 1m0s
Playwright Tests / test (pull_request) Successful in 4m47s
Build Nginx-based docker image / build-static-assets (push) Successful in 5m29s
2025-06-08 13:30:12 +02:00
f3799a26b6 Remove debug logs 2025-06-08 13:21:45 +02:00
1c0570d1a8 Fix additional use of localstorage in the server side
Some checks failed
Check usage of free licenses / build-static-assets (pull_request) Successful in 58s
Add copyright notice / copyright_notice (pull_request) Successful in 1m19s
Build Nginx-based docker image / build-static-assets (push) Successful in 5m39s
Playwright Tests / test (pull_request) Failing after 9m0s
2025-06-08 13:12:51 +02:00
7719929e43 Prevent slug from being null
Some checks failed
Check usage of free licenses / build-static-assets (pull_request) Successful in 25s
Add copyright notice / copyright_notice (pull_request) Successful in 32s
Build Nginx-based docker image / build-static-assets (push) Successful in 5m46s
Playwright Tests / test (pull_request) Failing after 14m45s
2025-06-08 12:42:46 +02:00
2c3480f980 Fix use of localstorage in server side
Some checks failed
Check usage of free licenses / build-static-assets (pull_request) Successful in 48s
Playwright Tests / test (pull_request) Failing after 54s
Add copyright notice / copyright_notice (pull_request) Successful in 1m4s
Build Nginx-based docker image / build-static-assets (push) Failing after 2m34s
2025-06-08 12:33:44 +02:00
22a2cba3fe Fix the path used for checking upstatus 2025-06-08 12:19:01 +02:00
01717b392c Revert "Try localhost"
This reverts commit e0da10c0006d8f984771dbdc38508b69f902835e.
2025-06-08 11:49:44 +02:00
85f918b397 Fix the waiting URL
Some checks failed
Check usage of free licenses / build-static-assets (pull_request) Successful in 33s
Add copyright notice / copyright_notice (pull_request) Successful in 47s
Playwright Tests / test (pull_request) Failing after 5m38s
Build Nginx-based docker image / build-static-assets (push) Successful in 6m4s
2025-06-08 11:42:59 +02:00
e0da10c000 Try localhost
Some checks failed
Check usage of free licenses / build-static-assets (pull_request) Successful in 1m40s
Add copyright notice / copyright_notice (pull_request) Successful in 1m40s
Playwright Tests / test (pull_request) Failing after 13m49s
Build Nginx-based docker image / build-static-assets (push) Successful in 14m7s
2025-06-08 11:10:00 +02:00
8b0cab93c9 Fix environment variable name
Some checks failed
Check usage of free licenses / build-static-assets (pull_request) Successful in 2m10s
Add copyright notice / copyright_notice (pull_request) Successful in 4m0s
Playwright Tests / test (pull_request) Failing after 13m38s
Build Nginx-based docker image / build-static-assets (push) Successful in 15m8s
2025-06-08 10:48:51 +02:00
6490e585ee Run the server in 127.0.0.1 host
Some checks failed
Check usage of free licenses / build-static-assets (pull_request) Successful in 5m14s
Add copyright notice / copyright_notice (pull_request) Successful in 6m34s
Playwright Tests / test (pull_request) Failing after 20m27s
Build Nginx-based docker image / build-static-assets (push) Has been cancelled
2025-06-08 10:27:13 +02:00
d6f603d70c Run standalone node server instead of development build
Some checks failed
Check usage of free licenses / build-static-assets (pull_request) Successful in 7m7s
Add copyright notice / copyright_notice (pull_request) Successful in 7m15s
Playwright Tests / test (pull_request) Failing after 21m40s
Build Nginx-based docker image / build-static-assets (push) Successful in 23m2s
2025-06-08 10:03:57 +02:00
acdbee40bf Wait for the service to be ready
Some checks failed
Add copyright notice / copyright_notice (pull_request) Successful in 2m34s
Check usage of free licenses / build-static-assets (pull_request) Successful in 2m53s
Playwright Tests / test (pull_request) Failing after 13m47s
Build Nginx-based docker image / build-static-assets (push) Has been cancelled
2025-06-08 09:47:48 +02:00
4455c0e6d4 Run playwright tests in verbose mode
Some checks failed
Add copyright notice / copyright_notice (pull_request) Successful in 2m0s
Check usage of free licenses / build-static-assets (pull_request) Successful in 2m21s
Build Nginx-based docker image / build-static-assets (push) Successful in 13m39s
Playwright Tests / test (pull_request) Has been cancelled
2025-06-08 09:33:11 +02:00
fed86088c3 Use pnpm to build and run the service
Some checks failed
Check usage of free licenses / build-static-assets (pull_request) Successful in 1m25s
Add copyright notice / copyright_notice (pull_request) Successful in 1m28s
Build Nginx-based docker image / build-static-assets (push) Successful in 6m24s
Playwright Tests / test (pull_request) Failing after 20m54s
2025-06-08 09:18:10 +02:00
Renovate Bot
e2c0f91b23 Update pnpm to v10.11.1
Some checks failed
Build Nginx-based docker image / build-static-assets (push) Failing after 51s
Playwright Tests / test (pull_request) Has been skipped
Add copyright notice / copyright_notice (pull_request) Failing after 32s
Check usage of free licenses / build-static-assets (pull_request) Failing after 21s
2025-06-04 02:08:28 +00:00
3748805377 WIP specs for guest creation
Some checks failed
Check usage of free licenses / build-static-assets (pull_request) Successful in 40s
Add copyright notice / copyright_notice (pull_request) Successful in 54s
Build Nginx-based docker image / build-static-assets (push) Successful in 5m28s
Playwright Tests / test (pull_request) Failing after 14m3s
2025-06-01 23:46:25 +02:00
e8551dd877 Upgrade playwright to the latest version
Some checks failed
Build Nginx-based docker image / build-static-assets (push) Failing after 1m23s
Add copyright notice / copyright_notice (pull_request) Successful in 2m20s
Check usage of free licenses / build-static-assets (pull_request) Successful in 2m20s
Playwright Tests / test (pull_request) Failing after 12m4s
2025-06-01 23:19:12 +02:00
0455fcd8da Enhance the specs for groups 2025-06-01 23:13:05 +02:00
19232477fd Enhance guests tests
Some checks failed
Add copyright notice / copyright_notice (pull_request) Successful in 3m38s
Check usage of free licenses / build-static-assets (pull_request) Successful in 3m53s
Build Nginx-based docker image / build-static-assets (push) Successful in 20m22s
Playwright Tests / test (pull_request) Failing after 23m25s
2025-06-01 22:50:41 +02:00
15ccccd927 Merge remote-tracking branch 'origin/main' into enhance-specs 2025-06-01 22:28:53 +02:00
0b9de712ff Restore basic Playwright tests
Some checks failed
Check usage of free licenses / build-static-assets (pull_request) Successful in 2m16s
Add copyright notice / copyright_notice (pull_request) Successful in 2m31s
Build Nginx-based docker image / build-static-assets (push) Successful in 14m52s
Playwright Tests / test (pull_request) Failing after 18m31s
2025-06-01 22:26:10 +02:00
4c7548a664 Merge branch 'main' into restore-playwright
Some checks failed
Add copyright notice / copyright_notice (pull_request) Successful in 40s
Check usage of free licenses / build-static-assets (pull_request) Successful in 44s
Build Nginx-based docker image / build-static-assets (push) Successful in 2m51s
Playwright Tests / test (pull_request) Failing after 15m12s
2025-06-01 13:53:56 +00:00
ff2a1ccf7f Restore execution of playwright tests
Some checks failed
Add copyright notice / copyright_notice (pull_request) Successful in 31s
Check usage of free licenses / build-static-assets (pull_request) Successful in 1m11s
Playwright Tests / test (pull_request) Failing after 8m21s
2024-12-02 08:14:36 +01:00
25 changed files with 1238 additions and 105 deletions

View File

@ -9,7 +9,6 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
test: test:
if: false
timeout-minutes: 60 timeout-minutes: 60
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
@ -22,10 +21,15 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm install -g pnpm && pnpm install run: npm install -g pnpm && pnpm install
- name: Build the service that will be tested - name: Build the service that will be tested
run: npm run build run: |
pnpm run build
cp -r public .next/standalone/
cp -r .next/static .next/standalone/.next/
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps run: pnpm exec playwright install --with-deps
- name: Run the service that will be tested - name: Run the service that will be tested
run: npm run start & run: HOSTNAME=127.0.0.1 node .next/standalone/server.js &
- name: Wait for the service to be ready
run: npx wait-on http://127.0.0.1:3000/default/ --timeout 30000
- name: Run Playwright tests - name: Run Playwright tests
run: pnpm exec playwright test run: pnpm exec playwright test

View File

@ -15,37 +15,43 @@ import SkeletonTable from '@/app/ui/guests/skeleton-row';
import GuestsTable from '@/app/ui/guests/table'; import GuestsTable from '@/app/ui/guests/table';
import { TabPanel, TabView } from 'primereact/tabview'; import { TabPanel, TabView } from 'primereact/tabview';
import { Toast } from 'primereact/toast'; import { Toast } from 'primereact/toast';
import { Suspense, useRef, useState } from 'react'; import { Suspense, useEffect, useRef, useState } from 'react';
import InvitationsBoard from '@/app/ui/invitations/board'; import InvitationsBoard from '@/app/ui/invitations/board';
import { Invitation, InvitationSerializer } from '@/app/lib/invitation'; import { Invitation, InvitationSerializer } from '@/app/lib/invitation';
export default function Page() { export default function Page() {
const [slug, setSlug] = useState<string>("default");
useEffect(() => {
setSlug(getSlug());
refreshGroups();
refreshGuests();
refreshInvitations();
}, []);
const toast = useRef<Toast>(null); const toast = useRef<Toast>(null);
function refreshGuests() { function refreshGuests() {
new AbstractApi<Guest>().getAll(new GuestSerializer(), (objects: Guest[]) => { new AbstractApi<Guest>().getAll(new GuestSerializer(), (objects: Guest[]) => {
setGuests(objects); setGuests(objects);
setGuestsLoaded(true);
}); });
} }
function refreshGroups() { function refreshGroups() {
new AbstractApi<Group>().getAll(new GroupSerializer(), (objects: Group[]) => { new AbstractApi<Group>().getAll(new GroupSerializer(), (objects: Group[]) => {
setGroups(objects); setGroups(objects);
setGroupsLoaded(true);
}); });
} }
function refreshInvitations() { function refreshInvitations() {
new AbstractApi<Invitation>().getAll(new InvitationSerializer(), (objects: Invitation[]) => { new AbstractApi<Invitation>().getAll(new InvitationSerializer(), (objects: Invitation[]) => {
setInvitations(objects); setInvitations(objects);
setInvitationsLoaded(true);
}); });
} }
function resetAffinities() { function resetAffinities() {
fetch(`/api/${getSlug()}/groups/affinities/reset`, { fetch(`/api/${slug}/groups/affinities/reset`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@ -72,23 +78,17 @@ export default function Page() {
}); });
} }
const [groupsLoaded, setGroupsLoaded] = useState(false);
const [groups, setGroups] = useState<Array<Group>>([]); const [groups, setGroups] = useState<Array<Group>>([]);
const [groupBeingEdited, setGroupBeingEdited] = useState<Group | undefined>(undefined); const [groupBeingEdited, setGroupBeingEdited] = useState<Group | undefined>(undefined);
const [groupAffinitiesBeingEditted, setGroupAffinitiesBeingEditted] = useState<Group | undefined>(undefined); const [groupAffinitiesBeingEditted, setGroupAffinitiesBeingEditted] = useState<Group | undefined>(undefined);
const [guestsLoaded, setGuestsLoaded] = useState(false);
const [guests, setGuests] = useState<Array<Guest>>([]); const [guests, setGuests] = useState<Array<Guest>>([]);
const [guestBeingEdited, setGuestBeingEdited] = useState<Guest | undefined>(undefined); const [guestBeingEdited, setGuestBeingEdited] = useState<Guest | undefined>(undefined);
const [invitationsLoaded, setInvitationsLoaded] = useState(false);
const [invitations, setInvitations] = useState<Array<Invitation>>([]); const [invitations, setInvitations] = useState<Array<Invitation>>([]);
const [invitationBeingEdited, setInvitationBeingEdited] = useState<Invitation | undefined>(undefined);
!groupsLoaded && refreshGroups();
!guestsLoaded && refreshGuests();
!invitationsLoaded && refreshInvitations();
return ( return (
<div className="w-full"> <div className="w-full">
@ -99,7 +99,10 @@ export default function Page() {
<GuestFormDialog <GuestFormDialog
key={guestBeingEdited?.id} key={guestBeingEdited?.id}
groups={groups} groups={groups}
onCreate={() => { refreshGuests(); setGuestBeingEdited(undefined) }} onCreate={(newGuest) => {
setGuests([newGuest!, ...guests]);
setGuestBeingEdited(undefined) ;
}}
guest={guestBeingEdited} guest={guestBeingEdited}
visible={guestBeingEdited !== undefined} visible={guestBeingEdited !== undefined}
onHide={() => { setGuestBeingEdited(undefined) }} onHide={() => { setGuestBeingEdited(undefined) }}

View File

@ -0,0 +1,65 @@
/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/
'use client'
import { AbstractApi } from '@/app/api/abstract-api';
import { getSlug } from '@/app/lib/utils';
import { Website, WebsiteSerializer } from '@/app/lib/website';
import { useEffect, useState } from 'react';
import Tiptap from '../../../components/Tiptap';
export default function Page() {
const [website, setWebsite] = useState<Website>()
const api = new AbstractApi<Website>();
const serializer = new WebsiteSerializer();
const [slug, setSlug] = useState<string>("default");
useEffect(() => { setSlug(getSlug()) }, []);
const [iframeKey, setIframeKey] = useState<number>(Math.random());
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | null>(null);
useEffect(() => {
api.get(serializer, undefined, (loadedWebsite) => {
setWebsite(loadedWebsite);
});
}, []);
const updateWebsite = (newContent: string) => {
// Debounce API update: send after 500ms of no further changes
if (timeoutId) {
clearTimeout(timeoutId);
}
setTimeoutId(
setTimeout(() => {
api.update(serializer, new Website('', newContent), () => {
setIframeKey(Math.random()); // Change key to force iframe reload
});
}, 500)
);
}
return (
<div className="flex">
<div className="w-1/2 border rounded-lg p-4">
<Tiptap
key={website?.content ?? 'empty'}
content={website?.content || ''}
onUpdate={updateWebsite}
/>
</div>
<div className="w-1/2 border rounded-lg p-4 ml-4">
<iframe
key={iframeKey}
src={`/${slug}/site`}
title="Website Preview"
className="w-full h-[80vh] rounded"
/>
</div>
</div>
);
}

View File

@ -16,11 +16,11 @@ export default async function Page() {
if (getCsrfToken() == 'unknown') { if (getCsrfToken() == 'unknown') {
retrieveCSRFToken(); retrieveCSRFToken();
} }
}, []);
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
localStorage.setItem('slug', await params.slug); localStorage.setItem('slug', params.slug);
} }
}, []);
return ( return (
<main className="flex min-h-screen flex-col p-6"> <main className="flex min-h-screen flex-col p-6">

View File

@ -0,0 +1,41 @@
/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/
import SideNav from '@/app/ui/dashboard/sidenav';
import Image from 'next/image';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen flex-col">
<div className="w-full lg:h-72 h-36 relative">
<Image
src="/header.png"
alt="Header"
fill
style={{ objectFit: 'cover', objectPosition: 'center', zIndex: 0 }}
priority
/>
</div>
<div className="flex-grow flex items-center justify-center lg:p-24 py-8 bg-[#e1d5c7] relative">
<div className="absolute left-1/2 lg:top-24 top-8 z-10 -translate-x-1/2 -translate-y-1/2 flex justify-center w-full pointer-events-none w-12 h-12 lg:w-24 lg:h-24">
<Image
src="/stamp.png"
alt="Stamp"
width={120}
height={120}
className="object-contain"
priority
/>
</div>
<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"
style={{ height: 'calc(100% - 3rem)' }}
>
{children}
</div>
</div>
</div>
</div>
);
}

26
app/[slug]/site/page.tsx Normal file
View File

@ -0,0 +1,26 @@
/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/
'use client'
import { AbstractApi } from '@/app/api/abstract-api';
import { Website, WebsiteSerializer } from '@/app/lib/website';
import { useState, useEffect } from 'react';
import DOMPurify from "dompurify";
export default function Page() {
const [websiteContent, setWebsiteContent] = useState<string>("");
const api = new AbstractApi<Website>();
const serializer = new WebsiteSerializer();
useEffect(() => {
api.get(serializer, undefined, (loadedWebsite) => {
setWebsiteContent(loadedWebsite.content || "");
});
}, []);
return (
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(websiteContent) }} />
);
}

View File

@ -6,7 +6,7 @@ import { getCsrfToken, getSlug } from '@/app/lib/utils';
export interface Api<T extends Entity> { export interface Api<T extends Entity> {
getAll(serializable: Serializable<T>, callback: (objets: T[]) => void): void; getAll(serializable: Serializable<T>, callback: (objets: T[]) => void): void;
get(serializable: Serializable<T>, id: string, callback: (object: T) => void): void; get(serializable: Serializable<T>, id: string, callback: (object: T) => void): void;
create(serializable: Serializable<T>, object: T, callback: () => void): void; create(serializable: Serializable<T>, object: T, callback: (object: T) => void): void;
update(serializable: Serializable<T>, object: T, callback: () => void): void; update(serializable: Serializable<T>, object: T, callback: () => void): void;
destroy(serializable: Serializable<T>, object: T, callback: () => void): void; destroy(serializable: Serializable<T>, object: T, callback: () => void): void;
} }
@ -30,8 +30,9 @@ export class AbstractApi<T extends Entity> implements Api<T> {
}); });
} }
get(serializable: Serializable<T>, id: string, callback: (object: T) => void): void { get(serializable: Serializable<T>, id: (string | undefined), callback: (object: T) => void): void {
fetch(`/api/${getSlug()}/${serializable.apiPath()}/${id}`) const endpoint = id ? `/api/${getSlug()}/${serializable.apiPath()}/${id}` : `/api/${getSlug()}/${serializable.apiPath()}`;
fetch(endpoint)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
callback(serializable.fromJson(data)); callback(serializable.fromJson(data));
@ -41,7 +42,9 @@ export class AbstractApi<T extends Entity> implements Api<T> {
} }
update(serializable: Serializable<T>, object: T, callback: () => void): void { update(serializable: Serializable<T>, object: T, callback: () => void): void {
fetch(`/api/${getSlug()}/${serializable.apiPath()}/${object.id}`, { const endpoint = object.id ? `/api/${getSlug()}/${serializable.apiPath()}/${object.id}` : `/api/${getSlug()}/${serializable.apiPath()}`;
fetch(endpoint, {
method: 'PUT', method: 'PUT',
body: serializable.toJson(object), body: serializable.toJson(object),
headers: { headers: {

22
app/components/Tiptap.tsx Normal file
View File

@ -0,0 +1,22 @@
/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/
'use client'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
const Tiptap = ({ content, onUpdate }: { content: string, onUpdate: (newContent:string) => void }) => {
const editor = useEditor({
extensions: [StarterKit],
content: content || '<p>Type something here...</p>',
onUpdate({ editor }) {
onUpdate(editor.getHTML());
},
immediatelyRender: false,
})
return <EditorContent editor={editor} />
}
export default Tiptap

View File

@ -2,6 +2,7 @@
import { Serializable } from "../api/abstract-api"; import { Serializable } from "../api/abstract-api";
import { Entity } from "./definitions"; import { Entity } from "./definitions";
import { Group } from "./group";
export const guestStatuses = ['considered', 'invited', 'confirmed', 'declined', 'tentative'] as const; export const guestStatuses = ['considered', 'invited', 'confirmed', 'declined', 'tentative'] as const;
export type GuestStatus = typeof guestStatuses[number]; export type GuestStatus = typeof guestStatuses[number];
@ -9,30 +10,28 @@ export type GuestStatus = typeof guestStatuses[number];
export class Guest implements Entity { export class Guest implements Entity {
id?: string; id?: string;
name?: string; name?: string;
group_name?: string;
groupId?: string;
color?: string; color?: string;
status?: GuestStatus; status?: GuestStatus;
children?: Guest[]; children?: Guest[];
group?: Group;
constructor(id?: string, name?: string, group_name?: string, groupId?: string, color?: string, status?: GuestStatus, children?: Guest[]) { constructor(id?: string, name?: string, color?: string, status?: GuestStatus, children?: Guest[], Group?: Group) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.group_name = group_name;
this.groupId = groupId;
this.color = color; this.color = color;
this.status = status; this.status = status;
this.children = children; this.children = children;
this.group = Group;
} }
} }
export class GuestSerializer implements Serializable<Guest> { export class GuestSerializer implements Serializable<Guest> {
fromJson(data: any): 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.color, data.status, data.children, new Group(data.group?.id, data.group?.name));
} }
toJson(guest: Guest): string { 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.group?.id } });
} }
apiPath(): string { apiPath(): string {

View File

@ -15,7 +15,7 @@ export class Invitation implements Entity {
export class InvitationSerializer { export class InvitationSerializer {
fromJson(data: any): Invitation { fromJson(data: any): Invitation {
return new Invitation(data.id, (data.guests || []).map((guest: any) => new Guest(guest.id, guest.name, guest.group_name, guest.groupId, guest.color, guest.status, guest.children))); return new Invitation(data.id, (data.guests || []).map((guest: any) => new Guest(guest.id, guest.name, guest.color, guest.status, guest.children)));
} }
toJson(invitation: Invitation): string { toJson(invitation: Invitation): string {

View File

@ -2,6 +2,7 @@
import { Serializable } from "../api/abstract-api"; import { Serializable } from "../api/abstract-api";
import { Entity } from "./definitions"; import { Entity } from "./definitions";
import { Group } from "./group";
import { Guest } from "./guest"; import { Guest } from "./guest";
export type Discomfort = { export type Discomfort = {
@ -53,10 +54,10 @@ export class TableSimulationSerializer implements Serializable<TableSimulation>
return { return {
id: guest.id, id: guest.id,
name: guest.name, name: guest.name,
group_id: guest.groupId,
color: guest.color, color: guest.color,
status: guest.status, status: guest.status,
children: guest.children, children: guest.children,
group: new Group(guest.group?.id, guest.group?.name)
} }
}), }),
discomfort: table.discomfort, discomfort: table.discomfort,

30
app/lib/website.tsx Normal file
View File

@ -0,0 +1,30 @@
/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/
import { Serializable } from "../api/abstract-api";
import { Entity } from "./definitions";
export class Website implements Entity {
id?: string;
content?: string;
constructor(id: string, content: string) {
this.id = id;
this.content = content;
}
}
export class WebsiteSerializer implements Serializable<Website> {
fromJson(data: any): Website {
return new Website("", data.content);
}
toJson(website: Website): string {
return JSON.stringify({ website: { content: website.content } });
}
apiPath(): string {
return 'website';
}
}

View File

@ -5,7 +5,6 @@
import { AbstractApi } from '@/app/api/abstract-api'; import { AbstractApi } from '@/app/api/abstract-api';
import { TableArrangement } from '@/app/lib/definitions'; import { TableArrangement } from '@/app/lib/definitions';
import { TableSimulation, TableSimulationSerializer } from '@/app/lib/tableSimulation'; import { TableSimulation, TableSimulationSerializer } from '@/app/lib/tableSimulation';
import { getSlug } from '@/app/lib/utils';
import { Table } from '@/app/ui/components/table'; import { Table } from '@/app/ui/components/table';
import { lusitana } from '@/app/ui/fonts'; import { lusitana } from '@/app/ui/fonts';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';

View File

@ -15,14 +15,14 @@ import { useState } from 'react';
export default function GuestFormDialog({ groups, onCreate, onHide, guest, visible }: { export default function GuestFormDialog({ groups, onCreate, onHide, guest, visible }: {
groups: Group[], groups: Group[],
onCreate?: () => void, onCreate?: (guest: Guest) => void,
onHide: () => void, onHide: () => void,
guest?: Guest, guest?: Guest,
visible: boolean, visible: boolean,
}) { }) {
const [name, setName] = useState(guest?.name || ''); const [name, setName] = useState(guest?.name || '');
const [group, setGroup] = useState(guest?.groupId || null); const [group, setGroup] = useState(guest?.group?.id || null);
const [status, setStatus] = useState<GuestStatus | null>(guest?.status || null); const [status, setStatus] = useState<GuestStatus | null>(guest?.status || null);
const api = new AbstractApi<Guest>(); const api = new AbstractApi<Guest>();
@ -41,17 +41,17 @@ export default function GuestFormDialog({ groups, onCreate, onHide, guest, visib
if (guest?.id !== undefined) { if (guest?.id !== undefined) {
guest.name = name; guest.name = name;
guest.groupId = group; guest.group!.id = group;
guest.status = status; guest.status = status;
api.update(serializer, guest, () => { api.update(serializer, guest, () => {
resetForm(); resetForm();
onCreate && onCreate(); onCreate && onCreate(guest);
}); });
} else { } else {
api.create(serializer, new Guest(undefined, name, undefined, group, undefined, status), ()=> { api.create(serializer, new Guest(undefined, name, undefined, status, [], groups.find((g) => g.id === group)), (newGuest)=> {
resetForm(); resetForm();
onCreate && onCreate(); onCreate && onCreate(newGuest);
}); });
} }
} }

View File

@ -15,6 +15,9 @@ export default function LoginForm() {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [slug, setSlug] = useState<string>("default");
useEffect(() => {setSlug(getSlug())}, []);
const router = useRouter(); const router = useRouter();
const [currentUser, setCurrentUser] = useState<User | null>(null); const [currentUser, setCurrentUser] = useState<User | null>(null);
@ -41,7 +44,7 @@ export default function LoginForm() {
password: password, password: password,
onLogin: (user) => { onLogin: (user) => {
setCurrentUser(user); setCurrentUser(user);
router.push(`${getSlug()}/dashboard`) router.push(`${slug}/dashboard`)
} }
})}> })}>
Sign in Sign in

View File

@ -17,7 +17,9 @@ export default function RegistrationForm() {
const [email, setEmail] = useState<string>(""); const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>(""); const [password, setPassword] = useState<string>("");
const [passwordConfirmation, setPasswordConfirmation] = useState<string>(""); const [passwordConfirmation, setPasswordConfirmation] = useState<string>("");
const [slug, setSlug] = useState<string>(getSlug());
const [slug, setSlug] = useState<string>("default");
useEffect(() => { setSlug(getSlug()) }, []);
const [captchaId, setCaptchaId] = useState<string>(""); const [captchaId, setCaptchaId] = useState<string>("");
const [captchaUrl, setCaptchaUrl] = useState<string>(""); const [captchaUrl, setCaptchaUrl] = useState<string>("");

View File

@ -6,24 +6,30 @@ import {
UserGroupIcon, UserGroupIcon,
RectangleGroupIcon, RectangleGroupIcon,
BanknotesIcon, BanknotesIcon,
GlobeAltIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import clsx from 'clsx'; import clsx from 'clsx';
import { getSlug } from '@/app/lib/utils'; import { getSlug } from '@/app/lib/utils';
import { useEffect, useState } from 'react';
// Map of links to display in the side navigation. // Map of links to display in the side navigation.
// Depending on the size of the application, this would be stored in a database. // Depending on the size of the application, this would be stored in a database.
const links = [
{ name: 'Guests', href: `/${getSlug()}/dashboard/guests`, icon: UserGroupIcon },
{ name: 'Expenses', href: `/${getSlug()}/dashboard/expenses`, icon: BanknotesIcon },
{ name: 'Table distributions', href: `/${getSlug()}/dashboard/tables`, icon: RectangleGroupIcon },
];
export default function NavLinks() { export default function NavLinks() {
const pathname = usePathname(); const pathname = usePathname();
const [slug, setSlug] = useState<string>("default");
useEffect(() => { setSlug(getSlug()) }, []);
const links = [
{ name: 'Guests', href: `/${slug}/dashboard/guests`, icon: UserGroupIcon },
{ name: 'Expenses', href: `/${slug}/dashboard/expenses`, icon: BanknotesIcon },
{ name: 'Table distributions', href: `/${slug}/dashboard/tables`, icon: RectangleGroupIcon },
{ name: 'Website builder', href: `/${slug}/dashboard/website`, icon: GlobeAltIcon },
];
return ( return (
<> <>
{links.map((link) => { {links.map((link) => {

View File

@ -9,15 +9,23 @@ import { gloriaHallelujah } from '@/app/ui/fonts';
import { logout } from '@/app/api/authentication'; import { logout } from '@/app/api/authentication';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { getSlug } from '@/app/lib/utils'; import { getSlug } from '@/app/lib/utils';
import { useEffect, useState } from 'react';
export default function SideNav() { export default function SideNav() {
const router = useRouter(); const router = useRouter();
const [slug, setSlug] = useState<string>("default");
useEffect(() => { setSlug(getSlug()) }, []);
const [currentUser, setCurrentUser] = useState<{ email: string } | null>(null);
useEffect(() => { setCurrentUser(JSON.parse(localStorage.getItem('currentUser') || '{}')) }, []);
return ( return (
<div className="flex h-full flex-col px-3 py-4 md:px-2"> <div className="flex h-full flex-col px-3 py-4 md:px-2">
<Link <Link
className="mb-2 flex h-20 items-center justify-start rounded-md bg-blue-600 p-4 md:h-20" className="mb-2 flex h-20 items-center justify-start rounded-md bg-blue-600 p-4 md:h-20"
href={`/${getSlug()}/dashboard`} href={`/${slug}/dashboard`}
> >
<div className={`${gloriaHallelujah.className} "w-32 text-white md:w-40 antialiased`}> <div className={`${gloriaHallelujah.className} "w-32 text-white md:w-40 antialiased`}>
<h1>Wedding Planner</h1> <h1>Wedding Planner</h1>
@ -26,14 +34,14 @@ export default function SideNav() {
<div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2"> <div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
<NavLinks /> <NavLinks />
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div> <div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
<span>Logged in as {JSON.parse(localStorage.getItem('currentUser') || '{}').email}</span> <span>Logged in as {currentUser?.email}</span>
<button <button
className="flex h-[48px] w-full grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3" className="flex h-[48px] w-full grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3"
onClick={() => { onClick={() => {
logout({ logout({
onLogout: () => { onLogout: () => {
localStorage.clear(); localStorage.clear();
router.push(`/${getSlug()}`); router.push(`/${slug}`);
} }
}); });
}} }}

View File

@ -28,7 +28,7 @@ export default function guestsTable({ guests, onUpdate, onEdit }: {
{guest.name} {guest.name}
</td> </td>
<td className="px-6 py-4"> <td className="px-6 py-4">
{guest.group_name} {guest.group?.name}
</td> </td>
<td className="px-6 py-4"> <td className="px-6 py-4">
<span className="flex items-center text-sm dark:text-white me-3"> <span className="flex items-center text-sm dark:text-white me-3">

View File

@ -9,9 +9,13 @@
"@atlaskit/pragmatic-drag-and-drop": "^1.7.0", "@atlaskit/pragmatic-drag-and-drop": "^1.7.0",
"@heroicons/react": "^2.1.4", "@heroicons/react": "^2.1.4",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@tiptap/pm": "^2.14.0",
"@tiptap/react": "^2.14.0",
"@tiptap/starter-kit": "^2.14.0",
"autoprefixer": "10.4.21", "autoprefixer": "10.4.21",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dompurify": "^3.2.6",
"next": "15.3.3", "next": "15.3.3",
"next-auth": "5.0.0-beta.28", "next-auth": "5.0.0-beta.28",
"postcss": "8.5.4", "postcss": "8.5.4",
@ -26,14 +30,15 @@
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.46.0", "@playwright/test": "^1.52.0",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/node": "22.15.29", "@types/node": "22.15.29",
"@types/react": "18.3.23", "@types/react": "18.3.23",
"@types/react-dom": "18.3.7" "@types/react-dom": "18.3.7",
"wait-on": "^8.0.3"
}, },
"engines": { "engines": {
"node": ">=23.0.0" "node": ">=23.0.0"
}, },
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977" "packageManager": "pnpm@10.11.1+sha512.e519b9f7639869dc8d5c3c5dfef73b3f091094b0a006d7317353c72b124e80e1afd429732e28705ad6bfa1ee879c1fce46c128ccebd3192101f43dd67c667912"
} }

View File

@ -43,11 +43,6 @@ export default defineConfig({
use: { ...devices['Desktop Firefox'] }, use: { ...devices['Desktop Firefox'] },
}, },
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */ /* Test against mobile viewports. */
// { // {
// name: 'Mobile Chrome', // name: 'Mobile Chrome',

843
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
public/header.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

BIN
public/stamp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -1,29 +1,44 @@
import { test, expect, Page } from '@playwright/test' import { test, expect, Page } from '@playwright/test'
import { mock } from 'node:test';
const mockGuestsAPI = ({ page }: { page: Page }) => { const mockGuestsAPI = ({ page }: { page: Page }) => {
page.route('*/**/api/default/guests', async route => { page.route('*/**/api/default/guests', async route => {
const json = [ if (route.request().method() === 'GET') {
{ const json = [
"id": "f4a09c28-40ea-4553-90a5-96935a59cac6", {
"status": "tentative", "id": "f4a09c28-40ea-4553-90a5-96935a59cac6",
"name": "Kristofer Rohan DVM", "status": "tentative",
"group": { "name": "Kristofer Rohan DVM",
"id": "2fcb8b22-6b07-4c34-92e3-a2535dbc5b14", "group": {
"name": "Childhood friends", "id": "2fcb8b22-6b07-4c34-92e3-a2535dbc5b14",
} "name": "Childhood friends",
}, }
{ },
"id": "bd585c40-0937-4cde-960a-bb23acfd6f18", {
"status": "invited", "id": "bd585c40-0937-4cde-960a-bb23acfd6f18",
"name": "Olevia Quigley Jr.", "status": "invited",
"group": { "name": "Olevia Quigley Jr.",
"id": "da8edf26-3e1e-4cbb-b985-450c49fffe01", "group": {
"name": "Work", "id": "da8edf26-3e1e-4cbb-b985-450c49fffe01",
} "name": "Work",
}, }
]; },
];
await route.fulfill({ json }) await route.fulfill({ json })
} else if (route.request().method() === 'POST') {
const json = {
"id":"ff58aa2d-643d-4c29-be9c-50e10ae6853c",
"name":"John Snow",
"status":"invited",
"group": {
"id": "da8edf26-3e1e-4cbb-b985-450c49fffe01",
"name": "Work",
}
};
await route.fulfill({ json });
}
}) })
} }
@ -34,27 +49,31 @@ const mockGroupsAPI = ({ page }: { page: Page }) => {
"id": "ee44ffb9-1147-4842-a378-9eaeb0f0871a", "id": "ee44ffb9-1147-4842-a378-9eaeb0f0871a",
"name": "Pam's family", "name": "Pam's family",
"icon": "pi pi-users", "icon": "pi pi-users",
"parent_id": "cd9645e1-02c6-4fb9-bba6-1a960754b01c", "parent_id": null,
"color": "#ff0000", "color": "#ff0000",
"total": 3, "attendance": {
"considered": 2, "total": 3,
"invited": 1, "considered": 2,
"confirmed": 0, "invited": 1,
"declined": 0, "confirmed": 0,
"tentative": 0 "declined": 0,
"tentative": 0
}
}, },
{ {
"id": "c8bda6ca-d8af-4bb8-b2bf-e6ec1c21b1e6", "id": "c8bda6ca-d8af-4bb8-b2bf-e6ec1c21b1e6",
"name": "Pam's work", "name": "Pam's work",
"icon": "pi pi-desktop", "icon": "pi pi-desktop",
"parent_id": "cd9645e1-02c6-4fb9-bba6-1a960754b01c", "parent_id": null,
"color": "#00ff00", "color": "#00ff00",
"total": 2, "attendance": {
"considered": 0, "total": 2,
"invited": 0, "considered": 0,
"confirmed": 0, "invited": 0,
"declined": 0, "confirmed": 0,
"tentative": 2 "declined": 0,
"tentative": 2
}
}, },
]; ];
@ -67,24 +86,55 @@ test('should display the list of guests', async ({ page }) => {
await page.goto('/default/dashboard/guests'); await page.goto('/default/dashboard/guests');
await expect(page.getByRole('tab', { name: 'Groups' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Add new' })).toBeVisible();
await expect(page.getByRole('tab', { name: 'Guests' })).toBeVisible(); await expect(page.getByRole('tab', { name: 'Guests' })).toBeVisible();
await expect(page.getByRole('tab', { name: 'Groups' })).toBeVisible();
await expect(page.getByRole('tab', { name: 'Invitations' })).toBeVisible();
await expect(page.getByText('There are 2 elements in the list')).toBeVisible(); await expect(page.getByText('There are 2 elements in the list')).toBeVisible();
await expect(page.getByRole('row')).toHaveCount(3); // 1 header row + 2 data rows
await expect(page.getByRole('row').nth(1).getByRole('cell', { name: 'Kristofer Rohan DVM' })).toBeVisible(); await expect(page.getByRole('row').nth(1).getByRole('cell', { name: 'Kristofer Rohan DVM' })).toBeVisible();
await expect(page.getByRole('row').nth(1).getByRole('cell', { name: 'Childhood friends' })).toBeVisible(); await expect(page.getByRole('row').nth(1).getByRole('cell', { name: 'Childhood friends' })).toBeVisible();
await expect(page.getByRole('row').nth(1).getByRole('cell', { name: 'Tentative' })).toBeVisible(); await expect(page.getByRole('row').nth(1).getByRole('cell', { name: 'Tentative' })).toBeVisible();
await expect(page.getByRole('row').nth(1).getByRole('button', { name: 'Confirm' })).toBeVisible(); await expect(page.getByRole('row').nth(1).locator('svg')).toHaveCount(2);
await expect(page.getByRole('row').nth(1).getByRole('button', { name: 'Decline' })).toBeVisible();
await expect(page.getByRole('row').nth(2).getByRole('cell', { name: 'Olevia Quigley Jr.' })).toBeVisible(); await expect(page.getByRole('row').nth(2).getByRole('cell', { name: 'Olevia Quigley Jr.' })).toBeVisible();
await expect(page.getByRole('row').nth(2).getByRole('cell', { name: 'Work' })).toBeVisible(); await expect(page.getByRole('row').nth(2).getByRole('cell', { name: 'Work' })).toBeVisible();
await expect(page.getByRole('row').nth(2).getByRole('cell', { name: 'Invited' })).toBeVisible(); await expect(page.getByRole('row').nth(2).getByRole('cell', { name: 'Invited' })).toBeVisible();
await expect(page.getByRole('row').nth(2).getByRole('button', { name: 'Confirm' })).toBeVisible(); await expect(page.getByRole('row').nth(2).locator('svg')).toHaveCount(2);
await expect(page.getByRole('row').nth(2).getByRole('button', { name: 'Tentative' })).toBeVisible(); });
await expect(page.getByRole('row').nth(2).getByRole('button', { name: 'Decline' })).toBeVisible();
test('should allow creating a new guest', async ({ page }) => {
await mockGuestsAPI({ page });
await mockGroupsAPI({ page });
await page.goto('/default/dashboard/guests');
await expect(page.getByText('There are 2 elements in the list')).toBeVisible();
await page.getByRole('button', { name: 'Add new' }).click();
await page.getByRole('dialog').getByLabel('Name').fill('John Snow');
await page.keyboard.press('Tab');
await page.keyboard.press('ArrowDown');
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
await page.keyboard.press('Tab');
await page.keyboard.press('ArrowDown');
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
await page.getByRole('dialog').getByRole('button', { name: 'Create' }).click();
await expect(page.getByText('There are 3 elements in the list')).toBeVisible();
await expect(page.getByRole('row').nth(1).getByRole('cell', { name: 'John Snow' })).toBeVisible();
await expect(page.getByRole('row').nth(1).getByRole('cell', { name: 'Work' })).toBeVisible();
await expect(page.getByRole('row').nth(1).getByRole('cell', { name: 'Invited' })).toBeVisible();
await expect(page.getByRole('row').nth(1).locator('svg')).toHaveCount(2);
}); });
test('should display the list of groups', async ({ page }) => { test('should display the list of groups', async ({ page }) => {
@ -93,8 +143,38 @@ test('should display the list of groups', async ({ page }) => {
await page.goto('/default/dashboard/guests'); await page.goto('/default/dashboard/guests');
await page.getByRole('tab', { name: 'Groups' }).click(); await page.getByRole('tab', { name: 'Groups' }).click();
await expect(page.getByText('There are 2 elements in the list')).toBeVisible(); await expect(page.getByRole('button', { name: 'Add new' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Reset affinities' })).toBeVisible();
await expect(page.getByRole('row')).toHaveCount(3); // 1 header row + 2 data rows
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(0)).toHaveText('Name');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(1)).toHaveText('Color');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(2)).toHaveText('Confirmed');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(3)).toHaveText('Tentative');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(4)).toHaveText('Pending');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(5)).toHaveText('Declined');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(6)).toHaveText('Considered');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(7)).toHaveText('Total');
await expect(page.getByRole('row').nth(0).getByRole('columnheader').nth(8)).toHaveText('Actions');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(0)).toContainText('Pam\'s family');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(2)).toHaveText('0');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(3)).toHaveText('0');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(4)).toHaveText('1');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(5)).toHaveText('0');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(6)).toHaveText('2');
await expect(page.getByRole('row').nth(1).getByRole('cell').nth(7)).toHaveText('3');
await expect(page.getByRole('row').nth(1).locator('svg:visible')).toHaveCount(3);
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(0)).toContainText('Pam\'s work');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(2)).toHaveText('0');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(3)).toHaveText('2');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(4)).toHaveText('0');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(5)).toHaveText('0');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(6)).toHaveText('0');
await expect(page.getByRole('row').nth(2).getByRole('cell').nth(7)).toHaveText('2');
await expect(page.getByRole('row').nth(2).locator('svg:visible')).toHaveCount(3);
await expect(page.getByRole('row').nth(1).getByRole('cell', { name: "Pam's family" })).toBeVisible();
await expect(page.getByRole('row').nth(2).getByRole('cell', { name: "Pam's work" })).toBeVisible();
}); });