From aacf26f374f48b88d532d1528d9d07aabf537610 Mon Sep 17 00:00:00 2001 From: Michael Hunteman Date: Sat, 5 Oct 2024 11:06:50 -0700 Subject: Add calendar invite --- client/public/madison-michael-qr-code.png | Bin 0 -> 10655 bytes client/public/madison-michael-wedding.ics | 55 ++++++++++++++++++++ client/src/App.tsx | 2 + client/src/components/AdminLogin.tsx | 2 +- client/src/components/CalendarDialog.tsx | 71 ++++++++++++++++++++++++++ client/src/components/GlobalSnackbar.test.tsx | 2 +- client/src/components/GlobalSnackbar.tsx | 6 +-- client/src/components/GuestLogin.tsx | 2 +- client/src/components/RsvpForm.tsx | 2 +- client/src/components/Schedule.test.tsx | 22 ++++++++ client/src/components/Schedule.tsx | 32 +++++++++++- client/src/slices/snackbarSlice.ts | 33 ------------ client/src/slices/uiSlice.ts | 42 +++++++++++++++ client/src/store.ts | 4 +- 14 files changed, 231 insertions(+), 44 deletions(-) create mode 100644 client/public/madison-michael-qr-code.png create mode 100644 client/public/madison-michael-wedding.ics create mode 100644 client/src/components/CalendarDialog.tsx create mode 100644 client/src/components/Schedule.test.tsx delete mode 100644 client/src/slices/snackbarSlice.ts create mode 100644 client/src/slices/uiSlice.ts diff --git a/client/public/madison-michael-qr-code.png b/client/public/madison-michael-qr-code.png new file mode 100644 index 0000000..8f2c550 Binary files /dev/null and b/client/public/madison-michael-qr-code.png differ diff --git a/client/public/madison-michael-wedding.ics b/client/public/madison-michael-wedding.ics new file mode 100644 index 0000000..be2c697 --- /dev/null +++ b/client/public/madison-michael-wedding.ics @@ -0,0 +1,55 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//ical.marudot.com//iCal Event Maker +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:America/Chicago +LAST-MODIFIED:20240422T053451Z +TZURL:https://www.tzurl.org/zoneinfo-outlook/America/Chicago +X-LIC-LOCATION:America/Chicago +BEGIN:DAYLIGHT +TZNAME:CDT +TZOFFSETFROM:-0600 +TZOFFSETTO:-0500 +DTSTART:19700308T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:CST +TZOFFSETFROM:-0500 +TZOFFSETTO:-0600 +DTSTART:19701101T020000 +RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTAMP:20241005T175732Z +UID:1728066967297-11106@ical.marudot.com +DTSTART;TZID=America/Chicago:20250426T163000 +DTEND;TZID=America/Chicago:20250426T170000 +SUMMARY:Madison and Michael Hunteman Wedding Ceremony +LOCATION:Divine Shepherd Lutheran Church\, 15005 Q St\, Omaha\, NE 68137 +TRANSP:OPAQUE +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Madison and Michael Hunteman Wedding Ceremony +TRIGGER:-P1W +END:VALARM +END:VEVENT +BEGIN:VEVENT +DTSTAMP:20241005T175732Z +UID:1728067166881-27887@ical.marudot.com +DTSTART;TZID=America/Chicago:20250426T180000 +DTEND;TZID=America/Chicago:20250427T000000 +SUMMARY:Madison and Michael Hunteman Wedding Reception +LOCATION:A Venue on the Ridge\, 20033 Elkhorn Ridge Dr\, Elkhorn\, NE 68022 +TRANSP:OPAQUE +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:Madison and Michael Hunteman Wedding Reception +TRIGGER:-P1W +END:VALARM +END:VEVENT +END:VCALENDAR \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index 403882b..9f0bf74 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -3,6 +3,7 @@ import { Outlet } from 'react-router-dom'; import CssBaseline from '@mui/material/CssBaseline'; import NavBar from './components/NavBar'; import GlobalSnackbar from './components/GlobalSnackbar'; +import CalendarDialog from './components/CalendarDialog'; function App() { return ( @@ -10,6 +11,7 @@ function App() { + ); diff --git a/client/src/components/AdminLogin.tsx b/client/src/components/AdminLogin.tsx index a4fce8d..bfc96d2 100644 --- a/client/src/components/AdminLogin.tsx +++ b/client/src/components/AdminLogin.tsx @@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form'; import { useAppDispatch } from '../hooks'; import { setAdmin } from '../slices/auth/adminSlice'; import { useLoginAdminMutation } from '../slices/api/adminSlice'; -import { showSnackbar } from '../slices/snackbarSlice'; +import { showSnackbar } from '../slices/uiSlice'; import { isFetchBaseQueryError } from '../error'; import type { Credentials } from '../models'; import type { Data } from '../error'; diff --git a/client/src/components/CalendarDialog.tsx b/client/src/components/CalendarDialog.tsx new file mode 100644 index 0000000..cff5512 --- /dev/null +++ b/client/src/components/CalendarDialog.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { + Dialog, + DialogContent, + DialogContentText, + DialogTitle, + IconButton, + useTheme, +} from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import { useAppDispatch, useAppSelector } from '../hooks'; +import { hideDialog, selectUIState } from '../slices/uiSlice'; + +function CalendarDialog() { + const dispatch = useAppDispatch(); + const { dialogOpen } = useAppSelector(selectUIState); + const theme = useTheme(); + + const handleClose = () => { + dispatch(hideDialog()); + }; + + return ( + + + Calendar Invitation + + ({ + position: 'absolute', + right: 8, + top: 8, + color: theme.palette.grey[500], + })} + > + + + + + Scan the QR code or click the link below to add the calendar invite to + your device. + + + + Add to calendar + + + + ); +} + +export default CalendarDialog; diff --git a/client/src/components/GlobalSnackbar.test.tsx b/client/src/components/GlobalSnackbar.test.tsx index 2643816..ce6a6ba 100644 --- a/client/src/components/GlobalSnackbar.test.tsx +++ b/client/src/components/GlobalSnackbar.test.tsx @@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'; import { createMemoryRouter, RouterProvider } from 'react-router-dom'; import { renderWithProviders } from '../renderWithProviders'; import routes from '../routes'; -import { showSnackbar } from '../slices/snackbarSlice'; +import { showSnackbar } from '../slices/uiSlice'; import setupStore from '../store'; describe('Global Snackbar', async () => { diff --git a/client/src/components/GlobalSnackbar.tsx b/client/src/components/GlobalSnackbar.tsx index c4457af..83d6582 100644 --- a/client/src/components/GlobalSnackbar.tsx +++ b/client/src/components/GlobalSnackbar.tsx @@ -1,18 +1,18 @@ import React from 'react'; import { Alert, Snackbar } from '@mui/material'; import { useAppDispatch, useAppSelector } from '../hooks'; -import { hideSnackbar, selectSnackbarState } from '../slices/snackbarSlice'; +import { hideSnackbar, selectUIState } from '../slices/uiSlice'; function GlobalSnackbar() { const dispatch = useAppDispatch(); - const { open, message, severity } = useAppSelector(selectSnackbarState); + const { snackbarOpen, message, severity } = useAppSelector(selectUIState); const handleClose = () => { dispatch(hideSnackbar()); }; return ( - +
{message} diff --git a/client/src/components/GuestLogin.tsx b/client/src/components/GuestLogin.tsx index 2f5a3eb..c2bfeb9 100644 --- a/client/src/components/GuestLogin.tsx +++ b/client/src/components/GuestLogin.tsx @@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form'; import { useAppDispatch } from '../hooks'; import { setGuest } from '../slices/auth/guestSlice'; import { useLoginGuestMutation } from '../slices/api/guestSlice'; -import { showSnackbar } from '../slices/snackbarSlice'; +import { showSnackbar } from '../slices/uiSlice'; import { isFetchBaseQueryError } from '../error'; import type { Data } from '../error'; import type { Name } from '../models'; diff --git a/client/src/components/RsvpForm.tsx b/client/src/components/RsvpForm.tsx index eae34c3..2a3552f 100644 --- a/client/src/components/RsvpForm.tsx +++ b/client/src/components/RsvpForm.tsx @@ -17,7 +17,7 @@ import { useForm, Controller, useFieldArray } from 'react-hook-form'; import { useAppDispatch } from '../hooks'; import { useUpdateGuestMutation } from '../slices/api/guestSlice'; import { isFetchBaseQueryError } from '../error'; -import { showSnackbar } from '../slices/snackbarSlice'; +import { showSnackbar } from '../slices/uiSlice'; import type { Data } from '../error'; import type { Guest } from '../models'; diff --git a/client/src/components/Schedule.test.tsx b/client/src/components/Schedule.test.tsx new file mode 100644 index 0000000..76f0f91 --- /dev/null +++ b/client/src/components/Schedule.test.tsx @@ -0,0 +1,22 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { fireEvent } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import { describe, expect, it } from 'vitest'; +import { createMemoryRouter, RouterProvider } from 'react-router-dom'; +import { renderWithProviders } from '../renderWithProviders'; +import routes from '../routes'; + +describe('Schedule', async () => { + const memoryRouter = createMemoryRouter(routes, { + initialEntries: ['/schedule'], + }); + it('displays calendar dialog', async () => { + const { getByLabelText, findByText } = renderWithProviders( + + ); + + fireEvent.click(getByLabelText(/insert invitation/i)); + expect(await findByText(/calendar invitation/i)).toBeInTheDocument(); + }); +}); diff --git a/client/src/components/Schedule.tsx b/client/src/components/Schedule.tsx index 0ce7e4c..64140e4 100644 --- a/client/src/components/Schedule.tsx +++ b/client/src/components/Schedule.tsx @@ -1,10 +1,25 @@ import React from 'react'; -import { Paper, Typography, useMediaQuery, useTheme } from '@mui/material'; +import { + IconButton, + Paper, + Typography, + useMediaQuery, + useTheme, +} from '@mui/material'; import divineShepherd from '/divine-shepherd.jpg'; +import InsertInvitationIcon from '@mui/icons-material/InsertInvitation'; +import { useAppDispatch } from '../hooks'; +import { showDialog } from '../slices/uiSlice'; function Schedule() { + const dispatch = useAppDispatch(); const theme = useTheme(); const isMobile = useMediaQuery('(max-width: 768px)'); + + const handleOpen = () => { + dispatch(showDialog()); + }; + return (

April 26, 2025

-
+
Wedding Schedule + + +

diff --git a/client/src/slices/snackbarSlice.ts b/client/src/slices/snackbarSlice.ts deleted file mode 100644 index 82532ec..0000000 --- a/client/src/slices/snackbarSlice.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit'; -import type { AlertColor } from '@mui/material/Alert/Alert'; -import type { RootState } from '../store'; - -export interface SnackbarState { - open: boolean; - message: string; - severity?: AlertColor; -} - -const initialState: SnackbarState = { - open: false, - message: '', -}; - -export const snackbarSlice = createSlice({ - name: 'snackbar', - initialState, - reducers: { - showSnackbar: (state, action) => { - state.open = true; - state.message = action.payload.message; - state.severity = action.payload.severity; - }, - hideSnackbar: (state) => { - state.open = false; - }, - }, -}); - -export const { showSnackbar, hideSnackbar } = snackbarSlice.actions; -export const selectSnackbarState = (state: RootState) => state.snackbar; -export default snackbarSlice.reducer; diff --git a/client/src/slices/uiSlice.ts b/client/src/slices/uiSlice.ts new file mode 100644 index 0000000..ac461d1 --- /dev/null +++ b/client/src/slices/uiSlice.ts @@ -0,0 +1,42 @@ +import { createSlice } from '@reduxjs/toolkit'; +import type { AlertColor } from '@mui/material/Alert/Alert'; +import type { RootState } from '../store'; + +export interface UIState { + snackbarOpen: boolean; + message: string; + severity?: AlertColor; + dialogOpen: boolean; +} + +const initialState: UIState = { + snackbarOpen: false, + message: '', + dialogOpen: false, +}; + +export const uiSlice = createSlice({ + name: 'ui', + initialState, + reducers: { + showSnackbar: (state, action) => { + state.snackbarOpen = true; + state.message = action.payload.message; + state.severity = action.payload.severity; + }, + hideSnackbar: (state) => { + state.snackbarOpen = false; + }, + showDialog: (state) => { + state.dialogOpen = true; + }, + hideDialog: (state) => { + state.dialogOpen = false; + }, + }, +}); + +export const { showSnackbar, hideSnackbar, showDialog, hideDialog } = + uiSlice.actions; +export const selectUIState = (state: RootState) => state.ui; +export default uiSlice.reducer; diff --git a/client/src/store.ts b/client/src/store.ts index e28bace..bc7be19 100644 --- a/client/src/store.ts +++ b/client/src/store.ts @@ -1,7 +1,7 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'; import guestReducer from './slices/auth/guestSlice'; import adminReducer from './slices/auth/adminSlice'; -import snackbarReducer from './slices/snackbarSlice'; +import uiReducer from './slices/uiSlice'; import { guestSlice } from './slices/api/guestSlice'; import { adminSlice } from './slices/api/adminSlice'; @@ -10,7 +10,7 @@ const rootReducer = combineReducers({ [adminSlice.reducerPath]: adminSlice.reducer, guest: guestReducer, admin: adminReducer, - snackbar: snackbarReducer, + ui: uiReducer, }); const setupStore = (preloadedState?: Partial) => { -- cgit v1.2.3