summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMichael Hunteman <michael@huntm.net>2024-02-24 10:04:10 -0600
committerMichael Hunteman <michael@huntm.net>2024-02-24 10:18:33 -0600
commit68a86b2f9c41717767443b6b9e1860cb73b2aa30 (patch)
tree33c8d6033a26ec70a1e116d2e1669f2dc12b3744 /src
parent589e53f152d7363074049dfd1bd5a34286ae74d6 (diff)
Use RTK query
Diffstat (limited to 'src')
-rw-r--r--src/App.tsx2
-rw-r--r--src/ThemeContextProvider.tsx12
-rw-r--r--src/apiSlice.ts38
-rw-r--r--src/components/Admin.tsx17
-rw-r--r--src/components/GuestLogin.tsx18
-rw-r--r--src/components/NavBar.tsx16
-rw-r--r--src/components/Registry.tsx2
-rw-r--r--src/components/Rsvp.tsx41
-rw-r--r--src/components/RsvpForm.tsx50
-rw-r--r--src/components/Schedule.tsx21
-rw-r--r--src/features/auth/GuestLogin.tsx61
-rw-r--r--src/features/auth/authSlice.ts25
-rw-r--r--src/main.tsx53
-rw-r--r--src/mocks/browser.ts4
-rw-r--r--src/mocks/handlers.ts18
-rw-r--r--src/store.ts15
16 files changed, 272 insertions, 121 deletions
diff --git a/src/App.tsx b/src/App.tsx
index c0ea612..bdc170d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -10,7 +10,7 @@ function App() {
<NavBar />
<Outlet />
</>
- )
+ );
}
export default App;
diff --git a/src/ThemeContextProvider.tsx b/src/ThemeContextProvider.tsx
index e1e928c..63f4e81 100644
--- a/src/ThemeContextProvider.tsx
+++ b/src/ThemeContextProvider.tsx
@@ -1,20 +1,24 @@
import { ReactNode, createContext, useMemo, useState } from 'react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
+import useMediaQuery from '@mui/material/useMediaQuery';
type ThemeContextType = {
toggleColorMode: () => void;
-};
+}
type ThemeProviderProps = {
children: ReactNode;
-};
+}
export const ThemeContext = createContext<ThemeContextType>({
toggleColorMode: () => {}
});
function ThemeContextProvider({ children }: ThemeProviderProps) {
- const [mode, setMode] = useState<'light' | 'dark'>('light');
+ const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
+ const [mode, setMode] = useState<'light' | 'dark'>(
+ prefersDarkMode ? 'dark' : 'light'
+ );
const toggleColorMode = () => {
setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light'));
@@ -37,6 +41,6 @@ function ThemeContextProvider({ children }: ThemeProviderProps) {
</ThemeProvider>
</ThemeContext.Provider>
);
-};
+}
export default ThemeContextProvider;
diff --git a/src/apiSlice.ts b/src/apiSlice.ts
index 6d779ed..fde9fff 100644
--- a/src/apiSlice.ts
+++ b/src/apiSlice.ts
@@ -1,8 +1,34 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
+import { setCredentials, logOut } from './features/auth/authSlice';
+
+export interface User {
+ firstName: string
+ lastName: string
+}
+
+export interface UserResponse {
+ user: User
+ token: string
+}
+
+export interface LoginRequest {
+ firstName: string
+ lastName: string
+}
export const apiSlice = createApi({
reducerPath: 'api',
- baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:3000' }),
+ baseQuery: fetchBaseQuery({
+ // baseUrl: 'http://localhost:3000',
+ 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({
@@ -16,11 +42,19 @@ export const apiSlice = createApi({
body: guest,
providesTags: ['Guests']
})
+ }),
+ login: builder.mutation<UserResponse, LoginRequest>({
+ query: credentials => ({
+ url: '/guest-login',
+ method: 'POST',
+ body: credentials
+ })
})
})
});
export const {
useGetGuestsQuery,
- useUpdateGuestMutation
+ useUpdateGuestMutation,
+ useLoginMutation
} = apiSlice;
diff --git a/src/components/Admin.tsx b/src/components/Admin.tsx
index 8f6ce12..fac3306 100644
--- a/src/components/Admin.tsx
+++ b/src/components/Admin.tsx
@@ -1,7 +1,6 @@
-import Paper from '@mui/material/Paper';
-import Typography from '@mui/material/Typography';
+import { Paper, Typography } from '@mui/material';
-import { useGetGuestsQuery } from '../apiSlice'
+import { useGetGuestsQuery } from '../apiSlice';
function Admin() {
const {
@@ -10,26 +9,26 @@ function Admin() {
isSuccess,
isError,
error
- } = useGetGuestsQuery()
+ } = useGetGuestsQuery();
- let content
+ let content;
if (isLoading) {
- content = <Typography variant="h4">Loading...</Typography>
+ content = <Typography variant="h6">Loading...</Typography>
} else if (isSuccess) {
- content = JSON.stringify(guests)
+ content = JSON.stringify(guests);
} else if (isError) {
content = <>{error.toString()}</>
}
return (
<Paper>
- <Typography variant="h4" component="h4">
+ <Typography variant="h6">
Admin
</Typography>
{content}
</Paper>
- )
+ );
}
export default Admin;
diff --git a/src/components/GuestLogin.tsx b/src/components/GuestLogin.tsx
deleted file mode 100644
index 5637276..0000000
--- a/src/components/GuestLogin.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import Button from '@mui/material/Button';
-import Paper from '@mui/material/Paper';
-import Typography from '@mui/material/Typography';
-
-function GuestLogin({ loggedIn, setLoggedIn }) {
- return (
- <Paper>
- <Typography variant="h6">
- Enter your name to RSVP
- </Typography>
- <Button onClick={() => setLoggedIn(!loggedIn)} variant="contained">
- Login
- </Button>
- </Paper>
- )
-}
-
-export default GuestLogin;
diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx
index 2cf1b31..68fc706 100644
--- a/src/components/NavBar.tsx
+++ b/src/components/NavBar.tsx
@@ -1,14 +1,8 @@
import { useContext } from 'react';
import { Link } from 'react-router-dom';
-import AppBar from '@mui/material/AppBar';
-import Box from '@mui/material/Box';
-import Button from '@mui/material/Button';
-import IconButton from '@mui/material/IconButton';
+import { AppBar, Box, Button, IconButton, Stack, Toolbar, Typography } from '@mui/material';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
-import Stack from '@mui/material/Stack';
-import Toolbar from '@mui/material/Toolbar';
-import Typography from '@mui/material/Typography';
import { useTheme } from '@mui/material/styles';
import { ThemeContext } from '../ThemeContextProvider';
@@ -19,7 +13,7 @@ function NavBar({ mode }) {
const pages = [
{ name: 'Schedule', to: '/schedule'},
- { name: 'RSVP', to: '/rsvp' },
+ { name: 'RSVP', to: '/guest-login' },
{ name: 'Registry', to: '/registry' },
{ name: 'Admin', to: '/admin' }
];
@@ -38,8 +32,8 @@ function NavBar({ mode }) {
</Typography>
<Stack direction="row" sx={{ marginLeft: 'auto' }}>
{pages.map(page => (
- <Button color="inherit" component={Link} to={page.to} key={page.name}>
- {page.name}
+ <Button color="inherit" component={Link} to={page?.to} key={page?.name}>
+ {page?.name}
</Button>
))}
<IconButton color="inherit" onClick={toggleColorMode}>
@@ -49,6 +43,6 @@ function NavBar({ mode }) {
</Toolbar>
</AppBar>
);
-};
+}
export default NavBar;
diff --git a/src/components/Registry.tsx b/src/components/Registry.tsx
index 5856909..8d7fff4 100644
--- a/src/components/Registry.tsx
+++ b/src/components/Registry.tsx
@@ -4,7 +4,7 @@ import Typography from '@mui/material/Typography';
function Registry() {
return (
<Paper>
- <Typography variant="h4" component="h4">
+ <Typography variant="h6">
Registry
</Typography>
</Paper>
diff --git a/src/components/Rsvp.tsx b/src/components/Rsvp.tsx
index 858ca71..466175e 100644
--- a/src/components/Rsvp.tsx
+++ b/src/components/Rsvp.tsx
@@ -1,29 +1,30 @@
-import { useState } 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 RsvpForm from './RsvpForm';
-import GuestLogin from './GuestLogin';
-import { useGetGuestsQuery } from '../apiSlice'
+import { selectCurrentUser } from '../features/auth/authSlice';
-function Rsvp() {
- // Enter your name to RSVP; query the database
- const [loggedIn, setLoggedIn] = useState(false);
+const authenticate = () => {
+ const user = useSelector(selectCurrentUser);
+ return useMemo(() => ({ user }), [user]);
+};
- const {
- data: guests,
- isLoading,
- isSuccess,
- isError,
- error
- } = useGetGuestsQuery()
+function Rsvp() {
+ const auth = authenticate();
+ const location = useLocation();
- return (
+ return auth.user ? (
<>
- {loggedIn ? (
- <RsvpForm />
- ) : (
- <GuestLogin loggedIn={loggedIn} setLoggedIn={setLoggedIn} />
- )}
+ <CssBaseline />
+ <NavBar />
+ <Outlet />
</>
- )
+ ) : (
+ <Navigate to="/guest-login" state={{ from: location }} replace />
+ );
}
export default Rsvp;
diff --git a/src/components/RsvpForm.tsx b/src/components/RsvpForm.tsx
index 9dbc54c..fe6d874 100644
--- a/src/components/RsvpForm.tsx
+++ b/src/components/RsvpForm.tsx
@@ -1,14 +1,17 @@
import { useState } from 'react';
-import Button from '@mui/material/Button';
-import Paper from '@mui/material/Paper';
-import Grid from '@mui/material/Grid';
-import TextField from '@mui/material/TextField';
-import Typography from '@mui/material/Typography';
-import Radio from '@mui/material/Radio';
-import RadioGroup from '@mui/material/RadioGroup';
-import FormControlLabel from '@mui/material/FormControlLabel';
-import FormControl from '@mui/material/FormControl';
-import FormLabel from '@mui/material/FormLabel';
+import { useSelector } from 'react-redux';
+import {
+ Button,
+ FormControl,
+ FormControlLabel,
+ FormLabel,
+ Grid,
+ Paper,
+ Radio,
+ RadioGroup,
+ TextField,
+ Typography
+} from '@mui/material';
import { useGetGuestsQuery, useUpdateGuestMutation } from '../apiSlice';
@@ -19,20 +22,19 @@ function RsvpForm() {
isSuccess,
isError,
error
- } = useGetGuestsQuery()
+ } = useGetGuestsQuery();
- const [updateGuest] = useUpdateGuestMutation()
+ const [updateGuest] = useUpdateGuestMutation();
const handleSubmit = (e) => {
- e.preventDefault()
- console.log('handle')
- let guest = guests[0]
+ e.preventDefault();
+ let guest = guests[0];
if (guest.attendance === 'true') {
- updateGuest({...guest, attendance: 'false'})
+ updateGuest({...guest, attendance: 'false'});
} else {
- updateGuest({...guest, attendance: 'true'})
+ updateGuest({...guest, attendance: 'true'});
}
- }
+ };
return (
<Paper>
@@ -87,23 +89,17 @@ function RsvpForm() {
</FormControl>
</Grid>
<Grid item xs={12} md={4} lg={4}>
- <TextField
- label="Dietary Restrictions"
- variant="outlined"
- />
+ <TextField label="Dietary Restrictions" variant="outlined" />
</Grid>
<Grid item xs={12} md={4} lg={4}>
- <TextField
- label="Advice"
- variant="outlined"
- />
+ <TextField label="Advice" variant="outlined" />
</Grid>
<Grid item>
<Button onClick={handleSubmit} variant="contained">Submit</Button>
</Grid>
</Grid>
</Paper>
- )
+ );
}
export default RsvpForm;
diff --git a/src/components/Schedule.tsx b/src/components/Schedule.tsx
index 002907a..73548bc 100644
--- a/src/components/Schedule.tsx
+++ b/src/components/Schedule.tsx
@@ -1,14 +1,13 @@
-import Typography from '@mui/material/Typography';
-import Paper from '@mui/material/Paper';
-import Timeline from '@mui/lab/Timeline';
-import TimelineItem from '@mui/lab/TimelineItem';
-import TimelineSeparator from '@mui/lab/TimelineSeparator';
-import TimelineConnector from '@mui/lab/TimelineConnector';
-import TimelineContent from '@mui/lab/TimelineContent';
-import TimelineDot from '@mui/lab/TimelineDot';
-import TimelineOppositeContent, {
- timelineOppositeContentClasses,
-} from '@mui/lab/TimelineOppositeContent';
+import { Paper, Typography } from '@mui/material';
+import {
+ Timeline,
+ TimelineConnector,
+ TimelineContent,
+ TimelineDot,
+ TimelineItem,
+ TimelineOppositeContent,
+ TimelineSeparator
+} from '@mui/lab';
function Schedule() {
return (
diff --git a/src/features/auth/GuestLogin.tsx b/src/features/auth/GuestLogin.tsx
new file mode 100644
index 0000000..fb305f7
--- /dev/null
+++ b/src/features/auth/GuestLogin.tsx
@@ -0,0 +1,61 @@
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useDispatch } from 'react-redux';
+import { Button, Grid, Paper, TextField, Typography } from '@mui/material';
+
+import { setCredentials } from './authSlice';
+import { useLoginMutation } from '../../apiSlice';
+import type { LoginRequest } from './authSlice';
+
+function GuestLogin() {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ // TODO: use react-hook-form
+ const [formState, setFormState] = useState<LoginRequest>({
+ firstName: '',
+ lastName: ''
+ });
+
+ const [login] = useLoginMutation();
+
+ const handleChange = ({
+ target: { name, value },
+ }: React.ChangeEvent<HTMLInputElement>) =>
+ setFormState(prev => ({ ...prev, [name]: value }));
+
+ const handleSubmit = async () => {
+ try {
+ const user = await login(formState).unwrap();
+ dispatch(setCredentials(user));
+ navigate('/rsvp');
+ } catch (e) {
+ console.log(e);
+ }
+ };
+
+ return (
+ <Paper>
+ <Grid container spacing={2}>
+ <Grid item xs={12} md={12} lg={12}>
+ <Typography variant="h6">
+ Guest Login
+ </Typography>
+ </Grid>
+ <Grid item xs={12} md={6} lg={6}>
+ <TextField label="First Name" variant="outlined" onChange={handleChange} />
+ </Grid>
+ <Grid item xs={12} md={6} lg={6}>
+ <TextField label="Last Name" variant="outlined" onChange={handleChange} />
+ </Grid>
+ <Grid item>
+ <Button onClick={handleSubmit} variant="contained">
+ Login
+ </Button>
+ </Grid>
+ </Grid>
+ </Paper>
+ );
+}
+
+export default GuestLogin;
diff --git a/src/features/auth/authSlice.ts b/src/features/auth/authSlice.ts
new file mode 100644
index 0000000..9716131
--- /dev/null
+++ b/src/features/auth/authSlice.ts
@@ -0,0 +1,25 @@
+import { createSlice } from '@reduxjs/toolkit';
+import type { RootState } from '../../store';
+
+type AuthState = {
+ user: User | null
+ token: string | null
+}
+
+const authSlice = createSlice({
+ name: 'auth',
+ initialState: { user: null, token: null } as AuthState,
+ reducers: {
+ setCredentials: (state, action) => {
+ const { user, token } = action.payload;
+ state.user = user;
+ state.token = token;
+ }
+ }
+});
+
+export const { setCredentials } = authSlice.actions;
+
+export default authSlice.reducer;
+
+export const selectCurrentUser = (state: RootState) => state.auth.user;
diff --git a/src/main.tsx b/src/main.tsx
index ecc4803..1e7c31a 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -2,47 +2,66 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import { render } from 'react-dom';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
-import { ApiProvider } from '@reduxjs/toolkit/query/react';
+import { Provider } from 'react-redux';
import App from './App';
+import store from './store';
import { apiSlice } from './apiSlice';
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';
const router = createBrowserRouter([
{
- path: "/",
+ path: '/',
element: <App />,
children: [
{
- path: "schedule",
+ path: 'schedule',
element: <Schedule />
},
{
- path: "registry",
+ path: 'registry',
element: <Registry />
},
{
- path: "rsvp",
- element: <Rsvp />
+ path: 'guest-login',
+ element: <GuestLogin />
},
{
- path: "admin",
+ path: 'admin',
element: <Admin />
}
+ ],
+ },
+ {
+ element: <Rsvp />,
+ children: [
+ {
+ path: 'rsvp',
+ element: <RsvpForm />
+ }
]
}
-])
+], { basename: '/wedding' });
+
+const enableMocking = async () => {
+ const { worker } = await import('./mocks/browser');
+ return worker.start();
+};
-ReactDOM.createRoot(document.getElementById('root')!).render(
- <React.StrictMode>
- <ApiProvider api={apiSlice}>
- <ThemeContextProvider>
- <RouterProvider router={router} />
- </ThemeContextProvider>
- </ApiProvider>
- </React.StrictMode>
-)
+enableMocking().then(() => {
+ ReactDOM.createRoot(document.getElementById('root')!).render(
+ <React.StrictMode>
+ <Provider store={store}>
+ <ThemeContextProvider>
+ <RouterProvider router={router} />
+ </ThemeContextProvider>
+ </Provider>
+ </React.StrictMode>
+ );
+});
diff --git a/src/mocks/browser.ts b/src/mocks/browser.ts
new file mode 100644
index 0000000..0a56427
--- /dev/null
+++ b/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/src/mocks/handlers.ts b/src/mocks/handlers.ts
new file mode 100644
index 0000000..483da83
--- /dev/null
+++ b/src/mocks/handlers.ts
@@ -0,0 +1,18 @@
+import { http, HttpResponse } from 'msw';
+import { nanoid } from '@reduxjs/toolkit';
+
+const token = nanoid();
+
+export const handlers = [
+ http.post('/guest-login', () => {
+ return HttpResponse.json(
+ {
+ user: {
+ firstName: 'Michael',
+ lastName: 'Hunteman'
+ },
+ token
+ }
+ )
+ })
+];
diff --git a/src/store.ts b/src/store.ts
new file mode 100644
index 0000000..3465bff
--- /dev/null
+++ b/src/store.ts
@@ -0,0 +1,15 @@
+import { configureStore } from '@reduxjs/toolkit';
+import { apiSlice } from './apiSlice';
+import authReducer from './features/auth/authSlice';
+
+export default configureStore({
+ reducer: {
+ [apiSlice.reducerPath]: apiSlice.reducer,
+ auth: authReducer
+ },
+ middleware: getDefaultMiddleware =>
+ getDefaultMiddleware().concat(apiSlice.middleware)
+});
+
+export type RootState = ReturnType<typeof store.getState>
+export type AppDispatch = typeof store.dispatch