-
-
Notifications
You must be signed in to change notification settings - Fork 288
Add precise location sharing option for chapter map #2644
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
base: main
Are you sure you want to change the base?
Conversation
Summary by CodeRabbitNew Features
WalkthroughAdds opt-in browser geolocation: new geolocation utilities, updates Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20–30 minutes
Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches✅ Passed checks (5 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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 |
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.
Actionable comments posted: 0
🧹 Nitpick comments (5)
frontend/src/utils/geolocationUtils.ts (3)
31-59: EnsuregetUserLocationFromBrowserstays client‑only and consider explicit “denied” handlingThe geolocation flow and error handling are reasonable, but this helper assumes a browser environment (
navigator.geolocation). Make sure it’s only imported and called from client components, and consider whether you want to distinguish “denied / failed” from “not requested yet” in the UI instead of always resolvingnullwith only a console warning.
61-80: Coordinate extraction is robust but could be tightened and deduplicatedThe multiple fallbacks for
lat/lngacross_geoloc,geoLocation, andlocationare helpful for handling legacy shapes, but:
- The repeated casts to
Record<string, unknown>could be wrapped in a small helper to avoid duplication.- If this is only intended for
Chapterobjects today, typing the parameter asChapter(or a narrower structural type) would give you better safety thanRecord<string, unknown>.
85-103: Distance sorting works, but the typing and filter could be more preciseThe map/filter/sort pipeline correctly excludes chapters without valid coordinates and orders the rest by distance. Two type‑level improvements would reduce assertions and make the return type easier to use:
- Narrow the filter with a type predicate instead of
item !== nullplusas/!.- Align the generic shape with actual usage (e.g.,
Chapter[]or a generic<T>that returnsArray<T & { distance: number }>), so you don’t lose the concrete type when calling this fromChapterMapWrapper.Example pattern:
.filter((item): item is { /* your shape */ } => item !== null)This will also help keep
sortedData’s type in sync with what’s returned.frontend/src/components/ChapterMapWrapper.tsx (2)
57-100: Avoid leakingshowLocationSharingprop intoChapterMapand alignsortedDatatypingTwo related refinements here:
ChapterMaplikely doesn’t declare ashowLocationSharingprop, but{...props}forwards it anyway. This can cause type errors ifChapterMaphas a strict props interface and also blurs the wrapper’s responsibility boundary.
sortedDatais typed asChapter[] | null, whilesortChaptersByDistancecurrently returns objects augmented with adistancefield. Even though this extra field is harmless at runtime, the mismatch can force casts or assertions elsewhere.You can address both by stripping the wrapper‑only prop and tightening the types:
-const ChapterMapWrapper: React.FC<ChapterMapWrapperProps> = (props) => { - const [userLocation, setUserLocation] = useState<UserLocation | null>(null) - const [isLoadingLocation, setIsLoadingLocation] = useState(false) - const [sortedData, setSortedData] = useState<Chapter[] | null>(null) +const ChapterMapWrapper: React.FC<ChapterMapWrapperProps> = (props) => { + const { showLocationSharing, ...mapProps } = props + const [userLocation, setUserLocation] = useState<UserLocation | null>(null) + const [isLoadingLocation, setIsLoadingLocation] = useState(false) + const [sortedData, setSortedData] = useState<Chapter[] | null>(null) // or Chapter & { distance: number }[] @@ - const enableLocationSharing = props.showLocationSharing === true + const enableLocationSharing = showLocationSharing === true @@ - setSortedData(sortChaptersByDistance(props.geoLocData, location)) + setSortedData(sortChaptersByDistance(mapProps.geoLocData, location)) @@ - const mapData = sortedData ?? props.geoLocData + const mapData = sortedData ?? mapProps.geoLocData @@ - <div className="space-y-4"> + <div className="space-y-4"> @@ - <ChapterMap {...props} geoLocData={mapData} /> + <ChapterMap {...mapProps} geoLocData={mapData} />Adjusting
sortChaptersByDistance’s return type as suggested ingeolocationUtils.tswill let you updatesortedData’s type accordingly.
60-97: Location‑sharing UI and accessibility are in good shape; consider a failure state messageThe button’s
aria-label,title, andisLoading/disabledflags make the interaction accessible, and the status text clearly indicates whether a location filter is active. One minor UX enhancement would be to surface a brief inline message when geolocation fails or is denied (instead of silently returning to the default “Find chapters near you” text), so users understand why nothing changed after clicking.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
frontend/src/app/chapters/page.tsx(1 hunks)frontend/src/components/ChapterMapWrapper.tsx(1 hunks)frontend/src/utils/geolocationUtils.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-13T11:29:25.245Z
Learnt from: Rajgupta36
Repo: OWASP/Nest PR: 1717
File: frontend/src/app/mentorship/programs/page.tsx:59-61
Timestamp: 2025-07-13T11:29:25.245Z
Learning: In Next.js 13+ app router, components with the 'use client' directive run entirely on the client side and don't require window object existence checks or SSR hydration considerations. Direct access to window.location and other browser APIs is safe in client components.
Applied to files:
frontend/src/components/ChapterMapWrapper.tsx
🧬 Code graph analysis (1)
frontend/src/components/ChapterMapWrapper.tsx (2)
frontend/src/types/chapter.ts (1)
Chapter(4-21)frontend/src/utils/geolocationUtils.ts (3)
UserLocation(1-6)getUserLocationFromBrowser(31-59)sortChaptersByDistance(85-103)
🔇 Additional comments (3)
frontend/src/app/chapters/page.tsx (1)
90-102: Prop wiring for enabling location sharing looks correctPassing
showLocationSharing={true}here cleanly enables the new opt‑in location UI for the chapters page without affecting behavior when no chapters are loaded.frontend/src/utils/geolocationUtils.ts (1)
13-29: Haversine distance implementation looks mathematically soundThe
calculateDistanceimplementation (degree→radian conversion,aterm, andR = 6371km) is correct for a haversine distance in kilometers and handles edge cases (identical points) as expected.frontend/src/components/ChapterMapWrapper.tsx (1)
22-57: Location‑sharing flow and toggle behavior are logically soundThe wrapper cleanly gates the feature behind
showLocationSharing, toggles the filter on repeated clicks (set/unsetuserLocationandsortedData), and correctly guards loading state aroundgetUserLocationFromBrowserwith afinallyblock. This matches the opt‑in requirement without altering default behavior when the feature is disabled.
|
Could you please review this? Let me know if its looks fine so that i can proceed with test part |
kasya
left a comment
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.
@anurag2787 this works great! Please proceed with working on tests 👍🏼 This looks good so far! Feel free to move this back into drafts if needed.
I do have a couple of questions!
When I use the button the map is pretty zoomed out 🤔 For example, it shows the full state I'm in. Is there a way to zoom in more or is it the best we can get? Also a few comments down below ⬇️
| // # NOSONAR Geolocation permission is necessary for "Find chapters near you" feature. | ||
| // User explicitly opts-in via button click. Location stays client-side only. | ||
| // eslint-disable-next-line no-unsanitized/property | ||
| navigator.geolocation.getCurrentPosition( |
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.
I can see Sonar still gets triggered on this for some reason 🤔
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.
Actionable comments posted: 3
🧹 Nitpick comments (4)
frontend/src/components/ChapterMap.tsx (1)
109-148: Consider extracting duplicate logic.The userLocation block (lines 109-128) and showLocal block (lines 129-148) share nearly identical logic with only two differences: maxZoom value (12 vs 7) and the triggering condition. This duplication increases maintenance burden.
Consider extracting the shared logic:
const applyLocalZoom = (zoom: number) => { const maxNearestChapters = 5 const localChapters = validGeoLocData.slice(0, maxNearestChapters) const localBounds = L.latLngBounds( localChapters.map((chapter) => [ chapter._geoloc?.lat ?? chapter.geoLocation?.lat, chapter._geoloc?.lng ?? chapter.geoLocation?.lng, ]) ) const nearestChapter = validGeoLocData[0] map.setView( [ nearestChapter._geoloc?.lat ?? nearestChapter.geoLocation?.lat, nearestChapter._geoloc?.lng ?? nearestChapter.geoLocation?.lng, ], zoom ) map.fitBounds(localBounds, { maxZoom: zoom }) } if (userLocation && validGeoLocData.length > 0) { applyLocalZoom(12) } else if (showLocal && validGeoLocData.length > 0) { applyLocalZoom(7) }frontend/src/utils/geolocationUtils.ts (2)
31-60: Consider enabling high accuracy for "precise location" feature.The function uses
enableHighAccuracy: falsewhich may provide less accurate results. Since the PR title and feature description specifically mention "precise location sharing," consider setting this totruefor better accuracy, acknowledging it may take longer and use more battery.If you want to prioritize accuracy:
{ - enableHighAccuracy: false, + enableHighAccuracy: true, timeout: 10000, maximumAge: 0, }
86-104: Consider stronger typing for sortChaptersByDistance.The function uses
Record<string, unknown>[]instead of the more specificChapter[]type, which loses type safety and IDE support. Since the function is specifically designed to sort chapters and ChapterMapWrapper passesprops.geoLocData(typed asChapter[]), consider using the Chapter type directly.import type { Chapter } from 'types/chapter' export const sortChaptersByDistance = ( chapters: Chapter[], userLocation: UserLocation ): Array<Chapter & { distance: number }> => { return chapters .map((chapter) => { const { lat, lng } = extractChapterCoordinates(chapter as unknown as Record<string, unknown>) if (typeof lat !== 'number' || typeof lng !== 'number') return null const distance = calculateDistance(userLocation.latitude, userLocation.longitude, lat, lng) return { ...chapter, distance } }) .filter((item): item is Chapter & { distance: number } => item !== null) .sort((a, b) => a.distance - b.distance) }This eliminates the non-null assertions and provides better type safety.
frontend/src/components/ChapterMapWrapper.tsx (1)
50-52: Consider providing user feedback on location errors.The error handler logs to console but doesn't provide user feedback when location detection fails. Users who grant permission but encounter errors (timeout, position unavailable, etc.) won't know what happened.
Consider adding error state and displaying a user-friendly message:
const [error, setError] = useState<string | null>(null) // In handleShareLocation: try { setError(null) const location = await getUserLocationFromBrowser() if (!location) { setError('Unable to determine your location. Please try again.') return } // ... rest of logic } catch (error) { console.error('Error detecting location:', error) setError('Failed to access location. Please check your browser permissions.') } // In render: {error && ( <div className="text-xs text-red-600 dark:text-red-400">{error}</div> )}
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
frontend/src/components/ChapterMap.tsx(4 hunks)frontend/src/components/ChapterMapWrapper.tsx(1 hunks)frontend/src/utils/geolocationUtils.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/src/components/ChapterMap.tsx (2)
frontend/src/types/chapter.ts (1)
Chapter(4-21)frontend/src/utils/geolocationUtils.ts (1)
UserLocation(1-6)
frontend/src/components/ChapterMapWrapper.tsx (2)
frontend/src/types/chapter.ts (1)
Chapter(4-21)frontend/src/utils/geolocationUtils.ts (3)
UserLocation(1-6)getUserLocationFromBrowser(31-60)sortChaptersByDistance(86-104)
🔇 Additional comments (7)
frontend/src/components/ChapterMap.tsx (1)
5-5: LGTM!The UserLocation type import and optional prop addition are properly implemented with correct TypeScript typing.
Also applies to: 16-16, 21-21
frontend/src/utils/geolocationUtils.ts (2)
1-6: LGTM!Clean interface definition with appropriate required and optional fields.
13-29: LGTM!The Haversine formula implementation is correct for calculating great-circle distance between two points on a sphere.
frontend/src/components/ChapterMapWrapper.tsx (4)
1-20: LGTM!Clean imports and proper TypeScript typing for the component props and function component.
Also applies to: 22-22
23-31: LGTM!Clean state initialization and early return pattern when location sharing is disabled.
77-97: LGTM! Clean UI feedback states.The location status display provides clear feedback for both active and inactive states, with appropriate styling and accessibility attributes.
Note: I see from past review comments that displaying coordinates instead of city names was already discussed (would require reverse geocoding). The current coordinate display is a reasonable approach.
Based on learnings
58-58: LGTM! Proper data flow to ChapterMap.The component correctly selects between sorted and original data, and properly passes both the data and userLocation to ChapterMap for rendering.
Also applies to: 100-100
@anurag2787 It seems that Sonar is totally against it and only suggests to use user input and go from there :( I'm not sure if we can override this. @arkid15r is there anything in the SQ admin that we could disable for this approach to work? |
arkid15r
left a comment
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.
Shouldn't the new button be part of the map and available after a click on "Click to interact with map" button only? I believe it'd be cleaner.
We could just add a small button consistent with the map style (with a good tooltip). Instead of showing coordinates values we could just add a pin (the red one) indicating user's location on the map.
@kasya wdyt?
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.
Actionable comments posted: 0
🧹 Nitpick comments (2)
frontend/src/utils/geolocationUtils.ts (2)
31-64: Harden browser-only usage and align// NOSONARwith the geolocation lineThe overall flow (opt‑in, local‑only usage, timeout, graceful error handling) looks solid. Two small tweaks would tighten things up and should help with Sonar:
- Guard against non‑browser environments (
typeof navigator === 'undefined') so this safely returnsnullinstead of throwing if ever called outside the browser.- Move
// NOSONARonto the line wherenavigator.geolocationis actually read, since Sonar only suppresses issues on the same line.Suggested refactor:
-export const getUserLocationFromBrowser = (): Promise<UserLocation | null> => { - return new Promise((resolve) => { - if (!navigator.geolocation) { - // eslint-disable-next-line no-console - console.warn('Geolocation API not supported') - resolve(null) - return - } +export const getUserLocationFromBrowser = (): Promise<UserLocation | null> => { + return new Promise((resolve) => { + if (typeof navigator === 'undefined' || !navigator.geolocation) { + // eslint-disable-next-line no-console + console.warn('Geolocation API not supported') + resolve(null) + return + } @@ - navigator.geolocation.getCurrentPosition( + const geolocation = navigator.geolocation // NOSONAR - geolocation used only after explicit user opt-in; data stays client-side (see comment above). + geolocation.getCurrentPosition( (position) => { resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude, }) @@ - { - enableHighAccuracy: false, - timeout: 10000, - maximumAge: 0, - } - ) - }) // NOSONAR + { + enableHighAccuracy: false, + timeout: 10000, + maximumAge: 0, + } + ) + }) }Please re-run the Sonar check after this change to confirm the warning is actually suppressed on the targeted line.
80-101: Consider a small typing cleanup insortChaptersByDistanceLogic is correct, but you can make the API more ergonomic and type‑safe by preserving the caller’s chapter type and avoiding the
null+ type assertion combo:
- Make the function generic over the chapter type.
- Use a type‑predicate in the filter so you don’t need
a!and the final cast.Example refactor:
-export const sortChaptersByDistance = ( - chapters: Record<string, unknown>[], - userLocation: UserLocation -): Array<Record<string, unknown> & { distance: number }> => { - return chapters - .map((chapter) => { - const { lat, lng } = extractChapterCoordinates(chapter) - - if (typeof lat !== 'number' || typeof lng !== 'number') return null - - const distance = calculateDistance(userLocation.latitude, userLocation.longitude, lat, lng) - - return { ...chapter, distance } - }) - .filter((item) => item !== null) // remove invalid ones - .sort((a, b) => a!.distance - b!.distance) as Array< - Record<string, unknown> & { distance: number } - > -} +export const sortChaptersByDistance = <T extends Record<string, unknown>>( + chapters: T[], + userLocation: UserLocation +): Array<T & { distance: number }> => { + return chapters + .map((chapter) => { + const { lat, lng } = extractChapterCoordinates(chapter) + + if (typeof lat !== 'number' || typeof lng !== 'number') return null + + const distance = calculateDistance(userLocation.latitude, userLocation.longitude, lat, lng) + + return { ...chapter, distance } + }) + .filter((item): item is T & { distance: number } => item !== null) // remove invalid ones + .sort((a, b) => a.distance - b.distance) +}This keeps the original chapter shape at call sites while maintaining the same runtime behavior.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
frontend/src/utils/geolocationUtils.ts(1 hunks)
🔇 Additional comments (3)
frontend/src/utils/geolocationUtils.ts (3)
1-11: Location and chapter coordinate shapes look goodThe
UserLocationandChapterCoordinatesstructures match how this module uses them (lat/lng plus optional metadata), so there are no functional or safety concerns here from my side.
13-29: Haversine distance calculation is implemented correctlyThe formula, radian conversions, and choice of radius (Earth ≈ 6371 km) are all correct for computing great‑circle distance in kilometers. No changes needed.
66-78: Coordinate extraction matches current chapter data modelThis helper now cleanly reflects the actual
_geoloc/geoLocationshape (lat/lng only) and defers validity checks to the caller. That’s simple and effective; no further changes needed here.
|
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.
Actionable comments posted: 1
🧹 Nitpick comments (2)
frontend/src/utils/geolocationUtils.ts (2)
13-29: LGTM! Consider adding coordinate validation.The Haversine formula implementation is mathematically correct and appropriately returns distance in kilometers.
Optionally, consider adding input validation to ensure coordinates are within valid ranges (latitude: -90 to 90, longitude: -180 to 180) to prevent nonsensical results from invalid inputs.
83-99: Consider stronger TypeScript types to eliminate assertions.The logic is correct, but using
Record<string, unknown>throughout loses type safety. The non-null assertion on line 98 is safe due to the filter on line 97, but indicates that TypeScript can't verify the logic.Consider typing
chaptersmore specifically (e.g., with a Chapter interface or type that includes the coordinate properties) to improve type safety and eliminate the need for type assertions.Example approach:
interface ChapterWithCoords { _geoloc?: { lat?: number; lng?: number } geoLocation?: { lat?: number; lng?: number } // other chapter properties... } export const sortChaptersByDistance = ( chapters: ChapterWithCoords[], userLocation: UserLocation ): Array<ChapterWithCoords & { distance: number }> => { return chapters .map((chapter) => { const { lat, lng } = extractChapterCoordinates(chapter as Record<string, unknown>) if (typeof lat !== 'number' || typeof lng !== 'number') return null const distance = calculateDistance(userLocation.latitude, userLocation.longitude, lat, lng) return { ...chapter, distance } }) .filter((item): item is ChapterWithCoords & { distance: number } => item !== null) .sort((a, b) => a.distance - b.distance) }Note the type predicate in the filter removes the need for non-null assertions.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
frontend/src/utils/geolocationUtils.ts(1 hunks)
🔇 Additional comments (3)
frontend/src/utils/geolocationUtils.ts (3)
8-11: LGTM!The internal
ChapterCoordinatestype appropriately models nullable coordinates for defensive extraction logic.
31-64: LGTM!The geolocation implementation follows best practices:
- Gracefully handles unsupported API
- Never rejects (resolves to null on error for good UX)
- Includes justification comment for security scanner
- Uses reasonable options (low accuracy for battery savings, 10s timeout)
- Implements opt-in pattern as required by PR objectives
The SonarCloud issue mentioned in PR comments is a configuration matter, not a code defect.
66-78: LGTM!The simplified extraction logic correctly addresses past review feedback and appropriately handles the fallback chain from
_geoloctogeoLocationwith null safety.
| export interface UserLocation { | ||
| latitude: number | ||
| longitude: number | ||
| city?: string | ||
| country?: string | ||
| } |
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.
Remove or document unused optional fields.
The city and country fields are defined but never populated by getUserLocationFromBrowser (which only sets latitude and longitude). Either remove these unused fields or add a comment indicating they're reserved for future enhancement.
Apply this diff to remove the unused fields:
export interface UserLocation {
latitude: number
longitude: number
- city?: string
- country?: string
}Alternatively, if these are planned for future use, add a comment:
export interface UserLocation {
latitude: number
longitude: number
+ // Reserved for future enhancement (reverse geocoding)
city?: string
country?: string
}📝 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.
| export interface UserLocation { | |
| latitude: number | |
| longitude: number | |
| city?: string | |
| country?: string | |
| } | |
| export interface UserLocation { | |
| latitude: number | |
| longitude: number | |
| } |
| export interface UserLocation { | |
| latitude: number | |
| longitude: number | |
| city?: string | |
| country?: string | |
| } | |
| export interface UserLocation { | |
| latitude: number | |
| longitude: number | |
| // Reserved for future enhancement (reverse geocoding) | |
| city?: string | |
| country?: string | |
| } |
🤖 Prompt for AI Agents
In frontend/src/utils/geolocationUtils.ts around lines 1 to 6, the UserLocation
interface currently declares optional city and country fields that are never
populated by getUserLocationFromBrowser; either remove these unused fields from
the interface or retain them but add a clear comment on their intended future
use (e.g., "reserved for future reverse-geocoding/population") so reviewers know
they are intentional; update the interface accordingly and, if keeping them, add
a brief comment above each field explaining why it's present and when it will be
populated.
@anurag2787 Are you planning to update the button location as per this feedback? 👀 |



Resolves #2583
Proposed change
This PR adds an opt-in precise location sharing option for the Chapter Map.
Checklist
make check-testlocally; all checks and tests passed.