Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { ProtectedRoute } from '@/components';
import { AuthProvider } from '@/contexts';
import {
Survey,
SurveyDetails,
SurveyEntryDashboard,
QrPage,
ApplyReferral,
LandingPage,
Login,
NewUser,
Profile,
ApplyReferral,
QrPage,
Signup,
StaffDashboard,
NewUser
Survey,
SurveyDetails,
SurveyEntryDashboard
} from '@/pages';
import { muiTheme } from '@/theme/muiTheme';
import { isTokenValid } from '@/utils/authTokenHandler';
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider } from '@mui/material/styles';
import {
Expand All @@ -20,8 +23,6 @@ import {
BrowserRouter as Router,
Routes
} from 'react-router-dom';
import { ProtectedRoute } from '@/components';
import { muiTheme } from '@/theme/muiTheme';

function App() {
return (
Expand All @@ -32,7 +33,14 @@ function App() {
<Routes>
<Route
path="/"
element={<Navigate replace to="/login" />}
element={
<Navigate
replace
to={
isTokenValid() ? '/dashboard' : '/login'
}
/>
}
/>
<Route path="/login" element={<Login />} />
<Route
Expand Down
34 changes: 34 additions & 0 deletions client/src/components/forms/FormInput.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useState } from 'react';

import { TextField, TextFieldProps } from '@mui/material';

import { PermissionTooltip } from './PermissionTooltip';
Expand All @@ -10,12 +12,44 @@ interface FormInputProps extends Omit<TextFieldProps, 'variant'> {
export const FormInput = ({
canEdit = true,
showTooltip = false,
required,
value,
error,
helperText,
onBlur,
...props
}: FormInputProps) => {
const [touched, setTouched] = useState(false);

const handleBlur = (
e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setTouched(true);
onBlur?.(e);
};

// Show error if required and value is empty, but only after field has been touched
const hasError =
error ||
(touched &&
required &&
(!value || (typeof value === 'string' && !value.trim())));

const input = (
<TextField
{...props}
required={required}
value={value}
error={hasError}
onBlur={handleBlur}
helperText={
hasError &&
touched &&
required &&
(!value || (typeof value === 'string' && !value.trim()))
? 'This field is required'
: helperText
}
disabled={!canEdit}
variant="outlined"
fullWidth
Expand Down
31 changes: 29 additions & 2 deletions client/src/components/forms/FormSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useState } from 'react';

import {
FormControl,
FormHelperText,
Expand Down Expand Up @@ -25,19 +27,39 @@ export const FormSelect = ({
showTooltip = false,
helperText,
error,
required,
value,
onBlur,
...props
}: FormSelectProps) => {
const labelId = `${props.name ?? 'select'}-label`;
const [touched, setTouched] = useState(false);

const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
setTouched(true);
onBlur?.(e);
};

// Show error if required and value is empty, but only after field has been touched
const hasError = error || (touched && required && !value);

const selectControl = (
<FormControl fullWidth error={error} disabled={!canEdit}>
<FormControl
fullWidth
error={hasError}
required={required}
disabled={!canEdit}
>
<InputLabel id={labelId}>{label}</InputLabel>
<Select
{...props}
labelId={labelId}
label={label}
variant="outlined"
disabled={!canEdit}
required={required}
value={value}
onBlur={handleBlur}
sx={{
backgroundColor: !canEdit ? '#f5f5f5' : 'white'
}}
Expand All @@ -48,7 +70,12 @@ export const FormSelect = ({
</MenuItem>
))}
</Select>
{helperText && <FormHelperText>{helperText}</FormHelperText>}
{hasError && touched && required && !value && (
<FormHelperText>This field is required</FormHelperText>
)}
{helperText && !hasError && (
<FormHelperText>{helperText}</FormHelperText>
)}
</FormControl>
);

Expand Down
24 changes: 24 additions & 0 deletions client/src/components/forms/PhoneInput.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useState } from 'react';

import { TextField, TextFieldProps } from '@mui/material';

import { PermissionTooltip } from './PermissionTooltip';
Expand All @@ -11,9 +13,19 @@ export const PhoneInput = ({
canEdit = true,
showTooltip = false,
value,
helperText,
onChange,
onBlur,
...props
}: PhoneInputProps) => {
const [touched, setTouched] = useState(false);

const handleBlur = (
e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setTouched(true);
onBlur?.(e);
};

const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!onChange) return;
Expand All @@ -40,12 +52,24 @@ export const PhoneInput = ({
onChange(syntheticEvent as React.ChangeEvent<HTMLInputElement>);
};

const hasError =
touched && (!value || (typeof value === 'string' && !value.trim()));

const input = (
<TextField
{...props}
type="tel"
value={value ?? ''}
error={hasError}
helperText={
hasError &&
touched &&
(!value || (typeof value === 'string' && !value.trim()))
? 'This field is required'
: helperText
}
onChange={handlePhoneChange}
onBlur={handleBlur}
disabled={!canEdit}
variant="outlined"
fullWidth
Expand Down
36 changes: 23 additions & 13 deletions client/src/pages/Login/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { useEffect, useState } from 'react';

import { useSurveyStore } from '@/stores';
import { Box, Button, Paper, TextField, Typography } from '@mui/material';
import { deleteAuthToken, isTokenValid } from '@/utils/authTokenHandler';
import {
Alert,
Box,
Button,
Paper,
TextField,
Typography
} from '@mui/material';
import { useNavigate } from 'react-router-dom';

import { useAuth } from '@/hooks/useAuth';
Expand All @@ -10,6 +18,16 @@ export default function Login() {
const { handleLogin } = useAuth();
const navigate = useNavigate();
const { clearSession } = useSurveyStore();

// Redirect if user already has a valid token
useEffect(() => {
if (isTokenValid()) {
navigate('/dashboard');
} else {
// Token is invalid or expired, clean it up
deleteAuthToken();
}
}, [navigate]);
// const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [otp, setOtp] = useState('');
Expand Down Expand Up @@ -267,22 +285,14 @@ export default function Login() {
)}

{errorMessage && (
<Typography
variant="body2"
color="error.main"
sx={{ mt: 1 }}
>
<Alert severity="error" sx={{ mt: 2 }}>
{errorMessage}
</Typography>
</Alert>
)}
{successMessage && (
<Typography
variant="body2"
color="success.main"
sx={{ mt: 1, fontWeight: 500 }}
>
<Alert severity="success" sx={{ mt: 2 }}>
{successMessage}
</Typography>
</Alert>
)}
</Paper>
</Box>
Expand Down
Loading
Loading