Skip to content

Commit 76a0a4e

Browse files
committed
Merge branch 'master' into bek76/pnpm-shared
2 parents 9756b8f + a3d00a5 commit 76a0a4e

69 files changed

Lines changed: 2861 additions & 1472 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/public/_redirects

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/* /index.html 200

frontend/src/components/AddRideButton/AddRideButton.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React, { useState } from 'react';
2-
import { Button } from '../FormElements/FormElements';
32
import RideDetailsComponent from '../RideDetails/RideDetailsComponent';
43
import { createEmptyRide } from '../../util/modelFixtures';
54
import { RideType } from '@carriage-web/shared/types/ride';
65
import { useRides } from '../../context/RidesContext';
6+
import buttonStyles from '../../styles/button.module.css';
77

88
const AddRideButton: React.FC = () => {
99
const [open, setOpen] = useState(false);
@@ -28,7 +28,15 @@ const AddRideButton: React.FC = () => {
2828

2929
return (
3030
<>
31-
<Button onClick={handleOpenModal}>+ Add ride</Button>
31+
<button
32+
style={{
33+
width: '8rem',
34+
}}
35+
onClick={handleOpenModal}
36+
className={`${buttonStyles.buttonLarge} ${buttonStyles.button} ${buttonStyles.buttonPrimary}`}
37+
>
38+
+ Add ride
39+
</button>
3240
<RideDetailsComponent
3341
key={modalKey} // Force remount when key changes
3442
open={open}

frontend/src/components/AuthManager/AuthManager.tsx

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,45 @@ const AuthManager = () => {
7373
}
7474
}, []);
7575

76+
// Common logic to complete login from a JWT issued by the backend
77+
const completeLoginFromToken = (serverJWT: string) => {
78+
setCookie('jwt', serverJWT);
79+
const decoded: any = jwtDecode(serverJWT);
80+
setId(decoded.id);
81+
localStorage.setItem('userId', decoded.id);
82+
localStorage.setItem('userType', decoded.userType);
83+
setAuthToken(serverJWT);
84+
85+
// Refresh user data
86+
const refreshFunc = createRefresh(decoded.id, decoded.userType, serverJWT);
87+
refreshFunc();
88+
setRefreshUser(() => refreshFunc);
89+
setSignedIn(true);
90+
91+
// Navigate to appropriate dashboard based on userType
92+
if (decoded.userType === 'Admin') {
93+
navigate('/admin/home', { replace: true });
94+
} else if (decoded.userType === 'Driver') {
95+
navigate('/driver/rides', { replace: true });
96+
} else if (decoded.userType === 'Rider') {
97+
navigate('/rider/schedule', { replace: true });
98+
} else {
99+
// Invalid userType - this should never happen if backend is working correctly
100+
setSsoError('Invalid user type received. Please contact support.');
101+
logout();
102+
}
103+
};
104+
76105
// SSO Callback handler - fetches profile and JWT after successful SSO login
77-
const handleSSOCallback = async () => {
106+
// This is now primarily a fallback for environments where server-side
107+
// sessions are same-site (e.g., local development). In production, we
108+
// prefer the stateless JWT passed via the URL query parameter.
109+
const handleSSOCallback = async (event?: React.FormEvent<HTMLFormElement>) => {
78110
try {
79111
const response = await fetch(
80112
`${process.env.REACT_APP_SERVER_URL}/api/sso/profile`,
81113
{
82-
credentials: 'include', // CRITICAL: Sends session cookie
114+
credentials: 'include', // Send session cookie
83115
}
84116
);
85117

@@ -91,40 +123,8 @@ const AuthManager = () => {
91123
const { user: ssoUser, token: serverJWT } = data;
92124

93125
if (serverJWT && ssoUser) {
94-
// Store JWT in encrypted cookie (matching Google OAuth pattern)
95-
setCookie('jwt', serverJWT);
96-
97-
// Decode JWT to get user info
98-
const decoded: any = jwtDecode(serverJWT);
99-
100-
// Set auth state
101-
setId(decoded.id);
102-
localStorage.setItem('userId', decoded.id);
103-
localStorage.setItem('userType', decoded.userType);
104-
setAuthToken(serverJWT);
105-
106-
// Refresh user data
107-
const refreshFunc = createRefresh(
108-
decoded.id,
109-
decoded.userType,
110-
serverJWT
111-
);
112-
refreshFunc();
113-
setRefreshUser(() => refreshFunc);
114-
setSignedIn(true);
115-
116-
// Navigate to appropriate dashboard based on userType
117-
if (decoded.userType === 'Admin') {
118-
navigate('/admin/home', { replace: true });
119-
} else if (decoded.userType === 'Driver') {
120-
navigate('/driver/rides', { replace: true });
121-
} else if (decoded.userType === 'Rider') {
122-
navigate('/rider/schedule', { replace: true });
123-
} else {
124-
// Invalid userType - this should never happen if backend is working correctly
125-
setSsoError('Invalid user type received. Please contact support.');
126-
logout();
127-
}
126+
// Reuse common JWT login logic
127+
completeLoginFromToken(serverJWT);
128128
} else {
129129
setSsoError('Failed to complete SSO login. Please try again.');
130130
logout();
@@ -140,6 +140,7 @@ const AuthManager = () => {
140140
useEffect(() => {
141141
const authParam = searchParams.get('auth');
142142
const errorParam = searchParams.get('error');
143+
const tokenParam = searchParams.get('token');
143144

144145
if (errorParam) {
145146
// Handle user_not_found specially - fetch unregistered user info
@@ -183,8 +184,12 @@ const AuthManager = () => {
183184
}
184185

185186
if (authParam === 'sso_success') {
186-
// Fetch profile and JWT token from backend
187-
handleSSOCallback();
187+
if (tokenParam) {
188+
completeLoginFromToken(tokenParam);
189+
} else {
190+
// Fallback to session-based profile fetch (useful for local dev)
191+
handleSSOCallback();
192+
}
188193
}
189194
// eslint-disable-next-line react-hooks/exhaustive-deps
190195
}, [searchParams]);
@@ -246,6 +251,7 @@ const AuthManager = () => {
246251
deleteCookie('jwt');
247252
setAuthToken('');
248253
setSignedIn(false);
254+
setRefreshUser(() => () => {});
249255
window.location.href = `${process.env.REACT_APP_SERVER_URL}/api/sso/logout`;
250256
}
251257

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { Menu, MenuItem } from '@mui/material';
2+
import { useDate } from 'context/date';
3+
import { useRides } from 'context/RidesContext';
4+
import { FC, useState } from 'react';
5+
import { format_date } from '../../util/index';
6+
import buttonStyles from '../../styles/button.module.css';
7+
import { FileDownload } from '@mui/icons-material';
8+
9+
export const CSVFromRidesButton: FC = () => {
10+
const { curDate } = useDate();
11+
const today = format_date(curDate);
12+
13+
const [menuAnchorElement, setMenuAnchorElement] =
14+
useState<null | HTMLElement>(null);
15+
16+
const { unscheduledRides, scheduledRides } = useRides();
17+
18+
const handleExportMenuClose = () => {
19+
setMenuAnchorElement(null);
20+
};
21+
const handleExportOption = (
22+
type: 'scheduled' | 'unscheduled' | 'combined'
23+
) => {
24+
setMenuAnchorElement(null);
25+
const rows: string[] = [];
26+
27+
// columns for the export csv
28+
const header =
29+
'Rider First Name,Rider Last Name,Rider Phone,Rider Email,Rider Accessibility,Start Location Address,Start Time,End Location Address,End Time,Driver First Name,Driver Last Name,Type,Status,Scheduling State';
30+
rows.push(header);
31+
32+
const rideToCSV = (ride: any) => {
33+
const rider = ride.riders && ride.riders[0];
34+
const driver = ride.driver;
35+
36+
return [
37+
rider?.firstName || '',
38+
rider?.lastName || '',
39+
rider?.phoneNumber || '',
40+
rider?.email || '',
41+
rider?.accessibility?.join('; ') || '',
42+
ride.startLocation?.address || '',
43+
format_date(ride.startTime, 'h:mm a') || '',
44+
ride.endLocation?.address || '',
45+
format_date(ride.endTime, 'h:mm a') || '',
46+
driver?.firstName || '',
47+
driver?.lastName || '',
48+
ride.type || '',
49+
ride.status || '',
50+
ride.schedulingState || '',
51+
]
52+
.map((field) => `"${String(field).replace(/"/g, '""')}"`)
53+
.join(',');
54+
};
55+
56+
if (type === 'scheduled' || type === 'combined') {
57+
scheduledRides.forEach((ride) => {
58+
rows.push(rideToCSV(ride));
59+
});
60+
}
61+
62+
if (type === 'unscheduled' || type === 'combined') {
63+
unscheduledRides.forEach((ride) => {
64+
rows.push(rideToCSV(ride));
65+
});
66+
}
67+
68+
// Create and download CSV file
69+
const csvContent = rows.join('\n');
70+
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
71+
const link = document.createElement('a');
72+
const url = URL.createObjectURL(blob);
73+
link.setAttribute('href', url);
74+
link.setAttribute('download', `${type}_rides_${today}.csv`);
75+
link.style.visibility = 'hidden';
76+
document.body.appendChild(link);
77+
link.click();
78+
document.body.removeChild(link);
79+
};
80+
81+
return (
82+
<>
83+
<button
84+
style={{ width: '14rem' }}
85+
onClick={(e) => setMenuAnchorElement(e.currentTarget)}
86+
className={`${buttonStyles.buttonLarge} ${buttonStyles.buttonSecondary} ${buttonStyles.button}`}
87+
>
88+
<span style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
89+
<FileDownload
90+
sx={{ width: '1.2rem', height: '1.2rem' }}
91+
></FileDownload>
92+
Export Rides to CSV
93+
</span>
94+
</button>
95+
<Menu
96+
id="basic-menu"
97+
anchorEl={menuAnchorElement}
98+
open={menuAnchorElement !== null}
99+
onClose={handleExportMenuClose}
100+
slotProps={{
101+
list: {
102+
'aria-labelledby': 'basic-button',
103+
},
104+
paper: {
105+
style: {
106+
borderRadius: '0.25rem',
107+
boxShadow: 'none',
108+
border: '1px solid #e0e0e0',
109+
},
110+
},
111+
}}
112+
>
113+
<MenuItem onClick={() => handleExportOption('scheduled')}>
114+
Scheduled
115+
</MenuItem>
116+
<MenuItem onClick={() => handleExportOption('unscheduled')}>
117+
Unscheduled
118+
</MenuItem>
119+
<MenuItem onClick={() => handleExportOption('combined')}>
120+
Combined
121+
</MenuItem>
122+
</Menu>
123+
</>
124+
);
125+
};

frontend/src/components/Card/card.module.css

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
.card {
22
display: inline-block;
3-
width: 17.2rem;
4-
/* height: 23rem; */
3+
width: 17rem;
4+
height: 20rem;
55
background-color: white;
66
border-radius: 0.5rem;
7+
border: solid 1px #ddd;
78
overflow: hidden;
8-
box-shadow: 0 0.25rem 2.2rem rgba(0, 0, 0, 0.1);
9+
box-shadow: 0px 5px 16px -7px rgba(0, 0, 0, 0.15);
910
}
1011

1112
.image {

frontend/src/components/Collapsible/Collapsible.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,20 @@ const Collapsible = ({ title, children }: CollapsibleSection) => {
1818
};
1919
return (
2020
<div className={styles.collapsible}>
21-
<div className={styles.banner} onClick={() => setExpanded(!expanded)}>
22-
<h2 className={styles.title}>{title}</h2>
23-
<img
24-
className={styles.icon}
25-
src={icon}
26-
role={'button'}
27-
alt={'see more'}
28-
tabIndex={0}
29-
onKeyPress={handleKeywordKeyPress}
30-
/>
21+
<div className={styles.collapsibleContent}>
22+
<div className={styles.banner} onClick={() => setExpanded(!expanded)}>
23+
<h2 className={styles.title}>{title}</h2>
24+
<img
25+
className={styles.icon}
26+
src={icon}
27+
role={'button'}
28+
alt={'see more'}
29+
tabIndex={0}
30+
onKeyDown={handleKeywordKeyPress}
31+
/>
32+
</div>
33+
{expanded && <div className={styles.contentContainer}>{children}</div>}
3134
</div>
32-
{expanded && <div className={styles.contentContainer}>{children}</div>}
3335
</div>
3436
);
3537
};
Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
.collapsible {
2+
width: 100%;
3+
}
4+
.collapsibleContent {
25
display: inline-block;
36
width: 100%;
4-
border-radius: 0.5rem;
5-
margin-bottom: 0.5rem;
7+
border-bottom: 1px solid #ddd;
68
}
79

810
.banner {
911
display: flex;
1012
width: 100%;
1113
padding: 0.9375rem 2.25rem;
12-
background-color: #f3f3f3;
1314
}
1415

1516
.title {
@@ -22,7 +23,3 @@
2223
.icon {
2324
margin-left: 0.75rem;
2425
}
25-
26-
.contentContainer {
27-
background-color: #ffffff;
28-
}

frontend/src/components/CopyButton/CopyButton.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { Button } from '../FormElements/FormElements';
21
import { useRiders } from '../../context/RidersContext';
3-
import styles from './copyButton.module.css';
2+
import buttonStyles from '../../styles/button.module.css';
43
import { useToast, ToastStatus } from '../../context/toastContext';
54

65
const CopyButton = () => {
@@ -20,9 +19,15 @@ const CopyButton = () => {
2019
};
2120

2221
return (
23-
<Button onClick={handleClick} outline className={styles.copyButton}>
22+
<button
23+
style={{
24+
width: '10rem',
25+
}}
26+
onClick={handleClick}
27+
className={`${buttonStyles.button} ${buttonStyles.buttonSecondary} ${buttonStyles.buttonLarge}`}
28+
>
2429
Copy Emails
25-
</Button>
30+
</button>
2631
);
2732
};
2833

frontend/src/components/CopyButton/copyButton.module.css

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)