Skip to content
Open
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
11 changes: 10 additions & 1 deletion frontend/src/components/Modal/RiderModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const RiderModal = ({

const closeModal = () => setIsOpen(false);

// Accumulates form data into state before calling the next step (supports multi-page forms)
const saveDataThen = (next: () => void) => (data: ObjectType) => {
setFormData((prev) => ({ ...prev, ...data }));
next();
Expand All @@ -41,6 +42,8 @@ const RiderModal = ({
closeModal();
};

// Modal closes first (submitData sets isSubmitted), then this effect fires the API call so the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work with the documentation!!!

// user isn't blocked waiting on the network before modal dismisses
useEffect(() => {
if (isSubmitted) {
const method = existingRider ? axios.put : axios.post;
Expand Down Expand Up @@ -71,7 +74,13 @@ const RiderModal = ({
return (
<>
<Modal
title={!existingRider ? 'Add a Student' : 'Edit a Student'}
title={
!existingRider
? 'Add a Student'
: isRiderWeb
? 'Edit Profile'
: 'Edit a Student'
}
isOpen={isOpen}
currentPage={0}
onClose={closeModal}
Expand Down
143 changes: 77 additions & 66 deletions frontend/src/components/Modal/RiderModalInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({
const [showCustomInput, setShowCustomInput] = useState(false);
const [customNeed, setCustomNeed] = useState('');

// Returns a validator function so both name fields share the same logic with different labels
const makeNameValidator =
(fieldLabel: 'First name' | 'Last name') => (value: string) => {
const trimmed = value.trim();
Expand Down Expand Up @@ -124,6 +125,8 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({
}),
};

// "Add Custom Need" is a sentinel option
// intercept it to show the free-text input instead of adding it as a real need
const handleNeedsChange = (
selectedOptions: readonly NeedOption[] | null,
{ action }: any
Expand All @@ -145,6 +148,7 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({
if (customNeed.trim()) {
const currentNeeds = getValues('needs') || [];
const newNeed: NeedOption = {
// Normalize to UPPER_SNAKE_CASE to match the Accessibility enum format on the backend
value: customNeed.toUpperCase().replace(/\s+/g, '_'),
label: customNeed.trim(),
};
Expand Down Expand Up @@ -173,6 +177,7 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({
const accessibility = needs.map((option) => option.value.toString());
const normalizedPhoneNumber = normalizePhoneNumber(phoneNumber);

// Derive active status client-side so backend doesn't need to recalculate it on every edit
const today = new Date().toISOString().slice(0, 10);
const active = joinDate <= today && today <= endDate;

Expand All @@ -192,12 +197,13 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({
};

const cancel = () => {
setFormData({});
setFormData({}); // Clear stale data so a reopened modal doesn't submit a partial previous entry
setIsOpen(false);
};

const localUserType = localStorage.getItem('userType');
const isEditing = rider !== undefined;
// Riders editing their own profile get a restricted form — no NetID or duration fields
const isStudentEditing = isEditing && localUserType === 'Rider';

const needsOptions: NeedOption[] = [
Expand Down Expand Up @@ -251,23 +257,24 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({
)}
</div>

<div className={cn(styles.gridR1, styles.gridCSmall3)}>
<Label className={styles.label} htmlFor="netid">
NetID:{' '}
</Label>
<Input
id="netid"
{...register('netid', {
required: true,
pattern: /^[a-zA-Z]+[0-9]+$/,
})}
type="text"
disabled={isStudentEditing}
className={styles.firstRow}
aria-required="true"
/>
{errors.netid && <p className={styles.error}>Invalid NetID</p>}
</div>
{!isStudentEditing && (
<div className={cn(styles.gridR1, styles.gridCSmall3)}>
<Label className={styles.label} htmlFor="netid">
NetID:{' '}
</Label>
<Input
id="netid"
{...register('netid', {
required: true,
pattern: /^[a-zA-Z]+[0-9]+$/,
})}
type="text"
className={styles.firstRow}
aria-required="true"
/>
{errors.netid && <p className={styles.error}>Invalid NetID</p>}
</div>
)}

<div className={cn(styles.gridR2, styles.gridCBig1)}>
<Label className={styles.label} htmlFor="phoneNumber">
Expand Down Expand Up @@ -354,55 +361,55 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({
</div>
</div>

<div className={cn(styles.gridR3, styles.gridCAll)}>
<p>Duration</p>
<div className={styles.lastRow}>
<div>
<Label className={styles.label} htmlFor="joinDate">
Join Date:{' '}
</Label>
<Input
id="joinDate"
{...register('joinDate', { required: true })}
type="date"
aria-required="true"
disabled={isStudentEditing}
className={styles.riderDate}
/>
{errors.joinDate && (
<p className={styles.error}>Please enter a join date</p>
)}
</div>
<div className={styles.to}>
<p>→</p>
</div>
<div>
<Label className={styles.label} htmlFor="endDate">
End Date:{' '}
</Label>
<Input
id="endDate"
{...register('endDate', {
required: true,
validate: (endDate) => {
const joinDate = getValues('joinDate');
return joinDate < endDate;
},
})}
type="date"
aria-required="true"
disabled={isStudentEditing}
className={styles.riderDate}
/>
{errors.endDate?.type === 'required' && (
<p className={styles.error}>Please enter an end date</p>
)}
{errors.endDate?.type === 'validate' && (
<p className={styles.error}>Invalid end time</p>
)}
{!isStudentEditing && (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modal UI interacts well and is responsive, nice stuff!

<div className={cn(styles.gridR3, styles.gridCAll)}>
<p>Duration</p>
<div className={styles.lastRow}>
<div>
<Label className={styles.label} htmlFor="joinDate">
Join Date:{' '}
</Label>
<Input
id="joinDate"
{...register('joinDate', { required: true })}
type="date"
aria-required="true"
className={styles.riderDate}
/>
{errors.joinDate && (
<p className={styles.error}>Please enter a join date</p>
)}
</div>
<div className={styles.to}>
<p>→</p>
</div>
<div>
<Label className={styles.label} htmlFor="endDate">
End Date:{' '}
</Label>
<Input
id="endDate"
{...register('endDate', {
required: true,
validate: (endDate) => {
const joinDate = getValues('joinDate');
return joinDate < endDate;
},
})}
type="date"
aria-required="true"
className={styles.riderDate}
/>
{errors.endDate?.type === 'required' && (
<p className={styles.error}>Please enter an end date</p>
)}
{errors.endDate?.type === 'validate' && (
<p className={styles.error}>Invalid end time</p>
)}
</div>
</div>
</div>
</div>
)}
</div>

<div className={styles.buttonContainer}>
Expand All @@ -415,7 +422,11 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({
Cancel
</Button>
<Button type="submit" className={styles.submit}>
{isEditing ? 'Edit a Student' : 'Add a Student'}
{isStudentEditing
? 'Save Changes'
: isEditing
? 'Edit a Student'
: 'Add a Student'}
</Button>
</div>
</form>
Expand Down
16 changes: 14 additions & 2 deletions frontend/src/pages/Rider/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { useContext, useEffect } from 'react';
import { useContext, useEffect, useState } from 'react';
import UserDetail, {
UserContactInfo,
} from '../../components/UserDetail/legacy/UserDetail';
import { phone, mail } from '../../icons/userInfo/index';
import AuthContext from '../../context/auth';
import pageStyles from '../Admin/page.module.css';
import { RiderType } from '@carriage-web/shared/types/rider';
import RiderModal from '../../components/Modal/RiderModal';
import { Button } from '../../components/FormElements/FormElements';

const Settings = () => {
const { user } = useContext(AuthContext);
const netId = user?.email.split('@')[0] || '';
const [isEditOpen, setIsEditOpen] = useState(false);

useEffect(() => {
document.title = 'Settings - Carriage';
Expand All @@ -19,7 +22,9 @@ const Settings = () => {
<main id="main">
<div className={pageStyles.pageTitle}>
<h1 className={pageStyles.header}>Settings</h1>
<div className={pageStyles.rightSection}></div>
<div className={pageStyles.rightSection}>
<Button onClick={() => setIsEditOpen(true)}>Edit Profile</Button>
</div>
</div>
<UserDetail
firstName={user?.firstName || ''}
Expand All @@ -36,6 +41,13 @@ const Settings = () => {
/>
<UserContactInfo icon={mail} alt="Email" text={user?.email || ''} />
</UserDetail>
{/* isRiderWeb triggers student-edit mode: hides NetID/duration fields and refreshes the auth user on save */}
<RiderModal
existingRider={user as RiderType}
isRiderWeb={true}
isOpen={isEditOpen}
setIsOpen={setIsEditOpen}
/>
</main>
);
};
Expand Down
Loading