-
Notifications
You must be signed in to change notification settings - Fork 3
feat: Add user profile page with detailed card, display saved routes, and introduce a route discovery panel. #10
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
Changes from all commits
4753ee4
84f3c35
558e13e
e16f2df
0598536
a5c32d8
3385dee
e8e953e
0e1e982
e430043
b870060
0faee97
b35e17f
4650c18
b01b129
4ab28ec
ecbae17
a8da103
02db6ae
24f4a94
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 | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
Contributor
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. Image intrinsic size doesn't match rendered CSS size.
Proposed fix- width={100}
- height={100}
+ width={112}
+ height={112}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| src={user.picture} | ||||||||||
| alt={user.name} | ||||||||||
| className="h-28 w-28 rounded-2xl border-4 border-white object-cover shadow-lg" | ||||||||||
|
|
||||||||||
| 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, | ||
| }: 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> | ||
| ); | ||
| } | ||
| 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'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
Contributor
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. 🧩 Analysis chain🏁 Script executed: fd "types.ts" -o "interface.ts" -o "*types*" | head -20Repository: kaihere14/BreathClean Length of output: 208 🏁 Script executed: rg "IRouteOption|lastComputedScore" -t ts -t tsx --max-count=50Repository: kaihere14/BreathClean Length of output: 92 🏁 Script executed: rg "getAqiBadge" -t ts -t tsx -A 10Repository: 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 -20Repository: kaihere14/BreathClean Length of output: 238 🏁 Script executed: rg "IRouteOption" --max-count=20Repository: kaihere14/BreathClean Length of output: 521 🏁 Script executed: rg "getAqiBadge" -A 15Repository: kaihere14/BreathClean Length of output: 7056 🏁 Script executed: rg "lastComputedScore" -B 3 -A 3Repository: kaihere14/BreathClean Length of output: 9106 AQI fallback of When Suggested fixChange the -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 |
||
|
|
||
| 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> | ||
|
|
||
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.
Non-deterministic fallback for
lastComputedScorepersists random data to the backend.When
route.aqiScoreis falsy,Math.floor(Math.random() * 100)generates a random score that gets saved to the database. This means:Consider using a sentinel value (e.g.,
0ornull) to clearly indicate "no score available" rather than fabricating data.📝 Committable suggestion
🤖 Prompt for AI Agents