Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4753ee4
feat: integrate Mapbox for enhanced mapping functionality
GURUDAS-DEV Feb 14, 2026
84f3c35
fix: remove error logging from token verification middleware response
GURUDAS-DEV Feb 14, 2026
558e13e
Merge branch 'kaihere14:main' into main
GURUDAS-DEV Feb 14, 2026
e16f2df
feat: Implement interactive map for selecting route source/destinatio…
GURUDAS-DEV Feb 14, 2026
0598536
Merge branch 'kaihere14:main' into main
GURUDAS-DEV Feb 14, 2026
a5c32d8
Merge branch 'kaihere14:main' into main
GURUDAS-DEV Feb 14, 2026
3385dee
feat: Implement interactive Mapbox map component for source and desti…
GURUDAS-DEV Feb 14, 2026
e8e953e
feat: Minute Bug Update
GURUDAS-DEV Feb 14, 2026
0e1e982
feat: implement interactive Mapbox map for location selection on the …
GURUDAS-DEV Feb 14, 2026
e430043
Merge branch 'kaihere14:main' into main
GURUDAS-DEV Feb 14, 2026
b870060
Merge branch 'kaihere14:main' into main
GURUDAS-DEV Feb 14, 2026
0faee97
feat: implement route discovery, comparison, and map visualization wi…
GURUDAS-DEV Feb 14, 2026
b35e17f
Merge branch 'kaihere14:main' into main
GURUDAS-DEV Feb 14, 2026
4650c18
Merge branch 'main' of https://github.com/GURUDAS-DEV/BreathClean
GURUDAS-DEV Feb 14, 2026
b01b129
feat1: implement route discovery, comparison, and map visualization w…
GURUDAS-DEV Feb 14, 2026
4ab28ec
feat: Implement route discovery and comparison features with pollutio…
GURUDAS-DEV Feb 14, 2026
ecbae17
Merge branch 'kaihere14:main' into main
GURUDAS-DEV Feb 14, 2026
a8da103
feat: Implement route discovery, comparison, and saving features with…
GURUDAS-DEV Feb 14, 2026
02db6ae
Merge branch 'kaihere14:main' into main
GURUDAS-DEV Feb 14, 2026
24f4a94
feat: Add user profile page with detailed card, display saved routes,…
GURUDAS-DEV Feb 14, 2026
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
35 changes: 10 additions & 25 deletions client/app/(private)/home/routes/(from)/(to)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { useSearchParams } from "next/navigation";

import { Bookmark } from "lucide-react";

Check warning on line 7 in client/app/(private)/home/routes/(from)/(to)/page.tsx

View workflow job for this annotation

GitHub Actions / Lint

'Bookmark' is defined but never used
import { toast } from "sonner";

import InsightToast from "@/components/routes/InsightToast";
Expand Down Expand Up @@ -245,18 +245,14 @@
coordinates: [destination.lng, destination.lat],
},
},
routes: [
{
distance: routes[selectedRouteIndex].distance,
duration: routes[selectedRouteIndex].duration,
routeGeometry: routes[selectedRouteIndex].geometry,
lastComputedScore:
routes[selectedRouteIndex].aqiScore ||
Math.floor(Math.random() * 100),
lastComputedAt: new Date(),
travelMode: selectedMode,
},
],
routes: routes.map((route) => ({
distance: route.distance / 1000,
duration: route.duration / 60,
routeGeometry: route.geometry,
lastComputedScore: route.aqiScore || Math.floor(Math.random() * 100),
lastComputedAt: new Date(),
travelMode: selectedMode,
})),
Comment on lines +248 to +255

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.

⚠️ Potential issue | 🟠 Major

Non-deterministic fallback for lastComputedScore persists random data to the backend.

When route.aqiScore is falsy, Math.floor(Math.random() * 100) generates a random score that gets saved to the database. This means:

  1. Retrying the same save produces different stored scores.
  2. The saved score has no relation to actual air quality.

Consider using a sentinel value (e.g., 0 or null) to clearly indicate "no score available" rather than fabricating data.

-          lastComputedScore: route.aqiScore || Math.floor(Math.random() * 100),
+          lastComputedScore: route.aqiScore ?? null,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
routes: routes.map((route) => ({
distance: route.distance / 1000,
duration: route.duration / 60,
routeGeometry: route.geometry,
lastComputedScore: route.aqiScore || Math.floor(Math.random() * 100),
lastComputedAt: new Date(),
travelMode: selectedMode,
})),
routes: routes.map((route) => ({
distance: route.distance / 1000,
duration: route.duration / 60,
routeGeometry: route.geometry,
lastComputedScore: route.aqiScore ?? null,
lastComputedAt: new Date(),
travelMode: selectedMode,
})),
🤖 Prompt for AI Agents
In `@client/app/`(private)/home/routes/(from)/(to)/page.tsx around lines 248 -
255, The mapping that builds route records uses a nondeterministic fallback for
lastComputedScore (route.aqiScore || Math.floor(Math.random() * 100)) which
causes random values to be persisted; change the logic in the routes.map block
to use a clear sentinel (e.g., null or 0) when route.aqiScore is falsy instead
of generating random data, update any DB/save code that consumes
lastComputedScore to accept the chosen sentinel, and ensure the field name
lastComputedScore and the source route.aqiScore are the ones updated so stored
values are deterministic and meaningful.

isFavorite: false,
};

Expand Down Expand Up @@ -304,6 +300,8 @@
onModeChange={handleModeChange}
routeName={routeName}
onRouteNameChange={setRouteName}
onSaveRoute={saveRoute}
canSave={!isLoading && !error && routes.length > 0}
/>
<RouteComparisonPanel
routes={routes}
Expand All @@ -315,19 +313,6 @@
/>
<InsightToast />
<MapControls />

{/* Save Route Button - Bottom Right */}
{!isLoading && !error && routes.length > 0 && (
<button
onClick={saveRoute}
className="absolute right-6 bottom-10 z-40 flex items-center gap-3 rounded-xl bg-white px-6 py-3.5 font-bold text-slate-800 shadow-2xl transition-all hover:scale-105 hover:bg-slate-50 active:scale-95 dark:bg-slate-800 dark:text-white dark:hover:bg-slate-700"
>
<div className="flex size-8 items-center justify-center rounded-full bg-[#2bee6c]/10 text-[#2bee6c]">
<Bookmark className="size-4" />
</div>
<span>Save Route</span>
</button>
)}
</main>
</div>
);
Expand Down
20 changes: 19 additions & 1 deletion client/app/(private)/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Suspense } from "react";

import { cookies } from "next/headers";
import { redirect } from "next/navigation";

Expand Down Expand Up @@ -42,7 +44,23 @@ export default async function ProfilePage() {
<div className="bg-bc-bg-light min-h-screen">
<main className="mx-auto max-w-4xl px-6 py-8">
<ProfileCard user={user} />
<SavedRoutes />
<Suspense
fallback={
<div className="mt-6 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
<div className="mb-4 h-6 w-32 animate-pulse rounded bg-slate-100" />
<div className="space-y-3">
{[1, 2, 3].map((i) => (
<div
key={i}
className="h-20 animate-pulse rounded-xl border border-slate-100 bg-slate-50"
/>
))}
</div>
</div>
}
>
<SavedRoutes />
</Suspense>
<NotificationPreference />
</main>
</div>
Expand Down
2 changes: 1 addition & 1 deletion client/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Metadata } from "next";
import { Outfit } from "next/font/google";

import { toast, Toaster } from "sonner";
import { Toaster } from "sonner";

import "./globals.css";

Expand Down
7 changes: 4 additions & 3 deletions client/components/home/HomeMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -318,14 +318,15 @@ export default function HomeMap({ className }: HomeMapProps) {

initMap();

const markersInstance = markersRef.current; // Capture ref value

return () => {
isCancelled = true;
cleanupRef.current();

// Safe Cleanup
const markers = markersRef.current;
if (markers.source) markers.source.remove();
if (markers.dest) markers.dest.remove();
if (markersInstance.source) markersInstance.source.remove();
if (markersInstance.dest) markersInstance.dest.remove();

mapRef.current = null;
};
Expand Down
2 changes: 2 additions & 0 deletions client/components/profile/ProfileCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export default function ProfileCard({ user }: { user: UserData }) {
<div className="relative">
{user.picture ? (
<Image
width={100}
height={100}
Comment on lines +29 to +30

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.

⚠️ Potential issue | 🟡 Minor

Image intrinsic size doesn't match rendered CSS size.

h-28 w-28 resolves to 112×112 px, but width={100} height={100} tells Next.js to generate a 100 px source image. This causes upscaling and slight blur.

Proposed fix
-                width={100}
-                height={100}
+                width={112}
+                height={112}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
width={100}
height={100}
width={112}
height={112}
🤖 Prompt for AI Agents
In `@client/components/profile/ProfileCard.tsx` around lines 29 - 30, The Next.js
Image in ProfileCard.tsx is being rendered with CSS classes h-28 w-28
(112×112px) but is created with width={100} height={100}, causing upscaling and
blur; update the Image props (the width and height attributes on the Image
component in ProfileCard) to match the rendered CSS size (use width={112}
height={112}) or alternatively switch to a responsive/sizes approach (e.g.,
remove fixed width/height and use layout/sizes) so the generated source matches
the displayed h-28 w-28 dimensions.

src={user.picture}
alt={user.name}
className="h-28 w-28 rounded-2xl border-4 border-white object-cover shadow-lg"
Expand Down
18 changes: 18 additions & 0 deletions client/components/profile/SavedRouteItemClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client";

import { Navigation } from "lucide-react";

interface SavedRouteLinkProps {
routeId: string;
}

export default function SavedRouteLink({
routeId: _routeId,

Check warning on line 10 in client/components/profile/SavedRouteItemClient.tsx

View workflow job for this annotation

GitHub Actions / Lint

'_routeId' is defined but never used
}: SavedRouteLinkProps) {
// This could handle navigation or tracking in the future
return (
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-emerald-50 text-[#2bee6c] transition-colors group-hover:bg-[#2bee6c] group-hover:text-white">
<Navigation className="h-5 w-5" />
</div>
);
}
136 changes: 91 additions & 45 deletions client/components/profile/SavedRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,75 +1,121 @@
import { Clock, MapPin, Navigation } from "lucide-react";
import { cookies } from "next/headers";
import Link from "next/link";

const savedRoutes = [
{
name: "Home to Office",
from: "Connaught Place",
to: "Cyber City, Gurugram",
aqiScore: 62,
lastUsed: "Today",
},
{
name: "Morning Jog Route",
from: "Lodhi Garden Gate 1",
to: "Lodhi Garden Gate 3",
aqiScore: 45,
lastUsed: "Yesterday",
},
{
name: "Weekend Market",
from: "Sarojini Nagar Metro",
to: "Sarojini Nagar Market",
aqiScore: 89,
lastUsed: "3 days ago",
},
];
import { Clock, Inbox, MapPin } from "lucide-react";

import type { ISavedRoute } from "../saved-routes/types";
import SavedRouteItemClient from "./SavedRouteItemClient";

function getAqiBadge(aqi: number) {
if (aqi <= 50) return { label: "Good", color: "bg-green-100 text-green-700" };
if (aqi <= 50)
return { label: "Good", color: "bg-emerald-100 text-emerald-700" };
if (aqi <= 100)
return { label: "Moderate", color: "bg-yellow-100 text-yellow-700" };
return { label: "Poor", color: "bg-red-100 text-red-700" };
}

export default function SavedRoutes() {
async function getTopRoutes(): Promise<ISavedRoute[]> {
const cookieStore = await cookies();
const refreshToken = cookieStore.get("refreshToken");

if (!refreshToken) return [];

try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/v1/saved-routes`,
{
headers: {
Cookie: `refreshToken=${refreshToken.value}`,
},
next: { revalidate: 0 }, // Ensure fresh data
}
);

if (!res.ok) return [];
const data = await res.json();
return data.success && data.routes ? data.routes.slice(0, 3) : [];
} catch (error) {
console.error("Failed to fetch routes on server:", error);
return [];
}
}

export default async function SavedRoutes() {
const routes = await getTopRoutes();

if (routes.length === 0) {
return (
<div className="mt-6 rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm">
<div className="flex flex-col items-center justify-center">
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-slate-50">
<Inbox className="h-8 w-8 text-slate-300" />
</div>
<h3 className="text-lg font-semibold text-slate-900">
No Routes Found
</h3>
<p className="mx-auto mt-2 max-w-xs text-sm text-slate-500">
You haven&apos;t saved any routes yet. Start exploring to find the
cleanest paths for your journey.
</p>
<Link
href="/home"
className="mt-6 inline-block rounded-xl bg-[#2bee6c] px-6 py-2.5 text-sm font-bold text-[#102216] shadow-md shadow-[#2bee6c]/10 transition-all hover:opacity-90 active:scale-95"
>
Find a Route
</Link>
</div>
</div>
);
}

return (
<div className="mt-6 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold text-slate-900">Saved Routes</h3>
<span className="text-sm text-slate-400">
{savedRoutes.length} routes
<span className="rounded-full bg-slate-50 px-2 py-1 text-xs font-medium text-slate-400">
Showing Top {routes.length}
</span>
</div>
<div className="space-y-3">
{savedRoutes.map((route) => {
const badge = getAqiBadge(route.aqiScore);
{routes.map((route) => {
const aqi = route.routes?.[0]?.lastComputedScore ?? 0;
const badge = getAqiBadge(aqi);
Comment on lines +81 to +82

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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd "types.ts" -o "interface.ts" -o "*types*" | head -20

Repository: kaihere14/BreathClean

Length of output: 208


🏁 Script executed:

rg "IRouteOption|lastComputedScore" -t ts -t tsx --max-count=50

Repository: kaihere14/BreathClean

Length of output: 92


🏁 Script executed:

rg "getAqiBadge" -t ts -t tsx -A 10

Repository: kaihere14/BreathClean

Length of output: 92


🏁 Script executed:

cat -n client/components/profile/SavedRoutes.tsx | sed -n '75,90p'

Repository: kaihere14/BreathClean

Length of output: 717


🏁 Script executed:

find . -type f -name "*.ts" -o -name "*.tsx" | grep -E "(type|interface)" | head -20

Repository: kaihere14/BreathClean

Length of output: 238


🏁 Script executed:

rg "IRouteOption" --max-count=20

Repository: kaihere14/BreathClean

Length of output: 521


🏁 Script executed:

rg "getAqiBadge" -A 15

Repository: kaihere14/BreathClean

Length of output: 7056


🏁 Script executed:

rg "lastComputedScore" -B 3 -A 3

Repository: kaihere14/BreathClean

Length of output: 9106


AQI fallback of 0 is misleading — missing data displays as "Good" air quality.

When lastComputedScore is missing or routes is empty, the fallback ?? 0 results in getAqiBadge(0) returning "Good". This contradicts the intent: users see excellent air quality when no data is available. The codebase already handles this correctly elsewhere (e.g., RouteInsightsPanel.tsx), where null/undefined scores display as "N/A". Align this component by explicitly handling the no-data case.

Suggested fix

Change the getAqiBadge function to accept and handle null values:

-function getAqiBadge(aqi: number) {
+function getAqiBadge(aqi: number | null | undefined) {
+  if (aqi == null)
+    return { label: "N/A", color: "bg-slate-100 text-slate-500" };
   if (aqi <= 50)
     return { label: "Good", color: "bg-emerald-100 text-emerald-700" };

Then update line 81:

-          const aqi = route.routes?.[0]?.lastComputedScore ?? 0;
+          const aqi = route.routes?.[0]?.lastComputedScore;

And update the display to handle null:

-                  AQI {aqi} · {badge.label}
+                  {aqi !== null && aqi !== undefined ? `AQI ${aqi} · ` : ""}{badge.label}
🤖 Prompt for AI Agents
In `@client/components/profile/SavedRoutes.tsx` around lines 81 - 82, The AQI
fallback currently uses "const aqi = route.routes?.[0]?.lastComputedScore ?? 0"
which causes missing scores to be treated as 0 and renders "Good"; instead
detect missing data and pass null/undefined through so the UI shows "N/A".
Update SavedRoutes.tsx to read the score from
route.routes?.[0]?.lastComputedScore without defaulting to 0 (e.g., let aqi =
route.routes?.[0]?.lastComputedScore), ensure getAqiBadge can accept
null/undefined (or add a local conditional) and change the display logic that
renders badge/text to handle a null score by showing "N/A" rather than calling
getAqiBadge(0).


const date = new Date(route.updatedAt);
const lastUsed = date.toLocaleDateString("en-IN", {
day: "numeric",
month: "short",
});

return (
<div
key={route.name}
className="flex items-center gap-4 rounded-xl border border-slate-100 p-4 transition-colors hover:bg-slate-50"
<Link
key={route._id}
href={`/saved-routes?id=${route._id}`}
className="group flex items-center gap-4 rounded-xl border border-slate-100 p-4 transition-all hover:border-[#2bee6c]/30 hover:bg-slate-50/50"
>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-emerald-50">
<Navigation className="text-bc-primary h-5 w-5" />
</div>
<SavedRouteItemClient routeId={route._id} />
<div className="min-w-0 flex-1">
<p className="font-medium text-slate-900">{route.name}</p>
<p className="truncate text-sm text-slate-500">
<p className="truncate font-semibold text-slate-900">
{route.name || "Untitled Route"}
</p>
<p className="mt-0.5 truncate text-xs text-slate-500">
<MapPin className="mr-1 inline h-3 w-3" />
{route.from} → {route.to}
{route.from.address.split(",")[0]} →{" "}
{route.to.address.split(",")[0]}
</p>
</div>
<div className="flex shrink-0 flex-col items-end gap-1">
<div className="flex shrink-0 flex-col items-end gap-1.5">
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium ${badge.color}`}
className={`rounded-full px-2.5 py-1 text-[10px] font-bold ${badge.color} border border-current opacity-90`}
>
AQI {route.aqiScore} · {badge.label}
AQI {aqi} · {badge.label}
</span>
<span className="flex items-center gap-1 text-xs text-slate-400">
<span className="flex items-center gap-1 text-[10px] font-medium text-slate-400">
<Clock className="h-3 w-3" />
{route.lastUsed}
{lastUsed}
</span>
</div>
</div>
</Link>
);
})}
</div>
Expand Down
47 changes: 33 additions & 14 deletions client/components/routes/RouteDiscoveryPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
"use client";

import { useState } from "react";

import { useRouter } from "next/navigation";

import {
Activity,
Bike,
Bookmark,
Car,
ChevronLeft,
CircleDot,
Info,
MapPin,
Expand All @@ -24,6 +23,8 @@ type RouteDiscoveryPanelProps = {
onModeChange: (mode: TravelMode) => void;
routeName: string;
onRouteNameChange: (name: string) => void;
onSaveRoute: () => void;
canSave: boolean;
};

export default function RouteDiscoveryPanel({
Expand All @@ -33,6 +34,8 @@ export default function RouteDiscoveryPanel({
onModeChange,
routeName,
onRouteNameChange,
onSaveRoute,
canSave,
}: RouteDiscoveryPanelProps) {
const router = useRouter();

Expand All @@ -43,13 +46,24 @@ export default function RouteDiscoveryPanel({
return (
<aside className="absolute top-6 left-6 z-40 w-80">
<div className="flex flex-col overflow-hidden rounded-xl border border-white bg-white/95 shadow-2xl backdrop-blur-xl dark:border-slate-700 dark:bg-slate-900/95">
<div className="border-b border-slate-100 p-5 dark:border-slate-800">
<h1 className="mb-1 text-lg font-bold text-slate-800 dark:text-white">
Route Discovery
</h1>
<p className="text-xs text-slate-500 dark:text-slate-400">
Find the path with lowest pollution
</p>
<div className="flex items-center justify-between border-b border-slate-100 p-5 dark:border-slate-800">
<div className="flex items-center justify-center gap-3">
<button
onClick={handleChangeRoute}
className="flex items-center justify-center rounded-lg bg-slate-100 p-2 text-slate-600 transition-all hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700"
title="Change Route"
>
<ChevronLeft size={18} />
</button>
<div>
<h1 className="mb-1 text-lg font-bold text-slate-800 dark:text-white">
Route Discovery
</h1>
<p className="text-xs text-slate-500 dark:text-slate-400">
Find the path with lowest pollution
</p>
</div>
</div>
</div>

<div className="space-y-5 overflow-y-auto p-5">
Expand Down Expand Up @@ -166,11 +180,16 @@ export default function RouteDiscoveryPanel({

<div className="border-t border-slate-100 bg-slate-50 p-5 dark:border-slate-800 dark:bg-slate-800/50">
<button
onClick={handleChangeRoute}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-[#2bee6c] py-3.5 font-bold text-slate-900 shadow-lg shadow-[#2bee6c]/20 transition-all hover:bg-[#2bee6c]/90"
onClick={onSaveRoute}
disabled={!canSave}
className={`flex w-full items-center justify-center gap-2 rounded-xl py-3.5 font-bold shadow-lg transition-all ${
canSave
? "bg-[#2bee6c] text-slate-900 shadow-[#2bee6c]/20 hover:bg-[#2bee6c]/90"
: "cursor-not-allowed bg-slate-200 text-slate-400 dark:bg-slate-700 dark:text-slate-500"
}`}
>
<Activity className="h-5 w-5" />
Change Route
<Bookmark className="h-5 w-5" />
Save Route
</button>
</div>
</div>
Expand Down
Loading