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/components | |
parent | fc5c111bcfe296bec82e1cf9fdb88fc80fb24f89 (diff) |
Add golang server
Diffstat (limited to 'client/src/components')
-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 |
10 files changed, 613 insertions, 0 deletions
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 +} |