Repository: EliteSpace-DSD/EliteSpace-Frontend
Pull request #31: ES-107 Connect guest access UI
Author: login: MsSanli
Created: 2025-03-21T03:09:26Z | Merged: 2025-03-22T19:20:09Z

- Connected the form to /accessCodes/generate endpoint 
- Implemented Redux state management for sharing data between components
- Used Redux Toolkit for state management [accessCodeSlice, added reducer to store.ts]
- Tested form submission and specific tenantId that exists in our Supabase table (no longer hard coded 123456"

Flow Diagram (can be improved if suggestions)
![Data Flow Diagram Whiteboard in Dark Yellow Light Yellow Black Monochromatic Style](https://github.com/user-attachments/assets/9cb03935-3318-44b9-a90d-edfe422ba5ed)

Supabase table and Screenshot of Successful Generated Key:
![Screenshot 2025-03-22 002825](https://github.com/user-attachments/assets/bf2499fc-7088-44d1-aa3a-4a2722ce5bf3)
![Screenshot 2025-03-22 002805](https://github.com/user-attachments/assets/442f0618-26a5-452f-b6f7-bc92c089bbb3)

 


Diff:
diff --git a/src/features/GuestAccess/GuestAccess.tsx b/src/features/GuestAccess/GuestAccess.tsx
index b99ad9a..c72758f 100644
--- a/src/features/GuestAccess/GuestAccess.tsx
+++ b/src/features/GuestAccess/GuestAccess.tsx
@@ -11,10 +11,58 @@ import {
   Paper,
   Box,
 } from '@mui/material';
-import { Link } from 'react-router';
+import { useNavigate } from 'react-router';
 import VpnKeyIcon from '@mui/icons-material/VpnKey';
+import { useState } from 'react';
+import { useDispatch } from 'react-redux';
+import { setGuestName, setTimeLimit, setCurrentCode } from '../../stores/accessCodesSlice';
+
+const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
 
 const GuestAccess = () => {
+  const [nameInput, setNameInput] = useState('');
+  const [timeLimitInput, setTimeLimitInput] = useState('60');
+
+  const dispatch = useDispatch();
+  const navigate = useNavigate();
+
+  const tenantId = '0ac4c0c4-4b59-44a0-8060-58f4dac3c4f4';
+
+  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
+    e.preventDefault();
+
+    // Save to Redux
+    dispatch(setGuestName(nameInput));
+    dispatch(setTimeLimit(timeLimitInput));
+
+    try {
+      const response = await fetch(`${API_BASE_URL}/accessCodes/generate`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          guestName: nameInput,
+          timeLimit: timeLimitInput,
+          tenantId,
+        }),
+        credentials: 'include',
+      });
+
+      if (!response.ok) {
+        throw new Error('Failed to generate access code');
+      }
+
+      const data = await response.json();
+      const code = data.accessCode.unhashedCode;
+
+      dispatch(setCurrentCode(code));
+      navigate('/guestaccess/key');
+    } catch (error) {
+      console.error('Error generating code:', error);
+    }
+  };
+
   return (
     <Container
       sx={{
@@ -77,66 +125,63 @@ const GuestAccess = () => {
             Generate a temporary access key for your visitors
           </Typography>
 
-          <InputLabel
-            sx={{
-              width: '100%',
-              marginBottom: '10px',
-            }}
-          >
-            Guest Name
-          </InputLabel>
-          <TextField
-            sx={{
-              width: '100%',
-              marginBottom: '20px',
-            }}
-          />
+          <form onSubmit={handleSubmit} style={{ width: '100%' }}>
+            <InputLabel
+              sx={{
+                width: '100%',
+                marginBottom: '10px',
+              }}
+            >
+              Guest Name
+            </InputLabel>
+            <TextField
+              sx={{
+                width: '100%',
+                marginBottom: '20px',
+              }}
+              value={nameInput}
+              onChange={(e) => setNameInput(e.target.value)}
+              required
+            />
+
+            <InputLabel
+              sx={{
+                width: '100%',
+                marginBottom: '10px',
+              }}
+            >
+              Time Limit
+            </InputLabel>
+            <FormControl
+              sx={{
+                width: '100%',
+                marginBottom: '20px',
+              }}
+            >
+              <Select value={timeLimitInput} onChange={(e) => setTimeLimitInput(e.target.value)}>
+                <MenuItem value='60'>60 minutes</MenuItem>
+                <MenuItem value='120'>2 hours</MenuItem>
+                <MenuItem value='240'>4 hours</MenuItem>
+                <MenuItem value='480'>8 hours</MenuItem>
+                <MenuItem value='1440'>24 hours (1 day)</MenuItem>
+                <MenuItem value='2880'>48 hours (2 days)</MenuItem>
+                <MenuItem value='10080'>1 week</MenuItem>
+              </Select>
+            </FormControl>
 
-          <InputLabel
-            sx={{
-              width: '100%',
-              marginBottom: '10px',
-            }}
-          >
-            Time Limit
-          </InputLabel>
-          <FormControl
-            sx={{
-              width: '100%',
-              marginBottom: '20px',
-            }}
-          >
-            <Select defaultValue='1'>
-              <MenuItem value='1'>60 minutes</MenuItem>
-              <MenuItem value='2'>2 hours</MenuItem>
-              <MenuItem value='4'>4 hours</MenuItem>
-              <MenuItem value='8'>8 hours</MenuItem>
-              <MenuItem value='24'>24 hours (1 day)</MenuItem>
-              <MenuItem value='48'>48 hours (2 days)</MenuItem>
-              <MenuItem value='168'>1 week</MenuItem>
-            </Select>
-          </FormControl>
-          <Link
-            to='/guestaccess/key'
-            style={{
-              textDecoration: 'none',
-              width: '100%',
-              margin: '0 auto',
-              display: 'flex',
-              justifyContent: 'center',
-            }}
-          >
             <Button
+              type='submit'
               variant='contained'
               sx={{
                 width: '50%',
                 height: '50px',
                 margin: '0 auto',
+                display: 'block',
               }}
             >
               Generate Key
             </Button>
-          </Link>
+          </form>
         </Stack>
       </Paper>
     </Container>
diff --git a/src/features/GuestAccess/GuestKey.tsx b/src/features/GuestAccess/GuestKey.tsx
index ac71810..031b1d4 100644
--- a/src/features/GuestAccess/GuestKey.tsx
+++ b/src/features/GuestAccess/GuestKey.tsx
@@ -1,8 +1,34 @@
 import { Container, Stack, Button, Typography, Paper, Box } from '@mui/material';
-import { Link } from 'react-router';
+import { Link, Navigate } from 'react-router';
 import ArrowBackIcon from '@mui/icons-material/ArrowBack';
+import { useSelector } from 'react-redux';
+
+interface RootState {
+  accessCodes: {
+    currentCode: string | null;
+    guestName: string;
+    timeLimit: string;
+  };
+}
 
 const GuestAccessKey = () => {
+  const { currentCode, guestName, timeLimit } = useSelector(
+    (state: RootState) => state.accessCodes,
+  );
+  if (!currentCode) {
+    return <Navigate to='/guestaccess' />;
+  }
+
+  const convertTimeLimit = (minutes: string): string => {
+    if (minutes === '60') return '60 minutes';
+    if (minutes === '120') return '2 hours';
+    if (minutes === '240') return '4 hours';
+    if (minutes === '480') return '8 hours';
+    if (minutes === '1440') return '24 hours (1 day)';
+    if (minutes === '2880') return '48 hours (2 days)';
+    if (minutes === '10080') return '1 week';
+    return `${minutes} minutes`;
+  };
   return (
     <Container
       sx={{
@@ -70,7 +96,7 @@ const GuestAccessKey = () => {
                 color: 'primary.main',
               }}
             >
-              123456
+              {currentCode}
             </Typography>
           </Box>
 
@@ -80,11 +106,11 @@ const GuestAccessKey = () => {
               maxWidth: '80%',
             }}
           >
-            One time code for <strong>Mars Bars</strong>.
+            One time code for <strong>{guestName}</strong>.
             <br />
             Instructions: Enter in the Keypad.
             <br />
-            Expires after 60 minutes.
+            Expires after {convertTimeLimit(timeLimit)}.
           </Typography>
 
           <Link
diff --git a/src/stores/accessCodesSlice.ts b/src/stores/accessCodesSlice.ts
new file mode 100644
index 0000000..8876bfb
--- /dev/null
+++ b/src/stores/accessCodesSlice.ts
@@ -0,0 +1,33 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+const initialState = {
+  currentCode: null,
+  guestName: '',
+  timeLimit: '',
+};
+
+const accessCodesSlice = createSlice({
+  name: 'accessCodes',
+  initialState,
+  reducers: {
+    setGuestName: (state, action) => {
+      state.guestName = action.payload;
+    },
+    setTimeLimit: (state, action) => {
+      state.timeLimit = action.payload;
+    },
+    clearAccessCode: (state) => {
+      state.currentCode = null;
+      state.guestName = '';
+      state.timeLimit = '';
+    },
+    setCurrentCode: (state, action) => {
+      state.currentCode = action.payload;
+    },
+  },
+});
+
+export const { setGuestName, setTimeLimit, clearAccessCode, setCurrentCode } =
+  accessCodesSlice.actions;
+export const accessCodesReducer = accessCodesSlice.reducer;
+export default accessCodesSlice.reducer;
diff --git a/src/stores/store.ts b/src/stores/store.ts
index 6a2b0d9..1aaa0e2 100644
--- a/src/stores/store.ts
+++ b/src/stores/store.ts
@@ -6,6 +6,7 @@ import { loginApi } from '../features/Login/api/loginApi';
 import { passwordResetApi } from '../features/PasswordReset/api/passwordresetApi';
 import { resetPasswordApi } from '../features/ResetPassword/api/Resetpasswordapi';
 import { leaseApi } from '../features/DigitalLease/api/leaseApi';
+import { accessCodesReducer } from './accessCodesSlice';
 
 const store = configureStore({
   reducer: {
@@ -16,6 +17,7 @@ const store = configureStore({
     [passwordResetApi.reducerPath]: passwordResetApi.reducer,
     [resetPasswordApi.reducerPath]: resetPasswordApi.reducer,
     [leaseApi.reducerPath]: leaseApi.reducer,
+    accessCodes: accessCodesReducer,
   },
   middleware: (getDefaultMiddleware) =>
     getDefaultMiddleware().concat(
@@ -29,4 +31,4 @@ const store = configureStore({
 
 export type RootState = ReturnType<typeof store.getState>;
 
-export default store;
\ No newline at end of file
+export default store;


Reviews:
user: login: MelisaIm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T05:24:25Z
user: login: jasminepvo
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T17:00:50Z
user: login: jasminepvo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T17:26:01Z
user: login: MsSanli
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T18:20:26Z
user: login: tk1ng
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-22T19:07:10Z
user: login: jasminepvo
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T19:19:03Z

Review comments:
diff_hunk: @@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
path: src/features/ResetPassword/ResetPassword.tsx
user: login: MelisaIm
body: Do we need this here? :) 
created_at: 2025-03-22T05:24:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,10 +11,56 @@ import {
   Paper,
   Box,
 } from '@mui/material';
-import { Link } from 'react-router';
+import { useNavigate } from 'react-router';
 import VpnKeyIcon from '@mui/icons-material/VpnKey';
+import { useState } from 'react';
+import { useDispatch } from 'react-redux';
+import { setGuestName, setTimeLimit, setCurrentCode } from '../../stores/accessCodesSlice';
 
 const GuestAccess = () => {
+  const [nameInput, setNameInput] = useState('');
+  const [timeLimitInput, setTimeLimitInput] = useState('60');
+
+  const dispatch = useDispatch();
+  const navigate = useNavigate();
+
+  const tenantId = '0ac4c0c4-4b59-44a0-8060-58f4dac3c4f4';
path: src/features/GuestAccess/GuestAccess.tsx
user: login: jasminepvo
body: question: should this be here? 
created_at: 2025-03-22T16:57:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,10 +11,56 @@ import {
   Paper,
   Box,
 } from '@mui/material';
-import { Link } from 'react-router';
+import { useNavigate } from 'react-router';
 import VpnKeyIcon from '@mui/icons-material/VpnKey';
+import { useState } from 'react';
+import { useDispatch } from 'react-redux';
+import { setGuestName, setTimeLimit, setCurrentCode } from '../../stores/accessCodesSlice';
 
 const GuestAccess = () => {
+  const [nameInput, setNameInput] = useState('');
+  const [timeLimitInput, setTimeLimitInput] = useState('60');
+
+  const dispatch = useDispatch();
+  const navigate = useNavigate();
+
+  const tenantId = '0ac4c0c4-4b59-44a0-8060-58f4dac3c4f4';
+
+  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
+    e.preventDefault();
+
+    // Save to Redux
+    dispatch(setGuestName(nameInput));
+    dispatch(setTimeLimit(timeLimitInput));
+
+    try {
+      const response = await fetch('http://localhost:3000/accessCodes/generate', {
path: src/features/GuestAccess/GuestAccess.tsx
user: login: jasminepvo
body: issue: update url
created_at: 2025-03-22T17:26:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,10 +11,56 @@ import {
   Paper,
   Box,
 } from '@mui/material';
-import { Link } from 'react-router';
+import { useNavigate } from 'react-router';
 import VpnKeyIcon from '@mui/icons-material/VpnKey';
+import { useState } from 'react';
+import { useDispatch } from 'react-redux';
+import { setGuestName, setTimeLimit, setCurrentCode } from '../../stores/accessCodesSlice';
 
 const GuestAccess = () => {
+  const [nameInput, setNameInput] = useState('');
+  const [timeLimitInput, setTimeLimitInput] = useState('60');
+
+  const dispatch = useDispatch();
+  const navigate = useNavigate();
+
+  const tenantId = '0ac4c0c4-4b59-44a0-8060-58f4dac3c4f4';
path: src/features/GuestAccess/GuestAccess.tsx
user: login: MsSanli
body: no longer needed. Will also remove tenantId from the fetch body
created_at: 2025-03-22T18:20:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,10 +11,56 @@ import {
   Paper,
   Box,
 } from '@mui/material';
-import { Link } from 'react-router';
+import { useNavigate } from 'react-router';
 import VpnKeyIcon from '@mui/icons-material/VpnKey';
+import { useState } from 'react';
+import { useDispatch } from 'react-redux';
+import { setGuestName, setTimeLimit, setCurrentCode } from '../../stores/accessCodesSlice';
 
 const GuestAccess = () => {
+  const [nameInput, setNameInput] = useState('');
+  const [timeLimitInput, setTimeLimitInput] = useState('60');
+
+  const dispatch = useDispatch();
+  const navigate = useNavigate();
+
+  const tenantId = '0ac4c0c4-4b59-44a0-8060-58f4dac3c4f4';
+
+  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
+    e.preventDefault();
+
+    // Save to Redux
+    dispatch(setGuestName(nameInput));
+    dispatch(setTimeLimit(timeLimitInput));
+
+    try {
+      const response = await fetch('http://localhost:3000/accessCodes/generate', {
path: src/features/GuestAccess/GuestAccess.tsx
user: login: tk1ng
body: pushed a commit to update the url
created_at: 2025-03-22T19:07:10Z
author_association: COLLABORATOR

PR comments:
user: login: netlify[bot]
created_at: 2025-03-21T03:09:41Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *elitespace* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | d30ab35d346166215eea35ad8ce7507ff18817c9 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/elitespace/deploys/67df0d10d7321700084cc86b |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-31--elitespace.netlify.app](https://deploy-preview-31--elitespace.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTMxLS1lbGl0ZXNwYWNlLm5ldGxpZnkuYXBwIn0.tK9G7i21qQmeCy_GlZgyNZI0tqx6Y8UKb7_DJ4wap9k)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [elitespace Preview](https://deploy-preview-31--elitespace.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/elitespace/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: MelisaIm
created_at: 2025-03-22T04:28:19Z
body: @MsSanli how come it says "do not merge" in the description? If that's the case, put it in the title so it's easier to see :) 
author_association: CONTRIBUTOR
user: login: MsSanli
created_at: 2025-03-22T04:37:49Z
body: > @MsSanli how come it says "do not merge" in the description? If that's the case, put it in the title so it's easier to see :)

Oh thanks for the tip! I've actually just clicked on "ready for review" so hopefully not a draft anymore
author_association: CONTRIBUTOR