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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ JUNO_EMAIL_SENDER_NAME=Able Alliance
NEXT_PUBLIC_WS_URL=http://localhost:4000

NEXT_PUBLIC_MAPBOX_TOKEN="put token here"

# Server-side Mapbox secret token (sk.) — directions:read scope only. Never expose to browser.
MAPBOX_TOKEN="put secret token here"
6 changes: 6 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[build]
command = "next build --turbopack"
publish = ".next"

[[plugins]]
package = "@netlify/plugin-nextjs"
15 changes: 13 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@netlify/plugin-nextjs": "^5.15.9",
"@tailwindcss/postcss": "^4.1.18",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
Expand Down
80 changes: 67 additions & 13 deletions scripts/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
* Idempotent — checks for existing data before inserting.
*/

import "dotenv/config";
import mongoose from "mongoose";
import { getMapboxTravelDuration } from "@/server/mapbox";

let MONGODB_URI =
process.argv[2] ??
Expand Down Expand Up @@ -153,12 +155,15 @@ async function seed() {
// ---------- Locations ----------
const locsCol = db.collection("locations");

const pickupCoords = { latitude: 33.7756, longitude: -84.4027 };
const dropoffCoords = { latitude: 33.7767, longitude: -84.3891 };
const dropoff2Coords = { latitude: 33.7739, longitude: -84.3983 };

let pickup = await locsCol.findOne({ name: "Exhibition Hall" });
if (!pickup) {
const res = await locsCol.insertOne({
name: "Exhibition Hall",
latitude: 33.7756,
longitude: -84.4027,
...pickupCoords,
});
pickup = { _id: res.insertedId, name: "Exhibition Hall" };
console.log("✓ Created location: Exhibition Hall");
Expand All @@ -170,8 +175,7 @@ async function seed() {
if (!dropoff) {
const res = await locsCol.insertOne({
name: "Tech Square Eastbound",
latitude: 33.7767,
longitude: -84.3891,
...dropoffCoords,
});
dropoff = { _id: res.insertedId, name: "Tech Square Eastbound" };
console.log("✓ Created location: Tech Square Eastbound");
Expand All @@ -183,8 +187,7 @@ async function seed() {
if (!dropoff2) {
const res = await locsCol.insertOne({
name: "Student Center",
latitude: 33.7739,
longitude: -84.3983,
...dropoff2Coords,
});
dropoff2 = { _id: res.insertedId, name: "Student Center" };
console.log("✓ Created location: Student Center");
Expand Down Expand Up @@ -290,22 +293,54 @@ async function seed() {
const makeRoute = (
label: string,
dropoffLocation: unknown,
dropoffLatLng: { latitude: number; longitude: number },
scheduledPickupTime: Date,
) => ({
label,
dropoffLocation,
dropoffLatLng,
scheduledPickupTime,
pickupWindowStart: addMinutes(scheduledPickupTime, -30),
pickupWindowEnd: addMinutes(scheduledPickupTime, 30),
});

const routeTemplates = [
makeRoute("today 10:00", dropoffId, withTime(todayStart, 10, 0)),
makeRoute("today 14:30", dropoffId, withTime(todayStart, 14, 30)),
makeRoute("today 16:00", dropoff2Id, withTime(todayStart, 16, 0)),
makeRoute("tomorrow 09:30", dropoffId, withTime(tomorrowStart, 9, 30)),
makeRoute("tomorrow 14:00", dropoff2Id, withTime(tomorrowStart, 14, 0)),
makeRoute("tomorrow 16:30", dropoffId, withTime(tomorrowStart, 16, 30)),
makeRoute(
"today 10:00",
dropoffId,
dropoffCoords,
withTime(todayStart, 10, 0),
),
makeRoute(
"today 14:30",
dropoffId,
dropoffCoords,
withTime(todayStart, 14, 30),
),
makeRoute(
"today 16:00",
dropoff2Id,
dropoff2Coords,
withTime(todayStart, 16, 0),
),
makeRoute(
"tomorrow 09:30",
dropoffId,
dropoffCoords,
withTime(tomorrowStart, 9, 30),
),
makeRoute(
"tomorrow 14:00",
dropoff2Id,
dropoff2Coords,
withTime(tomorrowStart, 14, 0),
),
makeRoute(
"tomorrow 16:30",
dropoffId,
dropoffCoords,
withTime(tomorrowStart, 16, 30),
),
];

let createdRoutes = 0;
Expand All @@ -324,6 +359,21 @@ async function seed() {
continue;
}

const durationSeconds = await getMapboxTravelDuration(
pickupCoords.latitude,
pickupCoords.longitude,
template.dropoffLatLng.latitude,
template.dropoffLatLng.longitude,
template.scheduledPickupTime,
);

const estimatedDropoffTime =
durationSeconds !== null
? new Date(
template.scheduledPickupTime.getTime() + durationSeconds * 1000,
)
: undefined;

await routesCol.insertOne({
pickupLocation: pickupId,
dropoffLocation: template.dropoffLocation,
Expand All @@ -333,11 +383,15 @@ async function seed() {
scheduledPickupTime: template.scheduledPickupTime,
pickupWindowStart: template.pickupWindowStart,
pickupWindowEnd: template.pickupWindowEnd,
estimatedDropoffTime,
status: "Scheduled",
});

createdRoutes += 1;
console.log(`✓ Created route: ${template.label}`);
const dropoffLabel = estimatedDropoffTime
? `est. dropoff ${estimatedDropoffTime.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true })}`
: "no dropoff estimate (MAPBOX_TOKEN not set)";
console.log(`✓ Created route: ${template.label} (${dropoffLabel})`);
}

console.log(
Expand Down
9 changes: 7 additions & 2 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,16 @@
font-style: normal;
font-display: swap;
}
html,
body {
html {
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: none;
overscroll-behavior: none;
}

body {
overflow-x: hidden;
scrollbar-width: none;
}

html::-webkit-scrollbar,
Expand Down
31 changes: 13 additions & 18 deletions src/app/rides/RideCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type RideCardRoute = {
driver?: string | RouteUser;
scheduledPickupTime: string;
pickupWindowEnd?: string;
estimatedDropoffTime?: string;
status: string;
student?: string | { firstName: string; lastName: string };
vehicle?: string | { licensePlate: string };
Expand Down Expand Up @@ -67,7 +68,6 @@ function getStudentStatusChipStyle(status: string): React.CSSProperties {
return { background: "#ffd17f", color: "#22070b" };
case "Completed":
return { background: "#70cd87", color: "#22070b" };
case "Cancelled by Driver":
case "Cancelled by Student":
case "Cancelled by Admin":
case "Missing":
Expand All @@ -83,7 +83,6 @@ function getDriverStatusChipColor(
switch (status) {
case "Completed":
return "green";
case "Cancelled by Driver":
case "Cancelled by Student":
case "Cancelled by Admin":
case "Missing":
Expand Down Expand Up @@ -121,9 +120,9 @@ export function RideCard({
? `${route.student.firstName} ${route.student.lastName}`.trim()
: null;

const dropoffTimeDisplay = route.pickupWindowEnd
? formatTime(route.pickupWindowEnd)
: null;
const dropoffTimeDisplay = route.estimatedDropoffTime
? formatTime(route.estimatedDropoffTime)
: "N/A";

const canStart = route.status === "Scheduled";

Expand All @@ -146,11 +145,9 @@ export function RideCard({
className={`${styles.rideCardStopBlock} ${styles.rideCardStopBlockRight}`}
>
<span className={styles.rideCardStopLabel}>Dropoff</span>
{dropoffTimeDisplay && (
<span className={styles.rideCardStopTime}>
{dropoffTimeDisplay}
</span>
)}
<span className={styles.rideCardStopTime}>
{dropoffTimeDisplay}
</span>
<span className={styles.rideCardStopLocation}>{dropoffName}</span>
</div>
</div>
Expand Down Expand Up @@ -204,9 +201,9 @@ export function RideCard({
}

// Student card — Figma design
const dropoffTimeDisplay = route.pickupWindowEnd
? formatTime(route.pickupWindowEnd)
: null;
const dropoffTimeDisplay = route.estimatedDropoffTime
? formatTime(route.estimatedDropoffTime)
: "N/A";

const chatEligible =
isToday(route.scheduledPickupTime) &&
Expand Down Expand Up @@ -234,11 +231,9 @@ export function RideCard({
className={`${styles.rideCardStopBlockNew} ${styles.rideCardStopBlockRight}`}
>
<span className={styles.rideCardStopLabelNew}>Dropoff</span>
{dropoffTimeDisplay && (
<span className={styles.rideCardStopTimeNew}>
{dropoffTimeDisplay}
</span>
)}
<span className={styles.rideCardStopTimeNew}>
{dropoffTimeDisplay}
</span>
<span className={styles.rideCardStopLocationNew}>
{dropoffName}
</span>
Expand Down
10 changes: 6 additions & 4 deletions src/app/rides/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type RouteData = {
seatCount: number;
};
scheduledPickupTime: string;
estimatedDropoffTime?: string;
status: string;
};

Expand Down Expand Up @@ -63,7 +64,6 @@ function getStatusChipColor(
switch (status) {
case "Completed":
return "green";
case "Cancelled by Driver":
case "Cancelled by Student":
case "Cancelled by Admin":
case "Missing":
Expand Down Expand Up @@ -516,7 +516,6 @@ export default function RideDetailPage({
const hasDriver = !!driverName;

const scheduledDate = new Date(route.scheduledPickupTime);
const dropoffDate = new Date(scheduledDate.getTime() + 15 * 60 * 1000);

const formatTime = (d: Date) =>
d.toLocaleTimeString("en-US", {
Expand All @@ -525,6 +524,10 @@ export default function RideDetailPage({
hour12: true,
});

const dropoffTimeDisplay = route.estimatedDropoffTime
? formatTime(new Date(route.estimatedDropoffTime))
: "N/A";

const formatDateLabel = (d: Date) => {
const today = new Date();
const isToday =
Expand All @@ -550,7 +553,6 @@ export default function RideDetailPage({
return { background: "#fef3c7", color: "#92400e" };
case "Completed":
return { background: "#d1fae5", color: "#065f46" };
case "Cancelled by Driver":
case "Cancelled by Student":
case "Cancelled by Admin":
case "Missing":
Expand Down Expand Up @@ -594,7 +596,7 @@ export default function RideDetailPage({
<div className={styles.stopDivider} />
<div className={`${styles.stopBlock} ${styles.stopBlockRight}`}>
<span className={styles.stopLabel}>Dropoff</span>
<span className={styles.stopTime}>{formatTime(dropoffDate)}</span>
<span className={styles.stopTime}>{dropoffTimeDisplay}</span>
<span className={styles.stopLocation}>{dropoffLocationName}</span>
</div>
</div>
Expand Down
6 changes: 5 additions & 1 deletion src/app/rides/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type Route = {
vehicle?: string;
scheduledPickupTime: string;
pickupWindowEnd?: string;
estimatedDropoffTime?: string;
status: string;
};

Expand Down Expand Up @@ -58,7 +59,10 @@ function formatDayGroupHeader(iso: string): string {

function getDateKey(iso: string): string {
const d = new Date(iso);
return d.toISOString().slice(0, 10);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}

function getWeekRange(offset: 0 | 1): [Date, Date] {
Expand Down
Loading
Loading