diff options
author | Michael Hunteman <michael@huntm.net> | 2024-05-17 15:20:30 -0700 |
---|---|---|
committer | Michael Hunteman <michael@huntm.net> | 2024-05-17 15:20:30 -0700 |
commit | 7103019890960e793deefb64987a09b33be60b42 (patch) | |
tree | c1c9402aa250c68b2cbe13d62598232bbf20b1e2 /client/src | |
parent | fc5c111bcfe296bec82e1cf9fdb88fc80fb24f89 (diff) |
Add golang server
Diffstat (limited to 'client/src')
-rw-r--r-- | client/src/App.tsx | 16 | ||||
-rw-r--r-- | client/src/ThemeContextProvider.tsx | 57 | ||||
-rw-r--r-- | client/src/apiSlice.ts | 67 | ||||
-rw-r--r-- | client/src/components/Admin.tsx | 31 | ||||
-rw-r--r-- | client/src/components/Desktop.tsx | 29 | ||||
-rw-r--r-- | client/src/components/Home.tsx | 73 | ||||
-rw-r--r-- | client/src/components/Mobile.tsx | 55 | ||||
-rw-r--r-- | client/src/components/NavBar.tsx | 29 | ||||
-rw-r--r-- | client/src/components/Registry.tsx | 11 | ||||
-rw-r--r-- | client/src/components/Rsvp.tsx | 29 | ||||
-rw-r--r-- | client/src/components/RsvpForm.tsx | 242 | ||||
-rw-r--r-- | client/src/components/Schedule.tsx | 107 | ||||
-rw-r--r-- | client/src/components/active.css | 7 | ||||
-rw-r--r-- | client/src/features/auth/GuestLogin.tsx | 79 | ||||
-rw-r--r-- | client/src/features/auth/authSlice.ts | 26 | ||||
-rw-r--r-- | client/src/main.css | 4 | ||||
-rw-r--r-- | client/src/main.tsx | 69 | ||||
-rw-r--r-- | client/src/mocks/browser.ts | 4 | ||||
-rw-r--r-- | client/src/mocks/handlers.ts | 30 | ||||
-rw-r--r-- | client/src/pages.ts | 8 | ||||
-rw-r--r-- | client/src/store.ts | 16 | ||||
-rw-r--r-- | client/src/vite-env.d.ts | 1 |
22 files changed, 990 insertions, 0 deletions
diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..27fc180 --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { Outlet } from 'react-router-dom'; +import CssBaseline from '@mui/material/CssBaseline'; +import NavBar from './components/NavBar'; + +function App() { + return ( + <> + <CssBaseline /> + <NavBar /> + <Outlet /> + </> + ); +} + +export default App; diff --git a/client/src/ThemeContextProvider.tsx b/client/src/ThemeContextProvider.tsx new file mode 100644 index 0000000..6ae1430 --- /dev/null +++ b/client/src/ThemeContextProvider.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { ReactNode, createContext, useMemo, useState } from 'react'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import type { PaletteMode } from '@mui/material'; + +type ThemeContextType = { + toggleColorMode: () => void; +}; + +type ThemeProviderProps = { + children: ReactNode; +}; + +export const ThemeContext = createContext<ThemeContextType>({ + toggleColorMode: () => {}, +}); + +function ThemeContextProvider({ children }: ThemeProviderProps) { + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); + const [mode, setMode] = useState<'light' | 'dark'>( + prefersDarkMode ? 'dark' : 'light' + ); + + const toggleColorMode = () => { + setMode((prevMode: PaletteMode) => + prevMode === 'light' ? 'dark' : 'light' + ); + }; + + const getDesignTokens = (mode: PaletteMode) => ({ + palette: { + mode, + ...(mode === 'light' + ? { + primary: { + main: '#007bff', + }, + } + : { + primary: { + main: '#78bef8', + }, + }), + }, + }); + + const theme = useMemo(() => createTheme(getDesignTokens(mode)), [mode]); + + return ( + <ThemeContext.Provider value={{ toggleColorMode }}> + <ThemeProvider theme={theme}>{children}</ThemeProvider> + </ThemeContext.Provider> + ); +} + +export default ThemeContextProvider; diff --git a/client/src/apiSlice.ts b/client/src/apiSlice.ts new file mode 100644 index 0000000..5d987f9 --- /dev/null +++ b/client/src/apiSlice.ts @@ -0,0 +1,67 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import type { RootState } from './store'; + +export interface LoginRequest { + firstName: string; + lastName: string; +} + +export interface LoginResponse { + guest: Guest; + token: string; +} + +export interface Guest { + id: number; + firstName: string; + lastName: string; + attendance: string; + email: string; + message: string; + partySize: number; + partyList: Array<PartyGuest>; +} + +export interface PartyGuest { + firstName: string; + lastName: string; +} + +export const apiSlice = createApi({ + reducerPath: 'api', + baseQuery: fetchBaseQuery({ + baseUrl: '/', + prepareHeaders: (headers, { getState }) => { + const token = (getState() as RootState).auth.token; + if (token) { + headers.set('authorization', `Bearer ${token}`); + } + return headers; + }, + }), + tagTypes: ['Guests'], + endpoints: (builder) => ({ + getGuests: builder.query<void, void>({ + query: () => '/guests', + providesTags: ['Guests'], + }), + updateGuest: builder.mutation<Guest, Guest>({ + query: (guest) => ({ + url: `/guests/${guest?.id}`, + method: 'PATCH', + body: guest, + providesTags: ['Guests'], + }), + }), + login: builder.mutation<LoginResponse, LoginRequest>({ + query: (credentials) => ({ + url: '/guest-login', + method: 'POST', + body: credentials, + }), + }), + }), +}); + +export const { useGetGuestsQuery, useUpdateGuestMutation, useLoginMutation } = + apiSlice; diff --git a/client/src/components/Admin.tsx b/client/src/components/Admin.tsx new file mode 100644 index 0000000..1c941a5 --- /dev/null +++ b/client/src/components/Admin.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { useGetGuestsQuery } from '../apiSlice'; + +function Admin() { + const { + data: guests, + isLoading, + isSuccess, + isError, + error, + } = useGetGuestsQuery(); + + let content; + + if (isLoading) { + content = <p>Loading...</p>; + } else if (isSuccess) { + content = JSON.stringify(guests); + } else if (isError) { + content = <>{error.toString()}</>; + } + + return ( + <> + <p>Admin</p> + {content} + </> + ); +} + +export default Admin; diff --git a/client/src/components/Desktop.tsx b/client/src/components/Desktop.tsx new file mode 100644 index 0000000..13de4ee --- /dev/null +++ b/client/src/components/Desktop.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { useContext } from 'react'; +import { Link } from 'react-router-dom'; +import { Button, IconButton } from '@mui/material'; +import DarkModeIcon from '@mui/icons-material/DarkMode'; +import LightModeIcon from '@mui/icons-material/LightMode'; +import { useTheme } from '@mui/material/styles'; +import { ThemeContext } from '../ThemeContextProvider'; +import pages from '../pages'; + +function Desktop() { + const theme = useTheme(); + const { toggleColorMode } = useContext(ThemeContext); + + return ( + <div style={{ marginLeft: 'auto' }}> + {pages.map((page) => ( + <Button color="inherit" component={Link} to={page?.to} key={page?.name}> + {page?.name} + </Button> + ))} + <IconButton color="inherit" onClick={toggleColorMode}> + {theme.palette.mode === 'dark' ? <LightModeIcon /> : <DarkModeIcon />} + </IconButton> + </div> + ); +} + +export default Desktop; diff --git a/client/src/components/Home.tsx b/client/src/components/Home.tsx new file mode 100644 index 0000000..839667a --- /dev/null +++ b/client/src/components/Home.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { useEffect, useRef, useState } from 'react'; +import './active.css'; + +function Home() { + const [index, setIndex] = useState(0); + const colors = ['#FF0000', '#00FF00', '#0000FF']; + const timeout = useRef(0); + + useEffect(() => { + resetTimeout(); + timeout.current = window.setTimeout( + () => + setIndex((prevIndex) => + prevIndex === colors.length - 1 ? 0 : prevIndex + 1 + ), + 2500 + ); + + return () => { + resetTimeout(); + }; + }, [index]); + + const resetTimeout = () => { + if (timeout.current) { + clearTimeout(timeout.current); + } + }; + + return ( + <div style={{ margin: 'auto', overflow: 'hidden' }}> + <div + style={{ + whiteSpace: 'nowrap', + transform: `translateX(${-index * 100}%)`, + transition: 'ease 1000ms', + }} + > + {colors.map((backgroundColor, colorIndex) => ( + <div + key={colorIndex} + style={{ + display: 'inline-block', + backgroundColor, + height: '80vh', + width: '100%', + }} + /> + ))} + </div> + <div style={{ display: 'flex', justifyContent: 'center' }}> + {colors.map((_, colorIndex) => ( + <div + key={colorIndex} + style={{ + height: '0.75rem', + width: '0.75rem', + borderRadius: '50%', + margin: '0.75rem', + }} + className={colorIndex === index ? 'active' : 'inactive'} + onClick={() => { + setIndex(colorIndex); + }} + /> + ))} + </div> + </div> + ); +} + +export default Home; diff --git a/client/src/components/Mobile.tsx b/client/src/components/Mobile.tsx new file mode 100644 index 0000000..2e7b15c --- /dev/null +++ b/client/src/components/Mobile.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { useContext, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { Button, IconButton, Menu, MenuItem } from '@mui/material'; +import DarkModeIcon from '@mui/icons-material/DarkMode'; +import LightModeIcon from '@mui/icons-material/LightMode'; +import { useTheme } from '@mui/material/styles'; +import { ThemeContext } from '../ThemeContextProvider'; +import MenuIcon from '@mui/icons-material/Menu'; +import pages from '../pages'; + +function Mobile() { + const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); + const theme = useTheme(); + const { toggleColorMode } = useContext(ThemeContext); + + const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => { + setAnchorEl(event.currentTarget); + }; + + const handleCloseNavMenu = () => { + setAnchorEl(null); + }; + + return ( + <> + <IconButton color="inherit" onClick={toggleColorMode}> + {theme.palette.mode === 'dark' ? <LightModeIcon /> : <DarkModeIcon />} + </IconButton> + <IconButton + color="inherit" + sx={{ ml: 'auto' }} + onClick={handleOpenNavMenu} + > + <MenuIcon /> + </IconButton> + <Menu anchorEl={anchorEl} open={!!anchorEl} onClose={handleCloseNavMenu}> + {pages.map((page) => ( + <MenuItem key={page.name} onClick={handleCloseNavMenu}> + <Button + color="inherit" + component={Link} + to={page?.to} + key={page?.name} + > + {page?.name} + </Button> + </MenuItem> + ))} + </Menu> + </> + ); +} + +export default Mobile; diff --git a/client/src/components/NavBar.tsx b/client/src/components/NavBar.tsx new file mode 100644 index 0000000..52e8e6c --- /dev/null +++ b/client/src/components/NavBar.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { AppBar, Toolbar, Typography } from '@mui/material'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import Desktop from './Desktop'; +import Mobile from './Mobile'; + +function NavBar() { + const isMobile = useMediaQuery('(max-width: 768px)'); + + return ( + <AppBar position="relative"> + <Toolbar> + <Typography + variant="h5" + component={Link} + to="/" + color="inherit" + sx={{ textDecoration: 'none' }} + > + Madison and Michael's Wedding + </Typography> + {isMobile ? <Mobile /> : <Desktop />} + </Toolbar> + </AppBar> + ); +} + +export default NavBar; diff --git a/client/src/components/Registry.tsx b/client/src/components/Registry.tsx new file mode 100644 index 0000000..60a73f9 --- /dev/null +++ b/client/src/components/Registry.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +function Registry() { + return ( + <> + <p>Registry</p> + </> + ); +} + +export default Registry; diff --git a/client/src/components/Rsvp.tsx b/client/src/components/Rsvp.tsx new file mode 100644 index 0000000..dad7213 --- /dev/null +++ b/client/src/components/Rsvp.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { useMemo } from 'react'; +import { useLocation, Navigate, Outlet } from 'react-router-dom'; +import { useSelector } from 'react-redux'; +import CssBaseline from '@mui/material/CssBaseline'; +import NavBar from './NavBar'; +import { selectCurrentGuest } from '../features/auth/authSlice'; + +const authenticate = () => { + const guest = useSelector(selectCurrentGuest); + return useMemo(() => ({ guest }), [guest]); +}; + +function Rsvp() { + const auth = authenticate(); + const location = useLocation(); + + return auth?.guest ? ( + <> + <CssBaseline /> + <NavBar /> + <Outlet context={auth?.guest} /> + </> + ) : ( + <Navigate to="/guest-login" state={{ from: location }} replace /> + ); +} + +export default Rsvp; diff --git a/client/src/components/RsvpForm.tsx b/client/src/components/RsvpForm.tsx new file mode 100644 index 0000000..71db0d8 --- /dev/null +++ b/client/src/components/RsvpForm.tsx @@ -0,0 +1,242 @@ +import React from 'react'; +import { useRef } from 'react'; +import { useOutletContext } from 'react-router-dom'; +import { + Button, + Container, + FormControl, + FormControlLabel, + FormLabel, + Grid, + Radio, + RadioGroup, + TextField, +} from '@mui/material'; +import { useForm, Controller, useFieldArray } from 'react-hook-form'; +import { useUpdateGuestMutation } from '../apiSlice'; +import type { Guest } from '../apiSlice'; + +type FormValues = { + id: number; + firstName: string; + lastName: string; + attendance: string; + email: string; + partySize: number; + message: string; + partyList: { + firstName: string; + lastName: string; + }[]; +}; + +function RsvpForm() { + const [updateGuest] = useUpdateGuestMutation(); + const guest: Guest = useOutletContext(); + const previousPartySize = useRef(0); + + const { + register, + handleSubmit, + control, + watch, + formState: { errors }, + } = useForm<FormValues>({ + defaultValues: { + id: guest?.id, + firstName: guest?.firstName, + lastName: guest?.lastName, + attendance: '', + email: '', + message: '', + partySize: 1, + partyList: [], + }, + }); + + const onSubmit = async (data: Guest) => { + updateGuest({ ...data }); + }; + + const { fields, append, remove } = useFieldArray({ + control, + name: 'partyList', + }); + + const handleParty = () => { + const partySize = Number(watch('partySize')) - 1; + if ( + partySize > previousPartySize.current && + partySize > 0 && + partySize < 10 + ) { + append( + new Array(partySize - previousPartySize.current).fill({ + firstName: '', + lastName: '', + }) + ); + previousPartySize.current = partySize; + } else if (partySize < previousPartySize.current && partySize >= 0) { + remove( + [...Array(previousPartySize.current - partySize).keys()].map( + (_, i) => partySize - 1 + i + ) + ); + previousPartySize.current = partySize; + } + }; + + return ( + <Container + component="form" + maxWidth="sm" + noValidate + onSubmit={handleSubmit(onSubmit)} + > + <Grid container spacing={2} sx={{ mt: 8 }}> + <Grid item xs={12}> + <p> + Please RSVP for the wedding by March 10, 2025. The ceremony will + commence at 3 PM on April 26 in Divine Shepherd. The reception will + follow at 5 PM in A Venue on the Ridge. + </p> + </Grid> + <Grid item xs={12}> + <div style={{ display: 'flex', justifyContent: 'center' }}> + <FormControl> + <div style={{ display: 'flex', alignItems: 'center' }}> + <FormLabel sx={{ mr: 2 }} error={!!errors.attendance} required> + Will you attend? + </FormLabel> + <Controller + name="attendance" + control={control} + rules={{ required: true }} + render={({ field }) => ( + <RadioGroup {...field} row> + <FormControlLabel + value="yes" + control={<Radio />} + label="Yes" + /> + <FormControlLabel + value="no" + control={<Radio />} + label="No" + /> + </RadioGroup> + )} + /> + </div> + </FormControl> + </div> + </Grid> + <Grid item xs={12} md={6} lg={6}> + <TextField + label="Email" + type="email" + variant="outlined" + fullWidth + error={!!errors.email} + helperText={errors.email?.message} + required + {...register('email', { + required: 'This field is required', + pattern: { + value: /\S+@\S+\.\S+/, + message: 'Please enter a valid email address', + }, + })} + /> + </Grid> + <Grid item xs={12} md={6} lg={6}> + <TextField + label="Party Size" + type="number" + variant="outlined" + fullWidth + onWheel={(event) => { + event.currentTarget.blur(); + }} + error={!!errors.partySize} + helperText={errors.partySize?.message} + required + {...register('partySize', { + onChange: handleParty, + required: 'This field is required', + min: { value: 1, message: 'Please enter a positive integer' }, + max: { + value: 9, + message: 'Please enter an integer less than 10', + }, + })} + /> + </Grid> + <Grid item xs={12}> + <TextField + label="Message to the couple" + variant="outlined" + fullWidth + multiline + rows={3} + {...register('message')} + /> + </Grid> + {fields.map((field, index) => { + return ( + <Grid + container + item + columnSpacing={2} + rowSpacing={{ xs: 1 }} + key={field.id} + > + <Grid item xs={12} md={6} lg={6}> + <TextField + label="First Name" + variant="outlined" + fullWidth + error={!!errors.partyList?.[index]?.firstName} + helperText={errors.partyList?.[index]?.firstName?.message} + required + {...register(`partyList.${index}.firstName`, { + required: 'This field is required', + })} + /> + </Grid> + <Grid item xs={12} md={6} lg={6}> + <TextField + label="Last Name" + variant="outlined" + fullWidth + error={!!errors.partyList?.[index]?.lastName} + helperText={errors.partyList?.[index]?.lastName?.message} + required + {...register(`partyList.${index}.lastName`, { + required: 'This field is required', + })} + /> + </Grid> + </Grid> + ); + })} + <Grid item xs={12}> + <div + style={{ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }} + > + <Button type="submit" variant="contained"> + RSVP + </Button> + </div> + </Grid> + </Grid> + </Container> + ); +} + +export default RsvpForm; diff --git a/client/src/components/Schedule.tsx b/client/src/components/Schedule.tsx new file mode 100644 index 0000000..808499c --- /dev/null +++ b/client/src/components/Schedule.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { Container, Paper, Typography, useTheme } from '@mui/material'; + +function Schedule() { + const theme = useTheme(); + return ( + <Container + maxWidth="sm" + sx={{ + display: 'flex', + justifyContent: 'center', + }} + > + <Paper + elevation={3} + sx={{ + mt: 8, + height: '100%', + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }} + > + <div + style={{ + height: '100%', + width: '90%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + }} + > + <div + style={{ + height: '100%', + width: '100%', + display: 'flex', + alignItems: 'center', + }} + > + <div style={{ width: '35%' }}> + <p>April 26, 2025</p> + </div> + <div style={{ width: '65%' }}> + <Typography variant="h5">Wedding Schedule</Typography> + </div> + </div> + <hr style={{ width: '100%' }} /> + <div + style={{ + height: '100%', + width: '100%', + display: 'flex', + alignItems: 'center', + }} + > + <div style={{ width: '35%' }}> + <p>2:00 PM</p> + </div> + <div style={{ width: '65%' }}> + <Typography variant="h6">Ceremony</Typography> + <p> + Divine Shepherd + <br /> + <a + href="https://maps.app.goo.gl/dGWvmjPiVjNGBVkZ9" + style={{ color: theme.palette.primary.main }} + > + 15005 Q St, Omaha, NE 68137 + </a> + </p> + </div> + </div> + <hr style={{ width: '100%' }} /> + <div + style={{ + height: '100%', + width: '100%', + display: 'flex', + alignItems: 'center', + }} + > + <div style={{ width: '35%' }}> + <p>4:00 PM</p> + </div> + <div style={{ width: '65%' }}> + <Typography variant="h6">Reception</Typography> + <p> + A Venue on the Ridge + <br /> + <a + href="https://maps.app.goo.gl/35RRqxzQdq6E4eSMA" + style={{ color: theme.palette.primary.main }} + > + 20033 Elkhorn Ridge Dr, Elkhorn, NE 68022 + </a> + </p> + </div> + </div> + </div> + </Paper> + </Container> + ); +} + +export default Schedule; diff --git a/client/src/components/active.css b/client/src/components/active.css new file mode 100644 index 0000000..e8b5f60 --- /dev/null +++ b/client/src/components/active.css @@ -0,0 +1,7 @@ +.active { + background-color: #1976d2 +} + +.inactive { + background-color: #c4c4c4 +} diff --git a/client/src/features/auth/GuestLogin.tsx b/client/src/features/auth/GuestLogin.tsx new file mode 100644 index 0000000..4da7e45 --- /dev/null +++ b/client/src/features/auth/GuestLogin.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; +import { Button, Container, TextField, Typography } from '@mui/material'; +import { useForm } from 'react-hook-form'; +import { setCredentials } from './authSlice'; +import { useLoginMutation } from '../../apiSlice'; +import type { LoginRequest } from '../../apiSlice'; + +function GuestLogin() { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [login] = useLoginMutation(); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm<LoginRequest>({ + defaultValues: { + firstName: '', + lastName: '', + }, + }); + + const onSubmit = async (data: LoginRequest) => { + try { + dispatch(setCredentials(await login(data).unwrap())); + navigate('/rsvp'); + } catch (e) { + console.log(e); + } + }; + + return ( + <Container + component="form" + maxWidth="xs" + noValidate + onSubmit={handleSubmit(onSubmit)} + > + <div + style={{ + marginTop: 80, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }} + > + <Typography variant="h6">Guest Login</Typography> + <TextField + label="First Name" + variant="outlined" + margin="normal" + fullWidth + error={!!errors.firstName} + helperText={errors.firstName?.message} + required + {...register('firstName', { required: 'This field is required' })} + /> + <TextField + label="Last Name" + variant="outlined" + margin="normal" + fullWidth + error={!!errors.lastName} + helperText={errors.lastName?.message} + required + {...register('lastName', { required: 'This field is required' })} + /> + <Button type="submit" variant="contained" fullWidth sx={{ mt: 2 }}> + Log in + </Button> + </div> + </Container> + ); +} + +export default GuestLogin; diff --git a/client/src/features/auth/authSlice.ts b/client/src/features/auth/authSlice.ts new file mode 100644 index 0000000..bff2bdd --- /dev/null +++ b/client/src/features/auth/authSlice.ts @@ -0,0 +1,26 @@ +import { createSlice } from '@reduxjs/toolkit'; +import type { RootState } from '../../store'; +import type { Guest } from '../../apiSlice'; + +type AuthState = { + guest?: Guest; + token?: string; +}; + +const authSlice = createSlice({ + name: 'auth', + initialState: { guest: undefined, token: undefined } as AuthState, + reducers: { + setCredentials: (state, action) => { + const { guest, token } = action.payload; + state.guest = guest; + state.token = token; + }, + }, +}); + +export const { setCredentials } = authSlice.actions; + +export default authSlice.reducer; + +export const selectCurrentGuest = (state: RootState) => state.auth.guest; diff --git a/client/src/main.css b/client/src/main.css new file mode 100644 index 0000000..5cb7cd6 --- /dev/null +++ b/client/src/main.css @@ -0,0 +1,4 @@ +#root, body { + height: 100vh; + margin: 0 +} diff --git a/client/src/main.tsx b/client/src/main.tsx new file mode 100644 index 0000000..8ada188 --- /dev/null +++ b/client/src/main.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import App from './App'; +import store from './store'; +import ThemeContextProvider from './ThemeContextProvider'; +import Schedule from './components/Schedule'; +import Registry from './components/Registry'; +import GuestLogin from './features/auth/GuestLogin'; +import Rsvp from './components/Rsvp'; +import RsvpForm from './components/RsvpForm'; +import Admin from './components/Admin'; +import Home from './components/Home'; +import './main.css'; + +const router = createBrowserRouter([ + { + element: <App />, + children: [ + { + path: '/', + element: <Home />, + }, + { + path: 'schedule', + element: <Schedule />, + }, + { + path: 'registry', + element: <Registry />, + }, + { + path: 'guest-login', + element: <GuestLogin />, + }, + { + path: 'admin', + element: <Admin />, + }, + ], + }, + { + element: <Rsvp />, + children: [ + { + path: 'rsvp', + element: <RsvpForm />, + }, + ], + }, +]); + +const enableMocking = async () => { + const { worker } = await import('./mocks/browser'); + return worker.start(); +}; + +enableMocking().then(() => { + ReactDOM.createRoot(document.getElementById('root')!).render( + <React.StrictMode> + <Provider store={store}> + <ThemeContextProvider> + <RouterProvider router={router} /> + </ThemeContextProvider> + </Provider> + </React.StrictMode> + ); +}); diff --git a/client/src/mocks/browser.ts b/client/src/mocks/browser.ts new file mode 100644 index 0000000..0a56427 --- /dev/null +++ b/client/src/mocks/browser.ts @@ -0,0 +1,4 @@ +import { setupWorker } from 'msw/browser'; +import { handlers } from './handlers'; + +export const worker = setupWorker(...handlers); diff --git a/client/src/mocks/handlers.ts b/client/src/mocks/handlers.ts new file mode 100644 index 0000000..217a7d5 --- /dev/null +++ b/client/src/mocks/handlers.ts @@ -0,0 +1,30 @@ +import { http, HttpResponse } from 'msw'; +import { nanoid } from '@reduxjs/toolkit'; + +const token = nanoid(); + +export const handlers = [ + http.post('/guest-login', () => { + return HttpResponse.json({ + guest: { + id: 1, + firstName: 'Michael', + lastName: 'Hunteman', + attendance: 'false', + email: '', + message: '', + }, + token, + }); + }), + http.patch('/guests/1', () => { + return HttpResponse.json({ + id: 1, + firstName: 'Michael', + lastName: 'Hunteman', + attendance: 'true', + email: '', + message: '', + }); + }), +]; diff --git a/client/src/pages.ts b/client/src/pages.ts new file mode 100644 index 0000000..bad57f6 --- /dev/null +++ b/client/src/pages.ts @@ -0,0 +1,8 @@ +const pages = [ + { name: 'Home', to: '/' }, + { name: 'Schedule', to: '/schedule' }, + { name: 'RSVP', to: '/guest-login' }, + { name: 'Registry', to: '/registry' }, +]; + +export default pages; diff --git a/client/src/store.ts b/client/src/store.ts new file mode 100644 index 0000000..264639e --- /dev/null +++ b/client/src/store.ts @@ -0,0 +1,16 @@ +import { configureStore } from '@reduxjs/toolkit'; +import { apiSlice } from './apiSlice'; +import authReducer from './features/auth/authSlice'; + +const store = configureStore({ + reducer: { + [apiSlice.reducerPath]: apiSlice.reducer, + auth: authReducer, + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat(apiSlice.middleware), +}); + +export default store; +export type RootState = ReturnType<typeof store.getState>; +export type AppDispatch = typeof store.dispatch; diff --git a/client/src/vite-env.d.ts b/client/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/client/src/vite-env.d.ts @@ -0,0 +1 @@ +/// <reference types="vite/client" /> |