diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b6b7a04 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +node_modules +npm-debug.log +Dockerfile* +docker-compose* +.dockerignore +.git +.gitignore +README.md +LICENSE +.vscode +.next +*.swp +/scripts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5edde29..1e73a81 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,15 +1,13 @@ name: Build Nginx-based docker image on: push: - branches: - - main - pull_request: concurrency: group: ${{ github.ref }} cancel-in-progress: true jobs: build-static-assets: runs-on: ubuntu-latest + timeout-minutes: 30 steps: - uses: actions/checkout@v4 with: @@ -24,13 +22,32 @@ jobs: registry: ${{ secrets.PRIVATE_REGISTRY_HOST }} username: ${{ secrets.PRIVATE_REGISTRY_USERNAME }} password: ${{ secrets.PRIVATE_REGISTRY_TOKEN }} - - - name: Build and push + + - name: Build and push intermediate stages (deps) uses: docker/build-push-action@v6 with: context: . - push: ${{ github.event_name != 'pull_request' }} - tags: | - ${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:latest - cache-from: type=registry,ref=user/app:latest + target: deps + push: ${{ github.ref == 'refs/heads/main' }} + tags: ${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:deps + cache-from: type=registry,ref=${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:deps + cache-to: type=inline + + - name: Build and push intermediate stages (builder) + uses: docker/build-push-action@v6 + with: + context: . + target: builder + push: ${{ github.ref == 'refs/heads/main' }} + tags: ${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:builder + cache-from: type=registry,ref=${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:builder + cache-to: type=inline + + - name: Build and push (final) + uses: docker/build-push-action@v6 + with: + context: . + push: ${{ github.ref == 'refs/heads/main' }} + tags: ${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:latest + cache-from: type=registry,ref=${{ secrets.PRIVATE_REGISTRY_HOST }}/${{ env.GITHUB_REPOSITORY }}:latest cache-to: type=inline \ No newline at end of file diff --git a/.github/workflows/copyright_notice.yml b/.github/workflows/copyright_notice.yml index fed4cc4..0536d2d 100644 --- a/.github/workflows/copyright_notice.yml +++ b/.github/workflows/copyright_notice.yml @@ -3,6 +3,9 @@ on: pull_request: permissions: contents: write +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true jobs: copyright_notice: runs-on: ubuntu-latest @@ -13,7 +16,7 @@ jobs: ref: ${{ github.head_ref }} - uses: VinnyBabuManjaly/copyright-action@v1.0.0 with: - CopyrightString: '/* Copyright (C) 2024 Manuel Bustillo*/\n\n' + CopyrightString: '/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/\n\n' FileType: '.tsx, .jsx, .ts' Path: 'app/, config/, db/' IgnorePath: 'testfolder1/a/, testfolder3' diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 1ca77f2..583303c 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -9,6 +9,7 @@ concurrency: cancel-in-progress: true jobs: test: + if: false timeout-minutes: 60 runs-on: ubuntu-latest container: diff --git a/.nvmrc b/.nvmrc index f842f93..47202b6 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -23.0.0 \ No newline at end of file +23.11.0 \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..7ed40bd --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,28 @@ +# Based on https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile + +FROM node:23-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +RUN \ + if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm ci; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ + else echo "Lockfile not found." && exit 1; \ + fi + +COPY . /app/ + +ENV NODE_ENV development +ENV NEXT_TELEMETRY_DISABLED 1 + +EXPOSE 3000 +ENV PORT=3000 + +CMD HOSTNAME="0.0.0.0" pnpm run dev \ No newline at end of file diff --git a/app/[slug]/dashboard/expenses/page.tsx b/app/[slug]/dashboard/expenses/page.tsx new file mode 100644 index 0000000..d289255 --- /dev/null +++ b/app/[slug]/dashboard/expenses/page.tsx @@ -0,0 +1,45 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +'use client' + +import { AbstractApi } from '@/app/api/abstract-api'; +import { Expense, ExpenseSerializer } from '@/app/lib/expense'; +import { classNames } from '@/app/ui/components/button'; +import ExpenseFormDialog from '@/app/ui/components/expense-form-dialog'; +import ExpensesTable from '@/app/ui/expenses/table'; +import SkeletonTable from '@/app/ui/guests/skeleton-row'; +import { Suspense, useEffect, useState } from 'react'; + +export default function Page() { + const refreshExpenses = () => { + new AbstractApi().getAll(new ExpenseSerializer(), (expenses: Expense[]) => { + setExpenses(expenses); + }); + } + + const [expenses, setExpenses] = useState([]); + const [expenseBeingEdited, setExpenseBeingEdited] = useState(undefined); + useEffect(() => { refreshExpenses() }, []); + + return ( +
+
+ + { refreshExpenses(); setExpenseBeingEdited(undefined) }} + expense={expenseBeingEdited} + visible={expenseBeingEdited !== undefined} + onHide={() => { setExpenseBeingEdited(undefined) }} + /> + }> + setExpenseBeingEdited(expense)} + /> + +
+
+ ); +} \ No newline at end of file diff --git a/app/[slug]/dashboard/guests/page.tsx b/app/[slug]/dashboard/guests/page.tsx new file mode 100644 index 0000000..e76e172 --- /dev/null +++ b/app/[slug]/dashboard/guests/page.tsx @@ -0,0 +1,119 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +'use client'; + +import { AbstractApi, } from '@/app/api/abstract-api'; +import { Group, GroupSerializer } from '@/app/lib/group'; +import { Guest, GuestSerializer } from '@/app/lib/guest'; +import { getCsrfToken, getSlug } from '@/app/lib/utils'; +import AffinitiesFormDialog from '@/app/ui/components/affinities-form-dialog'; +import { classNames } from '@/app/ui/components/button'; +import GroupFormDialog from '@/app/ui/components/group-form-dialog'; +import GuestFormDialog from '@/app/ui/components/guest-form-dialog'; +import GroupsTable from '@/app/ui/groups/table'; +import SkeletonTable from '@/app/ui/guests/skeleton-row'; +import GuestsTable from '@/app/ui/guests/table'; +import { TabPanel, TabView } from 'primereact/tabview'; +import { Suspense, useState } from 'react'; + + +export default function Page() { + function refreshGuests() { + new AbstractApi().getAll(new GuestSerializer(), (objects: Guest[]) => { + setGuests(objects); + setGuestsLoaded(true); + }); + } + + function refreshGroups() { + new AbstractApi().getAll(new GroupSerializer(), (objects: Group[]) => { + setGroups(objects); + setGroupsLoaded(true); + }); + } + + function resetAffinities() { + fetch(`/api/${getSlug()}/groups/affinities/reset`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'X-CSRF-TOKEN': getCsrfToken(), + } + }) + } + + const [groupsLoaded, setGroupsLoaded] = useState(false); + const [groups, setGroups] = useState>([]); + const [groupBeingEdited, setGroupBeingEdited] = useState(undefined); + + const [groupAffinitiesBeingEditted, setGroupAffinitiesBeingEditted] = useState(undefined); + + const [guestsLoaded, setGuestsLoaded] = useState(false); + const [guests, setGuests] = useState>([]); + const [guestBeingEdited, setGuestBeingEdited] = useState(undefined); + + !groupsLoaded && refreshGroups(); + !guestsLoaded && refreshGuests(); + + return ( +
+ + +
+ + { refreshGuests(); setGuestBeingEdited(undefined) }} + guest={guestBeingEdited} + visible={guestBeingEdited !== undefined} + onHide={() => { setGuestBeingEdited(undefined) }} + /> + }> + setGuestBeingEdited(guest)} + /> + +
+ + +
+ +
+ + +
+ + { refreshGroups(); setGroupBeingEdited(undefined) }} + group={groupBeingEdited} + visible={groupBeingEdited !== undefined} + onHide={() => { setGroupBeingEdited(undefined) }} + /> + + { setGroupAffinitiesBeingEditted(undefined) }} + /> + + }> + setGroupBeingEdited(group)} + onEditAffinities={(group) => setGroupAffinitiesBeingEditted(group)} + /> + +
+ + +
+ + ); +} \ No newline at end of file diff --git a/app/dashboard/layout.tsx b/app/[slug]/dashboard/layout.tsx similarity index 64% rename from app/dashboard/layout.tsx rename to app/[slug]/dashboard/layout.tsx index d179387..d765b83 100644 --- a/app/dashboard/layout.tsx +++ b/app/[slug]/dashboard/layout.tsx @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ import SideNav from '@/app/ui/dashboard/sidenav'; @@ -8,7 +8,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
-
{children}
+
{children}
); } \ No newline at end of file diff --git a/app/[slug]/dashboard/page.tsx b/app/[slug]/dashboard/page.tsx new file mode 100644 index 0000000..83f446e --- /dev/null +++ b/app/[slug]/dashboard/page.tsx @@ -0,0 +1,26 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +'use client' + +import { GlobalSummary as Summary } from '@/app/lib/definitions'; +import { getSlug } from '@/app/lib/utils'; +import GlobalSummary from '@/app/ui/dashboard/global-summary'; +import { useEffect, useState } from 'react'; + +export default function Page() { + const [globalSummary, setGlobalSummary] = useState(undefined); + + function refreshSummary() { + fetch(`/api/${getSlug()}/summary`) + .then((response) => response.json()) + .then((data) => { + setGlobalSummary(data); + }) + } + + useEffect(refreshSummary, []); + + return ( + globalSummary && + ); +} \ No newline at end of file diff --git a/app/[slug]/dashboard/tables/page.tsx b/app/[slug]/dashboard/tables/page.tsx new file mode 100644 index 0000000..8fd4a81 --- /dev/null +++ b/app/[slug]/dashboard/tables/page.tsx @@ -0,0 +1,44 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +'use client'; + +import { AbstractApi } from '@/app/api/abstract-api'; +import { TableSimulation, TableSimulationSerializer } from '@/app/lib/tableSimulation'; +import Arrangement from '@/app/ui/arrangements/arrangement'; +import ArrangementsTable from '@/app/ui/arrangements/arrangements-table'; +import { classNames } from '@/app/ui/components/button'; +import { Toast } from 'primereact/toast'; +import React, { useRef, useState } from 'react'; + +export default function Page() { + const toast = useRef(null); + + const show = () => { + toast.current?.show({ + severity: 'success', + summary: 'Simulation created', + detail: 'Table distributions will be calculated shortly, please come back in some minutes' + }); + }; + + const [currentArrangement, setCurrentArrangement] = useState(null); + + function createSimulation() { + const api = new AbstractApi(); + const serializer = new TableSimulationSerializer(); + api.create(serializer, new TableSimulation(), show); + } + + return ( + <> + +
+ + +
+ + + {currentArrangement && } + + ) +} \ No newline at end of file diff --git a/app/[slug]/page.tsx b/app/[slug]/page.tsx new file mode 100644 index 0000000..f189a6a --- /dev/null +++ b/app/[slug]/page.tsx @@ -0,0 +1,40 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +'use client'; + +import LoginForm from '@/app/ui/components/login-form'; +import RegistrationForm from '@/app/ui/components/registration-form'; +import { useParams } from 'next/navigation' +import { useEffect } from 'react'; +import { retrieveCSRFToken } from '../api/authentication'; +import { getCsrfToken } from '../lib/utils'; + +export default async function Page() { + const params = useParams<{ slug: string }>() + + useEffect(() => { + if (getCsrfToken() == 'unknown') { + retrieveCSRFToken(); + } + }, []); + + if (typeof window !== 'undefined') { + localStorage.setItem('slug', await params.slug); + } + + return ( +
+
+
+ Already have an account? Sign in + +
+ +
+ Don't have an account? Register now! + +
+
+
+ ); +} diff --git a/app/api/abstract-api.tsx b/app/api/abstract-api.tsx new file mode 100644 index 0000000..7201a08 --- /dev/null +++ b/app/api/abstract-api.tsx @@ -0,0 +1,76 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +import { Entity } from '@/app/lib/definitions'; +import { getCsrfToken, getSlug } from '@/app/lib/utils'; + +export interface Api { + getAll(serializable: Serializable ,callback: (objets: T[]) => void): void; + get(serializable: Serializable, id: string, callback: (object: T) => void): void; + create(serializable: Serializable, object: T, callback: () => void): void; + update(serializable: Serializable, object: T, callback: () => void): void; + destroy(serializable: Serializable, object: T, callback: () => void): void; +} + +export interface Serializable { + fromJson(json: any): T; + toJson(object: T): string; + apiPath(): string; +} + +export class AbstractApi implements Api { + getAll(serializable: Serializable, callback: (objets: T[]) => void): void { + fetch(`/api/${getSlug()}/${serializable.apiPath()}`) + .then((response) => response.json()) + .then((data) => { + callback(data.map((record: any) => { + return serializable.fromJson(record); + })); + }, (error) => { + return []; + }); + } + + get(serializable: Serializable, id: string, callback: (object: T) => void): void { + fetch(`/api/${getSlug()}/${serializable.apiPath()}/${id}`) + .then((response) => response.json()) + .then((data) => { + callback(serializable.fromJson(data)); + }, (error) => { + return []; + }); + } + + update(serializable: Serializable, object: T, callback: () => void): void { + fetch(`/api/${getSlug()}/${serializable.apiPath()}/${object.id}`, { + method: 'PUT', + body: serializable.toJson(object), + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': getCsrfToken(), + } + }).then(callback) + .catch((error) => console.error(error)); + } + + create(serializable: Serializable, object: T, callback: () => void): void { + fetch(`/api/${getSlug()}/${serializable.apiPath()}`, { + method: 'POST', + body: serializable.toJson(object), + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': getCsrfToken(), + } + }).then(callback) + .catch((error) => console.error(error)); + } + + destroy(serializable: Serializable, object: T, callback: () => void): void { + fetch(`/api/${getSlug()}/${serializable.apiPath()}/${object.id}`, { + method: 'DELETE', + headers: { + 'X-CSRF-TOKEN': getCsrfToken(), + } + }).then(callback) + .catch((error) => console.error(error)); + } +} diff --git a/app/api/authentication.tsx b/app/api/authentication.tsx new file mode 100644 index 0000000..e361260 --- /dev/null +++ b/app/api/authentication.tsx @@ -0,0 +1,86 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +import { asArray, getCsrfToken, getSlug } from '@/app/lib/utils'; +import { Captcha, StructuredErrors, User } from '@/app/lib/definitions'; + +export function login({ email, password, onLogin }: { email: string, password: string, onLogin: (user: User) => void }) { + return fetch(`/api/${getSlug()}/users/sign_in`, { + method: 'POST', + body: JSON.stringify({ user: { email, password } }), + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': getCsrfToken(), + } + }) + .then((response) => response.json()) + .then((data: any) => { + console.log(data); + onLogin({ + id: data.id || '', + email: data.email || '', + }); + }) + .catch((error) => console.error(error)); +} + +export function logout({ onLogout }: { onLogout: () => void }) { + fetch(`/api/${getSlug()}/users/sign_out`, { + method: 'DELETE', + headers: { + 'X-CSRF-TOKEN': getCsrfToken(), + } + }).then(onLogout) + .catch((error) => console.error(error)); +} + +function flattenErrors(errors: StructuredErrors): string[] { + if (errors instanceof Array) { + return errors; + } + return Object.keys(errors).map((key) => { + return `${key}: ${asArray(errors[key]).join(', ')}`; + }); +} + +// At this moment we're making an initial request to get a valid CSRF token +export function retrieveCSRFToken() { + return fetch(`/api/token`, { + headers: { + 'Accept': 'application/json', + } + }).then((response) => { return null }); +} + +export function register({ slug, email, password, passwordConfirmation, captcha, onRegister, onError }: { + slug: string, + email: string, + password: string, + passwordConfirmation: string, + captcha: Captcha, + onRegister: () => void, + onError: (errors: string[]) => void +}) { + fetch(`/api/${slug}/users`, { + method: 'POST', + body: JSON.stringify( + { + user: { email, password, password_confirmation: passwordConfirmation }, + captcha: { id: captcha.id, answer: captcha.answer } + } + ), + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': getCsrfToken(), + } + }).then((response) => { + if (response.ok) { + response.json().then(onRegister); + } else { + response.json().then((data: any) => { + onError(data.errors && flattenErrors(data.errors) || [data.error]); + }); + } + }) +} diff --git a/app/api/captcha.tsx b/app/api/captcha.tsx new file mode 100644 index 0000000..ff6ecb7 --- /dev/null +++ b/app/api/captcha.tsx @@ -0,0 +1,19 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +import { data } from "autoprefixer"; +import { getCsrfToken } from "../lib/utils"; + +export function getCaptchaChallenge({onRetrieve}: {onRetrieve: (id: string, url: string) => void}){ + return fetch('/api/captcha', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'X-CSRF-TOKEN': getCsrfToken(), + } + }) + .then((response) => response.json()) + .then((data: any) => { + onRetrieve(data.id, data.media_url) + }) + .catch((error) => console.error(error)); +} \ No newline at end of file diff --git a/app/api/health/route.ts b/app/api/health/route.ts new file mode 100644 index 0000000..f43cbdb --- /dev/null +++ b/app/api/health/route.ts @@ -0,0 +1,7 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +import { NextResponse } from "next/server"; + +export function GET() { + return NextResponse.json({}); +} \ No newline at end of file diff --git a/app/api/tableSimulations.tsx b/app/api/tableSimulations.tsx new file mode 100644 index 0000000..5b0f538 --- /dev/null +++ b/app/api/tableSimulations.tsx @@ -0,0 +1,21 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +import { TableArrangement } from '@/app/lib/definitions'; +import { getSlug } from '../lib/utils'; + +export function loadTableSimulations(onLoad?: (tableSimulations: TableArrangement[]) => void) { + fetch(`/api/${getSlug()}/tables_arrangements`) + .then((response) => response.json()) + .then((data) => { + onLoad && onLoad(data.map((record: any) => { + return ({ + id: record.id, + name: record.name, + discomfort: record.discomfort, + valid: record.valid, + }); + })); + }, (error) => { + return []; + }); +} \ No newline at end of file diff --git a/app/dashboard/expenses/page.tsx b/app/dashboard/expenses/page.tsx deleted file mode 100644 index 7e8cee5..0000000 --- a/app/dashboard/expenses/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -import { lusitana } from '@/app/ui/fonts'; -import ExpenseSummary from '@/app/ui/expenses/summary'; - -export default function Page () { - return ( -
-
-

Expenses

-

Summary

- -
-
- ); -} \ No newline at end of file diff --git a/app/dashboard/guests/page.tsx b/app/dashboard/guests/page.tsx deleted file mode 100644 index 2b65f56..0000000 --- a/app/dashboard/guests/page.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -import { lusitana } from '@/app/ui/fonts'; -import AffinityGroupsTree from '@/app/ui/guests/affinity-groups-tree'; -import GuestsTable from '@/app/ui/guests/table'; -import React, { Suspense } from 'react'; -import SkeletonTable from '@/app/ui/guests/skeleton-row'; - - - -export default function Page() { - return ( -
- - -

Guests

-
- }> - - -
-
- ); -} \ No newline at end of file diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx deleted file mode 100644 index 643e65f..0000000 --- a/app/dashboard/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -export default function Page() { - return

Dashboard Page

; -} \ No newline at end of file diff --git a/app/dashboard/tables/page.tsx b/app/dashboard/tables/page.tsx deleted file mode 100644 index 2aec187..0000000 --- a/app/dashboard/tables/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -import { lusitana } from '@/app/ui/fonts'; - -export default function Page () { - return ( -
-
-

Table distributions

-
-
- ); -} \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index e2cd6af..63ee3fd 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ import '@/app/ui/global.css' diff --git a/app/lib/affinities.tsx b/app/lib/affinities.tsx new file mode 100644 index 0000000..5576f1b --- /dev/null +++ b/app/lib/affinities.tsx @@ -0,0 +1,5 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +export class Affinities { + [key:string]: number; +} \ No newline at end of file diff --git a/app/lib/definitions.ts b/app/lib/definitions.ts index 70eacd2..5e6ffe8 100644 --- a/app/lib/definitions.ts +++ b/app/lib/definitions.ts @@ -1,106 +1,50 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ -// This file contains type definitions for your data. -// It describes the shape of the data, and what data type each property should accept. -// For simplicity of teaching, we're manually defining these types. -// However, these types are generated automatically if you're using an ORM such as Prisma. -export type User = { - id: string; - name: string; - email: string; - password: string; -}; +import { AttendanceSummary } from "./group"; +import { Guest } from "./guest"; -export type Customer = { - id: string; - name: string; - email: string; - image_url: string; -}; - -export type Guest = { - id: string; - name: string; - email: string; - group_name: string; - status: 'Considered' | 'Invited' | 'Confirmed' | 'Declined' | 'Tentative'; +export interface Entity { + id?: string; } -export type Group = { +export type TableArrangement = { id: string; + number: number; name: string; - guest_count: number; - icon: string; - children: Group[]; -}; + guests?: Guest[]; + discomfort?: number; + valid?: boolean; +} -export type Invoice = { +export type User = { id: string; - customer_id: string; - amount: number; - date: string; - // In TypeScript, this is called a string union type. - // It means that the "status" property can only be one of the two strings: 'pending' or 'paid'. - status: 'pending' | 'paid'; -}; - -export type Revenue = { - month: string; - revenue: number; -}; - -export type LatestInvoice = { - id: string; - name: string; - image_url: string; email: string; - amount: string; -}; +} -// The database returns a number for amount, but we later format it to a string with the formatCurrency function -export type LatestInvoiceRaw = Omit & { - amount: number; -}; - -export type guestsTable = { +export type Captcha = { id: string; - customer_id: string; - name: string; - email: string; - image_url: string; - date: string; - amount: number; - status: 'pending' | 'paid'; + answer: string; +} + +export type StructuredErrors = { + [key: string]: string[] | string; }; -export type CustomersTableType = { - id: string; - name: string; - email: string; - image_url: string; - total_guests: number; - total_pending: number; - total_paid: number; -}; +export type GlobalSummary = { + expenses: ExpenseSummary; + guests: AttendanceSummary +} -export type FormattedCustomersTable = { - id: string; - name: string; - email: string; - image_url: string; - total_guests: number; - total_pending: string; - total_paid: string; -}; +export type ExpenseSummary = { + projected: ExpensePossibleSummary; + confirmed: ExpensePossibleSummary; + status: StatusSummary; +} -export type CustomerField = { - id: string; - name: string; -}; - -export type InvoiceForm = { - id: string; - customer_id: string; - amount: number; - status: 'pending' | 'paid'; -}; +export type ExpensePossibleSummary = { + total: number; + guests: number; +} +export type StatusSummary = { + paid: number; +} \ No newline at end of file diff --git a/app/lib/expense.tsx b/app/lib/expense.tsx new file mode 100644 index 0000000..b024e1b --- /dev/null +++ b/app/lib/expense.tsx @@ -0,0 +1,41 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +import { Serializable } from "../api/abstract-api"; +import { Entity } from "./definitions"; + +export const pricingTypes = ['fixed', 'per_person'] as const; +export type PricingType = typeof pricingTypes[number]; + +export class Expense implements Entity { + id?: string; + name?: string; + amount?: number; + pricingType?: PricingType; + + constructor(id?: string, name?: string, amount?: number, pricingType?: PricingType) { + this.id = id; + this.name = name || ''; + this.amount = amount || 0; + this.pricingType = pricingType || 'fixed'; + } +} + +export class ExpenseSerializer implements Serializable{ + fromJson(data: any): Expense { + return new Expense(data.id, data.name, data.amount, data.pricing_type); + } + + toJson(expense: Expense): string { + return JSON.stringify({ + expense: { + name: expense.name, + amount: expense.amount, + pricing_type: expense.pricingType + } + }); + } + + apiPath(): string { + return 'expenses'; + } +} \ No newline at end of file diff --git a/app/lib/group.tsx b/app/lib/group.tsx new file mode 100644 index 0000000..aae5d18 --- /dev/null +++ b/app/lib/group.tsx @@ -0,0 +1,64 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +import { Entity } from "./definitions"; + +export type AttendanceSummary = { + considered: number; + invited: number; + confirmed: number; + declined: number; + tentative: number; + total: number; +} + +export class Group implements Entity { + id?: string; + name?: string; + guest_count?: number; + icon?: string; + children?: Group[]; + parentId?: string; + color?: string; + attendance?: AttendanceSummary + + constructor(id?: string, name?: string, guest_count?: number, icon?: string, children?: Group[], parentId?: string, color?: string, attendance?: AttendanceSummary) { + this.id = id; + this.name = name; + this.guest_count = guest_count; + this.icon = icon; + this.children = children; + this.parentId = parentId; + this.color = color; + this.attendance = attendance; + } +} + +export class GroupSerializer { + fromJson(data: any): Group { + return new Group( + data.id, + data.name, + data.guest_count, + data.icon, + data.children, + data.parent_id, + data.color, + data.attendance + ); + } + + toJson(group: Group): string { + return JSON.stringify({ + group: { + name: group.name, + color: group.color, + icon: group.icon, + parent_id: group.parentId + } + }); + } + + apiPath(): string { + return 'groups'; + } +} \ No newline at end of file diff --git a/app/lib/guest.tsx b/app/lib/guest.tsx new file mode 100644 index 0000000..80fcfb7 --- /dev/null +++ b/app/lib/guest.tsx @@ -0,0 +1,42 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +import { Serializable } from "../api/abstract-api"; +import { Entity } from "./definitions"; + +export const guestStatuses = ['considered', 'invited', 'confirmed', 'declined', 'tentative'] as const; +export type GuestStatus = typeof guestStatuses[number]; + +export class Guest implements Entity { + id?: string; + name?: string; + group_name?: string; + groupId?: string; + color?: string; + status?: GuestStatus; + children?: Guest[]; + + constructor(id?: string, name?: string, group_name?: string, groupId?: string, color?: string, status?: GuestStatus, children?: Guest[]) { + this.id = id; + this.name = name; + this.group_name = group_name; + this.groupId = groupId; + this.color = color; + this.status = status; + this.children = children; + } +} + +export class GuestSerializer implements Serializable { + fromJson(data: any): Guest { + return new Guest(data.id, data.name, data.group?.name, data.group?.id, data.color, data.status, data.children); + } + + toJson(guest: Guest): string { + return JSON.stringify({ guest: { name: guest.name, status: guest.status, group_id: guest.groupId } }); + } + + apiPath(): string { + return 'guests'; + } +} + diff --git a/app/lib/placeholder-data.ts b/app/lib/placeholder-data.ts deleted file mode 100644 index 2d78faa..0000000 --- a/app/lib/placeholder-data.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -// This file contains placeholder data that you'll be replacing with real data in the Data Fetching chapter: -// https://nextjs.org/learn/dashboard-app/fetching-data -const users = [ - { - id: '410544b2-4001-4271-9855-fec4b6a6442a', - name: 'User', - email: 'user@nextmail.com', - password: '123456', - }, -]; - -const customers = [ - { - id: 'd6e15727-9fe1-4961-8c5b-ea44a9bd81aa', - name: 'Evil Rabbit', - email: 'evil@rabbit.com', - image_url: '/customers/evil-rabbit.png', - }, - { - id: '3958dc9e-712f-4377-85e9-fec4b6a6442a', - name: 'Delba de Oliveira', - email: 'delba@oliveira.com', - image_url: '/customers/delba-de-oliveira.png', - }, - { - id: '3958dc9e-742f-4377-85e9-fec4b6a6442a', - name: 'Lee Robinson', - email: 'lee@robinson.com', - image_url: '/customers/lee-robinson.png', - }, - { - id: '76d65c26-f784-44a2-ac19-586678f7c2f2', - name: 'Michael Novotny', - email: 'michael@novotny.com', - image_url: '/customers/michael-novotny.png', - }, - { - id: 'CC27C14A-0ACF-4F4A-A6C9-D45682C144B9', - name: 'Amy Burns', - email: 'amy@burns.com', - image_url: '/customers/amy-burns.png', - }, - { - id: '13D07535-C59E-4157-A011-F8D2EF4E0CBB', - name: 'Balazs Orban', - email: 'balazs@orban.com', - image_url: '/customers/balazs-orban.png', - }, -]; - -const guests = [ - { - customer_id: customers[0].id, - amount: 15795, - status: 'pending', - date: '2022-12-06', - }, - { - customer_id: customers[1].id, - amount: 20348, - status: 'pending', - date: '2022-11-14', - }, - { - customer_id: customers[4].id, - amount: 3040, - status: 'paid', - date: '2022-10-29', - }, - { - customer_id: customers[3].id, - amount: 44800, - status: 'paid', - date: '2023-09-10', - }, - { - customer_id: customers[5].id, - amount: 34577, - status: 'pending', - date: '2023-08-05', - }, - { - customer_id: customers[2].id, - amount: 54246, - status: 'pending', - date: '2023-07-16', - }, - { - customer_id: customers[0].id, - amount: 666, - status: 'pending', - date: '2023-06-27', - }, - { - customer_id: customers[3].id, - amount: 32545, - status: 'paid', - date: '2023-06-09', - }, - { - customer_id: customers[4].id, - amount: 1250, - status: 'paid', - date: '2023-06-17', - }, - { - customer_id: customers[5].id, - amount: 8546, - status: 'paid', - date: '2023-06-07', - }, - { - customer_id: customers[1].id, - amount: 500, - status: 'paid', - date: '2023-08-19', - }, - { - customer_id: customers[5].id, - amount: 8945, - status: 'paid', - date: '2023-06-03', - }, - { - customer_id: customers[2].id, - amount: 1000, - status: 'paid', - date: '2022-06-05', - }, -]; - -const revenue = [ - { month: 'Jan', revenue: 2000 }, - { month: 'Feb', revenue: 1800 }, - { month: 'Mar', revenue: 2200 }, - { month: 'Apr', revenue: 2500 }, - { month: 'May', revenue: 2300 }, - { month: 'Jun', revenue: 3200 }, - { month: 'Jul', revenue: 3500 }, - { month: 'Aug', revenue: 3700 }, - { month: 'Sep', revenue: 2500 }, - { month: 'Oct', revenue: 2800 }, - { month: 'Nov', revenue: 3000 }, - { month: 'Dec', revenue: 4800 }, -]; - -export { users, customers, guests, revenue }; diff --git a/app/lib/tableSimulation.tsx b/app/lib/tableSimulation.tsx new file mode 100644 index 0000000..7a01b4d --- /dev/null +++ b/app/lib/tableSimulation.tsx @@ -0,0 +1,71 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +import { Serializable } from "../api/abstract-api"; +import { Entity } from "./definitions"; +import { Guest } from "./guest"; + +export type Discomfort = { + discomfort: number; + breakdown: { + tableSizePenalty: number; + cohesionPenalty: number; + } +} + +export type Table = { + number: number; + guests: Guest[]; + discomfort: Discomfort; +} + +export class TableSimulation implements Entity { + id?: string; + tables: Table[]; + + constructor(id?: string, tables?: Table[]) { + this.id = id; + this.tables = tables || []; + } +} + +export class TableSimulationSerializer implements Serializable { + fromJson(data: any): TableSimulation { + return new TableSimulation(data.id, data.tables.map((table: any) => { + return { + number: table.number, + guests: table.guests.map((guest: any) => new Guest(guest.id, guest.name, guest.group?.name, guest.group?.id, guest.color)), + discomfort: { + discomfort: table.discomfort.discomfort, + breakdown: { + tableSizePenalty: table.discomfort.breakdown.table_size_penalty, + cohesionPenalty: table.discomfort.breakdown.cohesion_penalty, + } + }, + } + })); + } + + toJson(simulation: TableSimulation): string { + return JSON.stringify({ simulation: { tables: simulation.tables.map((table) => { + return { + number: table.number, + guests: table.guests.map((guest) => { + return { + id: guest.id, + name: guest.name, + group_id: guest.groupId, + color: guest.color, + status: guest.status, + children: guest.children, + } + }), + discomfort: table.discomfort, + } + }) } }); + } + + apiPath(): string { + return 'tables_arrangements'; + } +} + diff --git a/app/lib/utils.ts b/app/lib/utils.ts index 4d9ce77..c590751 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -1,13 +1,4 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -import { Revenue } from './definitions'; - -export const formatCurrency = (amount: number) => { - return (amount / 100).toLocaleString('en-US', { - style: 'currency', - currency: 'USD', - }); -}; +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ export const getCsrfToken = () => { return document.cookie @@ -16,63 +7,14 @@ export const getCsrfToken = () => { ?.split("=")[1] || 'unknown'; } -export const formatDateToLocal = ( - dateStr: string, - locale: string = 'en-US', -) => { - const date = new Date(dateStr); - const options: Intl.DateTimeFormatOptions = { - day: 'numeric', - month: 'short', - year: 'numeric', - }; - const formatter = new Intl.DateTimeFormat(locale, options); - return formatter.format(date); -}; +export const getSlug = () => localStorage.getItem('slug') || 'default'; -export const generateYAxis = (revenue: Revenue[]) => { - // Calculate what labels we need to display on the y-axis - // based on highest record and in 1000s - const yAxisLabels = []; - const highestRecord = Math.max(...revenue.map((month) => month.revenue)); - const topLabel = Math.ceil(highestRecord / 1000) * 1000; +// From https://stackoverflow.com/a/1026087/3607039 +export const capitalize = (val:string) => { + return String(val).charAt(0).toUpperCase() + String(val).slice(1); +} - for (let i = topLabel; i >= 0; i -= 1000) { - yAxisLabels.push(`$${i / 1000}K`); - } - - return { yAxisLabels, topLabel }; -}; - -export const generatePagination = (currentPage: number, totalPages: number) => { - // If the total number of pages is 7 or less, - // display all pages without any ellipsis. - if (totalPages <= 7) { - return Array.from({ length: totalPages }, (_, i) => i + 1); - } - - // If the current page is among the first 3 pages, - // show the first 3, an ellipsis, and the last 2 pages. - if (currentPage <= 3) { - return [1, 2, 3, '...', totalPages - 1, totalPages]; - } - - // If the current page is among the last 3 pages, - // show the first 2, an ellipsis, and the last 3 pages. - if (currentPage >= totalPages - 2) { - return [1, 2, '...', totalPages - 2, totalPages - 1, totalPages]; - } - - // If the current page is somewhere in the middle, - // show the first page, an ellipsis, the current page and its neighbors, - // another ellipsis, and the last page. - return [ - 1, - '...', - currentPage - 1, - currentPage, - currentPage + 1, - '...', - totalPages, - ]; -}; +// From https://stackoverflow.com/a/62118163/3607039 +export function asArray(value: T | T[]): T[] { + return ([] as T[]).concat(value) +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index 95ca011..0000000 --- a/app/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -import Link from 'next/link'; -import styles from '@/app/ui/home.module.css'; - -export default function Page() { - return ( -
- -
- ); -} diff --git a/app/seed/route.ts b/app/seed/route.ts deleted file mode 100644 index db632cd..0000000 --- a/app/seed/route.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -// import bcrypt from 'bcrypt'; -// import { db } from '@vercel/postgres'; -// import { guests, customers, revenue, users } from '../lib/placeholder-data'; - -// const client = await db.connect(); - -// async function seedUsers() { -// await client.sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`; -// await client.sql` -// CREATE TABLE IF NOT EXISTS users ( -// id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, -// name VARCHAR(255) NOT NULL, -// email TEXT NOT NULL UNIQUE, -// password TEXT NOT NULL -// ); -// `; - -// const insertedUsers = await Promise.all( -// users.map(async (user) => { -// const hashedPassword = await bcrypt.hash(user.password, 10); -// return client.sql` -// INSERT INTO users (id, name, email, password) -// VALUES (${user.id}, ${user.name}, ${user.email}, ${hashedPassword}) -// ON CONFLICT (id) DO NOTHING; -// `; -// }), -// ); - -// return insertedUsers; -// } - -// async function seedguests() { -// await client.sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`; - -// await client.sql` -// CREATE TABLE IF NOT EXISTS guests ( -// id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, -// customer_id UUID NOT NULL, -// amount INT NOT NULL, -// status VARCHAR(255) NOT NULL, -// date DATE NOT NULL -// ); -// `; - -// const insertedguests = await Promise.all( -// guests.map( -// (invoice) => client.sql` -// INSERT INTO guests (customer_id, amount, status, date) -// VALUES (${invoice.customer_id}, ${invoice.amount}, ${invoice.status}, ${invoice.date}) -// ON CONFLICT (id) DO NOTHING; -// `, -// ), -// ); - -// return insertedguests; -// } - -// async function seedCustomers() { -// await client.sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`; - -// await client.sql` -// CREATE TABLE IF NOT EXISTS customers ( -// id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, -// name VARCHAR(255) NOT NULL, -// email VARCHAR(255) NOT NULL, -// image_url VARCHAR(255) NOT NULL -// ); -// `; - -// const insertedCustomers = await Promise.all( -// customers.map( -// (customer) => client.sql` -// INSERT INTO customers (id, name, email, image_url) -// VALUES (${customer.id}, ${customer.name}, ${customer.email}, ${customer.image_url}) -// ON CONFLICT (id) DO NOTHING; -// `, -// ), -// ); - -// return insertedCustomers; -// } - -// async function seedRevenue() { -// await client.sql` -// CREATE TABLE IF NOT EXISTS revenue ( -// month VARCHAR(4) NOT NULL UNIQUE, -// revenue INT NOT NULL -// ); -// `; - -// const insertedRevenue = await Promise.all( -// revenue.map( -// (rev) => client.sql` -// INSERT INTO revenue (month, revenue) -// VALUES (${rev.month}, ${rev.revenue}) -// ON CONFLICT (month) DO NOTHING; -// `, -// ), -// ); - -// return insertedRevenue; -// } - -export async function GET() { - return Response.json({ - message: - 'Uncomment this file and remove this line. You can delete this file when you are finished.', - }); - // try { - // await client.sql`BEGIN`; - // await seedUsers(); - // await seedCustomers(); - // await seedguests(); - // await seedRevenue(); - // await client.sql`COMMIT`; - - // return Response.json({ message: 'Database seeded successfully' }); - // } catch (error) { - // await client.sql`ROLLBACK`; - // return Response.json({ error }, { status: 500 }); - // } -} diff --git a/app/types.tsx b/app/types.tsx index 283e371..5c95b52 100644 --- a/app/types.tsx +++ b/app/types.tsx @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ import * as HeroIcon from '@heroicons/react/24/outline' import { ComponentProps } from 'react' diff --git a/app/ui/arrangements/arrangement.tsx b/app/ui/arrangements/arrangement.tsx new file mode 100644 index 0000000..d8bf0b1 --- /dev/null +++ b/app/ui/arrangements/arrangement.tsx @@ -0,0 +1,38 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +'use client'; + +import { AbstractApi } from '@/app/api/abstract-api'; +import { TableArrangement } from '@/app/lib/definitions'; +import { TableSimulation, TableSimulationSerializer } from '@/app/lib/tableSimulation'; +import { getSlug } from '@/app/lib/utils'; +import { Table } from '@/app/ui/components/table'; +import { lusitana } from '@/app/ui/fonts'; +import { useState, useEffect } from 'react'; + +export default function Arrangement({ id }: { id: string }) { + + const [simulation, setSimulation] = useState(undefined); + + function loadSimulation() { + new AbstractApi().get(new TableSimulationSerializer(), id, (object: TableSimulation) => { + setSimulation(object); + }); + } + + useEffect(loadSimulation, []); + + return ( +
+
+

Table distributions

+
+ {simulation && simulation.tables.map((table) => ( + + ))} + + + + ) + +} \ No newline at end of file diff --git a/app/ui/arrangements/arrangements-table.tsx b/app/ui/arrangements/arrangements-table.tsx new file mode 100644 index 0000000..07a3eff --- /dev/null +++ b/app/ui/arrangements/arrangements-table.tsx @@ -0,0 +1,63 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +'use client' + +import React, { useState } from "react" +import { TableArrangement } from '@/app/lib/definitions'; +import { classNames } from "../components/button"; +import TableOfContents from "../components/table-of-contents"; +import { loadTableSimulations } from "@/app/api/tableSimulations"; +import { ArchiveBoxXMarkIcon, CheckBadgeIcon } from "@heroicons/react/24/outline"; +import { Tooltip } from "primereact/tooltip"; +import clsx from "clsx"; + +export default function ArrangementsTable({ onArrangementSelected }: { onArrangementSelected: (arrangementId: string) => void }) { + const [arrangements, setArrangements] = useState>([]); + const [arrangementsLoaded, setArrangementsLoaded] = useState(false); + + function refreshSimulations() { + loadTableSimulations((arrangements) => { + setArrangements(arrangements); + setArrangementsLoaded(true); + }); + } + + function arrangementClicked(e: React.MouseEvent) { + onArrangementSelected(e.currentTarget.getAttribute('data-arrangement-id') || ''); + } + + !arrangementsLoaded && refreshSimulations(); + + return ( + ( + + + + + + + )} + /> + ); +} \ No newline at end of file diff --git a/app/ui/button.tsx b/app/ui/button.tsx index d30b980..1f1034f 100644 --- a/app/ui/button.tsx +++ b/app/ui/button.tsx @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ import clsx from 'clsx'; diff --git a/app/ui/components/affinities-form-dialog.tsx b/app/ui/components/affinities-form-dialog.tsx new file mode 100644 index 0000000..dc8bc72 --- /dev/null +++ b/app/ui/components/affinities-form-dialog.tsx @@ -0,0 +1,82 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +'use client'; + +import { Affinities } from '@/app/lib/affinities'; +import { Group } from '@/app/lib/group'; +import { getCsrfToken, getSlug } from '@/app/lib/utils'; +import { classNames } from '@/app/ui/components/button'; +import { Dialog } from 'primereact/dialog'; +import { useEffect, useState } from 'react'; +import AffinitySlider from './form/affinitySlider'; + +export default function AffinitiesFormDialog({ groups, group, visible, onHide }: { + groups: Group[], + group?: Group, + visible: boolean, + onHide: () => void, +}) { + const [affinities, setAffinities] = useState({}); + const [isLoadingAffinities, setIsLoadingAffinities] = useState(true); + + useEffect(() => { + setIsLoadingAffinities(true); + if (group?.id === undefined) { + setAffinities({}); + setIsLoadingAffinities(false) + } else { + fetch(`/api/${getSlug()}/groups/${group?.id}/affinities`) + .then((response) => response.json()) + .then((data) => { + setAffinities(data); + setIsLoadingAffinities(false) + }); + } + }, [group]); + + function submitAffinities() { + const formattedAffinities = Object.entries(affinities).map(([groupId, affinity]) => ({ group_id: groupId, affinity: affinity })); + fetch(`/api/${getSlug()}/groups/${group?.id}/affinities/bulk_update`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': getCsrfToken(), + }, + body: JSON.stringify({ affinities: formattedAffinities }) + }).then(() => { + onHide(); + }); + } + + function resetAffinities() { + fetch(`/api/${getSlug()}/groups/${group?.id}/affinities/default`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + } + }).then((response) => response.json()) + .then(setAffinities); + } + + return ( + + {!isLoadingAffinities &&
+ Describe the affinity with the rest of the groups + + { + groups.filter((currentGroup) => currentGroup.id !== group?.id).map((group) => +
+ {group.name} + setAffinities({ ...affinities, [group.id || "default"]: value })} /> +
) + } + +
+ + +
+
+ } +
+ ); +} \ No newline at end of file diff --git a/app/ui/components/button.tsx b/app/ui/components/button.tsx new file mode 100644 index 0000000..2ecfabb --- /dev/null +++ b/app/ui/components/button.tsx @@ -0,0 +1,15 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +import clsx from "clsx"; + +type ButtonColor = 'primary' | 'blue' | 'green' | 'red' | 'yellow' | 'gray'; + +export function classNames(type: ButtonColor) { + return (clsx("text-white py-1 px-2 mx-1 rounded disabled:opacity-50 disabled:cursor-not-allowed", { + 'bg-blue-400 hover:bg-blue-600': type === 'primary' || type === 'blue', + 'bg-green-500 hover:bg-green-600': type === 'green', + 'bg-red-500 hover:bg-red-600': type === 'red', + 'bg-yellow-500 hover:bg-yellow-700': type === 'yellow', + 'bg-gray-500 hover:bg-gray-700': type === 'gray' + })) +} \ No newline at end of file diff --git a/app/ui/components/dashboard-cards.tsx b/app/ui/components/dashboard-cards.tsx index 6c72ac4..26c7bf7 100644 --- a/app/ui/components/dashboard-cards.tsx +++ b/app/ui/components/dashboard-cards.tsx @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ import clsx from "clsx" import { Icon } from "../../types"; @@ -21,7 +21,7 @@ const colorClasses = (style: Style) => { } } -export async function MainCard({ amount, title, subtitle, style, iconName }: +export function MainCard({ amount, title, subtitle, style, iconName }: { amount: string, title: string, @@ -42,7 +42,7 @@ export async function MainCard({ amount, title, subtitle, style, iconName }: ); } -export async function SecondaryCard({ amount, title, iconName, style }: { amount: string, title: string, iconName: keyof typeof HeroIcon, style: Style }) { +export function SecondaryCard({ amount, title, iconName, style }: { amount: string, title: string, iconName: keyof typeof HeroIcon, style: Style }) { return (
diff --git a/app/ui/components/expense-form-dialog.tsx b/app/ui/components/expense-form-dialog.tsx new file mode 100644 index 0000000..18c7299 --- /dev/null +++ b/app/ui/components/expense-form-dialog.tsx @@ -0,0 +1,83 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +'use client'; + +import { AbstractApi } from '@/app/api/abstract-api'; +import { Expense, ExpenseSerializer, PricingType, pricingTypes } from '@/app/lib/expense'; +import { capitalize } from '@/app/lib/utils'; +import { classNames } from '@/app/ui/components/button'; +import { Dialog } from 'primereact/dialog'; +import { Dropdown } from 'primereact/dropdown'; +import { FloatLabel } from 'primereact/floatlabel'; +import { InputText } from 'primereact/inputtext'; +import { useState } from 'react'; + +export default function ExpenseFormDialog({ onCreate, onHide, expense, visible }: { + onCreate?: () => void, + onHide: () => void, + expense?: Expense, + visible: boolean, +}) { + + const [name, setName] = useState(expense?.name || ''); + const [amount, setAmount] = useState(expense?.amount || 0); + const [pricingType, setPricingType] = useState(expense?.pricingType || 'fixed'); + + const api = new AbstractApi(); + const serializer = new ExpenseSerializer(); + + function resetForm() { + setName(''); + setAmount(0); + setPricingType('fixed'); + } + + function submitGroup() { + if (expense?.id !== undefined) { + expense.name = name; + expense.amount = amount; + expense.pricingType = pricingType; + + api.update(serializer, expense, () => { + resetForm(); + onCreate && onCreate(); + }); + } else { + + api.create(serializer, new Expense(undefined, name, amount, pricingType), () => { + resetForm(); + onCreate && onCreate(); + }); + } + } + + return ( + <> + + +
+ + setName(e.target.value)} /> + + + + setAmount(parseFloat(e.target.value))} /> + + + + setPricingType(e.target.value)} options={ + pricingTypes.map((type) => { + return { label: capitalize(type), value: type }; + }) + } /> + + + + +
+
+ + ); +} \ No newline at end of file diff --git a/app/ui/components/form/affinitySlider.tsx b/app/ui/components/form/affinitySlider.tsx new file mode 100644 index 0000000..99f7ce6 --- /dev/null +++ b/app/ui/components/form/affinitySlider.tsx @@ -0,0 +1,40 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +import { Slider } from 'primereact/slider'; + +export default function AffinitySlider({ value, onChange }: { value: number, onChange: (value: number) => void }) { + + const toNumber = (value : number | [number, number]) => { + if(value instanceof Array) { + return value[0]; + } + return value; + } + + const label = (value: number) => { + if (value < 0.2) { + return 'Nemesis'; + } else if (value < 0.5) { + return 'Enemies'; + } else if (value < 0.9) { + return 'Bad vibes'; + } else if (value < 1.1) { + return 'Neutral'; + } else if (value < 1.5) { + return 'Good vibes'; + } else if (value < 1.8) { + return 'Good friends'; + } else { + return 'Besties'; + } + } + + return ( + <> + onChange(toNumber(e.value))} className='w-80 bg-gray-400' /> + + {label(value)} + + + ) +} \ No newline at end of file diff --git a/app/ui/components/form/inlineTextField.tsx b/app/ui/components/form/inlineTextField.tsx new file mode 100644 index 0000000..8e07935 --- /dev/null +++ b/app/ui/components/form/inlineTextField.tsx @@ -0,0 +1,34 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +import React, { useState } from 'react'; + +export default function InlineTextField({ initialValue, onChange }: { initialValue: string, onChange: (value: string) => void }) { + const [editing, setEditing] = useState(false); + const [value, setValue] = useState(initialValue); + + const renderText = () => setEditing(true)}>{value} + + const onConfirm = () => { + onChange(value); + setEditing(false); + } + + function renderForm() { + return ( +
+ setValue(e.target.value)} + onBlur={onConfirm} + autoFocus + /> +
+ ) + } + + return ( + editing ? (renderForm()) : (renderText()) + ); +} \ No newline at end of file diff --git a/app/ui/components/group-form-dialog.tsx b/app/ui/components/group-form-dialog.tsx new file mode 100644 index 0000000..03d5c92 --- /dev/null +++ b/app/ui/components/group-form-dialog.tsx @@ -0,0 +1,95 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +'use client'; + +import { AbstractApi } from '@/app/api/abstract-api'; +import { Group, GroupSerializer } from '@/app/lib/group'; +import { classNames } from '@/app/ui/components/button'; +import { ColorPicker } from 'primereact/colorpicker'; +import { Dialog } from 'primereact/dialog'; +import { Dropdown } from 'primereact/dropdown'; +import { FloatLabel } from 'primereact/floatlabel'; +import { InputText } from 'primereact/inputtext'; +import { useState } from 'react'; + +export default function GroupFormDialog({ groups, onCreate, onHide, group, visible }: { + groups: Group[], + onCreate?: () => void, + onHide: () => void, + group?: Group, + visible: boolean, +}) { + + const [name, setName] = useState(group?.name || ''); + const [icon, setIcon] = useState(group?.icon || ''); + const [color, setColor] = useState(group?.color || ''); + const [parentId, setParentId] = useState(group?.parentId || ''); + + const api = new AbstractApi(); + const serializer = new GroupSerializer(); + + function resetForm() { + setName(''); + setIcon(''); + setColor(''); + setParentId(''); + } + + function submitGroup() { + if (!(name)) { + return + } + + if (group?.id !== undefined) { + group.name = name; + group.icon = icon; + group.color = color; + group.parentId = parentId; + + api.update(serializer, group, () => { + resetForm(); + onCreate && onCreate(); + }); + } else { + + api.create(serializer, new Group(undefined, name, undefined, icon, undefined, parentId, color), () => { + resetForm(); + onCreate && onCreate(); + }); + } + } + + return ( + <> + + +
+ + setName(e.target.value)} /> + + + + setIcon(e.target.value)} /> + + + + setColor(`#${e.value}`)} /> + + + setParentId(e.target.value)} options={ + groups.map((group) => { + return { label: group.name, value: group.id }; + }) + } /> + + + + +
+
+ + ); +} \ No newline at end of file diff --git a/app/ui/components/guest-form-dialog.tsx b/app/ui/components/guest-form-dialog.tsx new file mode 100644 index 0000000..c0b20e0 --- /dev/null +++ b/app/ui/components/guest-form-dialog.tsx @@ -0,0 +1,91 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +'use client'; + +import { AbstractApi } from '@/app/api/abstract-api'; +import { Group } from '@/app/lib/group'; +import { Guest, GuestSerializer, GuestStatus, guestStatuses } from '@/app/lib/guest'; +import { capitalize } from '@/app/lib/utils'; +import { classNames } from '@/app/ui/components/button'; +import { Dialog } from 'primereact/dialog'; +import { Dropdown } from 'primereact/dropdown'; +import { FloatLabel } from 'primereact/floatlabel'; +import { InputText } from 'primereact/inputtext'; +import { useState } from 'react'; + +export default function GuestFormDialog({ groups, onCreate, onHide, guest, visible }: { + groups: Group[], + onCreate?: () => void, + onHide: () => void, + guest?: Guest, + visible: boolean, +}) { + + const [name, setName] = useState(guest?.name || ''); + const [group, setGroup] = useState(guest?.groupId || null); + const [status, setStatus] = useState(guest?.status || null); + + const api = new AbstractApi(); + const serializer = new GuestSerializer(); + + function resetForm() { + setName(''); + setGroup(null); + setStatus(null); + } + + function submitGuest() { + if (!(name && group && status)) { + return + } + + if (guest?.id !== undefined) { + guest.name = name; + guest.groupId = group; + guest.status = status; + + api.update(serializer, guest, () => { + resetForm(); + onCreate && onCreate(); + }); + } else { + api.create(serializer, new Guest(undefined, name, undefined, group, undefined, status), ()=> { + resetForm(); + onCreate && onCreate(); + }); + } + } + + return ( + <> + + +
+ + setName(e.target.value)} /> + + + + setGroup(e.target.value)} options={ + groups.map((group) => { + return { label: group.name, value: group.id }; + }) + } /> + + + + setStatus(e.target.value)} options={ + guestStatuses.map((status) => { + return { label: capitalize(status), value: status }; + }) + } /> + + + +
+
+ + ); +} \ No newline at end of file diff --git a/app/ui/components/login-form.tsx b/app/ui/components/login-form.tsx new file mode 100644 index 0000000..7b808c2 --- /dev/null +++ b/app/ui/components/login-form.tsx @@ -0,0 +1,51 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +'use client'; + +import { FloatLabel } from 'primereact/floatlabel'; +import { InputText } from 'primereact/inputtext'; +import { login } from '../../api/authentication'; +import { useState, useEffect } from 'react'; +import { classNames } from './button'; +import { useRouter } from 'next/navigation' +import { User } from '../../lib/definitions'; +import { getSlug } from '@/app/lib/utils'; + +export default function LoginForm() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + + const router = useRouter(); + + const [currentUser, setCurrentUser] = useState(null); + + useEffect(() => { + localStorage.setItem('currentUser', JSON.stringify(currentUser)); + }, [currentUser]); + + return ( +
+ + setEmail(e.target.value)} /> + + + + setPassword(e.target.value)} /> + + + +
+ ) +} \ No newline at end of file diff --git a/app/ui/components/registration-form.tsx b/app/ui/components/registration-form.tsx new file mode 100644 index 0000000..3de63ea --- /dev/null +++ b/app/ui/components/registration-form.tsx @@ -0,0 +1,98 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +'use client'; + +import { FloatLabel } from 'primereact/floatlabel'; +import { InputText } from 'primereact/inputtext'; +import { useState, useEffect } from 'react'; +import { classNames } from './button'; +import { getSlug } from '@/app/lib/utils'; +import { register } from '@/app/api/authentication'; +import { getCaptchaChallenge } from '@/app/api/captcha'; + +export default function RegistrationForm() { + const [submitted, setSubmitted] = useState(false); + const [errors, setErrors] = useState([]); + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [passwordConfirmation, setPasswordConfirmation] = useState(""); + const [slug, setSlug] = useState(getSlug()); + + const [captchaId, setCaptchaId] = useState(""); + const [captchaUrl, setCaptchaUrl] = useState(""); + const [captchaAnswer, setCaptchaAnswer] = useState(""); + + const refreshCaptcha = () => { + getCaptchaChallenge({ + onRetrieve: (id, url) => { + console.log(id, url); + setCaptchaId(id); + setCaptchaUrl(url); + setCaptchaAnswer(""); + } + }); + } + + useEffect(refreshCaptcha, []) + + return ( + submitted ? ( +
+
Registration successful. Check your email for a confirmation link.
+
+ ) : ( + +
+ + setEmail(e.target.value)} /> + + + + setPassword(e.target.value)} /> + + + + setPasswordConfirmation(e.target.value)} /> + + + + setSlug(e.target.value)} /> + + + captcha + + setCaptchaAnswer(e.target.value)} /> + + + + {errors.map((error, index) => ( +
{error}
+ ))} + + + +
+ ) + + ); +} \ No newline at end of file diff --git a/app/ui/components/table-of-contents.tsx b/app/ui/components/table-of-contents.tsx new file mode 100644 index 0000000..410f7b6 --- /dev/null +++ b/app/ui/components/table-of-contents.tsx @@ -0,0 +1,28 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +export default function TableOfContents({ headers, caption, elements, rowRender }: { headers: string[], caption: string, elements: Type[], rowRender: (element: Type) => JSX.Element }) { + return ( +
+
+ {arrangement.name} + + {arrangement.discomfort} + + + + + + { + arrangement.valid ? + : + + } +
+ + + + {headers.map((header) => ( + + ))} + + + + {elements.map((element) => rowRender(element))} + +
+ {caption} +

+ There are {elements.length} elements in the list +

+
+ {header} +
+
+ ) +} diff --git a/app/ui/components/table.tsx b/app/ui/components/table.tsx new file mode 100644 index 0000000..ec8a999 --- /dev/null +++ b/app/ui/components/table.tsx @@ -0,0 +1,107 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +import { Guest } from "@/app/lib/guest"; +import { Table as TableType } from "@/app/lib/tableSimulation"; +import { RectangleGroupIcon, UserGroupIcon } from "@heroicons/react/24/outline"; +import { v4 as uuidv4 } from 'uuid'; +import { Tooltip } from "primereact/tooltip"; +import clsx from "clsx"; + + +function Dish({ guest, rotation }: { guest: Guest, rotation?: number }) { + rotation = rotation || 0 + return ( +
+ {guest.name} +
+ ) +} + +function GuestRow({ guests }: { guests: Guest[] }) { + return ( +
+ {guests.map((guest) => )} +
+ ) +} + +function RectangularTable({ table }: { table: TableType }) { + const guests = table.guests; + const halfwayThrough = Math.floor(guests.length / 2) + const arrayFirstHalf = guests.slice(0, halfwayThrough); + const arraySecondHalf = guests.slice(halfwayThrough, guests.length); + + return ( +
+ + +
+ ) +} + +function RoundedTable({ table }: { table: TableType }) { + const guests = table.guests; + const size = 500 + const rotation = 360 / guests.length + + const className = (penalty: number) => { + return clsx("px-2 tooltip-cohesion", { + "hidden": penalty === 0, + "text-orange-300": penalty <= 5, + "text-orange-500": penalty > 5 && penalty <= 10, + "text-orange-700": penalty > 10, + }) + } + + + return ( +
+ + { + guests.map((guest, index) => { + return ( +
+ +
+ ) + }) + } + +
+
{`Table #${table.number}`}
+ + + + + + + + +
+
+ ) +} + +export function Table({ table, style }: { table: TableType, style: "rectangular" | "rounded" }) { + return ( + <> + {style === "rectangular" && } + {style === "rounded" && } + + ) +} \ No newline at end of file diff --git a/app/ui/customers/table.tsx b/app/ui/customers/table.tsx deleted file mode 100644 index 6a41e3f..0000000 --- a/app/ui/customers/table.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -import Image from 'next/image'; -import { lusitana } from '@/app/ui/fonts'; -import Search from '@/app/ui/search'; -import { - CustomersTableType, - FormattedCustomersTable, -} from '@/app/lib/definitions'; - -export default async function CustomersTable({ - customers, -}: { - customers: FormattedCustomersTable[]; -}) { - return ( -
-

- Customers -

- -
-
-
-
-
- {customers?.map((customer) => ( -
-
-
-
-
- {`${customer.name}'s -

{customer.name}

-
-
-

- {customer.email} -

-
-
-
-
-

Pending

-

{customer.total_pending}

-
-
-

Paid

-

{customer.total_paid}

-
-
-
-

{customer.total_guests} guests

-
-
- ))} -
- - - - - - - - - - - - - {customers.map((customer) => ( - - - - - - - - ))} - -
- Name - - Email - - Total guests - - Total Pending - - Total Paid -
-
- {`${customer.name}'s -

{customer.name}

-
-
- {customer.email} - - {customer.total_guests} - - {customer.total_pending} - - {customer.total_paid} -
-
-
-
-
-
- ); -} diff --git a/app/ui/dashboard/cards.tsx b/app/ui/dashboard/cards.tsx deleted file mode 100644 index 687043c..0000000 --- a/app/ui/dashboard/cards.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -import { - BanknotesIcon, - ClockIcon, - UserGroupIcon, - InboxIcon, -} from '@heroicons/react/24/outline'; -import { lusitana } from '@/app/ui/fonts'; - -const iconMap = { - collected: BanknotesIcon, - customers: UserGroupIcon, - pending: ClockIcon, - guests: InboxIcon, -}; - -export default async function CardWrapper() { - return ( - <> - {/* NOTE: Uncomment this code in Chapter 9 */} - - {/* - - - */} - - ); -} - -export function Card({ - title, - value, - type, -}: { - title: string; - value: number | string; - type: 'guests' | 'customers' | 'pending' | 'collected'; -}) { - const Icon = iconMap[type]; - - return ( -
-
- {Icon ? : null} -

{title}

-
-

- {value} -

-
- ); -} diff --git a/app/ui/dashboard/global-summary.tsx b/app/ui/dashboard/global-summary.tsx new file mode 100644 index 0000000..810a95b --- /dev/null +++ b/app/ui/dashboard/global-summary.tsx @@ -0,0 +1,41 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +import { GlobalSummary as Summary} from '@/app/lib/definitions'; +import { MainCard, SecondaryCard } from '../components/dashboard-cards'; + +export default function GlobalSummary({ summary }: { summary: Summary }) { + return ( +
+
+
+ + +
+
+ + + +
+
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/app/ui/dashboard/latest-invoices.tsx b/app/ui/dashboard/latest-invoices.tsx deleted file mode 100644 index 7c668e9..0000000 --- a/app/ui/dashboard/latest-invoices.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -import { ArrowPathIcon } from '@heroicons/react/24/outline'; -import clsx from 'clsx'; -import Image from 'next/image'; -import { lusitana } from '@/app/ui/fonts'; -import { LatestInvoice } from '@/app/lib/definitions'; -export default async function Latestguests({ - latestguests, -}: { - latestguests: LatestInvoice[]; -}) { - return ( -
-

- Latest guests -

-
- {/* NOTE: Uncomment this code in Chapter 7 */} - - {/*
- {latestguests.map((invoice, i) => { - return ( -
-
- {`${invoice.name}'s -
-

- {invoice.name} -

-

- {invoice.email} -

-
-
-

- {invoice.amount} -

-
- ); - })} -
*/} -
- -

Updated just now

-
-
-
- ); -} diff --git a/app/ui/dashboard/loading.tsx b/app/ui/dashboard/loading.tsx index 2614837..ca17e8b 100644 --- a/app/ui/dashboard/loading.tsx +++ b/app/ui/dashboard/loading.tsx @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ export default function Loading() { return
Loading...
; diff --git a/app/ui/dashboard/nav-links.tsx b/app/ui/dashboard/nav-links.tsx index 7d88618..ef3e4ea 100644 --- a/app/ui/dashboard/nav-links.tsx +++ b/app/ui/dashboard/nav-links.tsx @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ 'use client' @@ -10,13 +10,14 @@ import { import Link from 'next/link'; import { usePathname } from 'next/navigation'; import clsx from 'clsx'; +import { getSlug } from '@/app/lib/utils'; // Map of links to display in the side navigation. // Depending on the size of the application, this would be stored in a database. const links = [ - { name: 'Guests', href: '/dashboard/guests', icon: UserGroupIcon }, - { name: 'Expenses', href: '/dashboard/expenses', icon: BanknotesIcon }, - { name: 'Table distributions', href: '/dashboard/tables', icon: RectangleGroupIcon }, + { 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 }, ]; diff --git a/app/ui/dashboard/revenue-chart.tsx b/app/ui/dashboard/revenue-chart.tsx deleted file mode 100644 index 023e61d..0000000 --- a/app/ui/dashboard/revenue-chart.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -import { generateYAxis } from '@/app/lib/utils'; -import { CalendarIcon } from '@heroicons/react/24/outline'; -import { lusitana } from '@/app/ui/fonts'; -import { Revenue } from '@/app/lib/definitions'; - -// This component is representational only. -// For data visualization UI, check out: -// https://www.tremor.so/ -// https://www.chartjs.org/ -// https://airbnb.io/visx/ - -export default async function RevenueChart({ - revenue, -}: { - revenue: Revenue[]; -}) { - const chartHeight = 350; - // NOTE: Uncomment this code in Chapter 7 - - // const { yAxisLabels, topLabel } = generateYAxis(revenue); - - // if (!revenue || revenue.length === 0) { - // return

No data available.

; - // } - - return ( -
-

- Recent Revenue -

- {/* NOTE: Uncomment this code in Chapter 7 */} - - {/*
-
-
- {yAxisLabels.map((label) => ( -

{label}

- ))} -
- - {revenue.map((month) => ( -
-
-

- {month.month} -

-
- ))} -
-
- -

Last 12 months

-
-
*/} -
- ); -} diff --git a/app/ui/dashboard/sidenav.tsx b/app/ui/dashboard/sidenav.tsx index a13a2e6..fbba770 100644 --- a/app/ui/dashboard/sidenav.tsx +++ b/app/ui/dashboard/sidenav.tsx @@ -1,30 +1,46 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +'use client'; import Link from 'next/link'; import NavLinks from '@/app/ui/dashboard/nav-links'; import { PowerIcon } from '@heroicons/react/24/outline'; import { gloriaHallelujah } from '@/app/ui/fonts'; +import { logout } from '@/app/api/authentication'; +import { useRouter } from 'next/navigation'; +import { getSlug } from '@/app/lib/utils'; export default function SideNav() { + const router = useRouter(); + return (
-
+

Wedding Planner

-
- -
+ Logged in as {JSON.parse(localStorage.getItem('currentUser') || '{}').email} +
); diff --git a/app/ui/expenses/summary.tsx b/app/ui/expenses/summary.tsx deleted file mode 100644 index d838c4b..0000000 --- a/app/ui/expenses/summary.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -import { MainCard, SecondaryCard } from '../components/dashboard-cards'; - -export default async function ExpenseSummary() { - return ( -
- -
- - -
- - -
-
- - -
- -
- -
- - -
- - -
- -
- - - -
-
- -
- - - -
-
- ); -} \ No newline at end of file diff --git a/app/ui/expenses/table.tsx b/app/ui/expenses/table.tsx new file mode 100644 index 0000000..a90abbd --- /dev/null +++ b/app/ui/expenses/table.tsx @@ -0,0 +1,46 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +'use client' + +import { AbstractApi } from '@/app/api/abstract-api'; +import { Expense, ExpenseSerializer } from '@/app/lib/expense'; +import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; +import TableOfContents from "../components/table-of-contents"; + + +export default function ExpensesTable({ expenses, onUpdate, onEdit }: { + expenses: Expense[], + onUpdate: () => void, + onEdit: (expense: Expense) => void, +}) { + + const api = new AbstractApi(); + const serializer = new ExpenseSerializer(); + + return ( + ( + + + {expense.name} + + + {expense.amount} + + + {expense.pricingType} + + +
+ { api.destroy(serializer, expense, onUpdate) }} /> + onEdit(expense)} /> +
+ + + )} + /> + ); +} \ No newline at end of file diff --git a/app/ui/fonts.ts b/app/ui/fonts.ts index e520f6f..f32d5f9 100644 --- a/app/ui/fonts.ts +++ b/app/ui/fonts.ts @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ import { Inter, Lusitana, Gloria_Hallelujah} from 'next/font/google'; diff --git a/app/ui/groups/table.tsx b/app/ui/groups/table.tsx new file mode 100644 index 0000000..d22909c --- /dev/null +++ b/app/ui/groups/table.tsx @@ -0,0 +1,105 @@ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ + +'use client'; + +import { AbstractApi } from '@/app/api/abstract-api'; +import { Group, GroupSerializer } from '@/app/lib/group'; +import { AdjustmentsHorizontalIcon, PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; +import { Column } from 'primereact/column'; +import { TreeNode } from 'primereact/treenode'; +import { TreeTable } from 'primereact/treetable'; + +export default function GroupsTable({ groups, onUpdate, onEdit, onEditAffinities }: { + groups: Group[], + onUpdate: () => void, + onEdit: (group: Group) => void, + onEditAffinities: (group: Group) => void, +}) { + + const api = new AbstractApi(); + const serializer = new GroupSerializer(); + + const actions = (group: Group) => ( +
+ { api.destroy(serializer, group, onUpdate) }} /> + onEdit(group)} /> + onEditAffinities(group)} /> +
+ ); + + const index = groups.reduce((acc, group) => { + if (group.id) { + acc.set(group.id, group) + } + + return acc; + }, new Map()); + + groups.forEach(group => group.children = []); + groups.forEach(group => { + if (group.parentId) { + const parent = index.get(group.parentId); + if (parent) { + if (!parent.children) { + parent.children = []; + } + parent.children.push(group); + } + } + }); + + const renderTree = (group: Group): TreeNode => { + const childrenAttendance = (group.children || []).reduce((acc, child) => { + acc.confirmed += child.attendance?.confirmed || 0; + acc.tentative += child.attendance?.tentative || 0; + acc.invited += child.attendance?.invited || 0; + acc.declined += child.attendance?.declined || 0; + acc.considered += child.attendance?.considered || 0; + acc.total += child.attendance?.total || 0; + return acc; + }, { confirmed: 0, tentative: 0, invited: 0, declined: 0, considered: 0, total: 0 }); + + return { + id: group.id, + key: group.id, + label: group.name, + data: { + name: group.name, + color:
, + confirmed: childrenAttendance.confirmed + (group.attendance?.confirmed || 0), + tentative: childrenAttendance.tentative + (group.attendance?.tentative || 0), + pending: childrenAttendance.invited + (group.attendance?.invited || 0), + declined: childrenAttendance.declined + (group.attendance?.declined || 0), + considered: childrenAttendance.considered + (group.attendance?.considered || 0), + total: childrenAttendance.total + (group.attendance?.total || 0), + actions: actions(group), + }, + children: group.children?.map(renderTree), + } + } + + const nodes: TreeNode[] = groups + .filter(group => !group.parentId) + .map(renderTree) + + const headers = ['Name', 'Color', 'Confirmed', 'Tentative', 'Pending', 'Declined', 'Considered', 'Total', 'Actions']; + const rowClassName = () => { + return { 'border-b odd:bg-white even:bg-gray-50 hover:bg-gray-100': true }; + } + + return ( + <> + + + + + + + + + + + + + ) +} diff --git a/app/ui/guests/affinity-groups-tree.tsx b/app/ui/guests/affinity-groups-tree.tsx deleted file mode 100644 index 0082070..0000000 --- a/app/ui/guests/affinity-groups-tree.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -'use client' - -import React, { useState, useEffect, Suspense } from 'react'; -import { Tree } from 'primereact/tree'; -import { PrimeIcons } from 'primereact/api'; -import { debug } from 'console'; -import { Group } from '@/app/lib/definitions'; - -export default function AffinityGroupsTree() { - const [nodes, setNodes] = useState([]); - - const groupToNode = (group: Group): any => { - return({ - key: group.id, - label: `${group.name} (${group.guest_count})`, - icon: group.icon, - children: group.children.map((child) => groupToNode(child)), - className: "px-4", - }) - } - - const parseNode = (record: any, included: any[]): Group => { - if (!record.attributes) { - record = included.find((a) => a.id === record.id); - } - - const children: Group[] = (record?.relationships?.children?.data || []).map((child: any) => { - return (parseNode(child, included)); - }); - - const children_guest_count: number = children.reduce((acc: number, child: Group) => acc + child.guest_count, 0); - - return ({ - id: record.id, - name: record.attributes.name, - guest_count: record.attributes.guest_count + children_guest_count, - icon: record.attributes.icon, - children: children, - }) - } - - - useEffect(() => { - if (nodes.length > 0) { - return; - } - fetch("/api/groups.json") - .then((response) => response.json()) - .then((data) => { - setNodes(data.data.map((record: any) => { - return (groupToNode(parseNode(record, data.included))); - })) - }); - }); - - return ( -
- - setNodes(e.value as any)} className="w-full md:w-30rem" /> - -
- ) -} \ No newline at end of file diff --git a/app/ui/guests/breadcrumbs.tsx b/app/ui/guests/breadcrumbs.tsx deleted file mode 100644 index 828a614..0000000 --- a/app/ui/guests/breadcrumbs.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -import { clsx } from 'clsx'; -import Link from 'next/link'; -import { lusitana } from '@/app/ui/fonts'; - -interface Breadcrumb { - label: string; - href: string; - active?: boolean; -} - -export default function Breadcrumbs({ - breadcrumbs, -}: { - breadcrumbs: Breadcrumb[]; -}) { - return ( - - ); -} diff --git a/app/ui/guests/buttons.tsx b/app/ui/guests/buttons.tsx deleted file mode 100644 index d4e2183..0000000 --- a/app/ui/guests/buttons.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline'; -import Link from 'next/link'; - -export function CreateInvoice() { - return ( - - Create Invoice{' '} - - - ); -} - -export function UpdateInvoice({ id }: { id: string }) { - return ( - - - - ); -} - -export function DeleteInvoice({ id }: { id: string }) { - return ( - <> - - - ); -} diff --git a/app/ui/guests/create-form.tsx b/app/ui/guests/create-form.tsx deleted file mode 100644 index d66a259..0000000 --- a/app/ui/guests/create-form.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -import { CustomerField } from '@/app/lib/definitions'; -import Link from 'next/link'; -import { - CheckIcon, - ClockIcon, - CurrencyDollarIcon, - UserCircleIcon, -} from '@heroicons/react/24/outline'; -import { Button } from '@/app/ui/button'; - -export default function Form({ customers }: { customers: CustomerField[] }) { - return ( -
-
- {/* Customer Name */} -
- -
- - -
-
- - {/* Invoice Amount */} -
- -
-
- - -
-
-
- - {/* Invoice Status */} -
- - Set the invoice status - -
-
-
- - -
-
- - -
-
-
-
-
-
- - Cancel - - -
-
- ); -} diff --git a/app/ui/guests/edit-form.tsx b/app/ui/guests/edit-form.tsx deleted file mode 100644 index d71a275..0000000 --- a/app/ui/guests/edit-form.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -'use client'; - -import { CustomerField, InvoiceForm } from '@/app/lib/definitions'; -import { - CheckIcon, - ClockIcon, - CurrencyDollarIcon, - UserCircleIcon, -} from '@heroicons/react/24/outline'; -import Link from 'next/link'; -import { Button } from '@/app/ui/button'; - -export default function EditInvoiceForm({ - invoice, - customers, -}: { - invoice: InvoiceForm; - customers: CustomerField[]; -}) { - return ( -
-
- {/* Customer Name */} -
- -
- - -
-
- - {/* Invoice Amount */} -
- -
-
- - -
-
-
- - {/* Invoice Status */} -
- - Set the invoice status - -
-
-
- - -
-
- - -
-
-
-
-
-
- - Cancel - - -
-
- ); -} diff --git a/app/ui/guests/pagination.tsx b/app/ui/guests/pagination.tsx deleted file mode 100644 index 42488ae..0000000 --- a/app/ui/guests/pagination.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -'use client'; - -import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline'; -import clsx from 'clsx'; -import Link from 'next/link'; -import { generatePagination } from '@/app/lib/utils'; - -export default function Pagination({ totalPages }: { totalPages: number }) { - // NOTE: Uncomment this code in Chapter 11 - - // const allPages = generatePagination(currentPage, totalPages); - - return ( - <> - {/* NOTE: Uncomment this code in Chapter 11 */} - - {/*
- - -
- {allPages.map((page, index) => { - let position: 'first' | 'last' | 'single' | 'middle' | undefined; - - if (index === 0) position = 'first'; - if (index === allPages.length - 1) position = 'last'; - if (allPages.length === 1) position = 'single'; - if (page === '...') position = 'middle'; - - return ( - - ); - })} -
- - = totalPages} - /> -
*/} - - ); -} - -function PaginationNumber({ - page, - href, - isActive, - position, -}: { - page: number | string; - href: string; - position?: 'first' | 'last' | 'middle' | 'single'; - isActive: boolean; -}) { - const className = clsx( - 'flex h-10 w-10 items-center justify-center text-sm border', - { - 'rounded-l-md': position === 'first' || position === 'single', - 'rounded-r-md': position === 'last' || position === 'single', - 'z-10 bg-blue-600 border-blue-600 text-white': isActive, - 'hover:bg-gray-100': !isActive && position !== 'middle', - 'text-gray-300': position === 'middle', - }, - ); - - return isActive || position === 'middle' ? ( -
{page}
- ) : ( - - {page} - - ); -} - -function PaginationArrow({ - href, - direction, - isDisabled, -}: { - href: string; - direction: 'left' | 'right'; - isDisabled?: boolean; -}) { - const className = clsx( - 'flex h-10 w-10 items-center justify-center rounded-md border', - { - 'pointer-events-none text-gray-300': isDisabled, - 'hover:bg-gray-100': !isDisabled, - 'mr-2 md:mr-4': direction === 'left', - 'ml-2 md:ml-4': direction === 'right', - }, - ); - - const icon = - direction === 'left' ? ( - - ) : ( - - ); - - return isDisabled ? ( -
{icon}
- ) : ( - - {icon} - - ); -} diff --git a/app/ui/guests/skeleton-row.tsx b/app/ui/guests/skeleton-row.tsx index 87de2cc..3a46273 100644 --- a/app/ui/guests/skeleton-row.tsx +++ b/app/ui/guests/skeleton-row.tsx @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ import Skeleton from '@/app/ui/skeleton'; diff --git a/app/ui/guests/status.tsx b/app/ui/guests/status.tsx deleted file mode 100644 index f8a3a78..0000000 --- a/app/ui/guests/status.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -import { CheckIcon, ClockIcon } from '@heroicons/react/24/outline'; -import clsx from 'clsx'; - -export default function gueststatus({ status }: { status: string }) { - return ( - - {status === 'pending' ? ( - <> - Pending - - - ) : null} - {status === 'paid' ? ( - <> - Paid - - - ) : null} - - ); -} diff --git a/app/ui/guests/table.tsx b/app/ui/guests/table.tsx index af2752b..93972d0 100644 --- a/app/ui/guests/table.tsx +++ b/app/ui/guests/table.tsx @@ -1,125 +1,59 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ 'use client'; +import { AbstractApi } from '@/app/api/abstract-api'; +import { Guest , GuestSerializer} from '@/app/lib/guest'; +import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline'; import clsx from 'clsx'; -import React, { useState, useEffect } from 'react'; -import { Guest } from '@/app/lib/definitions'; -import { getCsrfToken } from '@/app/lib/utils'; +import TableOfContents from '../components/table-of-contents'; -export default function guestsTable() { - const [guests, setGuests] = useState>([]); +export default function guestsTable({ guests, onUpdate, onEdit }: { + guests: Guest[], + onUpdate: () => void, + onEdit: (guest: Guest) => void +}) { - function loadGuests() { - fetch("/api/guests.json") - .then((response) => response.json()) - .then((data) => { - setGuests(data.data.map((record: any) => { - return ({ - id: record.id, - name: record.attributes.name, - email: record.attributes.email, - group_name: record.attributes.group_name, - status: record.attributes.status - }); - })); - }, (error) => { - return []; - }); - }; - - const handleInviteGuest = (e: React.MouseEvent) => handleGuestChange(e, 'invited'); - const handleConfirmGuest = (e: React.MouseEvent) => handleGuestChange(e, 'confirmed'); - const handleDeclineGuest = (e: React.MouseEvent) => handleGuestChange(e, 'declined'); - const handleTentativeGuest = (e: React.MouseEvent) => handleGuestChange(e, 'tentative'); - - const handleGuestChange = (e: React.MouseEvent, status:string) => { - fetch("/api/guests/bulk_update.json", - { - method: 'POST', - body: JSON.stringify({ properties: { status: status }, guest_ids: [e.currentTarget.getAttribute('data-guest-id')] }), - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-TOKEN': getCsrfToken(), - } - }) - .then(() => loadGuests()) - .catch((error) => console.error(error)); - } - - guests.length === 0 && loadGuests(); - const ctaClassName = "text-white py-1 px-2 mx-1 rounded"; + const api = new AbstractApi(); + const serializer = new GuestSerializer(); return ( -
- - - - - - - - - - - - - {guests.map((guest) => ( - - - - - - - - ))} - -
- Guests -

- There are {guests.length} guests in the list -

-
- Name - - Email - - Group - - Status - Actions
- {guest.name} - - {guest.email} - - {guest.group_name} - - - - - {guest.status} - - - {guest.status === 'Considered' && ()} - {(guest.status === 'Invited' || guest.status === 'Tentative') && ( - <> - - {guest.status != 'Tentative' && } - - - )} -
-
+ ( + + + {guest.name} + + + {guest.group_name} + + + + + + {guest.status} + + + +
+ { api.destroy(serializer, guest, onUpdate)}} /> + onEdit(guest)} /> +
+ + + )} + /> ); } diff --git a/app/ui/login-form.tsx b/app/ui/login-form.tsx deleted file mode 100644 index 6e6241e..0000000 --- a/app/ui/login-form.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ - -import { lusitana } from '@/app/ui/fonts'; -import { - AtSymbolIcon, - KeyIcon, - ExclamationCircleIcon, -} from '@heroicons/react/24/outline'; -import { ArrowRightIcon } from '@heroicons/react/20/solid'; -import { Button } from './button'; - -export default function LoginForm() { - return ( -
-
-

- Please log in to continue. -

-
-
- -
- - -
-
-
- -
- - -
-
-
- -
- {/* Add form errors here */} -
-
-
- ); -} diff --git a/app/ui/search.tsx b/app/ui/search.tsx index b50d837..6ac5d51 100644 --- a/app/ui/search.tsx +++ b/app/ui/search.tsx @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ 'use client'; diff --git a/app/ui/skeleton.tsx b/app/ui/skeleton.tsx index 1a2bdc8..4c64e31 100644 --- a/app/ui/skeleton.tsx +++ b/app/ui/skeleton.tsx @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 Manuel Bustillo*/ +/* Copyright (C) 2024-2025 LibreWeddingPlanner contributors*/ export default function Skeleton({ className }: { className: string }) { return
; diff --git a/package.json b/package.json index 938e546..953ad94 100644 --- a/package.json +++ b/package.json @@ -8,29 +8,31 @@ "dependencies": { "@heroicons/react": "^2.1.4", "@tailwindcss/forms": "^0.5.7", - "autoprefixer": "10.4.20", + "autoprefixer": "10.4.21", "bcrypt": "^5.1.1", "clsx": "^2.1.1", - "next": "15.0.2", - "next-auth": "5.0.0-beta.25", - "postcss": "8.4.47", + "next": "15.3.1", + "next-auth": "5.0.0-beta.27", + "postcss": "8.5.3", "primeicons": "^7.0.0", "primereact": "^10.8.2", "react": "19.0.0-rc-f38c22b244-20240704", "react-dom": "19.0.0-rc-f38c22b244-20240704", - "tailwindcss": "3.4.14", - "typescript": "5.6.3", + "tailwindcss": "3.4.17", + "typescript": "5.8.3", "use-debounce": "^10.0.1", + "uuid": "11.1.0", "zod": "^3.23.8" }, "devDependencies": { "@playwright/test": "^1.46.0", "@types/bcrypt": "^5.0.2", - "@types/node": "22.8.6", - "@types/react": "18.3.12", - "@types/react-dom": "18.3.1" + "@types/node": "22.15.2", + "@types/react": "18.3.20", + "@types/react-dom": "18.3.6" }, "engines": { "node": ">=23.0.0" - } + }, + "packageManager": "pnpm@10.9.0+sha512.0486e394640d3c1fb3c9d43d49cf92879ff74f8516959c235308f5a8f62e2e19528a65cdc2a3058f587cde71eba3d5b56327c8c33a97e4c4051ca48a10ca2d5f" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 597bde8..26f50e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,13 +10,13 @@ importers: dependencies: '@heroicons/react': specifier: ^2.1.4 - version: 2.1.5(react@19.0.0-rc-f38c22b244-20240704) + version: 2.2.0(react@19.0.0-rc-f38c22b244-20240704) '@tailwindcss/forms': specifier: ^0.5.7 - version: 0.5.9(tailwindcss@3.4.14) + version: 0.5.10(tailwindcss@3.4.17) autoprefixer: - specifier: 10.4.20 - version: 10.4.20(postcss@8.4.47) + specifier: 10.4.21 + version: 10.4.21(postcss@8.5.3) bcrypt: specifier: ^5.1.1 version: 5.1.1 @@ -24,20 +24,20 @@ importers: specifier: ^2.1.1 version: 2.1.1 next: - specifier: 15.0.2 - version: 15.0.2(@playwright/test@1.48.2)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704) + specifier: 15.3.1 + version: 15.3.1(@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-auth: - specifier: 5.0.0-beta.25 - version: 5.0.0-beta.25(next@15.0.2(@playwright/test@1.48.2)(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) + specifier: 5.0.0-beta.27 + version: 5.0.0-beta.27(next@15.3.1(@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) postcss: - specifier: 8.4.47 - version: 8.4.47 + specifier: 8.5.3 + version: 8.5.3 primeicons: specifier: ^7.0.0 version: 7.0.0 primereact: specifier: ^10.8.2 - version: 10.8.4(@types/react@18.3.12)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704) + version: 10.9.5(@types/react@18.3.20)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704) react: specifier: 19.0.0-rc-f38c22b244-20240704 version: 19.0.0-rc-f38c22b244-20240704 @@ -45,33 +45,36 @@ importers: specifier: 19.0.0-rc-f38c22b244-20240704 version: 19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704) tailwindcss: - specifier: 3.4.14 - version: 3.4.14 + specifier: 3.4.17 + version: 3.4.17 typescript: - specifier: 5.6.3 - version: 5.6.3 + specifier: 5.8.3 + version: 5.8.3 use-debounce: specifier: ^10.0.1 version: 10.0.4(react@19.0.0-rc-f38c22b244-20240704) + uuid: + specifier: 11.1.0 + version: 11.1.0 zod: specifier: ^3.23.8 - version: 3.23.8 + version: 3.24.3 devDependencies: '@playwright/test': specifier: ^1.46.0 - version: 1.48.2 + version: 1.52.0 '@types/bcrypt': specifier: ^5.0.2 version: 5.0.2 '@types/node': - specifier: 22.8.6 - version: 22.8.6 + specifier: 22.15.2 + version: 22.15.2 '@types/react': - specifier: 18.3.12 - version: 18.3.12 + specifier: 18.3.20 + version: 18.3.20 '@types/react-dom': - specifier: 18.3.1 - version: 18.3.1 + specifier: 18.3.6 + version: 18.3.6(@types/react@18.3.20) packages: @@ -79,8 +82,8 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@auth/core@0.37.2': - resolution: {integrity: sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==} + '@auth/core@0.39.0': + resolution: {integrity: sha512-jusviw/sUSfAh6S/wjY5tRmJOq0Itd3ImF+c/b4HB9DfmfChtcfVJTNJeqCeExeCG8oh4PBKRsMQJsn2W6NhFQ==} peerDependencies: '@simplewebauthn/browser': ^9.0.1 '@simplewebauthn/server': ^9.0.2 @@ -93,119 +96,124 @@ packages: nodemailer: optional: true - '@babel/runtime@7.25.7': - resolution: {integrity: sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==} + '@babel/runtime@7.27.0': + resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} engines: {node: '>=6.9.0'} - '@emnapi/runtime@1.2.0': - resolution: {integrity: sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==} + '@emnapi/runtime@1.4.0': + resolution: {integrity: sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw==} - '@heroicons/react@2.1.5': - resolution: {integrity: sha512-FuzFN+BsHa+7OxbvAERtgBTNeZpUjgM/MIizfVkSCL2/edriN0Hx/DWRCR//aPYwO5QX/YlgLGXk+E3PcfZwjA==} + '@heroicons/react@2.2.0': + resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==} peerDependencies: - react: '>= 16' + react: '>= 16 || ^19.0.0-rc' - '@img/sharp-darwin-arm64@0.33.5': - resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + '@img/sharp-darwin-arm64@0.34.1': + resolution: {integrity: sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.33.5': - resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + '@img/sharp-darwin-x64@0.34.1': + resolution: {integrity: sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.0.4': - resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + '@img/sharp-libvips-darwin-arm64@1.1.0': + resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.0.4': - resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + '@img/sharp-libvips-darwin-x64@1.1.0': + resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.0.4': - resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + '@img/sharp-libvips-linux-arm64@1.1.0': + resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.0.5': - resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + '@img/sharp-libvips-linux-arm@1.1.0': + resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-s390x@1.0.4': - resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + '@img/sharp-libvips-linux-ppc64@1.1.0': + resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.1.0': + resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.0.4': - resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + '@img/sharp-libvips-linux-x64@1.1.0': + resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.0.4': - resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.0.4': - resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + '@img/sharp-libvips-linuxmusl-x64@1.1.0': + resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.33.5': - resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + '@img/sharp-linux-arm64@0.34.1': + resolution: {integrity: sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.33.5': - resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + '@img/sharp-linux-arm@0.34.1': + resolution: {integrity: sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - '@img/sharp-linux-s390x@0.33.5': - resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + '@img/sharp-linux-s390x@0.34.1': + resolution: {integrity: sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.33.5': - resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + '@img/sharp-linux-x64@0.34.1': + resolution: {integrity: sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.33.5': - resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + '@img/sharp-linuxmusl-arm64@0.34.1': + resolution: {integrity: sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.33.5': - resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + '@img/sharp-linuxmusl-x64@0.34.1': + resolution: {integrity: sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.33.5': - resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + '@img/sharp-wasm32@0.34.1': + resolution: {integrity: sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-win32-ia32@0.33.5': - resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + '@img/sharp-win32-ia32@0.34.1': + resolution: {integrity: sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.33.5': - resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + '@img/sharp-win32-x64@0.34.1': + resolution: {integrity: sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] @@ -236,53 +244,53 @@ packages: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true - '@next/env@15.0.2': - resolution: {integrity: sha512-c0Zr0ModK5OX7D4ZV8Jt/wqoXtitLNPwUfG9zElCZztdaZyNVnN40rDXVZ/+FGuR4CcNV5AEfM6N8f+Ener7Dg==} + '@next/env@15.3.1': + resolution: {integrity: sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==} - '@next/swc-darwin-arm64@15.0.2': - resolution: {integrity: sha512-GK+8w88z+AFlmt+ondytZo2xpwlfAR8U6CRwXancHImh6EdGfHMIrTSCcx5sOSBei00GyLVL0ioo1JLKTfprgg==} + '@next/swc-darwin-arm64@15.3.1': + resolution: {integrity: sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.0.2': - resolution: {integrity: sha512-KUpBVxIbjzFiUZhiLIpJiBoelqzQtVZbdNNsehhUn36e2YzKHphnK8eTUW1s/4aPy5kH/UTid8IuVbaOpedhpw==} + '@next/swc-darwin-x64@15.3.1': + resolution: {integrity: sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.0.2': - resolution: {integrity: sha512-9J7TPEcHNAZvwxXRzOtiUvwtTD+fmuY0l7RErf8Yyc7kMpE47MIQakl+3jecmkhOoIyi/Rp+ddq7j4wG6JDskQ==} + '@next/swc-linux-arm64-gnu@15.3.1': + resolution: {integrity: sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.0.2': - resolution: {integrity: sha512-BjH4ZSzJIoTTZRh6rG+a/Ry4SW0HlizcPorqNBixBWc3wtQtj4Sn9FnRZe22QqrPnzoaW0ctvSz4FaH4eGKMww==} + '@next/swc-linux-arm64-musl@15.3.1': + resolution: {integrity: sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.0.2': - resolution: {integrity: sha512-i3U2TcHgo26sIhcwX/Rshz6avM6nizrZPvrDVDY1bXcLH1ndjbO8zuC7RoHp0NSK7wjJMPYzm7NYL1ksSKFreA==} + '@next/swc-linux-x64-gnu@15.3.1': + resolution: {integrity: sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.0.2': - resolution: {integrity: sha512-AMfZfSVOIR8fa+TXlAooByEF4OB00wqnms1sJ1v+iu8ivwvtPvnkwdzzFMpsK5jA2S9oNeeQ04egIWVb4QWmtQ==} + '@next/swc-linux-x64-musl@15.3.1': + resolution: {integrity: sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.0.2': - resolution: {integrity: sha512-JkXysDT0/hEY47O+Hvs8PbZAeiCQVxKfGtr4GUpNAhlG2E0Mkjibuo8ryGD29Qb5a3IOnKYNoZlh/MyKd2Nbww==} + '@next/swc-win32-arm64-msvc@15.3.1': + resolution: {integrity: sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.0.2': - resolution: {integrity: sha512-foaUL0NqJY/dX0Pi/UcZm5zsmSk5MtP/gxx3xOPyREkMFN+CTjctPfu3QaqrQHinaKdPnMWPJDKt4VjDfTBe/Q==} + '@next/swc-win32-x64-msvc@15.3.1': + resolution: {integrity: sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -306,42 +314,43 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.48.2': - resolution: {integrity: sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==} + '@playwright/test@1.52.0': + resolution: {integrity: sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==} engines: {node: '>=18'} hasBin: true '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - '@swc/helpers@0.5.13': - resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@tailwindcss/forms@0.5.9': - resolution: {integrity: sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg==} + '@tailwindcss/forms@0.5.10': + resolution: {integrity: sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==} peerDependencies: - tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20' + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' '@types/bcrypt@5.0.2': resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==} - '@types/cookie@0.6.0': - resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - - '@types/node@22.8.6': - resolution: {integrity: sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==} + '@types/node@22.15.2': + resolution: {integrity: sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==} '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} - '@types/react-dom@18.3.1': - resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} + '@types/react-dom@18.3.6': + resolution: {integrity: sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw==} + peerDependencies: + '@types/react': ^18.0.0 - '@types/react-transition-group@4.4.11': - resolution: {integrity: sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==} + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' - '@types/react@18.3.12': - resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} + '@types/react@18.3.20': + resolution: {integrity: sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==} abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -384,8 +393,8 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - autoprefixer@10.4.20: - resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -412,8 +421,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.23.3: - resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} + browserslist@4.24.4: + resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -425,8 +434,8 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - caniuse-lite@1.0.30001651: - resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==} + caniuse-lite@1.0.30001702: + resolution: {integrity: sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==} chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} @@ -471,10 +480,6 @@ packages: console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} - cookie@0.7.1: - resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} - engines: {node: '>= 0.6'} - cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -515,8 +520,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.6: - resolution: {integrity: sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==} + electron-to-chromium@1.5.113: + resolution: {integrity: sha512-wjT2O4hX+wdWPJ76gWSkMhcHAV2PTMX+QetUCPYEdCIe+cxmgzzSSiGRCKW8nuh4mwKZlpv0xvoW7OF2X+wmHg==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -524,8 +529,8 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} fast-glob@3.3.2: @@ -639,22 +644,18 @@ packages: resolution: {integrity: sha512-htOzIMPbpLid/Gq9/zaz9SfExABxqRe1sSCdxntlO/aMD6u0issZQiY25n2GKQUtJ02j7z5sfptlAOMpWWOmvw==} engines: {node: '>=14'} - jiti@1.21.0: - resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} + jiti@1.21.6: + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true - jose@5.9.6: - resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} + jose@6.0.10: + resolution: {integrity: sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==} js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} - engines: {node: '>=10'} - - lilconfig@3.1.1: - resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==} + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} lines-and-columns@1.2.4: @@ -676,8 +677,8 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - micromatch@4.0.7: - resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} mini-svg-data-uri@1.4.4: @@ -718,13 +719,13 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - next-auth@5.0.0-beta.25: - resolution: {integrity: sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog==} + next-auth@5.0.0-beta.27: + resolution: {integrity: sha512-/QtP9C0C99YpEuBEJqMaDXH3ISWMgObQalwVZEoC7sskaIPhv5fQl6fXS/rXJQKqLY6MNJ42rqjqmRdoXZH2EQ==} peerDependencies: '@simplewebauthn/browser': ^9.0.1 '@simplewebauthn/server': ^9.0.2 @@ -739,16 +740,16 @@ packages: nodemailer: optional: true - next@15.0.2: - resolution: {integrity: sha512-rxIWHcAu4gGSDmwsELXacqAPUk+j8dV/A9cDF5fsiCMpkBDYkO2AEaL1dfD+nNmDiU6QMCFN8Q30VEKapT9UHQ==} - engines: {node: '>=18.18.0'} + next@15.3.1: + resolution: {integrity: sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 '@playwright/test': ^1.41.2 babel-plugin-react-compiler: '*' - react: ^18.2.0 || 19.0.0-rc-02c0e824-20241028 - react-dom: ^18.2.0 || 19.0.0-rc-02c0e824-20241028 + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 sass: ^1.3.0 peerDependenciesMeta: '@opentelemetry/api': @@ -772,8 +773,8 @@ packages: encoding: optional: true - node-releases@2.0.18: - resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} nopt@5.0.0: resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} @@ -792,8 +793,8 @@ packages: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} deprecated: This package is no longer supported. - oauth4webapi@3.1.1: - resolution: {integrity: sha512-0h4FZjsntbKQ5IHGM9mFT7uOwQCRdcTG7YhC0xXlWIcCch24wUa6Vggaipa3Sw6Ab7nEnmO4rctROmyuHBfP7Q==} + oauth4webapi@3.5.0: + resolution: {integrity: sha512-DF3mLWNuxPkxJkHmWxbSFz4aE5CjWOsm465VBfBdWzmzX4Mg3vF8icxK+iKqfdWrIumBJ2TaoNQWx+SQc2bsPQ==} object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -821,11 +822,8 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - picocolors@1.0.1: - resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} - - picocolors@1.1.0: - resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} @@ -839,13 +837,13 @@ packages: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} - playwright-core@1.48.2: - resolution: {integrity: sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==} + playwright-core@1.52.0: + resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==} engines: {node: '>=18'} hasBin: true - playwright@1.48.2: - resolution: {integrity: sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==} + playwright@1.52.0: + resolution: {integrity: sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==} engines: {node: '>=18'} hasBin: true @@ -873,14 +871,14 @@ packages: ts-node: optional: true - postcss-nested@6.0.1: - resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} engines: {node: '>=12.0'} peerDependencies: postcss: ^8.2.14 - postcss-selector-parser@6.1.0: - resolution: {integrity: sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==} + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} postcss-value-parser@4.2.0: @@ -890,26 +888,23 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.4.47: - resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} - preact-render-to-string@5.2.3: - resolution: {integrity: sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==} + preact-render-to-string@6.5.11: + resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} peerDependencies: preact: '>=10' - preact@10.11.3: - resolution: {integrity: sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==} - - pretty-format@3.8.0: - resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} + preact@10.24.3: + resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==} primeicons@7.0.0: resolution: {integrity: sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==} - primereact@10.8.4: - resolution: {integrity: sha512-jwkSzq6pOHayzEh+9dgk2M71gEZtoQakwPKVo3FUJO3eEX3SoAOchON+Xm1tGCNqtd66ca8RgOWQcpv3AQoMvg==} + primereact@10.9.5: + resolution: {integrity: sha512-4O6gm0LrKF7Ml8zQmb8mGiWS/ugJ94KBOAS/CAxWFQh9qyNgfNw/qcpCeomPIkjWd98jrM2XDiEbgq+W0395Hw==} engines: {node: '>=14.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -988,16 +983,16 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} hasBin: true set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - sharp@0.33.5: - resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + sharp@0.34.1: + resolution: {integrity: sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} shebang-command@2.0.0: @@ -1067,8 +1062,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - tailwindcss@3.4.14: - resolution: {integrity: sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==} + tailwindcss@3.4.17: + resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} engines: {node: '>=14.0.0'} hasBin: true @@ -1093,19 +1088,19 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - tslib@2.6.3: - resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - typescript@5.6.3: - resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} hasBin: true - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - update-browserslist-db@1.1.0: - resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -1119,6 +1114,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -1152,109 +1151,110 @@ packages: engines: {node: '>= 14'} hasBin: true - zod@3.23.8: - resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zod@3.24.3: + resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==} snapshots: '@alloc/quick-lru@5.2.0': {} - '@auth/core@0.37.2': + '@auth/core@0.39.0': dependencies: '@panva/hkdf': 1.2.1 - '@types/cookie': 0.6.0 - cookie: 0.7.1 - jose: 5.9.6 - oauth4webapi: 3.1.1 - preact: 10.11.3 - preact-render-to-string: 5.2.3(preact@10.11.3) + jose: 6.0.10 + oauth4webapi: 3.5.0 + preact: 10.24.3 + preact-render-to-string: 6.5.11(preact@10.24.3) - '@babel/runtime@7.25.7': + '@babel/runtime@7.27.0': dependencies: regenerator-runtime: 0.14.1 - '@emnapi/runtime@1.2.0': + '@emnapi/runtime@1.4.0': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 optional: true - '@heroicons/react@2.1.5(react@19.0.0-rc-f38c22b244-20240704)': + '@heroicons/react@2.2.0(react@19.0.0-rc-f38c22b244-20240704)': dependencies: react: 19.0.0-rc-f38c22b244-20240704 - '@img/sharp-darwin-arm64@0.33.5': + '@img/sharp-darwin-arm64@0.34.1': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-arm64': 1.1.0 optional: true - '@img/sharp-darwin-x64@0.33.5': + '@img/sharp-darwin-x64@0.34.1': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.1.0 optional: true - '@img/sharp-libvips-darwin-arm64@1.0.4': + '@img/sharp-libvips-darwin-arm64@1.1.0': optional: true - '@img/sharp-libvips-darwin-x64@1.0.4': + '@img/sharp-libvips-darwin-x64@1.1.0': optional: true - '@img/sharp-libvips-linux-arm64@1.0.4': + '@img/sharp-libvips-linux-arm64@1.1.0': optional: true - '@img/sharp-libvips-linux-arm@1.0.5': + '@img/sharp-libvips-linux-arm@1.1.0': optional: true - '@img/sharp-libvips-linux-s390x@1.0.4': + '@img/sharp-libvips-linux-ppc64@1.1.0': optional: true - '@img/sharp-libvips-linux-x64@1.0.4': + '@img/sharp-libvips-linux-s390x@1.1.0': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + '@img/sharp-libvips-linux-x64@1.1.0': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.0.4': + '@img/sharp-libvips-linuxmusl-arm64@1.1.0': optional: true - '@img/sharp-linux-arm64@0.33.5': + '@img/sharp-libvips-linuxmusl-x64@1.1.0': + optional: true + + '@img/sharp-linux-arm64@0.34.1': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-arm64': 1.1.0 optional: true - '@img/sharp-linux-arm@0.33.5': + '@img/sharp-linux-arm@0.34.1': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm': 1.1.0 optional: true - '@img/sharp-linux-s390x@0.33.5': + '@img/sharp-linux-s390x@0.34.1': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.1.0 optional: true - '@img/sharp-linux-x64@0.33.5': + '@img/sharp-linux-x64@0.34.1': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.1.0 optional: true - '@img/sharp-linuxmusl-arm64@0.33.5': + '@img/sharp-linuxmusl-arm64@0.34.1': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 optional: true - '@img/sharp-linuxmusl-x64@0.33.5': + '@img/sharp-linuxmusl-x64@0.34.1': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 optional: true - '@img/sharp-wasm32@0.33.5': + '@img/sharp-wasm32@0.34.1': dependencies: - '@emnapi/runtime': 1.2.0 + '@emnapi/runtime': 1.4.0 optional: true - '@img/sharp-win32-ia32@0.33.5': + '@img/sharp-win32-ia32@0.34.1': optional: true - '@img/sharp-win32-x64@0.33.5': + '@img/sharp-win32-x64@0.34.1': optional: true '@isaacs/cliui@8.0.2': @@ -1298,30 +1298,30 @@ snapshots: - encoding - supports-color - '@next/env@15.0.2': {} + '@next/env@15.3.1': {} - '@next/swc-darwin-arm64@15.0.2': + '@next/swc-darwin-arm64@15.3.1': optional: true - '@next/swc-darwin-x64@15.0.2': + '@next/swc-darwin-x64@15.3.1': optional: true - '@next/swc-linux-arm64-gnu@15.0.2': + '@next/swc-linux-arm64-gnu@15.3.1': optional: true - '@next/swc-linux-arm64-musl@15.0.2': + '@next/swc-linux-arm64-musl@15.3.1': optional: true - '@next/swc-linux-x64-gnu@15.0.2': + '@next/swc-linux-x64-gnu@15.3.1': optional: true - '@next/swc-linux-x64-musl@15.0.2': + '@next/swc-linux-x64-musl@15.3.1': optional: true - '@next/swc-win32-arm64-msvc@15.0.2': + '@next/swc-win32-arm64-msvc@15.3.1': optional: true - '@next/swc-win32-x64-msvc@15.0.2': + '@next/swc-win32-x64-msvc@15.3.1': optional: true '@nodelib/fs.scandir@2.1.5': @@ -1341,42 +1341,40 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.48.2': + '@playwright/test@1.52.0': dependencies: - playwright: 1.48.2 + playwright: 1.52.0 '@swc/counter@0.1.3': {} - '@swc/helpers@0.5.13': + '@swc/helpers@0.5.15': dependencies: - tslib: 2.6.3 + tslib: 2.8.1 - '@tailwindcss/forms@0.5.9(tailwindcss@3.4.14)': + '@tailwindcss/forms@0.5.10(tailwindcss@3.4.17)': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 3.4.14 + tailwindcss: 3.4.17 '@types/bcrypt@5.0.2': dependencies: - '@types/node': 22.8.6 + '@types/node': 22.15.2 - '@types/cookie@0.6.0': {} - - '@types/node@22.8.6': + '@types/node@22.15.2': dependencies: - undici-types: 6.19.8 + undici-types: 6.21.0 '@types/prop-types@15.7.12': {} - '@types/react-dom@18.3.1': + '@types/react-dom@18.3.6(@types/react@18.3.20)': dependencies: - '@types/react': 18.3.12 + '@types/react': 18.3.20 - '@types/react-transition-group@4.4.11': + '@types/react-transition-group@4.4.12(@types/react@18.3.20)': dependencies: - '@types/react': 18.3.12 + '@types/react': 18.3.20 - '@types/react@18.3.12': + '@types/react@18.3.20': dependencies: '@types/prop-types': 15.7.12 csstype: 3.1.3 @@ -1415,14 +1413,14 @@ snapshots: arg@5.0.2: {} - autoprefixer@10.4.20(postcss@8.4.47): + autoprefixer@10.4.21(postcss@8.5.3): dependencies: - browserslist: 4.23.3 - caniuse-lite: 1.0.30001651 + browserslist: 4.24.4 + caniuse-lite: 1.0.30001702 fraction.js: 4.3.7 normalize-range: 0.1.2 - picocolors: 1.0.1 - postcss: 8.4.47 + picocolors: 1.1.1 + postcss: 8.5.3 postcss-value-parser: 4.2.0 balanced-match@1.0.2: {} @@ -1450,12 +1448,12 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.23.3: + browserslist@4.24.4: dependencies: - caniuse-lite: 1.0.30001651 - electron-to-chromium: 1.5.6 - node-releases: 2.0.18 - update-browserslist-db: 1.1.0(browserslist@4.23.3) + caniuse-lite: 1.0.30001702 + electron-to-chromium: 1.5.113 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.24.4) busboy@1.6.0: dependencies: @@ -1463,7 +1461,7 @@ snapshots: camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001651: {} + caniuse-lite@1.0.30001702: {} chokidar@3.6.0: dependencies: @@ -1509,8 +1507,6 @@ snapshots: console-control-strings@1.1.0: {} - cookie@0.7.1: {} - cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -1535,18 +1531,18 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.25.7 + '@babel/runtime': 7.27.0 csstype: 3.1.3 eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.6: {} + electron-to-chromium@1.5.113: {} emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} - escalade@3.1.2: {} + escalade@3.2.0: {} fast-glob@3.3.2: dependencies: @@ -1554,7 +1550,7 @@ snapshots: '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 - micromatch: 4.0.7 + micromatch: 4.0.8 fastq@1.17.1: dependencies: @@ -1671,15 +1667,13 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jiti@1.21.0: {} + jiti@1.21.6: {} - jose@5.9.6: {} + jose@6.0.10: {} js-tokens@4.0.0: {} - lilconfig@2.1.0: {} - - lilconfig@3.1.1: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -1695,7 +1689,7 @@ snapshots: merge2@1.4.1: {} - micromatch@4.0.7: + micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 @@ -1733,36 +1727,36 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.7: {} + nanoid@3.3.8: {} - next-auth@5.0.0-beta.25(next@15.0.2(@playwright/test@1.48.2)(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.27(next@15.3.1(@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): dependencies: - '@auth/core': 0.37.2 - next: 15.0.2(@playwright/test@1.48.2)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704) + '@auth/core': 0.39.0 + next: 15.3.1(@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@15.0.2(@playwright/test@1.48.2)(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.1(@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): dependencies: - '@next/env': 15.0.2 + '@next/env': 15.3.1 '@swc/counter': 0.1.3 - '@swc/helpers': 0.5.13 + '@swc/helpers': 0.5.15 busboy: 1.6.0 - caniuse-lite: 1.0.30001651 + caniuse-lite: 1.0.30001702 postcss: 8.4.31 react: 19.0.0-rc-f38c22b244-20240704 react-dom: 19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704) styled-jsx: 5.1.6(react@19.0.0-rc-f38c22b244-20240704) optionalDependencies: - '@next/swc-darwin-arm64': 15.0.2 - '@next/swc-darwin-x64': 15.0.2 - '@next/swc-linux-arm64-gnu': 15.0.2 - '@next/swc-linux-arm64-musl': 15.0.2 - '@next/swc-linux-x64-gnu': 15.0.2 - '@next/swc-linux-x64-musl': 15.0.2 - '@next/swc-win32-arm64-msvc': 15.0.2 - '@next/swc-win32-x64-msvc': 15.0.2 - '@playwright/test': 1.48.2 - sharp: 0.33.5 + '@next/swc-darwin-arm64': 15.3.1 + '@next/swc-darwin-x64': 15.3.1 + '@next/swc-linux-arm64-gnu': 15.3.1 + '@next/swc-linux-arm64-musl': 15.3.1 + '@next/swc-linux-x64-gnu': 15.3.1 + '@next/swc-linux-x64-musl': 15.3.1 + '@next/swc-win32-arm64-msvc': 15.3.1 + '@next/swc-win32-x64-msvc': 15.3.1 + '@playwright/test': 1.52.0 + sharp: 0.34.1 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -1773,7 +1767,7 @@ snapshots: dependencies: whatwg-url: 5.0.0 - node-releases@2.0.18: {} + node-releases@2.0.19: {} nopt@5.0.0: dependencies: @@ -1790,7 +1784,7 @@ snapshots: gauge: 3.0.2 set-blocking: 2.0.0 - oauth4webapi@3.1.1: {} + oauth4webapi@3.5.0: {} object-assign@4.1.1: {} @@ -1811,9 +1805,7 @@ snapshots: lru-cache: 10.2.2 minipass: 7.1.2 - picocolors@1.0.1: {} - - picocolors@1.1.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -1821,39 +1813,39 @@ snapshots: pirates@4.0.6: {} - playwright-core@1.48.2: {} + playwright-core@1.52.0: {} - playwright@1.48.2: + playwright@1.52.0: dependencies: - playwright-core: 1.48.2 + playwright-core: 1.52.0 optionalDependencies: fsevents: 2.3.2 - postcss-import@15.1.0(postcss@8.4.47): + postcss-import@15.1.0(postcss@8.5.3): dependencies: - postcss: 8.4.47 + postcss: 8.5.3 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.8 - postcss-js@4.0.1(postcss@8.4.47): + postcss-js@4.0.1(postcss@8.5.3): dependencies: camelcase-css: 2.0.1 - postcss: 8.4.47 + postcss: 8.5.3 - postcss-load-config@4.0.2(postcss@8.4.47): + postcss-load-config@4.0.2(postcss@8.5.3): dependencies: - lilconfig: 3.1.1 + lilconfig: 3.1.3 yaml: 2.4.3 optionalDependencies: - postcss: 8.4.47 + postcss: 8.5.3 - postcss-nested@6.0.1(postcss@8.4.47): + postcss-nested@6.2.0(postcss@8.5.3): dependencies: - postcss: 8.4.47 - postcss-selector-parser: 6.1.0 + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 - postcss-selector-parser@6.1.0: + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 @@ -1862,35 +1854,32 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.7 - picocolors: 1.1.0 + nanoid: 3.3.8 + picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.4.47: + postcss@8.5.3: dependencies: - nanoid: 3.3.7 - picocolors: 1.1.0 + nanoid: 3.3.8 + picocolors: 1.1.1 source-map-js: 1.2.1 - preact-render-to-string@5.2.3(preact@10.11.3): + preact-render-to-string@6.5.11(preact@10.24.3): dependencies: - preact: 10.11.3 - pretty-format: 3.8.0 + preact: 10.24.3 - preact@10.11.3: {} - - pretty-format@3.8.0: {} + preact@10.24.3: {} primeicons@7.0.0: {} - primereact@10.8.4(@types/react@18.3.12)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704): + primereact@10.9.5(@types/react@18.3.20)(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704): dependencies: - '@types/react-transition-group': 4.4.11 + '@types/react-transition-group': 4.4.12(@types/react@18.3.20) react: 19.0.0-rc-f38c22b244-20240704 react-dom: 19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704) react-transition-group: 4.4.5(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704) optionalDependencies: - '@types/react': 18.3.12 + '@types/react': 18.3.20 prop-types@15.8.1: dependencies: @@ -1909,7 +1898,7 @@ snapshots: react-transition-group@4.4.5(react-dom@19.0.0-rc-f38c22b244-20240704(react@19.0.0-rc-f38c22b244-20240704))(react@19.0.0-rc-f38c22b244-20240704): dependencies: - '@babel/runtime': 7.25.7 + '@babel/runtime': 7.27.0 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -1958,36 +1947,37 @@ snapshots: semver@7.6.2: {} - semver@7.6.3: + semver@7.7.1: optional: true set-blocking@2.0.0: {} - sharp@0.33.5: + sharp@0.34.1: dependencies: color: 4.2.3 detect-libc: 2.0.3 - semver: 7.6.3 + semver: 7.7.1 optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.5 - '@img/sharp-darwin-x64': 0.33.5 - '@img/sharp-libvips-darwin-arm64': 1.0.4 - '@img/sharp-libvips-darwin-x64': 1.0.4 - '@img/sharp-libvips-linux-arm': 1.0.5 - '@img/sharp-libvips-linux-arm64': 1.0.4 - '@img/sharp-libvips-linux-s390x': 1.0.4 - '@img/sharp-libvips-linux-x64': 1.0.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - '@img/sharp-linux-arm': 0.33.5 - '@img/sharp-linux-arm64': 0.33.5 - '@img/sharp-linux-s390x': 0.33.5 - '@img/sharp-linux-x64': 0.33.5 - '@img/sharp-linuxmusl-arm64': 0.33.5 - '@img/sharp-linuxmusl-x64': 0.33.5 - '@img/sharp-wasm32': 0.33.5 - '@img/sharp-win32-ia32': 0.33.5 - '@img/sharp-win32-x64': 0.33.5 + '@img/sharp-darwin-arm64': 0.34.1 + '@img/sharp-darwin-x64': 0.34.1 + '@img/sharp-libvips-darwin-arm64': 1.1.0 + '@img/sharp-libvips-darwin-x64': 1.1.0 + '@img/sharp-libvips-linux-arm': 1.1.0 + '@img/sharp-libvips-linux-arm64': 1.1.0 + '@img/sharp-libvips-linux-ppc64': 1.1.0 + '@img/sharp-libvips-linux-s390x': 1.1.0 + '@img/sharp-libvips-linux-x64': 1.1.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + '@img/sharp-linux-arm': 0.34.1 + '@img/sharp-linux-arm64': 0.34.1 + '@img/sharp-linux-s390x': 0.34.1 + '@img/sharp-linux-x64': 0.34.1 + '@img/sharp-linuxmusl-arm64': 0.34.1 + '@img/sharp-linuxmusl-x64': 0.34.1 + '@img/sharp-wasm32': 0.34.1 + '@img/sharp-win32-ia32': 0.34.1 + '@img/sharp-win32-x64': 0.34.1 optional: true shebang-command@2.0.0: @@ -2050,7 +2040,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - tailwindcss@3.4.14: + tailwindcss@3.4.17: dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -2060,18 +2050,18 @@ snapshots: fast-glob: 3.3.2 glob-parent: 6.0.2 is-glob: 4.0.3 - jiti: 1.21.0 - lilconfig: 2.1.0 - micromatch: 4.0.7 + jiti: 1.21.6 + lilconfig: 3.1.3 + micromatch: 4.0.8 normalize-path: 3.0.0 object-hash: 3.0.0 - picocolors: 1.1.0 - postcss: 8.4.47 - postcss-import: 15.1.0(postcss@8.4.47) - postcss-js: 4.0.1(postcss@8.4.47) - postcss-load-config: 4.0.2(postcss@8.4.47) - postcss-nested: 6.0.1(postcss@8.4.47) - postcss-selector-parser: 6.1.0 + picocolors: 1.1.1 + postcss: 8.5.3 + postcss-import: 15.1.0(postcss@8.5.3) + postcss-js: 4.0.1(postcss@8.5.3) + postcss-load-config: 4.0.2(postcss@8.5.3) + postcss-nested: 6.2.0(postcss@8.5.3) + postcss-selector-parser: 6.1.2 resolve: 1.22.8 sucrase: 3.35.0 transitivePeerDependencies: @@ -2102,17 +2092,17 @@ snapshots: ts-interface-checker@0.1.13: {} - tslib@2.6.3: {} + tslib@2.8.1: {} - typescript@5.6.3: {} + typescript@5.8.3: {} - undici-types@6.19.8: {} + undici-types@6.21.0: {} - update-browserslist-db@1.1.0(browserslist@4.23.3): + update-browserslist-db@1.1.3(browserslist@4.24.4): dependencies: - browserslist: 4.23.3 - escalade: 3.1.2 - picocolors: 1.0.1 + browserslist: 4.24.4 + escalade: 3.2.0 + picocolors: 1.1.1 use-debounce@10.0.4(react@19.0.0-rc-f38c22b244-20240704): dependencies: @@ -2120,6 +2110,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@11.1.0: {} + webidl-conversions@3.0.1: {} whatwg-url@5.0.0: @@ -2153,4 +2145,4 @@ snapshots: yaml@2.4.3: {} - zod@3.23.8: {} + zod@3.24.3: {} diff --git a/public/customers/amy-burns.png b/public/customers/amy-burns.png deleted file mode 100644 index 7b29d72..0000000 Binary files a/public/customers/amy-burns.png and /dev/null differ diff --git a/public/customers/balazs-orban.png b/public/customers/balazs-orban.png deleted file mode 100644 index 7fbc009..0000000 Binary files a/public/customers/balazs-orban.png and /dev/null differ diff --git a/public/customers/delba-de-oliveira.png b/public/customers/delba-de-oliveira.png deleted file mode 100644 index 08db1b8..0000000 Binary files a/public/customers/delba-de-oliveira.png and /dev/null differ diff --git a/public/customers/evil-rabbit.png b/public/customers/evil-rabbit.png deleted file mode 100644 index fe7990f..0000000 Binary files a/public/customers/evil-rabbit.png and /dev/null differ diff --git a/public/customers/lee-robinson.png b/public/customers/lee-robinson.png deleted file mode 100644 index 633ae98..0000000 Binary files a/public/customers/lee-robinson.png and /dev/null differ diff --git a/public/customers/michael-novotny.png b/public/customers/michael-novotny.png deleted file mode 100644 index 96a13a6..0000000 Binary files a/public/customers/michael-novotny.png and /dev/null differ diff --git a/tests/guests.spec.ts b/tests/guests.spec.ts index 05e7215..8bf139c 100644 --- a/tests/guests.spec.ts +++ b/tests/guests.spec.ts @@ -1,7 +1,100 @@ -import { test, expect } from '@playwright/test' - -test('should navigate to the guests page', async ({ page }) => { - await page.goto('/dashboard/guests') +import { test, expect, Page } from '@playwright/test' - await expect(page.getByRole('heading', { name: 'Guests' })).toContainText('Guests') -}) \ No newline at end of file +const mockGuestsAPI = ({ page }: { page: Page }) => { + page.route('*/**/api/default/guests', async route => { + const json = [ + { + "id": "f4a09c28-40ea-4553-90a5-96935a59cac6", + "status": "tentative", + "name": "Kristofer Rohan DVM", + "group": { + "id": "2fcb8b22-6b07-4c34-92e3-a2535dbc5b14", + "name": "Childhood friends", + } + }, + { + "id": "bd585c40-0937-4cde-960a-bb23acfd6f18", + "status": "invited", + "name": "Olevia Quigley Jr.", + "group": { + "id": "da8edf26-3e1e-4cbb-b985-450c49fffe01", + "name": "Work", + } + }, + ]; + + await route.fulfill({ json }) + }) +} + +const mockGroupsAPI = ({ page }: { page: Page }) => { + page.route('*/**/api/default/groups', async route => { + const json = [ + { + "id": "ee44ffb9-1147-4842-a378-9eaeb0f0871a", + "name": "Pam's family", + "icon": "pi pi-users", + "parent_id": "cd9645e1-02c6-4fb9-bba6-1a960754b01c", + "color": "#ff0000", + "total": 3, + "considered": 2, + "invited": 1, + "confirmed": 0, + "declined": 0, + "tentative": 0 + }, + { + "id": "c8bda6ca-d8af-4bb8-b2bf-e6ec1c21b1e6", + "name": "Pam's work", + "icon": "pi pi-desktop", + "parent_id": "cd9645e1-02c6-4fb9-bba6-1a960754b01c", + "color": "#00ff00", + "total": 2, + "considered": 0, + "invited": 0, + "confirmed": 0, + "declined": 0, + "tentative": 2 + }, + ]; + + await route.fulfill({ json }) + }) +} + +test('should display the list of guests', async ({ page }) => { + await mockGuestsAPI({ page }); + + await page.goto('/default/dashboard/guests'); + + await expect(page.getByRole('tab', { name: 'Groups' })).toBeVisible(); + await expect(page.getByRole('tab', { name: 'Guests' })).toBeVisible(); + + await expect(page.getByText('There are 2 elements in the list')).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: 'Tentative' })).toBeVisible(); + await expect(page.getByRole('row').nth(1).getByRole('button', { name: 'Confirm' })).toBeVisible(); + 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: 'Work' })).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).getByRole('button', { name: 'Tentative' })).toBeVisible(); + await expect(page.getByRole('row').nth(2).getByRole('button', { name: 'Decline' })).toBeVisible(); +}); + +test('should display the list of groups', async ({ page }) => { + await mockGroupsAPI({ page }); + + await page.goto('/default/dashboard/guests'); + await page.getByRole('tab', { name: 'Groups' }).click(); + + await expect(page.getByText('There are 2 elements in the list')).toBeVisible(); + + 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(); +}); \ No newline at end of file