Add Clean Air Forum selfie wall display and harden submission API#3756
Conversation
Backs the mobile app's new selfie filter feature with a `/selfies` wall page for the conference venue display (auto-polling grid, double-tap + staff PIN to moderate) and a temporary in-memory submissions API, so the feature is demoable end-to-end before the real AirQo backend has equivalent endpoints. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The wall page used generic black/white styling with no AirQo branding, all active submissions in one long scrolling grid, and a POST endpoint that accepted any imageUrl with no auth — all flagged by the user and CodeRabbit as needing fixes before this goes live at the event. - Wall page now uses the site's real logo and brand blue (#145DFF) instead of generic dark-UI colors, and shows 8 selfies at a time in an auto-advancing slideshow (framer-motion crossfade + page dots) rather than everything at once - Long-press added alongside double-tap for moderation, since a wall display has no cursor to hover/right-click with - POST now requires a shared secret header from the mobile app (x-clean-air-forum-secret) and validates imageUrl actually points at the Cloudinary folder the app uploads to, closing the "anyone who finds this endpoint can push arbitrary images onto the public wall" gap - Both the submission secret and moderation PIN checks now fail closed (with a logged warning) if unset in production, instead of silently leaving the endpoint open — they still skip the check in dev/preview - Added a best-effort per-IP rate limit (5/minute) on submissions to blunt spam before staff can react Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Documents what this feature still needs from other teams before it's production-grade: real backend endpoints to replace the temporary in-memory store, Cloudinary upload-preset hardening, the new env vars/secrets that need wiring into the deploy workflows (left as docs rather than editing the workflow YAML directly), and a note to confirm the hardcoded event dates before launch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR adds the Faces of Clean Air carousel feature and its wiring/docs, rewrites the website CI failure-comment script to use job/step data, and updates several site pages with motion, copy, and presentation changes. ChangesFaces of Clean Air Feature
CI Failure Comment Workflow
Site Motion and Copy Polish
Sequence Diagram(s)sequenceDiagram
participant FacesOfCleanAirPage
participant usePollingWithVisibility
participant facesOfCleanAirService
participant API as GET /users/selfies
FacesOfCleanAirPage->>usePollingWithVisibility: register fetchSubmissions()
usePollingWithVisibility->>FacesOfCleanAirPage: initial callback + refresh on visibility
FacesOfCleanAirPage->>facesOfCleanAirService: getSubmissions(eventId)
facesOfCleanAirService->>API: GET eventId
API-->>facesOfCleanAirService: selfies data
facesOfCleanAirService-->>FacesOfCleanAirPage: CleanAirSubmission[]
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsErrors were encountered while retrieving linked issues. Errors (1)
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. Comment |
|
New azure website changes available for preview here |
|
New azure website changes available for preview here |
There was a problem hiding this comment.
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 (1)
src/website/src/config/cleanAirForumConfig.ts (1)
1-24: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick winWrong directory name breaks the build —
config/vsconfigs/.This file lives at
src/website/src/config/cleanAirForumConfig.ts, but every consumer imports it as@/configs/cleanAirForumConfig(plural), matching the@/configs/*alias from the coding guidelines. The CI logs confirm this exact mismatch: tsc fails withTS2307: Cannot find module '@/configs/cleanAirForumConfig'and the webpack build fails withModule not found: Can't resolve '@/configs/cleanAirForumConfig'. This is the root cause blocking the entire pipeline (affectsroute.tsandSelfiesWallPage.tsxdownstream, which import the same path).As per coding guidelines,
src/website/src/**/*.{ts,tsx,js,jsx}should "Use path aliases configured in tsconfig.json (@/, ...@/configs/, ...) instead of relative paths" — the alias is plural, so the file must live underconfigs/.🛠️ Proposed fix
mkdir -p src/website/src/configs git mv src/website/src/config/cleanAirForumConfig.ts src/website/src/configs/cleanAirForumConfig.ts🤖 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 `@src/website/src/config/cleanAirForumConfig.ts` around lines 1 - 24, The Clean Air Forum config file is in the wrong folder for the alias that consumers use, so imports from `@/configs/cleanAirForumConfig` fail to resolve. Move `cleanAirForumConfig` from `config/` to `configs/` (keeping the same exported constants like `CLEAN_AIR_FORUM_CURRENT_EVENT_ID`), and make sure all imports in callers such as `route.ts` and `SelfiesWallPage.tsx` continue to use the `@/configs/*` path.Sources: Coding guidelines, Pipeline failures
🧹 Nitpick comments (3)
docs/clean-air-forum-followups.md (1)
33-35: 📐 Maintainability & Code Quality | 🔵 TrivialKeep the website on the Next.js proxy path.
Calling the real API directly from
SelfiesWallPage.tsxwould reintroduce backend URL coupling on the web side. Keep the proxy route and have it forward to the backend instead of deleting it outright. As per coding guidelines,src/website/**/*.{ts,tsx,js,jsx}client-side calls should go through the Next.js proxy at/api/v2and avoid hard-coded backend URLs.🤖 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 `@docs/clean-air-forum-followups.md` around lines 33 - 35, Keep the website on the Next.js proxy path: do not delete the Next.js routes or switch `SelfiesWallPage.tsx` to call the backend directly. Update the proxy handlers so the web app continues to forward requests through `/api/v2` to the real API, preserving the Next.js proxy layer and avoiding hard-coded backend URLs in `src/website/**/*.{ts,tsx,js,jsx}`.Source: Coding guidelines
src/website/src/app/api/clean-air-forum/selfies/route.ts (1)
57-63: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winUnbounded growth of
submissionTimestampsByIp.Entries are added per distinct IP but never removed once their recent-timestamp array empties out implicitly (it never actually empties —
recent.push(now)guarantees at least one entry). Over a multi-day conference with many distinct visitor IPs, this Map grows indefinitely for the life of the process.♻️ Proposed cleanup
function isRateLimited(ip: string): boolean { const now = Date.now(); const recent = (submissionTimestampsByIp.get(ip) ?? []).filter( (timestamp) => now - timestamp < RATE_LIMIT_WINDOW_MS, ); recent.push(now); - submissionTimestampsByIp.set(ip, recent); + if (recent.length === 0) { + submissionTimestampsByIp.delete(ip); + } else { + submissionTimestampsByIp.set(ip, recent); + } return recent.length > RATE_LIMIT_MAX_SUBMISSIONS; }Note: since
recentalways has at least the pushednow, a periodic full sweep of stale keys (e.g. on each call, occasionally) would be more effective than per-call pruning alone.Also applies to: 71-79
🤖 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 `@src/website/src/app/api/clean-air-forum/selfies/route.ts` around lines 57 - 63, The rate-limit cache in selflies/route.ts grows without bound because submissionTimestampsByIp only accumulates keys and never evicts stale IPs. Update the logic around globalForRateLimit, submissionTimestampsByIp, and the request-handling path that uses recent.push(now) to periodically sweep and delete IP entries whose timestamps are all older than the window, or otherwise cap/expire the Map so inactive visitors are removed over time.src/website/src/services/cleanAirForumAuth.ts (1)
21-31: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winUse a constant-time comparison for shared secrets.
providedSecret === configuredSecretis a standard string comparison, which leaks timing information proportional to matching prefix length. Since this guards both the submission secret and the wall moderation PIN, considercrypto.timingSafeEqual(with a length check first, since it throws on mismatched buffer lengths).🔒 Proposed fix
+import { timingSafeEqual } from 'crypto'; + +function safeCompare(a: string, b: string): boolean { + const bufA = Buffer.from(a); + const bufB = Buffer.from(b); + return bufA.length === bufB.length && timingSafeEqual(bufA, bufB); +} + export function checkSharedSecret({ ... - return providedSecret === configuredSecret; + return ( + typeof providedSecret === 'string' && + safeCompare(providedSecret, configuredSecret) + ); }🤖 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 `@src/website/src/services/cleanAirForumAuth.ts` around lines 21 - 31, The shared-secret check in cleanAirForumAuth currently uses a normal string equality comparison, which should be replaced with a constant-time comparison. Update the secret validation logic to compare providedSecret and configuredSecret with a timing-safe method, after first verifying both values have the same length so the comparison does not throw. Keep the existing behavior for the isProduction and missing-secret branches unchanged.
🤖 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 `@src/website/src/app/api/clean-air-forum/selfies/`[id]/route.ts:
- Around line 18-36: The PATCH handler in the selfies route performs a
shared-secret PIN check without any throttling, making repeated guesses
brute-forceable. Add the same per-IP rate limiting pattern used in the sibling
POST route before or around the checkSharedSecret call in PATCH, using the
existing route handler and params flow, so repeated unauthorized attempts are
limited before returning Incorrect PIN.
In `@src/website/src/app/api/clean-air-forum/selfies/route.ts`:
- Around line 65-69: The getClientIp helper is trusting the first
x-forwarded-for entry, which can be client-controlled and bypass rate limiting.
Update getClientIp in the selfie route to use the ingress-added IP from the
forwarded chain instead of split(',')[0], and keep the x-real-ip fallback only
when the trusted proxy IP is unavailable. Make the change in the getClientIp
function so the 5/min limit keys off the server-added address rather than a
spoofable header value.
---
Outside diff comments:
In `@src/website/src/config/cleanAirForumConfig.ts`:
- Around line 1-24: The Clean Air Forum config file is in the wrong folder for
the alias that consumers use, so imports from `@/configs/cleanAirForumConfig`
fail to resolve. Move `cleanAirForumConfig` from `config/` to `configs/`
(keeping the same exported constants like `CLEAN_AIR_FORUM_CURRENT_EVENT_ID`),
and make sure all imports in callers such as `route.ts` and
`SelfiesWallPage.tsx` continue to use the `@/configs/*` path.
---
Nitpick comments:
In `@docs/clean-air-forum-followups.md`:
- Around line 33-35: Keep the website on the Next.js proxy path: do not delete
the Next.js routes or switch `SelfiesWallPage.tsx` to call the backend directly.
Update the proxy handlers so the web app continues to forward requests through
`/api/v2` to the real API, preserving the Next.js proxy layer and avoiding
hard-coded backend URLs in `src/website/**/*.{ts,tsx,js,jsx}`.
In `@src/website/src/app/api/clean-air-forum/selfies/route.ts`:
- Around line 57-63: The rate-limit cache in selflies/route.ts grows without
bound because submissionTimestampsByIp only accumulates keys and never evicts
stale IPs. Update the logic around globalForRateLimit, submissionTimestampsByIp,
and the request-handling path that uses recent.push(now) to periodically sweep
and delete IP entries whose timestamps are all older than the window, or
otherwise cap/expire the Map so inactive visitors are removed over time.
In `@src/website/src/services/cleanAirForumAuth.ts`:
- Around line 21-31: The shared-secret check in cleanAirForumAuth currently uses
a normal string equality comparison, which should be replaced with a
constant-time comparison. Update the secret validation logic to compare
providedSecret and configuredSecret with a timing-safe method, after first
verifying both values have the same length so the comparison does not throw.
Keep the existing behavior for the isProduction and missing-secret branches
unchanged.
🪄 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: 3d1003ae-2717-4d8c-b6a8-f3cb00092dd9
📒 Files selected for processing (9)
docs/clean-air-forum-followups.mdsrc/website/.env.samplesrc/website/src/app/api/clean-air-forum/selfies/[id]/route.tssrc/website/src/app/api/clean-air-forum/selfies/route.tssrc/website/src/app/selfies/page.tsxsrc/website/src/config/cleanAirForumConfig.tssrc/website/src/services/cleanAirForumAuth.tssrc/website/src/services/cleanAirForumSelfiesStore.tssrc/website/src/views/selfies/SelfiesWallPage.tsx
- Deleted selfies API routes and associated logic for handling submissions and moderation. - Removed SelfiesWallPage component and its related styles. - Introduced new Faces of Clean Air page and service to display air quality selfies. - Updated footer and navigation to include links to the new Faces of Clean Air section. - Implemented a new service for fetching submissions from the backend. - Added new UI components for displaying submissions with improved styling and animations.
Codecov Report❌ Patch coverage is Additional details and impacted files
Flags with carried forward coverage won't be shown. Click here to find out more. @@ Coverage Diff @@
## staging #3756 +/- ##
===========================================
- Coverage 65.69% 64.79% -0.90%
===========================================
Files 156 158 +2
Lines 7812 7922 +110
Branches 1918 1920 +2
===========================================
+ Hits 5132 5133 +1
- Misses 2620 2727 +107
- Partials 60 62 +2 🚀 New features to boost your workflow:
|
|
New azure website changes available for preview here |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
.github/workflows/website-ci.yml (1)
167-175: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winDownloading the full job-log archive here is wasted work.
downloadJobLogsForWorkflowRunfollows the redirect and pulls the entire log archive for the job, yet the result is only used as a truthiness gate —logUrlon Line 174 is built purely fromcontextvalues and doesn't needlogsat all. On top of that, when a single job has multiple failed steps you re-download the same archive and push duplicate log links pointing at the same job URL.Since the URL is static, you can drop the download entirely (or hoist it to once-per-job if you actually intend to inspect content later).
♻️ Build the log link without the archive download
- // Attempt to fetch the step log for context - try { - const { data: logs } = await github.rest.actions.downloadJobLogsForWorkflowRun({ - owner: context.repo.owner, - repo: context.repo.repo, - job_id: job.id - }); - if (logs) { - failedLogs.push({ step: step.name, logUrl: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/job/${job.id}` }); - } - } catch (e) { - // Logs may not be available; continue without them - } + failedLogs.push({ + step: step.name, + logUrl: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/job/${job.id}` + });🤖 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 @.github/workflows/website-ci.yml around lines 167 - 175, The failed-step log collection in the workflow is downloading the full job archive unnecessarily in the `downloadJobLogsForWorkflowRun` path. Remove that call from the loop in the log-building logic and build the `logUrl` directly from `context` and `job.id` wherever `failedLogs.push` is used; if you still need to inspect logs later, hoist any download to once per job instead of repeating it per failed step. Also ensure duplicate failed steps for the same job do not append repeated identical log links.src/website/src/features/faces-of-clean-air/FacesOfCleanAirPage.tsx (1)
450-455: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
cn()for conditional Tailwind classes.Both spots build conditional class strings via raw template literals. As per coding guidelines,
src/website/src/**/*.{ts,tsx}: "Usecn()from@/lib/utilsfor conditional Tailwind class names."♻️ Suggested cn() usage
+import { cn } from '`@/lib/utils`'; ... <div - className={`relative overflow-hidden ${ - hasSecondRow - ? 'min-h-[280px] lg:min-h-[580px]' - : 'min-h-[280px]' - }`} + className={cn( + 'relative overflow-hidden min-h-[280px]', + hasSecondRow && 'lg:min-h-[580px]', + )} > ... <button key={index} type="button" onClick={() => goToPage(index)} aria-label={`Show selfie page ${index + 1} of ${totalPages}`} aria-current={isActive ? 'page' : undefined} - className={`h-2.5 rounded-full transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 ${ - isActive - ? 'w-10 bg-white' - : 'w-2.5 bg-white/40 hover:bg-white/60' - }`} + className={cn( + 'h-2.5 rounded-full transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2', + isActive ? 'w-10 bg-white' : 'w-2.5 bg-white/40 hover:bg-white/60', + )} />Also applies to: 504-508
🤖 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 `@src/website/src/features/faces-of-clean-air/FacesOfCleanAirPage.tsx` around lines 450 - 455, The conditional Tailwind class strings in FacesOfCleanAirPage should use cn() instead of raw template literals. Update the div className logic in FacesOfCleanAirPage (including the second conditional spot referenced in the review) to build classes through cn() from `@/lib/utils`, keeping the existing static and conditional min-h classes equivalent.Source: Coding guidelines
🤖 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 @.github/workflows/website-ci.yml:
- Around line 196-201: The "Failed step(s)" section in the workflow message
currently emits only table body rows from the failedSteps loop, so GitHub won’t
render it as a table. Update the body construction in the notification-building
logic to stop using table-style rows here and instead format failedSteps as a
simple bullet list or other plain markdown list, keeping the existing Failed
step(s) heading and the loop over failedSteps.
In `@CLAUDE.md`:
- Around line 7-20: The fenced directory tree block in CLAUDE.md is missing a
language tag, which will trigger markdown lint warnings. Update that code fence
to specify a language such as text so the block stays lint-clean and renders
correctly.
In `@src/website/src/app/`(programs)/faces-of-clean-air/page.tsx:
- Around line 5-10: The page metadata is being defined manually instead of using
the shared metadata configuration. Update the `metadata` export in
`faces-of-clean-air/page.tsx` to source its values from `METADATA_CONFIGS` via
`@/lib/metadata`, following the pattern used by other `page`/`layout` files, and
keep the page-specific title/description/robots settings within that
config-driven setup.
---
Nitpick comments:
In @.github/workflows/website-ci.yml:
- Around line 167-175: The failed-step log collection in the workflow is
downloading the full job archive unnecessarily in the
`downloadJobLogsForWorkflowRun` path. Remove that call from the loop in the
log-building logic and build the `logUrl` directly from `context` and `job.id`
wherever `failedLogs.push` is used; if you still need to inspect logs later,
hoist any download to once per job instead of repeating it per failed step. Also
ensure duplicate failed steps for the same job do not append repeated identical
log links.
In `@src/website/src/features/faces-of-clean-air/FacesOfCleanAirPage.tsx`:
- Around line 450-455: The conditional Tailwind class strings in
FacesOfCleanAirPage should use cn() instead of raw template literals. Update the
div className logic in FacesOfCleanAirPage (including the second conditional
spot referenced in the review) to build classes through cn() from `@/lib/utils`,
keeping the existing static and conditional min-h classes equivalent.
🪄 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: 4b4dffe8-b2c5-49ed-bd6d-0409deacc8e9
⛔ Files ignored due to path filters (2)
src/website/public/assets/images/white-logo.pngis excluded by!**/*.pngsrc/website/public/favicon.icois excluded by!**/*.ico
📒 Files selected for processing (12)
.github/workflows/website-ci.ymlCLAUDE.mdsrc/website/.env.samplesrc/website/AGENTS.mdsrc/website/src/app/(programs)/faces-of-clean-air/page.tsxsrc/website/src/components/FloatingMiniBillboardWrapper.tsxsrc/website/src/components/layout/Footer.tsxsrc/website/src/config/cleanAirForumConfig.tssrc/website/src/features/faces-of-clean-air/FacesOfCleanAirPage.tsxsrc/website/src/services/api/api-routes.tssrc/website/src/services/external/faces-of-clean-air.service.tssrc/website/src/services/external/index.ts
✅ Files skipped from review due to trivial changes (2)
- src/website/src/services/api/api-routes.ts
- src/website/.env.sample
| ``` | ||
| src/ | ||
| ├── website/ # Next.js 14 website (airqo.net) - PRIMARY APP | ||
| ├── platform/ # Analytics platform | ||
| ├── mobile/ # Flutter mobile app | ||
| ├── mobile-dashboard/ # Mobile dashboard | ||
| ├── beacon/ # Beacon device UI | ||
| ├── calibrate/ # Calibration tool | ||
| ├── vertex/ # Vertex device UI | ||
| ├── vertex-template/ # Vertex template | ||
| ├── vertex-desktop/ # Vertex desktop app | ||
| ├── docs-website/ # Documentation site | ||
| └── .github/ # GitHub Actions workflows | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add a language to the fenced block.
markdownlint-cli2 will flag this bare code fence. Adding a language (for example text) keeps the doc lint-clean and improves rendering.
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 7-7: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 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 `@CLAUDE.md` around lines 7 - 20, The fenced directory tree block in CLAUDE.md
is missing a language tag, which will trigger markdown lint warnings. Update
that code fence to specify a language such as text so the block stays lint-clean
and renders correctly.
Source: Linters/SAST tools
…remove unused configurations
|
New azure website changes available for preview here |
…improved animations
|
New azure website changes available for preview here |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/website/src/features/blogs/BlogsPage.tsx (1)
20-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider extracting shared animation variants to a common utility.
The
containerVariantsanditemVariantsobjects are identical to those defined inAirQoDevConPage.tsxandPackagesPage.tsx. Extracting them to a shared module (e.g.,lib/animation-variants.ts) would eliminate triplication and provide a single source of truth for scroll-reveal animation tuning.♻️ Suggested shared variants module
+// src/website/src/lib/animation-variants.ts +import type { Variants } from 'framer-motion'; + +export const scrollContainerVariants: Variants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { staggerChildren: 0.1, duration: 0.5, ease: 'easeOut' }, + }, +}; + +export const scrollItemVariants: Variants = { + hidden: { opacity: 0, y: 30 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: 'easeOut' } }, +};Then in each page file:
-const containerVariants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { staggerChildren: 0.1, duration: 0.5, ease: 'easeOut' }, - }, -}; - -const itemVariants = { - hidden: { opacity: 0, y: 30 }, - visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: 'easeOut' } }, -}; +import { scrollContainerVariants, scrollItemVariants } from '`@/lib/animation-variants`';🤖 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 `@src/website/src/features/blogs/BlogsPage.tsx` around lines 20 - 32, The animation variant objects in BlogsPage should not be duplicated across pages; extract the shared scroll-reveal definitions used by containerVariants and itemVariants into a common utility module, then import and reuse them in BlogsPage, AirQoDevConPage, and PackagesPage so there is a single source of truth for these motion settings.
🤖 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 `@src/website/src/features/developers/airqo-devcon/AirQoDevConPage.tsx`:
- Around line 120-121: The status copy in AirQoDevConPage now says “Applications
closed” while the CTA section and both Apply Now buttons in the same component
still invite users to apply, so align the whole flow in AirQoDevConPage. Update
the CTA text near the registration section to reflect closed registration, and
either remove or disable the apply actions that currently point to
DEVCON_APPLY_URL so they no longer contradict the closed status. Keep the
wording consistent across the status pill, the descriptive CTA copy, and the
button labels/behavior.
---
Nitpick comments:
In `@src/website/src/features/blogs/BlogsPage.tsx`:
- Around line 20-32: The animation variant objects in BlogsPage should not be
duplicated across pages; extract the shared scroll-reveal definitions used by
containerVariants and itemVariants into a common utility module, then import and
reuse them in BlogsPage, AirQoDevConPage, and PackagesPage so there is a single
source of truth for these motion settings.
🪄 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: 0212a744-5ca6-4b10-9ff6-ade124b09408
📒 Files selected for processing (10)
.github/workflows/website-ci.ymlsrc/website/src/app/(programs)/faces-of-clean-air/page.tsxsrc/website/src/components/dialogs/EngagementDialog.tsxsrc/website/src/config/cleanAirForumConfig.tssrc/website/src/features/blogs/BlogsPage.tsxsrc/website/src/features/developers/airqo-devcon/AirQoDevConPage.tsxsrc/website/src/features/explore-data/ExplorePage.tsxsrc/website/src/features/faces-of-clean-air/FacesOfCleanAirPage.tsxsrc/website/src/features/home/HomePage.tsxsrc/website/src/features/packages/PackagesPage.tsx
✅ Files skipped from review due to trivial changes (3)
- src/website/src/features/home/HomePage.tsx
- src/website/src/components/dialogs/EngagementDialog.tsx
- src/website/src/features/explore-data/ExplorePage.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- .github/workflows/website-ci.yml
- src/website/src/features/faces-of-clean-air/FacesOfCleanAirPage.tsx
OchiengPaul442
left a comment
There was a problem hiding this comment.
Previous implementation has been cleaned up
|
New azure website changes available for preview here |
…d update styles for better visibility
…ess on FacesOfCleanAirPage
|
New azure website changes available for preview here |
There was a problem hiding this comment.
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 (1)
src/website/src/features/developers/airqo-devcon/AirQoDevConPage.tsx (1)
170-173: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winCountdown copy still invites registration despite "Applications closed" status.
The countdown section says "Register your interest now and we will share joining details and preparation notes with registered students," which contradicts the "Applications closed" status pill and the final panel. Update this copy to match the closed state.
📝 Suggested copy update
<p className="text-[15px] text-[`#71717a`] leading-[1.6]"> AirQo DevCon runs on 17–18 June 2026 at Makerere University. - Register your interest now and we will share joining details and - preparation notes with registered students. + Applications are now closed. We will share joining details and + preparation notes with registered students. </p>🤖 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 `@src/website/src/features/developers/airqo-devcon/AirQoDevConPage.tsx` around lines 170 - 173, Update the countdown copy in AirQoDevConPage so it matches the closed state shown by the status pill and final panel. In the section that renders the event details and interest message, replace the “Register your interest now” wording with closed-state language that no longer invites new registration, while still referencing the 17–18 June 2026 event details in a way consistent with the existing AirQoDevConPage content.
🧹 Nitpick comments (1)
src/website/src/hooks/usePollingWithVisibility.ts (1)
48-55: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winFetch immediately when the tab regains visibility.
When the tab becomes visible again,
start()creates a new interval but does not invoke the callback right away. IfactiveIntervalMsis large, users see stale data until the first tick fires. The PR objective calls out "visibility-aware fetching for better data loading" — an immediate refresh on resume would align with that goal.♻️ Proposed fix: trigger immediate fetch on visibility regain
const handleVisibilityChange = () => { if (document.visibilityState === 'visible') { + void invoke(); start(); } else { stop(); } };🤖 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 `@src/website/src/hooks/usePollingWithVisibility.ts` around lines 48 - 55, The visibility handler in usePollingWithVisibility should trigger an immediate refresh when the tab becomes visible again, not just restart the interval. Update the handleVisibilityChange logic inside useEffect so that the visible branch still calls start() but also invokes the polling callback immediately (or uses the existing polling helper that does so) to avoid stale data after resume. Keep the change localized around the useEffect/handleVisibilityChange behavior in usePollingWithVisibility.
🤖 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 `@src/website/src/features/products/AnalyticsPage.tsx`:
- Around line 146-151: The Documentation link in AnalyticsPage opens in the same
tab because the onClick handler calls window.open without a target. Update the
existing window.open call in the AnalyticsPage component’s Documentation button
to use '_blank', matching the other external link on this page so it opens in a
new tab consistently.
In `@src/website/src/hooks/usePollingWithVisibility.ts`:
- Around line 28-33: The start callback in usePollingWithVisibility currently
schedules callbackRef.current() on a fixed setInterval cadence, which can
overlap async runs and let stale responses win; update the start/stop polling
flow to track an in-flight flag (or similar) around the interval handler so a
new tick is skipped while the previous callback is still pending. Use the
existing start, stop, callbackRef, and intervalRef logic in
usePollingWithVisibility to implement the guard cleanly without changing the
consumer API.
---
Outside diff comments:
In `@src/website/src/features/developers/airqo-devcon/AirQoDevConPage.tsx`:
- Around line 170-173: Update the countdown copy in AirQoDevConPage so it
matches the closed state shown by the status pill and final panel. In the
section that renders the event details and interest message, replace the
“Register your interest now” wording with closed-state language that no longer
invites new registration, while still referencing the 17–18 June 2026 event
details in a way consistent with the existing AirQoDevConPage content.
---
Nitpick comments:
In `@src/website/src/hooks/usePollingWithVisibility.ts`:
- Around line 48-55: The visibility handler in usePollingWithVisibility should
trigger an immediate refresh when the tab becomes visible again, not just
restart the interval. Update the handleVisibilityChange logic inside useEffect
so that the visible branch still calls start() but also invokes the polling
callback immediately (or uses the existing polling helper that does so) to avoid
stale data after resume. Keep the change localized around the
useEffect/handleVisibilityChange behavior in usePollingWithVisibility.
🪄 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: 485b0d49-6b8e-4ab6-83c0-990ca92ae029
📒 Files selected for processing (9)
src/website/README.mdsrc/website/src/app/layout.tsxsrc/website/src/components/FloatingMiniBillboard.tsxsrc/website/src/config/cleanAirForumConfig.tssrc/website/src/features/developers/airqo-devcon/AirQoDevConPage.tsxsrc/website/src/features/faces-of-clean-air/FacesOfCleanAirPage.tsxsrc/website/src/features/products/AnalyticsPage.tsxsrc/website/src/hooks/usePollingWithVisibility.tssrc/website/src/lib/utils/airQuality.ts
✅ Files skipped from review due to trivial changes (4)
- src/website/src/app/layout.tsx
- src/website/README.md
- src/website/src/components/FloatingMiniBillboard.tsx
- src/website/src/lib/utils/airQuality.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/website/src/config/cleanAirForumConfig.ts
- src/website/src/features/faces-of-clean-air/FacesOfCleanAirPage.tsx
…tial backoff for error handling in usePollingWithVisibility hook
…ors and improve polling logic to prevent stale data
|
New azure website changes available for preview here |
Can access via the Link on the footer (Faces of clean air)

Summary by CodeRabbit
New Features
Bug Fixes
Documentation