diff options
Diffstat (limited to 'client/src')
-rw-r--r-- | client/src/components/Admin.tsx | 29 | ||||
-rw-r--r-- | client/src/components/AdminLogin.tsx | 90 | ||||
-rw-r--r-- | client/src/components/Dashboard.tsx | 60 | ||||
-rw-r--r-- | client/src/components/GuestLogin.tsx | 14 | ||||
-rw-r--r-- | client/src/components/Rsvp.tsx | 4 | ||||
-rw-r--r-- | client/src/components/RsvpForm.tsx | 4 | ||||
-rw-r--r-- | client/src/main.tsx | 16 | ||||
-rw-r--r-- | client/src/pages.ts | 1 | ||||
-rw-r--r-- | client/src/slices/api/adminSlice.ts | 53 | ||||
-rw-r--r-- | client/src/slices/api/guestSlice.ts (renamed from client/src/slices/apiSlice.ts) | 37 | ||||
-rw-r--r-- | client/src/slices/auth/adminSlice.ts | 29 | ||||
-rw-r--r-- | client/src/slices/auth/guestSlice.ts | 29 | ||||
-rw-r--r-- | client/src/slices/authSlice.ts | 31 | ||||
-rw-r--r-- | client/src/store.ts | 14 | ||||
-rw-r--r-- | client/src/vite-env.d.ts | 8 |
15 files changed, 350 insertions, 69 deletions
diff --git a/client/src/components/Admin.tsx b/client/src/components/Admin.tsx new file mode 100644 index 0000000..6e772ab --- /dev/null +++ b/client/src/components/Admin.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 { selectGuests } from '../slices/auth/adminSlice'; + +const authenticate = () => { + const guests = useSelector(selectGuests); + return useMemo(() => ({ guests }), [guests]); +}; + +function Rsvp() { + const auth = authenticate(); + const location = useLocation(); + + return auth?.guests ? ( + <> + <CssBaseline /> + <NavBar /> + <Outlet context={auth?.guests} /> + </> + ) : ( + <Navigate to="/admin/login" state={{ from: location }} replace /> + ); +} + +export default Rsvp; diff --git a/client/src/components/AdminLogin.tsx b/client/src/components/AdminLogin.tsx new file mode 100644 index 0000000..d9c1260 --- /dev/null +++ b/client/src/components/AdminLogin.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; +import { Button, Paper, TextField, Typography } from '@mui/material'; +import { useForm } from 'react-hook-form'; +import { setAdmin } from '../slices/auth/adminSlice'; +import { useLoginAdminMutation } from '../slices/api/adminSlice'; +import type { AdminLoginRequest } from '../slices/api/adminSlice'; + +function GuestLogin() { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [login] = useLoginAdminMutation(); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm<AdminLoginRequest>({ + defaultValues: { + username: '', + password: '', + }, + }); + + const onSubmit = async (data: AdminLoginRequest) => { + try { + dispatch(setAdmin(await login(data).unwrap())); + navigate('/dashboard'); + } catch (e) { + console.log(e); + } + }; + + return ( + <form + style={{ + height: '100%', + width: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'start', + }} + noValidate + onSubmit={handleSubmit(onSubmit)} + > + <Paper + elevation={3} + sx={{ + '&:hover': { boxShadow: 8 }, + width: { xs: '90%', md: 400 }, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + mt: 16, + p: 2, + borderRadius: '8px', + }} + > + <Typography variant="h6">Admin Login</Typography> + <TextField + label="Username" + variant="outlined" + margin="normal" + fullWidth + error={!!errors.username} + helperText={errors.username?.message} + required + {...register('username', { required: 'This field is required' })} + /> + <TextField + label="Password" + variant="outlined" + margin="normal" + fullWidth + error={!!errors.password} + helperText={errors.password?.message} + required + {...register('password', { required: 'This field is required' })} + /> + <Button type="submit" variant="contained" fullWidth sx={{ mt: 2 }}> + Log in + </Button> + </Paper> + </form> + ); +} + +export default GuestLogin; diff --git a/client/src/components/Dashboard.tsx b/client/src/components/Dashboard.tsx new file mode 100644 index 0000000..20758fc --- /dev/null +++ b/client/src/components/Dashboard.tsx @@ -0,0 +1,60 @@ +import React, { useMemo } from 'react'; +import { useOutletContext } from 'react-router-dom'; +import { + MaterialReactTable, + useMaterialReactTable, + type MRT_ColumnDef, +} from 'material-react-table'; +import type { Guest } from '../slices/api/adminSlice'; + +function Dashboard() { + const guests: Guest[] = useOutletContext(); + const columns = useMemo<MRT_ColumnDef<Guest>[]>( + () => [ + { + accessorKey: 'firstName', + header: 'First Name', + size: 150, + }, + { + accessorKey: 'lastName', + header: 'Last Name', + size: 150, + }, + { + accessorKey: 'attendance', + header: 'Attendance', + size: 50, + }, + { + accessorKey: 'email', + header: 'Email', + size: 150, + }, + { + accessorKey: 'message', + header: 'Message', + size: 200, + }, + { + accessorKey: 'partySize', + header: 'Party Size', + size: 50, + }, + // { + // accessorKey: 'partyList', + // header: 'Party List', + // size: 150, + // }, + ], + [] + ); + const table = useMaterialReactTable({ + columns, + data: guests, + }); + + return <MaterialReactTable table={table} />; +} + +export default Dashboard; diff --git a/client/src/components/GuestLogin.tsx b/client/src/components/GuestLogin.tsx index cca2179..0e47384 100644 --- a/client/src/components/GuestLogin.tsx +++ b/client/src/components/GuestLogin.tsx @@ -3,29 +3,29 @@ import { useNavigate } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import { Button, Paper, TextField, Typography } from '@mui/material'; import { useForm } from 'react-hook-form'; -import { setCredentials } from '../slices/authSlice'; -import { useLoginMutation } from '../slices/apiSlice'; -import type { LoginRequest } from '../slices/apiSlice'; +import { setGuest } from '../slices/auth/guestSlice'; +import { useLoginGuestMutation } from '../slices/api/guestSlice'; +import type { GuestLoginRequest } from '../slices/api/guestSlice'; function GuestLogin() { const dispatch = useDispatch(); const navigate = useNavigate(); - const [login] = useLoginMutation(); + const [login] = useLoginGuestMutation(); const { register, handleSubmit, formState: { errors }, - } = useForm<LoginRequest>({ + } = useForm<GuestLoginRequest>({ defaultValues: { firstName: '', lastName: '', }, }); - const onSubmit = async (data: LoginRequest) => { + const onSubmit = async (data: GuestLoginRequest) => { try { - dispatch(setCredentials(await login(data).unwrap())); + dispatch(setGuest(await login(data).unwrap())); navigate('/rsvp'); } catch (e) { console.log(e); diff --git a/client/src/components/Rsvp.tsx b/client/src/components/Rsvp.tsx index d3d9677..ab83cd7 100644 --- a/client/src/components/Rsvp.tsx +++ b/client/src/components/Rsvp.tsx @@ -4,10 +4,10 @@ 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 '../slices/authSlice'; +import { selectGuest } from '../slices/auth/guestSlice'; const authenticate = () => { - const guest = useSelector(selectCurrentGuest); + const guest = useSelector(selectGuest); return useMemo(() => ({ guest }), [guest]); }; diff --git a/client/src/components/RsvpForm.tsx b/client/src/components/RsvpForm.tsx index 1e03227..d72b92d 100644 --- a/client/src/components/RsvpForm.tsx +++ b/client/src/components/RsvpForm.tsx @@ -16,8 +16,8 @@ import { } from '@mui/material'; import MailIcon from '@mui/icons-material/Mail'; import { useForm, Controller, useFieldArray } from 'react-hook-form'; -import { useUpdateGuestMutation } from '../slices/apiSlice'; -import type { Guest } from '../slices/apiSlice'; +import { useUpdateGuestMutation } from '../slices/api/guestSlice'; +import type { Guest } from '../slices/api/guestSlice'; interface StatusProps { isError: boolean; diff --git a/client/src/main.tsx b/client/src/main.tsx index 70aad60..2268d5f 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -11,6 +11,9 @@ import GuestLogin from './components/GuestLogin'; import Rsvp from './components/Rsvp'; import RsvpForm from './components/RsvpForm'; import Home from './components/Home'; +import AdminLogin from './components/AdminLogin'; +import Admin from './components/Admin'; +import Dashboard from './components/Dashboard'; import './main.css'; const router = createBrowserRouter([ @@ -33,6 +36,10 @@ const router = createBrowserRouter([ path: 'guest/login', element: <GuestLogin />, }, + { + path: 'admin/login', + element: <AdminLogin />, + }, ], }, { @@ -44,6 +51,15 @@ const router = createBrowserRouter([ }, ], }, + { + element: <Admin />, + children: [ + { + path: 'dashboard', + element: <Dashboard />, + }, + ], + }, ]); ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/client/src/pages.ts b/client/src/pages.ts index 8bf5d23..5ebb9c4 100644 --- a/client/src/pages.ts +++ b/client/src/pages.ts @@ -3,6 +3,7 @@ const pages = [ { name: 'Schedule', to: '/schedule' }, { name: 'RSVP', to: '/guest/login' }, { name: 'Registry', to: '/registry' }, + { name: 'Dashboard', to: '/admin/login' }, ]; export default pages; diff --git a/client/src/slices/api/adminSlice.ts b/client/src/slices/api/adminSlice.ts new file mode 100644 index 0000000..cd1638d --- /dev/null +++ b/client/src/slices/api/adminSlice.ts @@ -0,0 +1,53 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import type { RootState } from '../../store'; + +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 interface AdminLoginRequest { + username: string; + password: string; +} + +export interface AdminLoginResponse { + guests: Guest[]; + token: string; +} + +export const adminSlice = createApi({ + reducerPath: 'adminApi', + baseQuery: fetchBaseQuery({ + baseUrl: import.meta.env.VITE_BASE_URL, + prepareHeaders: (headers, { getState }) => { + const token = (getState() as RootState).admin.token; + if (token) { + headers.set('authorization', `${token}`); + } + return headers; + }, + }), + endpoints: (builder) => ({ + loginAdmin: builder.mutation<AdminLoginResponse, AdminLoginRequest>({ + query: (credentials) => ({ + url: 'admin/login', + method: 'POST', + body: credentials, + }), + }), + }), +}); + +export const { useLoginAdminMutation } = adminSlice; diff --git a/client/src/slices/apiSlice.ts b/client/src/slices/api/guestSlice.ts index 90cdc48..38deb9a 100644 --- a/client/src/slices/apiSlice.ts +++ b/client/src/slices/api/guestSlice.ts @@ -1,12 +1,12 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; -import type { RootState } from '../store'; +import type { RootState } from '../../store'; -export interface LoginRequest { +export interface GuestLoginRequest { firstName: string; lastName: string; } -export interface LoginResponse { +export interface GuestLoginResponse { guest: Guest; token: string; } @@ -27,41 +27,34 @@ export interface PartyGuest { lastName: string; } -export const apiSlice = createApi({ - reducerPath: 'api', +export const guestSlice = createApi({ + reducerPath: 'guestApi', baseQuery: fetchBaseQuery({ - baseUrl: 'http://192.168.1.41:8080/', + baseUrl: import.meta.env.VITE_BASE_URL + 'guest/', prepareHeaders: (headers, { getState }) => { - const token = (getState() as RootState).auth.token; + const token = (getState() as RootState).guest.token; if (token) { headers.set('authorization', `${token}`); } return headers; }, }), - tagTypes: ['Guest'], endpoints: (builder) => ({ - getGuests: builder.query<void, void>({ - query: () => 'guest', - providesTags: ['Guest'], + loginGuest: builder.mutation<GuestLoginResponse, GuestLoginRequest>({ + query: (credentials) => ({ + url: 'login', + method: 'POST', + body: credentials, + }), }), updateGuest: builder.mutation<Guest, Guest>({ query: (guest) => ({ - url: `guest/${guest?.id}`, + url: `${guest?.id}`, method: 'PUT', body: guest, - providesTags: ['Guest'], - }), - }), - login: builder.mutation<LoginResponse, LoginRequest>({ - query: (credentials) => ({ - url: 'guest/login', - method: 'POST', - body: credentials, }), }), }), }); -export const { useGetGuestsQuery, useUpdateGuestMutation, useLoginMutation } = - apiSlice; +export const { useLoginGuestMutation, useUpdateGuestMutation } = guestSlice; diff --git a/client/src/slices/auth/adminSlice.ts b/client/src/slices/auth/adminSlice.ts new file mode 100644 index 0000000..8753b55 --- /dev/null +++ b/client/src/slices/auth/adminSlice.ts @@ -0,0 +1,29 @@ +import { createSlice } from '@reduxjs/toolkit'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import type { RootState } from '../../store'; +import type { Guest } from '../api/guestSlice'; + +type AdminAuth = { + guests?: Guest[]; + token?: string; +}; + +const adminSlice = createSlice({ + name: 'admin', + initialState: { guest: undefined, token: undefined } as AdminAuth, + reducers: { + setAdmin: ( + state, + { payload: { guests, token } }: PayloadAction<AdminAuth> + ) => { + state.guests = guests; + state.token = token; + }, + }, +}); + +export const { setAdmin } = adminSlice.actions; + +export default adminSlice.reducer; + +export const selectGuests = (state: RootState) => state.admin.guests; diff --git a/client/src/slices/auth/guestSlice.ts b/client/src/slices/auth/guestSlice.ts new file mode 100644 index 0000000..701148e --- /dev/null +++ b/client/src/slices/auth/guestSlice.ts @@ -0,0 +1,29 @@ +import { createSlice } from '@reduxjs/toolkit'; +import type { PayloadAction } from '@reduxjs/toolkit'; +import type { RootState } from '../../store'; +import type { Guest } from '../api/guestSlice'; + +type GuestAuth = { + guest?: Guest; + token?: string; +}; + +const guestSlice = createSlice({ + name: 'guest', + initialState: { guest: undefined, token: undefined } as GuestAuth, + reducers: { + setGuest: ( + state, + { payload: { guest, token } }: PayloadAction<GuestAuth> + ) => { + state.guest = guest; + state.token = token; + }, + }, +}); + +export const { setGuest } = guestSlice.actions; + +export default guestSlice.reducer; + +export const selectGuest = (state: RootState) => state.guest.guest; diff --git a/client/src/slices/authSlice.ts b/client/src/slices/authSlice.ts deleted file mode 100644 index e1fec78..0000000 --- a/client/src/slices/authSlice.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit'; -import type { PayloadAction } 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, - { - payload: { guest, token }, - }: PayloadAction<{ guest: Guest; token: string }> - ) => { - 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/store.ts b/client/src/store.ts index 18b3461..c30a30d 100644 --- a/client/src/store.ts +++ b/client/src/store.ts @@ -1,14 +1,18 @@ import { configureStore } from '@reduxjs/toolkit'; -import { apiSlice } from './slices/apiSlice'; -import authReducer from './slices/authSlice'; +import guestReducer from './slices/auth/guestSlice'; +import adminReducer from './slices/auth/adminSlice'; +import { guestSlice } from './slices/api/guestSlice'; +import { adminSlice } from './slices/api/adminSlice'; const store = configureStore({ reducer: { - [apiSlice.reducerPath]: apiSlice.reducer, - auth: authReducer, + [guestSlice.reducerPath]: guestSlice.reducer, + [adminSlice.reducerPath]: adminSlice.reducer, + guest: guestReducer, + admin: adminReducer, }, middleware: (getDefaultMiddleware) => - getDefaultMiddleware().concat(apiSlice.middleware), + getDefaultMiddleware().concat(guestSlice.middleware, adminSlice.middleware), }); export default store; diff --git a/client/src/vite-env.d.ts b/client/src/vite-env.d.ts index 11f02fe..72eb128 100644 --- a/client/src/vite-env.d.ts +++ b/client/src/vite-env.d.ts @@ -1 +1,9 @@ /// <reference types="vite/client" /> + +interface ImportMetaEnv { + readonly VITE_BASE_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} |