Skip to content

scroller site#105

Open
wabinyai wants to merge 6 commits into
stagingfrom
feature/admin-controls
Open

scroller site#105
wabinyai wants to merge 6 commits into
stagingfrom
feature/admin-controls

Conversation

@wabinyai

@wabinyai wabinyai commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a dynamic daily air quality forecast ticker on the home page, including geolocation-based selection plus pause/resume and saved viewing preferences.
  • Style

    • Improved responsive UI across the Categorize, Locate, and Reports pages (better spacing, button sizing, and map/legend layout on small screens).
    • Refreshed chart and map visualization spacing, and updated global typography and ticker scrolling behavior (with reduced-motion support).
  • Bug Fixes

    • Refined hourly forecast display to start at the current hour for more accurate timelines.

@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
air-track Ready Ready Preview, Comment Jun 25, 2026 12:59pm
air-vista Ready Ready Preview, Comment Jun 25, 2026 12:59pm
breeze-mind Ready Ready Preview, Comment Jun 25, 2026 12:59pm
clean-aria Ready Ready Preview, Comment Jun 25, 2026 12:59pm
clean-stats Ready Ready Preview, Comment Jun 25, 2026 12:59pm
frontend Ready Ready Preview, Comment Jun 25, 2026 12:59pm
pure-sphere Ready Ready Preview, Comment Jun 25, 2026 12:59pm

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@wabinyai, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 12 minutes and 25 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 62b4c7b4-1667-492e-ac5c-54e9c6021906

📥 Commits

Reviewing files that changed from the base of the PR and between e93d285 and 4bb0b48.

📒 Files selected for processing (2)
  • frontend/package.json
  • frontend/src/app/page.tsx
📝 Walkthrough

Walkthrough

This PR adds a homepage daily forecast ticker, filters hourly forecast display to the current hour onward, exports MapNode, and updates chart and page layouts with responsive styling changes.

Changes

Daily Forecast Ticker and Platform Styling

Layer / File(s) Summary
API exports and ticker styling
frontend/src/services/apiService.tsx, frontend/src/styles/globals.css
MapNode is exported, the body font family changes to Geist Sans with fallbacks, and ticker scrolling/pause/reduced-motion CSS is added.
Homepage ticker and layout
frontend/src/app/page.tsx
DailyForecastTicker loads forecasts, map nodes, saved state, and location data, then renders matched forecasts on the homepage while the homepage layout and calls to action are redesigned.

Hourly Forecast & Responsive Layouts

Layer / File(s) Summary
Hourly forecast time window filtering
frontend/src/components/map/LeafletMap.tsx
Hourly forecast rendering uses visibleChartData starting at the current hour and applies that filtered dataset through grouping, selection, hover, and empty-state logic.
Responsive charts and pages
frontend/src/components/charts/AirQualityChart.tsx, frontend/src/app/categorize/page.tsx, frontend/src/app/locate/page.tsx, frontend/src/app/reports/page.tsx
Chart cards and page layouts gain breakpoint-aware padding, grid, width, height, and visibility changes for smaller screens.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

☀️ A ticker drifts across the page,
⏳ Hours trim their starting stage,
🌍 Maps and forecasts find their way,
📱 Screens adapt for night and day,
🎨 A cleaner layout joins the fray.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is too vague to clearly describe the main changes, which center on a new scrolling homepage ticker and UI layout updates. Use a specific title like "Add daily air quality forecast ticker to homepage".
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/admin-controls

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • JIRA integration encountered authorization issues. Please disconnect and reconnect the integration in the CodeRabbit UI.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (6)
frontend/src/components/map/LeafletMap.tsx (2)

1344-1345: 💤 Low value

Consider clarifying the empty state message.

The empty state check correctly uses visibleChartData.length, which is the right behavior for time-window-aware rendering.

The message "No hourly forecast data." could optionally be made more specific to indicate that data might exist but is outside the current time window (e.g., "No hourly forecast data available from this time onwards."). However, the current message is acceptable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/map/LeafletMap.tsx` around lines 1344 - 1345, Update
the empty-state text in the LeafletMap component where it checks
visibleChartData.length to make it clearer the absence may be due to the
selected time window; replace "No hourly forecast data." with a more specific
message such as "No hourly forecast data available for the selected time
window." so users know data might exist but is outside the current time range.

1115-1122: ⚡ Quick win

Consider adding a brief comment to clarify the filtering intent.

The time-window filtering logic is correct and well-implemented. However, a short comment explaining that this filters to show only forecast hours from the current hour onwards would improve code maintainability for future developers.

📝 Suggested comment
+  // Filter forecast data to show only hours from the current hour onwards
   const visibleChartData = useMemo(() => {
     if (!currentTime) return chartData
 
     const forecastWindowStart = new Date(currentTime)
     forecastWindowStart.setMinutes(0, 0, 0)
 
     return chartData.filter((row) => row.time >= forecastWindowStart.getTime())
   }, [chartData, currentTime])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/map/LeafletMap.tsx` around lines 1115 - 1122, Add a
brief inline comment above the useMemo that computes visibleChartData explaining
the intent: it filters chartData to only include forecast rows from the current
hour onward by creating forecastWindowStart (a Date derived from currentTime
with minutes/seconds/milliseconds zeroed) and comparing row.time to
forecastWindowStart.getTime(); reference visibleChartData, forecastWindowStart,
chartData, currentTime and useMemo so future readers understand the time-window
filtering behavior.
frontend/src/components/charts/AirQualityChart.tsx (1)

457-692: ⚡ Quick win

Consider applying the same responsive patterns to WeeklyComparisonChart and AQIIndexVisual.

The responsive header padding (p-4 sm:p-6), grid-based control layouts, and responsive content padding (p-3 sm:p-6) were applied to PM25BarChart and AQICategoryChart, but WeeklyComparisonChart (lines 457-692) and AQIIndexVisual (lines 695-803) still use the older layout patterns.

While this may be intentional based on the layer scope, applying consistent responsive patterns across all chart components would improve maintainability and user experience uniformity.

♻️ Example responsive updates for WeeklyComparisonChart

For the CardHeader (around line 553):

-      <CardHeader>
+      <CardHeader className="p-4 sm:p-6">

For the CardContent (around line 632):

-      <CardContent>
+      <CardContent className="p-3 sm:p-6">

And optionally update the control rows to use the grid pattern for consistency.

Also applies to: 695-803

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/charts/AirQualityChart.tsx` around lines 457 - 692,
WeeklyComparisonChart and AQIIndexVisual use older layout patterns; update their
CardHeader and CardContent to match the responsive patterns used elsewhere
(e.g., add header padding classes like p-4 sm:p-6 and content padding like p-3
sm:p-6), refactor the controls container (the div that holds the Select
components) to a responsive grid (e.g., grid with grid-cols-1 sm:grid-cols-2
md:grid-cols-4 and gap utilities) so controls wrap consistently, and ensure the
warning paragraph and chart container retain appropriate spacing (adjust
margins/padding to match the new CardContent spacing); locate these changes
around the WeeklyComparisonChart and AQIIndexVisual components to apply the same
class updates.
frontend/src/services/apiService.tsx (1)

44-60: ⚡ Quick win

Use the exported MapNode as the single contract.

Now that MapNode is exported here, frontend/src/components/map/LeafletMap.tsx should import this type instead of keeping a second local interface MapNode. Leaving both in place makes the API shape easy to drift out of sync across the homepage ticker and map views.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/services/apiService.tsx` around lines 44 - 60, Replace the
duplicate local interface in LeafletMap.tsx with the exported MapNode from
frontend/src/services/apiService.tsx: remove the local interface MapNode
declaration in LeafletMap.tsx and add an import for MapNode (the exported
symbol) then update any references in the file to use that imported MapNode type
so both map and other components share the single contract.
frontend/src/app/page.tsx (1)

1-3: ⚡ Quick win

Keep the page server-rendered and isolate the ticker behind its own client boundary.

Only DailyForecastTicker needs browser APIs, but the page-level "use client" moves the entire landing page tree into the client bundle. Extract the ticker into a separate client component and keep page.tsx as a server component so the rest of the homepage stays lighter.

Also applies to: 433-643

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/page.tsx` around lines 1 - 3, page.tsx is marked as a client
component but only DailyForecastTicker needs browser APIs; remove the top-level
"use client" from page.tsx so the page remains a server component, and move the
ticker logic into its own client component file (e.g., DailyForecastTicker.tsx)
that begins with "use client" and contains all useEffect/useState hooks and
browser-only code from the previous DailyForecastTicker; then update the import
in page.tsx to import the new DailyForecastTicker client component (default or
named) so the server-rendered page composes the ticker as a client boundary
while the rest of the homepage stays server-rendered.
frontend/src/app/locate/page.tsx (1)

297-297: 💤 Low value

Consider map legend visibility on mobile.

The legend is completely hidden on small screens (hidden sm:block), which means mobile users won't have a reference for understanding marker symbols. While marker popups provide this information on interaction, having the legend visible could improve immediate comprehension.

Consider either:

  1. Keeping a more compact version of the legend visible on mobile
  2. Adding a collapsible legend button on mobile
  3. Accepting the current UX if analytics show mobile users primarily interact with markers anyway

The current implementation is functional since popups fill the gap, but it's worth evaluating mobile user behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/locate/page.tsx` at line 297, The legend div currently uses
the Tailwind classes "hidden sm:block" which fully hides it on mobile; update
the UI so mobile users can access a compact or collapsible legend: either remove
the "hidden" and replace with a mobile-compact variant (e.g., smaller padding,
icons-only) or implement a toggle button and state (e.g., showLegend boolean)
that renders a compact Legend component on screens <sm and the full legend on
>=sm; locate the div with className "absolute bottom-5 left-5 z-[1000] hidden
rounded-2xl border border-white/70 bg-white/95 p-3 shadow-lg backdrop-blur
sm:block" in page.tsx and refactor it into a responsive Legend component plus an
optional mobile button to open/close it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/app/page.tsx`:
- Around line 131-135: The formatForecastDate function treats "YYYY-MM-DD"
strings as UTC, causing a day shift for some users; update formatForecastDate to
detect date-only inputs (e.g., /^\d{4}-\d{2}-\d{2}$/) and construct a local Date
via new Date(year, monthIndex, day) instead of new Date(value), then fall back
to the existing parsing for other inputs and keep returning the
toLocaleDateString with { weekday: "short", month: "short", day: "numeric" } as
before.
- Around line 154-185: The resolveCountry function currently falls back to the
nearest MapNode anywhere in the world; guard that fallback with a configurable
coverage radius (e.g., const FALLBACK_RADIUS_KM) and only return the nearest
node's siteDetails.country if its distance (computed with getDistanceInKm) is <=
FALLBACK_RADIUS_KM; otherwise return undefined/null. Update the reduce logic in
resolveCountry (or compute nearest with a loop) to capture the nearest distance
and compare it to FALLBACK_RADIUS_KM before returning
node.siteDetails?.country?.trim(), and add the new constant and unit tests or
comments indicating the chosen radius.
- Around line 265-269: The error callback currently unconditionally sets state
to "denied" and persists that to FORECAST_TICKER_LOCATION_STATE_KEY; instead
inspect the GeolocationPositionError.code in the callback (compare against
error.PERMISSION_DENIED, error.POSITION_UNAVAILABLE, error.TIMEOUT) and map to
distinct states (e.g., "denied" for PERMISSION_DENIED, "unavailable" for
POSITION_UNAVAILABLE, "timeout" for TIMEOUT, or "error" fallback), call
setLocationState with that mapped value, store that mapped value to
FORECAST_TICKER_LOCATION_STATE_KEY, and only remove FORECAST_TICKER_COUNTRY_KEY
when the mapped state is "denied" (permission permanently denied) so temporary
failures don’t persist as "denied".

---

Nitpick comments:
In `@frontend/src/app/locate/page.tsx`:
- Line 297: The legend div currently uses the Tailwind classes "hidden sm:block"
which fully hides it on mobile; update the UI so mobile users can access a
compact or collapsible legend: either remove the "hidden" and replace with a
mobile-compact variant (e.g., smaller padding, icons-only) or implement a toggle
button and state (e.g., showLegend boolean) that renders a compact Legend
component on screens <sm and the full legend on >=sm; locate the div with
className "absolute bottom-5 left-5 z-[1000] hidden rounded-2xl border
border-white/70 bg-white/95 p-3 shadow-lg backdrop-blur sm:block" in page.tsx
and refactor it into a responsive Legend component plus an optional mobile
button to open/close it.

In `@frontend/src/app/page.tsx`:
- Around line 1-3: page.tsx is marked as a client component but only
DailyForecastTicker needs browser APIs; remove the top-level "use client" from
page.tsx so the page remains a server component, and move the ticker logic into
its own client component file (e.g., DailyForecastTicker.tsx) that begins with
"use client" and contains all useEffect/useState hooks and browser-only code
from the previous DailyForecastTicker; then update the import in page.tsx to
import the new DailyForecastTicker client component (default or named) so the
server-rendered page composes the ticker as a client boundary while the rest of
the homepage stays server-rendered.

In `@frontend/src/components/charts/AirQualityChart.tsx`:
- Around line 457-692: WeeklyComparisonChart and AQIIndexVisual use older layout
patterns; update their CardHeader and CardContent to match the responsive
patterns used elsewhere (e.g., add header padding classes like p-4 sm:p-6 and
content padding like p-3 sm:p-6), refactor the controls container (the div that
holds the Select components) to a responsive grid (e.g., grid with grid-cols-1
sm:grid-cols-2 md:grid-cols-4 and gap utilities) so controls wrap consistently,
and ensure the warning paragraph and chart container retain appropriate spacing
(adjust margins/padding to match the new CardContent spacing); locate these
changes around the WeeklyComparisonChart and AQIIndexVisual components to apply
the same class updates.

In `@frontend/src/components/map/LeafletMap.tsx`:
- Around line 1344-1345: Update the empty-state text in the LeafletMap component
where it checks visibleChartData.length to make it clearer the absence may be
due to the selected time window; replace "No hourly forecast data." with a more
specific message such as "No hourly forecast data available for the selected
time window." so users know data might exist but is outside the current time
range.
- Around line 1115-1122: Add a brief inline comment above the useMemo that
computes visibleChartData explaining the intent: it filters chartData to only
include forecast rows from the current hour onward by creating
forecastWindowStart (a Date derived from currentTime with
minutes/seconds/milliseconds zeroed) and comparing row.time to
forecastWindowStart.getTime(); reference visibleChartData, forecastWindowStart,
chartData, currentTime and useMemo so future readers understand the time-window
filtering behavior.

In `@frontend/src/services/apiService.tsx`:
- Around line 44-60: Replace the duplicate local interface in LeafletMap.tsx
with the exported MapNode from frontend/src/services/apiService.tsx: remove the
local interface MapNode declaration in LeafletMap.tsx and add an import for
MapNode (the exported symbol) then update any references in the file to use that
imported MapNode type so both map and other components share the single
contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d0a07baa-e251-4bb2-96d8-082c70fa3092

📥 Commits

Reviewing files that changed from the base of the PR and between 2a81171 and 3006282.

📒 Files selected for processing (8)
  • frontend/src/app/categorize/page.tsx
  • frontend/src/app/locate/page.tsx
  • frontend/src/app/page.tsx
  • frontend/src/app/reports/page.tsx
  • frontend/src/components/charts/AirQualityChart.tsx
  • frontend/src/components/map/LeafletMap.tsx
  • frontend/src/services/apiService.tsx
  • frontend/src/styles/globals.css

Comment thread frontend/src/app/page.tsx
Comment on lines +131 to +135
function formatForecastDate(value: string) {
const date = new Date(value)
if (Number.isNaN(date.getTime())) return "Daily outlook"
return date.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" })
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Parse date-only forecast strings as local dates.

new Date("YYYY-MM-DD") is interpreted as UTC, so users west of UTC can see the previous day in the ticker. This will shift the forecast label by a day for some visitors.

Suggested fix
 function formatForecastDate(value: string) {
-  const date = new Date(value)
+  const [year, month, day] = value.split("-").map(Number)
+  const date =
+    Number.isInteger(year) && Number.isInteger(month) && Number.isInteger(day)
+      ? new Date(year, month - 1, day)
+      : new Date(value)
   if (Number.isNaN(date.getTime())) return "Daily outlook"
   return date.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" })
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/page.tsx` around lines 131 - 135, The formatForecastDate
function treats "YYYY-MM-DD" strings as UTC, causing a day shift for some users;
update formatForecastDate to detect date-only inputs (e.g.,
/^\d{4}-\d{2}-\d{2}$/) and construct a local Date via new Date(year, monthIndex,
day) instead of new Date(value), then fall back to the existing parsing for
other inputs and keep returning the toLocaleDateString with { weekday: "short",
month: "short", day: "numeric" } as before.

Comment thread frontend/src/app/page.tsx
Comment on lines +154 to +185
async function resolveCountry(latitude: number, longitude: number, mapNodes: MapNode[]) {
const mapboxToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN
if (mapboxToken) {
const url = new URL("https://api.mapbox.com/search/geocode/v6/reverse")
url.searchParams.set("longitude", String(longitude))
url.searchParams.set("latitude", String(latitude))
url.searchParams.set("types", "country")
url.searchParams.set("limit", "1")
url.searchParams.set("access_token", mapboxToken)

try {
const response = await fetch(url, { cache: "no-store" })
if (response.ok) {
const data = (await response.json()) as MapboxCountryResponse
const country =
data.features?.[0]?.properties?.name ||
data.features?.[0]?.properties?.context?.country?.name
if (country) return country
}
} catch (error) {
console.error("Unable to reverse geocode visitor country:", error)
}
}

return mapNodes
.filter((node) => Boolean(node.siteDetails?.country))
.reduce<{ node: MapNode; distance: number } | null>((nearest, node) => {
const distance = getDistanceInKm(latitude, longitude, node)
return !nearest || distance < nearest.distance ? { node, distance } : nearest
}, null)
?.node.siteDetails?.country?.trim()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard the nearest-node fallback with a coverage radius.

If reverse geocoding is unavailable, this code always assigns the country of the closest AirQo node anywhere in the world. For users outside the monitored footprint, that can silently select the wrong country and show irrelevant forecasts. Return no country unless the nearest node is within a sane fallback radius.

Suggested fix
 async function resolveCountry(latitude: number, longitude: number, mapNodes: MapNode[]) {
+  const MAX_FALLBACK_DISTANCE_KM = 250
   const mapboxToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN
   if (mapboxToken) {
     const url = new URL("https://api.mapbox.com/search/geocode/v6/reverse")
@@
-  return mapNodes
+  const nearest = mapNodes
     .filter((node) => Boolean(node.siteDetails?.country))
     .reduce<{ node: MapNode; distance: number } | null>((nearest, node) => {
       const distance = getDistanceInKm(latitude, longitude, node)
       return !nearest || distance < nearest.distance ? { node, distance } : nearest
     }, null)
-    ?.node.siteDetails?.country?.trim()
+
+  return nearest && nearest.distance <= MAX_FALLBACK_DISTANCE_KM
+    ? nearest.node.siteDetails?.country?.trim()
+    : undefined
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/page.tsx` around lines 154 - 185, The resolveCountry
function currently falls back to the nearest MapNode anywhere in the world;
guard that fallback with a configurable coverage radius (e.g., const
FALLBACK_RADIUS_KM) and only return the nearest node's siteDetails.country if
its distance (computed with getDistanceInKm) is <= FALLBACK_RADIUS_KM; otherwise
return undefined/null. Update the reduce logic in resolveCountry (or compute
nearest with a loop) to capture the nearest distance and compare it to
FALLBACK_RADIUS_KM before returning node.siteDetails?.country?.trim(), and add
the new constant and unit tests or comments indicating the chosen radius.

Comment thread frontend/src/app/page.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
frontend/src/app/page.tsx (3)

462-495: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Close the map preview with </Link>, not </div>.

Line 462 opens a Link, but Line 495 closes a div, which breaks JSX parsing.

Suggested fix
-            </div>
+            </Link>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/page.tsx` around lines 462 - 495, The map preview block in
page.tsx is mismatched: the card starts with a Link but is closed with a div,
which breaks JSX. Update the closing tag at the end of this preview section so
it matches the opening Link component, keeping the nested Image and overlay divs
unchanged.

Source: Linters/SAST tools


643-681: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Replace the stray </footer> in ProcessCard.

ProcessCard opens div elements only; the </footer> at Line 678 makes the component invalid JSX.

Suggested fix
-      </footer>
+      </div>
     </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/page.tsx` around lines 643 - 681, ProcessCard contains an
invalid closing tag: it opens only div elements, so the stray </footer> must be
removed and the JSX closed with the matching div tags only. Update the
ProcessCard component in page.tsx to ensure the wrapper structure around the
Image, icon, and number sections is properly balanced and valid JSX.

Source: Linters/SAST tools


560-607: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix the workflow section tag balance.

The first section closes before its container div, and Line 606 adds an extra </div> in the next section.

Suggested fix
-        </section>
+        </div>
+      </section>
 
         <section className="py-16 sm:py-20">
...
-        </div>
       </section>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/page.tsx` around lines 560 - 607, The section markup in the
workflow area is unbalanced: the first section closes before its container div,
and there is an extra closing div before the next section. Update the JSX in
page.tsx around the “How AirQo AI Works” and “Built for the full decision
journey.” blocks so each opening section/div has a matching close, keeping the
container hierarchy intact and preserving the workflowSteps map content inside
the correct section.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/app/page.tsx`:
- Around line 501-557: The features block in page.tsx has an unbalanced JSX
structure because the section is closed before the FeatureCard grid starts.
Remove the premature closing tag in the main section and keep the container/div
hierarchy intact so the heading, description, and grid all remain inside the
same section; verify the surrounding JSX around the section that contains
FeatureCard and the text heading is properly nested.
- Around line 357-377: The nearest-country fallback in the page selection logic
should be constrained so it only applies when the visitor is actually within the
monitored footprint. Update the `selectedSites`/`nearestForecastNode` branch in
`page.tsx` to avoid using `getCoordinatesForCountry`, `getDistanceInKm`, and the
`collection.forecasts.reduce` fallback for visitors outside supported coverage,
and keep the existing `visitorCountry` selection behavior intact. Use the
existing `selectedSites`, `selectedCountry`, `visitorCoordinates`, and
`visitorCountry` checks to gate the global nearest-country lookup, so irrelevant
forecasts are not shown.

---

Outside diff comments:
In `@frontend/src/app/page.tsx`:
- Around line 462-495: The map preview block in page.tsx is mismatched: the card
starts with a Link but is closed with a div, which breaks JSX. Update the
closing tag at the end of this preview section so it matches the opening Link
component, keeping the nested Image and overlay divs unchanged.
- Around line 643-681: ProcessCard contains an invalid closing tag: it opens
only div elements, so the stray </footer> must be removed and the JSX closed
with the matching div tags only. Update the ProcessCard component in page.tsx to
ensure the wrapper structure around the Image, icon, and number sections is
properly balanced and valid JSX.
- Around line 560-607: The section markup in the workflow area is unbalanced:
the first section closes before its container div, and there is an extra closing
div before the next section. Update the JSX in page.tsx around the “How AirQo AI
Works” and “Built for the full decision journey.” blocks so each opening
section/div has a matching close, keeping the container hierarchy intact and
preserving the workflowSteps map content inside the correct section.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bfc5c732-791d-4a7a-bcdb-026423ae5bd3

📥 Commits

Reviewing files that changed from the base of the PR and between 3006282 and e93d285.

📒 Files selected for processing (7)
  • frontend/src/app/categorize/page.tsx
  • frontend/src/app/locate/page.tsx
  • frontend/src/app/page.tsx
  • frontend/src/app/reports/page.tsx
  • frontend/src/components/map/LeafletMap.tsx
  • frontend/src/services/apiService.tsx
  • frontend/src/styles/globals.css
✅ Files skipped from review due to trivial changes (3)
  • frontend/src/services/apiService.tsx
  • frontend/src/app/categorize/page.tsx
  • frontend/src/app/locate/page.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/src/components/map/LeafletMap.tsx
  • frontend/src/app/reports/page.tsx
  • frontend/src/styles/globals.css

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
frontend/src/app/page.tsx (3)

462-495: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Close the map preview with </Link>, not </div>.

Line 462 opens a Link, but Line 495 closes a div, which breaks JSX parsing.

Suggested fix
-            </div>
+            </Link>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/page.tsx` around lines 462 - 495, The map preview block in
page.tsx is mismatched: the card starts with a Link but is closed with a div,
which breaks JSX. Update the closing tag at the end of this preview section so
it matches the opening Link component, keeping the nested Image and overlay divs
unchanged.

Source: Linters/SAST tools


643-681: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Replace the stray </footer> in ProcessCard.

ProcessCard opens div elements only; the </footer> at Line 678 makes the component invalid JSX.

Suggested fix
-      </footer>
+      </div>
     </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/page.tsx` around lines 643 - 681, ProcessCard contains an
invalid closing tag: it opens only div elements, so the stray </footer> must be
removed and the JSX closed with the matching div tags only. Update the
ProcessCard component in page.tsx to ensure the wrapper structure around the
Image, icon, and number sections is properly balanced and valid JSX.

Source: Linters/SAST tools


560-607: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix the workflow section tag balance.

The first section closes before its container div, and Line 606 adds an extra </div> in the next section.

Suggested fix
-        </section>
+        </div>
+      </section>
 
         <section className="py-16 sm:py-20">
...
-        </div>
       </section>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/page.tsx` around lines 560 - 607, The section markup in the
workflow area is unbalanced: the first section closes before its container div,
and there is an extra closing div before the next section. Update the JSX in
page.tsx around the “How AirQo AI Works” and “Built for the full decision
journey.” blocks so each opening section/div has a matching close, keeping the
container hierarchy intact and preserving the workflowSteps map content inside
the correct section.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/app/page.tsx`:
- Around line 501-557: The features block in page.tsx has an unbalanced JSX
structure because the section is closed before the FeatureCard grid starts.
Remove the premature closing tag in the main section and keep the container/div
hierarchy intact so the heading, description, and grid all remain inside the
same section; verify the surrounding JSX around the section that contains
FeatureCard and the text heading is properly nested.
- Around line 357-377: The nearest-country fallback in the page selection logic
should be constrained so it only applies when the visitor is actually within the
monitored footprint. Update the `selectedSites`/`nearestForecastNode` branch in
`page.tsx` to avoid using `getCoordinatesForCountry`, `getDistanceInKm`, and the
`collection.forecasts.reduce` fallback for visitors outside supported coverage,
and keep the existing `visitorCountry` selection behavior intact. Use the
existing `selectedSites`, `selectedCountry`, `visitorCoordinates`, and
`visitorCountry` checks to gate the global nearest-country lookup, so irrelevant
forecasts are not shown.

---

Outside diff comments:
In `@frontend/src/app/page.tsx`:
- Around line 462-495: The map preview block in page.tsx is mismatched: the card
starts with a Link but is closed with a div, which breaks JSX. Update the
closing tag at the end of this preview section so it matches the opening Link
component, keeping the nested Image and overlay divs unchanged.
- Around line 643-681: ProcessCard contains an invalid closing tag: it opens
only div elements, so the stray </footer> must be removed and the JSX closed
with the matching div tags only. Update the ProcessCard component in page.tsx to
ensure the wrapper structure around the Image, icon, and number sections is
properly balanced and valid JSX.
- Around line 560-607: The section markup in the workflow area is unbalanced:
the first section closes before its container div, and there is an extra closing
div before the next section. Update the JSX in page.tsx around the “How AirQo AI
Works” and “Built for the full decision journey.” blocks so each opening
section/div has a matching close, keeping the container hierarchy intact and
preserving the workflowSteps map content inside the correct section.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bfc5c732-791d-4a7a-bcdb-026423ae5bd3

📥 Commits

Reviewing files that changed from the base of the PR and between 3006282 and e93d285.

📒 Files selected for processing (7)
  • frontend/src/app/categorize/page.tsx
  • frontend/src/app/locate/page.tsx
  • frontend/src/app/page.tsx
  • frontend/src/app/reports/page.tsx
  • frontend/src/components/map/LeafletMap.tsx
  • frontend/src/services/apiService.tsx
  • frontend/src/styles/globals.css
✅ Files skipped from review due to trivial changes (3)
  • frontend/src/services/apiService.tsx
  • frontend/src/app/categorize/page.tsx
  • frontend/src/app/locate/page.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/src/components/map/LeafletMap.tsx
  • frontend/src/app/reports/page.tsx
  • frontend/src/styles/globals.css
🛑 Comments failed to post (2)
frontend/src/app/page.tsx (2)

357-377: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Bound the nearest-country forecast fallback.

When visitorCountry has no forecast sites, this picks the nearest forecast country globally. For visitors outside the monitored footprint, that can show irrelevant forecasts.

Suggested fix
+const NEAREST_FORECAST_FALLBACK_RADIUS_KM = 250
+
 ...
-      if (nearestCountry) {
+      if (nearestCountry && nearestForecastNode.distance <= NEAREST_FORECAST_FALLBACK_RADIUS_KM) {
         selectedCountry = nearestCountry
📝 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.

    const NEAREST_FORECAST_FALLBACK_RADIUS_KM = 250

    if (!selectedSites.length) {
      const origin = visitorCoordinates || getCoordinatesForCountry(visitorCountry, mapNodes)
      const nearestForecastNode = origin
        ? collection.forecasts.reduce<{ node: MapNode; distance: number } | null>((nearest, site) => {
            const node = nodeBySiteId.get(site.site_details.site_id)
            if (!node?.siteDetails?.country) return nearest
            const distance = getDistanceInKm(origin.latitude, origin.longitude, node)
            if (!Number.isFinite(distance)) return nearest
            return !nearest || distance < nearest.distance ? { node, distance } : nearest
          }, null)
        : null
      const nearestCountry = nearestForecastNode?.node.siteDetails.country?.trim()

      if (nearestCountry && nearestForecastNode.distance <= NEAREST_FORECAST_FALLBACK_RADIUS_KM) {
        selectedCountry = nearestCountry
        selectedSites = collection.forecasts.filter(
          (site) =>
            nodeBySiteId.get(site.site_details.site_id)?.siteDetails?.country?.trim().toLowerCase() ===
            nearestCountry.toLowerCase(),
        )
      }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/page.tsx` around lines 357 - 377, The nearest-country
fallback in the page selection logic should be constrained so it only applies
when the visitor is actually within the monitored footprint. Update the
`selectedSites`/`nearestForecastNode` branch in `page.tsx` to avoid using
`getCoordinatesForCountry`, `getDistanceInKm`, and the
`collection.forecasts.reduce` fallback for visitors outside supported coverage,
and keep the existing `visitorCountry` selection behavior intact. Use the
existing `selectedSites`, `selectedCountry`, `visitorCoordinates`, and
`visitorCountry` checks to gate the global nearest-country lookup, so irrelevant
forecasts are not shown.

501-557: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the premature </section> in the features block.

Line 510 closes the section before the feature grid, leaving the container/section tags unbalanced.

Suggested fix
           </div>
-        </section>
 
           <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
📝 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.

      <section className="bg-white py-16 dark:bg-slate-950 md:py-24">
        <div className="container mx-auto px-4">
          <div className="text-center mb-16">
            <h2 className="text-3xl md:text-4xl font-bold mb-4">Powered by Artificial Intelligence</h2>
            <p className="mx-auto max-w-3xl text-lg text-gray-600 dark:text-slate-300">
              Our platform leverages cutting-edge AI to provide accurate, real-time air quality data and insights for
              researchers, policymakers, and citizens.
            </p>
          </div>

          <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
            <FeatureCard
              imageSrc="/images/model/locate.webp"
              Icon={MapPin}
              title="Optimal Site Location"
              description="Use AI algorithms to determine the best locations for air quality monitors based on population density, pollution sources, and geographic factors."
              href="/locate/optimal-site-location"
            />
            <FeatureCard
              Icon={Wind}
              title="Air Quality Categorization"
              description="Automatically categorize monitoring sites based on surrounding land use, traffic patterns, and environmental factors."
              href="/categorize/site-categorization"
              imageSrc="/images/model/categorisemap.webp"
            />
            <FeatureCard
              Icon={BarChart3}
              title="Data Analytics"
              description="Generate comprehensive reports with trends, forecasts, and actionable insights from air quality data."
              href="/analytics/data-analytics"
              imageSrc="/images/model/analyticsHome.webp"
            />
            <FeatureCard
              Icon={BrainCircuit}
              title="Machine Learning Models"
              description="Continuously improving prediction models that account for seasonal variations, weather patterns, and human activities."
              href="/models"
              imageSrc="/images/model/modelapi.webp"
            />
            <FeatureCard
              Icon={Shield}
              title="Health Impact Assessment"
              description="Evaluate potential health impacts of air pollution on different population groups and geographic areas."
              href="/comingsoon/health-impact"
              imageSrc="/images/model/calibration-header.webp"
            />
            <FeatureCard
              Icon={MapPin}
              title="Interactive Mapping"
              description="Visualize air quality data across regions with interactive maps showing real-time pollution levels."
              href="/map/interactive-mapping"
              imageSrc="/images/homemap.webp"
            />
          </div>
        </div>
      </section>
🧰 Tools
🪛 Biome (2.5.0)

[error] 502-502: Expected corresponding JSX closing tag for 'div'.

(parse)


[error] 501-501: Expected corresponding JSX closing tag for 'section'.

(parse)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/app/page.tsx` around lines 501 - 557, The features block in
page.tsx has an unbalanced JSX structure because the section is closed before
the FeatureCard grid starts. Remove the premature closing tag in the main
section and keep the container/div hierarchy intact so the heading, description,
and grid all remain inside the same section; verify the surrounding JSX around
the section that contains FeatureCard and the text heading is properly nested.

Source: Linters/SAST tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant