-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Integration of React-Select into Add Student, proper styling and cust… #554
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import React, { useState } from 'react'; | ||
import { useForm, SubmitHandler } from 'react-hook-form'; | ||
import { useForm, SubmitHandler, Controller } from 'react-hook-form'; | ||
import Select, { StylesConfig } from 'react-select'; | ||
import cn from 'classnames'; | ||
import { Button, Input, Label } from '../FormElements/FormElements'; | ||
import styles from './ridermodal.module.css'; | ||
|
@@ -12,11 +13,16 @@ type ModalFormProps = { | |
rider?: Rider; | ||
}; | ||
|
||
type NeedOption = { | ||
value: Accessibility | string; | ||
label: string; | ||
}; | ||
|
||
type FormData = { | ||
name: string; | ||
netid: string; | ||
phoneNumber: string; | ||
needs: Accessibility[]; | ||
needs: NeedOption[]; | ||
address: string; | ||
joinDate: string; | ||
endDate: string; | ||
|
@@ -29,23 +35,99 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({ | |
setFormData, | ||
rider, | ||
}) => { | ||
const [showCustomInput, setShowCustomInput] = useState(false); | ||
const [customNeed, setCustomNeed] = useState(''); | ||
|
||
const { | ||
register, | ||
control, | ||
formState: { errors }, | ||
handleSubmit, | ||
getValues, | ||
setValue, | ||
} = useForm<FormData>({ | ||
defaultValues: { | ||
name: (rider?.firstName ?? '') + (rider?.lastName ?? ''), | ||
netid: rider?.email.split('@')[0] ?? '', | ||
phoneNumber: rider?.phoneNumber ?? '', | ||
needs: [], | ||
needs: | ||
rider?.accessibility?.map((need) => ({ | ||
value: need as Accessibility, | ||
label: need, | ||
})) ?? [], | ||
address: rider?.address ?? '', | ||
joinDate: rider?.joinDate ?? '', | ||
endDate: rider?.endDate ?? '', | ||
}, | ||
}); | ||
|
||
const customStyles: StylesConfig<NeedOption, true> = { | ||
option: (baseStyles, { data }) => ({ | ||
...baseStyles, | ||
...(data.value === 'OTHER' && { | ||
color: '#0066cc', | ||
fontStyle: 'italic', | ||
backgroundColor: '#f8f9fa', | ||
borderTop: '1px solid #e9ecef', | ||
marginTop: '4px', | ||
paddingTop: '8px', | ||
cursor: 'pointer', | ||
display: 'flex', | ||
alignItems: 'center', | ||
'&:before': { | ||
content: '"+"', | ||
marginRight: '8px', | ||
fontSize: '14px', | ||
fontWeight: 'bold', | ||
}, | ||
'&:hover': { | ||
backgroundColor: '#e9ecef', | ||
color: '#004c99', | ||
}, | ||
}), | ||
}), | ||
menu: (baseStyles) => ({ | ||
...baseStyles, | ||
padding: '4px 0', | ||
}), | ||
}; | ||
|
||
const handleNeedsChange = ( | ||
selectedOptions: readonly NeedOption[] | null, | ||
{ action }: any | ||
) => { | ||
if (selectedOptions?.some((option) => option.value === 'OTHER')) { | ||
const filteredOptions = [ | ||
...selectedOptions.filter((opt) => opt.value !== 'OTHER'), | ||
]; | ||
setValue('needs', filteredOptions); | ||
setShowCustomInput(true); | ||
setCustomNeed(''); | ||
} else { | ||
setValue('needs', selectedOptions ? [...selectedOptions] : []); | ||
setShowCustomInput(false); | ||
} | ||
}; | ||
|
||
const handleAddCustomNeed = () => { | ||
if (customNeed.trim()) { | ||
const currentNeeds = getValues('needs') || []; | ||
const newNeed: NeedOption = { | ||
value: customNeed.toUpperCase().replace(/\s+/g, '_'), | ||
label: customNeed.trim(), | ||
}; | ||
|
||
setValue('needs', [...currentNeeds, newNeed]); | ||
setCustomNeed(''); | ||
setShowCustomInput(false); | ||
} | ||
}; | ||
|
||
const handleCancelCustomNeed = () => { | ||
setCustomNeed(''); | ||
setShowCustomInput(false); | ||
}; | ||
|
||
const beforeSubmit: SubmitHandler<FormData> = ({ | ||
name, | ||
netid, | ||
|
@@ -56,7 +138,7 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({ | |
endDate, | ||
}) => { | ||
const email = netid ? `${netid}@cornell.edu` : undefined; | ||
const accessibility = needs; | ||
const accessibility = needs.map((option) => option.value.toString()); | ||
const nameParts = name.trim().split(/\s+/); | ||
const firstName = | ||
nameParts.length > 1 ? nameParts.slice(0, -1).join(' ') : nameParts[0]; | ||
|
@@ -74,7 +156,6 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({ | |
}; | ||
|
||
console.log('Form payload:', payload); | ||
|
||
onSubmit(payload); | ||
}; | ||
|
||
|
@@ -86,7 +167,14 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({ | |
const localUserType = localStorage.getItem('userType'); | ||
const isEditing = rider !== undefined; | ||
const isStudentEditing = isEditing && localUserType === 'Rider'; | ||
const [needsOption, setNeedsOption] = useState(''); | ||
|
||
const needsOptions: NeedOption[] = [ | ||
...Object.values(Accessibility).map((value) => ({ | ||
value: value as Accessibility, | ||
label: value, | ||
})), | ||
{ value: 'OTHER', label: 'Add Custom Need' }, | ||
]; | ||
|
||
return ( | ||
<form onSubmit={handleSubmit(beforeSubmit)} className={styles.form}> | ||
|
@@ -106,6 +194,7 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({ | |
/> | ||
{errors.name && <p className={styles.error}>Name cannot be empty</p>} | ||
</div> | ||
|
||
<div className={cn(styles.gridR1, styles.gridCSmall2)}> | ||
<Label className={styles.label} htmlFor="netid"> | ||
NetID:{' '} | ||
|
@@ -125,6 +214,7 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({ | |
<p className={styles.error}>NetId cannot be empty</p> | ||
)} | ||
</div> | ||
|
||
<div className={cn(styles.gridR1, styles.gridCSmall3)}> | ||
<Label className={styles.label} htmlFor="phoneNumber"> | ||
Phone Number:{' '} | ||
|
@@ -144,26 +234,69 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({ | |
)} | ||
</div> | ||
|
||
{/* Replacing SelectComponent with native <select> */} | ||
<div className={cn(styles.gridR2, styles.gridCBig1)}> | ||
<Label className={styles.label} htmlFor="needs"> | ||
Needs:{' '} | ||
</Label> | ||
<select | ||
id="needs" | ||
{...register('needs', { required: true })} | ||
multiple | ||
className={styles.firstRow} | ||
> | ||
{Object.entries(Accessibility).map(([key, value]) => ( | ||
<option key={key} value={value}> | ||
{value} | ||
</option> | ||
))} | ||
</select> | ||
{errors.needs && ( | ||
<p className={styles.error}>Please select at least one need</p> | ||
)} | ||
<div className={styles.needsContainer}> | ||
<Controller | ||
name="needs" | ||
control={control} | ||
rules={{ required: true }} | ||
render={({ field: { onChange, value, ...field } }) => ( | ||
<Select<NeedOption, true> | ||
{...field} | ||
value={value} | ||
isMulti | ||
options={needsOptions} | ||
className={styles.customSelect} | ||
classNamePrefix="customSelectValueContainer" | ||
placeholder="Select needs..." | ||
styles={customStyles} | ||
onChange={(newValue, actionMeta) => | ||
handleNeedsChange(newValue, actionMeta) | ||
} | ||
/> | ||
)} | ||
/> | ||
{showCustomInput && ( | ||
<div className={styles.customNeedInput}> | ||
<input | ||
type="text" | ||
value={customNeed} | ||
onChange={(e) => setCustomNeed(e.target.value)} | ||
onKeyDown={(e) => { | ||
if (e.key === 'Enter') { | ||
e.preventDefault(); | ||
handleAddCustomNeed(); | ||
} | ||
}} | ||
placeholder="Type custom need" | ||
className={styles.customNeedField} | ||
autoFocus | ||
/> | ||
<div className={styles.customNeedActions}> | ||
<button | ||
type="button" | ||
onClick={handleAddCustomNeed} | ||
className={styles.customNeedButton} | ||
> | ||
✓ | ||
</button> | ||
<button | ||
type="button" | ||
onClick={handleCancelCustomNeed} | ||
className={styles.customNeedButton} | ||
> | ||
✕ | ||
</button> | ||
</div> | ||
</div> | ||
)} | ||
{errors.needs && ( | ||
<p className={styles.error}>Please select at least one need</p> | ||
)} | ||
</div> | ||
</div> | ||
|
||
<div className={cn(styles.gridR2, styles.gridCBig2)}> | ||
|
@@ -178,11 +311,13 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({ | |
})} | ||
type="text" | ||
aria-required="true" | ||
style={{ height: '60px' }} | ||
/> | ||
{errors.address && ( | ||
<p className={styles.error}>Please enter an address</p> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. change to |
||
)} | ||
</div> | ||
|
||
<div className={cn(styles.gridR3, styles.gridCAll)}> | ||
<p>Duration</p> | ||
<div className={styles.lastRow}> | ||
|
@@ -202,7 +337,9 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({ | |
<p className={styles.error}>Please enter a join date</p> | ||
)} | ||
</div> | ||
<p className={styles.to}>to</p> | ||
<div className={styles.to}> | ||
<p>→</p> | ||
</div> | ||
<div> | ||
<Label className={styles.label} htmlFor="endDate"> | ||
End Date:{' '} | ||
|
@@ -231,6 +368,7 @@ const RiderModalInfo: React.FC<ModalFormProps> = ({ | |
</div> | ||
</div> | ||
</div> | ||
|
||
<div className={styles.buttonContainer}> | ||
<Button | ||
type="button" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,167 +1,268 @@ | ||
.form { | ||
display: flex; | ||
flex-direction: column; | ||
align-items: center; | ||
max-width: 100%; | ||
width: 100%; | ||
height: 100%; | ||
} | ||
|
||
.inputContainer { | ||
margin-bottom: 1.5rem; | ||
width: 100%; | ||
} | ||
|
||
.inputContainer input[type='text'], | ||
.inputContainer input[type='tel'], | ||
.inputContainer input[type='date'], | ||
.inputContainer select { | ||
border: 1px solid #808080; | ||
width: 100%; | ||
border-radius: 6px; | ||
height: 1.75rem; /* Adjust height as needed */ | ||
font-size: 1rem; /* Adjust font size as needed */ | ||
/* Add any other relevant styling here */ | ||
.error { | ||
color: #dc3545; | ||
font-size: 0.8rem; | ||
margin-top: 0.25rem; | ||
} | ||
|
||
.label { | ||
display: block; | ||
margin-bottom: 0.5rem; | ||
font-weight: 600; | ||
color: #333; | ||
} | ||
|
||
/* Input styles */ | ||
.inputContainer input[type='text'], | ||
.inputContainer input[type='tel'], | ||
.inputContainer input[type='date'], | ||
.inputContainer select { | ||
border: 1px solid #808080; | ||
.inputContainer input[type='date'] { | ||
width: 100%; | ||
border-radius: 6px; | ||
height: 1.75rem; /* Adjust height as needed */ | ||
font-size: 1rem; /* Adjust font size as needed */ | ||
/* Add any other relevant styling here */ | ||
height: 42px; | ||
padding: 0 0.75rem; | ||
font-size: 1rem; | ||
border: 1px solid #ced4da; | ||
border-radius: 4px; | ||
background-color: #fff; | ||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; | ||
} | ||
|
||
.inputContainer input[type='text']:focus, | ||
.inputContainer input[type='tel']:focus, | ||
.inputContainer input[type='date']:focus { | ||
border-color: #000; | ||
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.25); | ||
outline: none; | ||
} | ||
|
||
/* Grid Layout */ | ||
.rideTime { | ||
display: grid; | ||
grid-template-columns: repeat(10, 1fr); | ||
grid-template-columns: repeat(10, 1fr); | ||
grid-template-rows: repeat(3, 1fr); | ||
column-gap: 2rem; | ||
row-gap: 2rem; | ||
column-gap: 2rem; | ||
row-gap: 2rem; | ||
grid-template-columns: repeat(12, 1fr); | ||
grid-template-rows: repeat(3, auto); | ||
gap: 1.25rem; | ||
} | ||
|
||
/* Grid positioning classes */ | ||
.gridCSmall1 { | ||
grid-column: 1 / 5; | ||
grid-column: 1 / 5; | ||
} | ||
|
||
.gridCSmall2 { | ||
grid-column: 5 / 7; | ||
grid-column: 5 / 7; | ||
grid-column: 5 / 8; | ||
} | ||
|
||
.gridCSmall3 { | ||
grid-column: 7 / 11; | ||
grid-column: 7 / 11; | ||
grid-column: 8 / 13; | ||
} | ||
|
||
.gridCBig1 { | ||
grid-column: 1 / 5; | ||
grid-column: 1 / 5; | ||
grid-column: 1 / 7; | ||
} | ||
.gridCBig2 { | ||
grid-column: 5 / 11; | ||
grid-column: 5 / 11; | ||
grid-column: 7 / 13; | ||
} | ||
.gridCAll { | ||
grid-column: 1 / span 6; | ||
grid-column: 1 / span 7; | ||
} | ||
|
||
.duration { | ||
font-size: 0.9rem; | ||
.gridR1 { | ||
grid-row: 1; | ||
} | ||
.gridR2 { | ||
grid-row: 2; | ||
} | ||
.gridR3 { | ||
grid-row: 3; | ||
} | ||
|
||
/* Duration section */ | ||
.lastRow { | ||
display: flex; | ||
gap: 1.25rem; | ||
align-items: center; | ||
} | ||
|
||
.to { | ||
font-size: 0.9rem; | ||
grid-column: 3 / span 1; | ||
margin-left: 70%; | ||
margin-top: 5%; | ||
font-size: 1.5rem; | ||
height: 100%; | ||
} | ||
|
||
.end { | ||
grid-column: 4 / span 2; | ||
/* Select input styles */ | ||
.customSelect { | ||
width: 100%; | ||
} | ||
|
||
.gridR1 { | ||
grid-row: 1; | ||
.customSelect :global(.customSelectValueContainer__control) { | ||
max-height: 60px; | ||
min-height: 60px; | ||
overflow-y: auto; | ||
border: 1px solid #ced4da; | ||
border-radius: 4px; | ||
background-color: #fff; | ||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; | ||
} | ||
.gridR2 { | ||
grid-row: 2; | ||
|
||
.customSelect :global(.customSelectValueContainer__value-container) { | ||
max-height: 55px; | ||
overflow-y: auto; | ||
padding: 0 8px; | ||
} | ||
.gridR3 { | ||
grid-row: 3; | ||
|
||
.customSelect :global(.customSelectValueContainer__input-container input) { | ||
box-shadow: none !important; | ||
} | ||
|
||
.error { | ||
color: #eb0023; | ||
font-size: 0.8rem; | ||
padding: 0.25rem; | ||
.needsContainer { | ||
display: flex; | ||
flex-direction: column; | ||
gap: 8px; | ||
} | ||
|
||
.riderDate { | ||
font-size: 1rem !important; | ||
color: #808080; | ||
color: #808080; | ||
.customNeedInput { | ||
display: flex; | ||
align-items: center; | ||
gap: 8px; | ||
margin-top: 8px; | ||
} | ||
|
||
.firstRow { | ||
max-width: 100%; | ||
max-width: 100%; | ||
.customNeedField { | ||
flex: 1; | ||
height: 32px; | ||
padding: 0 12px; | ||
border: 1px solid #ced4da; | ||
border-radius: 4px; | ||
font-size: 0.875rem; | ||
} | ||
|
||
.lastRow { | ||
.customNeedField:focus { | ||
outline: none; | ||
border-color: #000; | ||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1); | ||
} | ||
|
||
.customNeedActions { | ||
display: flex; | ||
gap: 4px; | ||
} | ||
|
||
.to { | ||
margin: 0 1rem; | ||
.customNeedButton { | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
width: 32px; | ||
height: 32px; | ||
border: 1px solid #ced4da; | ||
border-radius: 4px; | ||
background: white; | ||
cursor: pointer; | ||
transition: all 0.2s; | ||
font-size: 16px; | ||
} | ||
|
||
/* Green confirm button */ | ||
.customNeedButton:first-child { | ||
color: #28a745; | ||
border-color: #28a745; | ||
} | ||
|
||
.customNeedButton:first-child:hover { | ||
background: #28a745; | ||
color: white; | ||
} | ||
|
||
/* Red cancel button */ | ||
.customNeedButton:last-child { | ||
color: #dc3545; | ||
border-color: #dc3545; | ||
} | ||
|
||
.customNeedButton:last-child:hover { | ||
background: #dc3545; | ||
color: white; | ||
} | ||
|
||
/* Button container */ | ||
.buttonContainer { | ||
display: flex; | ||
justify-content: flex-end; | ||
width: 100%; | ||
margin-top: 2rem; | ||
gap: 1rem; | ||
} | ||
|
||
.buttonContainer button { | ||
padding: 0.75rem 1.5rem; | ||
font-size: 1rem; | ||
border-radius: 4px; | ||
cursor: pointer; | ||
transition: all 0.2s ease; | ||
} | ||
|
||
.submit { | ||
margin-left: 1.5rem; | ||
background-color: #000; | ||
color: #fff; | ||
border: none; | ||
} | ||
|
||
.editIcon { | ||
width: 4rem; | ||
height: auto; | ||
.submit:hover { | ||
background-color: #333; | ||
} | ||
|
||
.editRiderButton { | ||
margin: 0; | ||
border: 0; | ||
padding: 0; | ||
background: none; | ||
cursor: pointer; | ||
.cancel { | ||
background-color: #fff; | ||
color: #000; | ||
border: 1px solid #000; | ||
} | ||
|
||
.label { | ||
font-size: smaller; | ||
color: #000000; | ||
.cancel:hover { | ||
background-color: #000; | ||
color: #fff; | ||
} | ||
|
||
@media screen and (max-width: 1092px) { | ||
/* Responsive styles */ | ||
@media (max-width: 1092px) { | ||
.form { | ||
display: block; | ||
max-width: 100%; | ||
} | ||
|
||
.rideTime { | ||
display: block; | ||
display: flex; | ||
flex-direction: column; | ||
gap: 1.25rem; | ||
} | ||
|
||
.label { | ||
font-weight: bold; | ||
} | ||
|
||
.lastRow { | ||
display: block; | ||
flex-direction: column; | ||
gap: 1rem; | ||
} | ||
|
||
.buttonContainer { | ||
flex-direction: column; | ||
} | ||
|
||
.buttonContainer button { | ||
width: 100%; | ||
} | ||
} | ||
|
||
/* Date input specific styles */ | ||
.riderDate { | ||
width: 100%; | ||
min-width: 200px; | ||
} | ||
|
||
/* First row inputs */ | ||
.firstRow { | ||
width: 100%; | ||
} |
Unchanged files with check annotations Beta
const Toast = ({ message, toastType }: toastProps) => { | ||
return typeof toastType === 'undefined' || | ||
toastType == ToastStatus.SUCCESS ? ( | ||
Check warning on line 13 in frontend/src/components/ConfirmationToast/ConfirmationToast.tsx GitHub Actions / check
|
||
<div className={`${styles.toast} ${styles.successToast}`}> | ||
<img alt="toast check" src={check} /> | ||
<p className={styles.toasttext}>{message}</p> |
const netId = email.split('@')[0]; | ||
const fmtPhone = formatPhone(phoneNumber); | ||
const formatAvail = (availability: { | ||
Check warning on line 38 in frontend/src/components/EmployeeCards/EmployeeCards.tsx GitHub Actions / check
|
||
[key: string]: { startTime: string; endTime: string }; | ||
}) => { | ||
if (!availability) { | ||
}; | ||
const isAdmin = isDriver !== undefined; | ||
const isBoth = isDriver && isDriver == true; | ||
Check warning on line 55 in frontend/src/components/EmployeeCards/EmployeeCards.tsx GitHub Actions / check
|
||
const roles = (): string => { | ||
if (isBoth) return 'Admin • Driver'; | ||
if (isAdmin) return 'Admin'; | ||
return 'Driver'; | ||
}; | ||
const userInfo = { | ||
Check warning on line 62 in frontend/src/components/EmployeeCards/EmployeeCards.tsx GitHub Actions / check
|
||
id, | ||
firstName, | ||
lastName, |
import React, { useCallback, useEffect, useState } from 'react'; | ||
import cn from 'classnames'; | ||
import moment from 'moment'; | ||
import { useFormContext, UseFormRegister } from 'react-hook-form'; | ||
Check warning on line 4 in frontend/src/components/EmployeeModal/WorkingHours.tsx GitHub Actions / check
|
||
import styles from './employeemodal.module.css'; | ||
import { Input, Label } from '../FormElements/FormElements'; | ||
import { WeekProvider, useWeek } from './WeekContext'; |
import React, { useState, useRef } from 'react'; | ||
import { CSVLink } from 'react-csv'; | ||
import ScheduledTable from '../UserTables/ScheduledTable'; | ||
import { Driver } from '../../types/index'; | ||
Check warning on line 4 in frontend/src/components/ExportPreview/ExportPreview.tsx GitHub Actions / check
|
||
import styles from './exportPreview.module.css'; | ||
import { useEmployees } from '../../context/EmployeesContext'; | ||
import ExportButton from '../ExportButton/ExportButton'; | ||
Check warning on line 7 in frontend/src/components/ExportPreview/ExportPreview.tsx GitHub Actions / check
|
||
import { useDate } from '../../context/date'; | ||
import { format_date } from '../../util'; | ||
import axios from '../../util/axios'; | ||
const ExportPreview = () => { | ||
const { drivers } = useEmployees(); | ||
Check warning on line 13 in frontend/src/components/ExportPreview/ExportPreview.tsx GitHub Actions / check
|
||
const [downloadData, setDownloadData] = useState<string>(''); | ||
const { curDate } = useDate(); | ||
const csvLink = useRef< | ||
const today = format_date(curDate); | ||
const downloadCSV = () => { | ||
Check warning on line 22 in frontend/src/components/ExportPreview/ExportPreview.tsx GitHub Actions / check
|
||
axios | ||
.get(`/api/rides/download?date=${today}`, { | ||
responseType: 'text', |
import React, { SelectHTMLAttributes } from 'react'; | ||
Check warning on line 1 in frontend/src/components/FormElements/FormElements.tsx GitHub Actions / check
|
||
import cn from 'classnames'; | ||
import styles from './formelements.module.css'; | ||
import Select, { ActionMeta, Props as SelectProps } from 'react-select'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add
validate: isAddress,
under pattern