diff options
Diffstat (limited to 'client')
-rw-r--r-- | client/public/madison-michael-qr-code.png | bin | 0 -> 10655 bytes | |||
-rw-r--r-- | client/public/madison-michael-wedding.ics | 55 | ||||
-rw-r--r-- | client/src/App.tsx | 2 | ||||
-rw-r--r-- | client/src/components/AdminLogin.tsx | 2 | ||||
-rw-r--r-- | client/src/components/CalendarDialog.tsx | 71 | ||||
-rw-r--r-- | client/src/components/GlobalSnackbar.test.tsx | 2 | ||||
-rw-r--r-- | client/src/components/GlobalSnackbar.tsx | 6 | ||||
-rw-r--r-- | client/src/components/GuestLogin.tsx | 2 | ||||
-rw-r--r-- | client/src/components/RsvpForm.tsx | 2 | ||||
-rw-r--r-- | client/src/components/Schedule.test.tsx | 22 | ||||
-rw-r--r-- | client/src/components/Schedule.tsx | 32 | ||||
-rw-r--r-- | client/src/slices/snackbarSlice.ts | 33 | ||||
-rw-r--r-- | client/src/slices/uiSlice.ts | 42 | ||||
-rw-r--r-- | client/src/store.ts | 4 |
14 files changed, 231 insertions, 44 deletions
diff --git a/client/public/madison-michael-qr-code.png b/client/public/madison-michael-qr-code.png Binary files differnew file mode 100644 index 0000000..8f2c550 --- /dev/null +++ b/client/public/madison-michael-qr-code.png 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() { <CssBaseline /> <NavBar /> <GlobalSnackbar /> + <CalendarDialog /> <Outlet /> </> ); 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 ( + <Dialog + open={dialogOpen} + onClose={handleClose} + PaperProps={{ sx: { borderRadius: 2 } }} + > + <DialogTitle sx={{ textAlign: 'center' }}> + Calendar Invitation + </DialogTitle> + <IconButton + aria-label="close" + onClick={handleClose} + sx={(theme) => ({ + position: 'absolute', + right: 8, + top: 8, + color: theme.palette.grey[500], + })} + > + <CloseIcon /> + </IconButton> + <DialogContent + sx={{ + height: '100%', + width: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + gap: 4, + }} + > + <DialogContentText color="inherit"> + Scan the QR code or click the link below to add the calendar invite to + your device. + </DialogContentText> + <img src="/madison-michael-qr-code.png" /> + <a + href="/madison-michael-wedding.ics" + style={{ color: theme.palette.primary.main }} + > + Add to calendar + </a> + </DialogContent> + </Dialog> + ); +} + +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 ( - <Snackbar open={open} onClose={handleClose} autoHideDuration={5000}> + <Snackbar open={snackbarOpen} onClose={handleClose} autoHideDuration={5000}> <div> <Alert severity={severity} onClose={handleClose}> {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( + <RouterProvider router={memoryRouter} /> + ); + + 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 ( <div style={{ @@ -28,10 +43,23 @@ function Schedule() { <div style={{ width: '35%' }}> <p>April 26, 2025</p> </div> - <div style={{ width: '65%' }}> + <div + style={{ + display: 'flex', + justifyContent: 'space-around', + width: '65%', + }} + > <Typography variant="h5" sx={{ lineHeight: 1.6 }}> Wedding Schedule </Typography> + <IconButton + color="inherit" + onClick={handleOpen} + aria-label="insert invitation" + > + <InsertInvitationIcon /> + </IconButton> </div> </div> <hr style={{ width: '100%' }} /> 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<RootState>) => { |