summaryrefslogtreecommitdiff
path: root/client/src
diff options
context:
space:
mode:
Diffstat (limited to 'client/src')
-rw-r--r--client/src/App.tsx2
-rw-r--r--client/src/components/AdminLogin.tsx2
-rw-r--r--client/src/components/CalendarDialog.tsx71
-rw-r--r--client/src/components/GlobalSnackbar.test.tsx2
-rw-r--r--client/src/components/GlobalSnackbar.tsx6
-rw-r--r--client/src/components/GuestLogin.tsx2
-rw-r--r--client/src/components/RsvpForm.tsx2
-rw-r--r--client/src/components/Schedule.test.tsx22
-rw-r--r--client/src/components/Schedule.tsx32
-rw-r--r--client/src/slices/snackbarSlice.ts33
-rw-r--r--client/src/slices/uiSlice.ts42
-rw-r--r--client/src/store.ts4
12 files changed, 176 insertions, 44 deletions
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>) => {