From 68a86b2f9c41717767443b6b9e1860cb73b2aa30 Mon Sep 17 00:00:00 2001 From: Michael Hunteman Date: Sat, 24 Feb 2024 10:04:10 -0600 Subject: Use RTK query --- src/App.tsx | 2 +- src/ThemeContextProvider.tsx | 12 +++++--- src/apiSlice.ts | 38 +++++++++++++++++++++++-- src/components/Admin.tsx | 17 ++++++----- src/components/GuestLogin.tsx | 18 ------------ src/components/NavBar.tsx | 16 ++++------- src/components/Registry.tsx | 2 +- src/components/Rsvp.tsx | 41 ++++++++++++++------------- src/components/RsvpForm.tsx | 50 +++++++++++++++----------------- src/components/Schedule.tsx | 21 +++++++------- src/features/auth/GuestLogin.tsx | 61 ++++++++++++++++++++++++++++++++++++++++ src/features/auth/authSlice.ts | 25 ++++++++++++++++ src/main.tsx | 53 +++++++++++++++++++++++----------- src/mocks/browser.ts | 4 +++ src/mocks/handlers.ts | 18 ++++++++++++ src/store.ts | 15 ++++++++++ 16 files changed, 272 insertions(+), 121 deletions(-) delete mode 100644 src/components/GuestLogin.tsx create mode 100644 src/features/auth/GuestLogin.tsx create mode 100644 src/features/auth/authSlice.ts create mode 100644 src/mocks/browser.ts create mode 100644 src/mocks/handlers.ts create mode 100644 src/store.ts (limited to 'src') 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() { - ) + ); } 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({ 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) { ); -}; +} 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({ + 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 = Loading... + content = Loading... } else if (isSuccess) { - content = JSON.stringify(guests) + content = JSON.stringify(guests); } else if (isError) { content = <>{error.toString()} } return ( - + Admin {content} - ) + ); } 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 ( - - - Enter your name to RSVP - - - - ) -} - -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 }) { {pages.map(page => ( - ))} @@ -49,6 +43,6 @@ function NavBar({ mode }) { ); -}; +} 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 ( - + Registry 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 ? ( - - ) : ( - - )} + + + - ) + ) : ( + + ); } 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 ( @@ -87,23 +89,17 @@ function RsvpForm() { - + - + - ) + ); } 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({ + firstName: '', + lastName: '' + }); + + const [login] = useLoginMutation(); + + const handleChange = ({ + target: { name, value }, + }: React.ChangeEvent) => + 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 ( + + + + + Guest Login + + + + + + + + + + + + + + ); +} + +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: , children: [ { - path: "schedule", + path: 'schedule', element: }, { - path: "registry", + path: 'registry', element: }, { - path: "rsvp", - element: + path: 'guest-login', + element: }, { - path: "admin", + path: 'admin', element: } + ], + }, + { + element: , + children: [ + { + path: 'rsvp', + element: + } ] } -]) +], { basename: '/wedding' }); + +const enableMocking = async () => { + const { worker } = await import('./mocks/browser'); + return worker.start(); +}; -ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - - - -) +enableMocking().then(() => { + ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + + ); +}); 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 +export type AppDispatch = typeof store.dispatch -- cgit v1.2.3