From 096a08708e2310becba56a237ef63b5cf6e3c4c4 Mon Sep 17 00:00:00 2001 From: Michael Hunteman Date: Sun, 25 Aug 2024 12:44:32 -0700 Subject: Add admin dashboard --- client/src/components/Admin.tsx | 29 ++++++++++++ client/src/components/AdminLogin.tsx | 90 ++++++++++++++++++++++++++++++++++++ client/src/components/Dashboard.tsx | 60 ++++++++++++++++++++++++ client/src/components/GuestLogin.tsx | 14 +++--- client/src/components/Rsvp.tsx | 4 +- client/src/components/RsvpForm.tsx | 4 +- client/src/main.tsx | 16 +++++++ client/src/pages.ts | 1 + client/src/slices/api/adminSlice.ts | 53 +++++++++++++++++++++ client/src/slices/api/guestSlice.ts | 60 ++++++++++++++++++++++++ client/src/slices/apiSlice.ts | 67 --------------------------- client/src/slices/auth/adminSlice.ts | 29 ++++++++++++ client/src/slices/auth/guestSlice.ts | 29 ++++++++++++ client/src/slices/authSlice.ts | 31 ------------- client/src/store.ts | 14 ++++-- client/src/vite-env.d.ts | 8 ++++ 16 files changed, 395 insertions(+), 114 deletions(-) create mode 100644 client/src/components/Admin.tsx create mode 100644 client/src/components/AdminLogin.tsx create mode 100644 client/src/components/Dashboard.tsx create mode 100644 client/src/slices/api/adminSlice.ts create mode 100644 client/src/slices/api/guestSlice.ts delete mode 100644 client/src/slices/apiSlice.ts create mode 100644 client/src/slices/auth/adminSlice.ts create mode 100644 client/src/slices/auth/guestSlice.ts delete mode 100644 client/src/slices/authSlice.ts (limited to 'client/src') 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 ? ( + <> + + + + + ) : ( + + ); +} + +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({ + defaultValues: { + username: '', + password: '', + }, + }); + + const onSubmit = async (data: AdminLoginRequest) => { + try { + dispatch(setAdmin(await login(data).unwrap())); + navigate('/dashboard'); + } catch (e) { + console.log(e); + } + }; + + return ( +
+ + Admin Login + + + + +
+ ); +} + +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[]>( + () => [ + { + 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 ; +} + +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({ + } = useForm({ 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: , }, + { + path: 'admin/login', + element: , + }, ], }, { @@ -44,6 +51,15 @@ const router = createBrowserRouter([ }, ], }, + { + element: , + children: [ + { + path: 'dashboard', + element: , + }, + ], + }, ]); 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; +} + +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({ + query: (credentials) => ({ + url: 'admin/login', + method: 'POST', + body: credentials, + }), + }), + }), +}); + +export const { useLoginAdminMutation } = adminSlice; diff --git a/client/src/slices/api/guestSlice.ts b/client/src/slices/api/guestSlice.ts new file mode 100644 index 0000000..38deb9a --- /dev/null +++ b/client/src/slices/api/guestSlice.ts @@ -0,0 +1,60 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; +import type { RootState } from '../../store'; + +export interface GuestLoginRequest { + firstName: string; + lastName: string; +} + +export interface GuestLoginResponse { + guest: Guest; + token: string; +} + +export interface Guest { + id: number; + firstName: string; + lastName: string; + attendance: string; + email: string; + message: string; + partySize: number; + partyList: Array; +} + +export interface PartyGuest { + firstName: string; + lastName: string; +} + +export const guestSlice = createApi({ + reducerPath: 'guestApi', + baseQuery: fetchBaseQuery({ + baseUrl: import.meta.env.VITE_BASE_URL + 'guest/', + prepareHeaders: (headers, { getState }) => { + const token = (getState() as RootState).guest.token; + if (token) { + headers.set('authorization', `${token}`); + } + return headers; + }, + }), + endpoints: (builder) => ({ + loginGuest: builder.mutation({ + query: (credentials) => ({ + url: 'login', + method: 'POST', + body: credentials, + }), + }), + updateGuest: builder.mutation({ + query: (guest) => ({ + url: `${guest?.id}`, + method: 'PUT', + body: guest, + }), + }), + }), +}); + +export const { useLoginGuestMutation, useUpdateGuestMutation } = guestSlice; diff --git a/client/src/slices/apiSlice.ts b/client/src/slices/apiSlice.ts deleted file mode 100644 index 90cdc48..0000000 --- a/client/src/slices/apiSlice.ts +++ /dev/null @@ -1,67 +0,0 @@ -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; -} - -export interface PartyGuest { - firstName: string; - lastName: string; -} - -export const apiSlice = createApi({ - reducerPath: 'api', - baseQuery: fetchBaseQuery({ - baseUrl: 'http://192.168.1.41:8080/', - prepareHeaders: (headers, { getState }) => { - const token = (getState() as RootState).auth.token; - if (token) { - headers.set('authorization', `${token}`); - } - return headers; - }, - }), - tagTypes: ['Guest'], - endpoints: (builder) => ({ - getGuests: builder.query({ - query: () => 'guest', - providesTags: ['Guest'], - }), - updateGuest: builder.mutation({ - query: (guest) => ({ - url: `guest/${guest?.id}`, - method: 'PUT', - body: guest, - providesTags: ['Guest'], - }), - }), - login: builder.mutation({ - query: (credentials) => ({ - url: 'guest/login', - method: 'POST', - body: credentials, - }), - }), - }), -}); - -export const { useGetGuestsQuery, useUpdateGuestMutation, useLoginMutation } = - apiSlice; 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 + ) => { + 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 + ) => { + 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 @@ /// + +interface ImportMetaEnv { + readonly VITE_BASE_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} -- cgit v1.2.3