-
-
Notifications
You must be signed in to change notification settings - Fork 648
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
Changes from 21 commits
27b4a44
7d4efc6
47e56a2
4d2aa62
cc99765
cfc8424
927f614
bd0111b
81d3bd7
b613afe
a5038f3
180d1ed
02ebf66
a520752
01dae6d
d27230b
2137935
1ea9a97
6776976
bb06775
eba33fa
8342e2d
f75ab07
deaef06
3ce07f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,64 @@ | ||
| import dynamic from 'next/dynamic' | ||
| import React from 'react' | ||
| import React, { useState } from 'react' | ||
| import type { Chapter } from 'types/chapter' | ||
| import { | ||
| getUserLocationFromBrowser, | ||
| sortChaptersByDistance, | ||
| type UserLocation, | ||
| } from 'utils/geolocationUtils' | ||
|
|
||
| const ChapterMap = dynamic(() => import('./ChapterMap'), { ssr: false }) | ||
|
|
||
| const ChapterMapWrapper = (props: { | ||
| interface ChapterMapWrapperProps { | ||
| geoLocData: Chapter[] | ||
| showLocal: boolean | ||
| style: React.CSSProperties | ||
| }) => { | ||
| return <ChapterMap {...props} /> | ||
| showLocationSharing?: boolean | ||
| } | ||
|
|
||
| const ChapterMapWrapper: React.FC<ChapterMapWrapperProps> = (props) => { | ||
| const [userLocation, setUserLocation] = useState<UserLocation | null>(null) | ||
| const [sortedData, setSortedData] = useState<Chapter[] | null>(null) | ||
|
|
||
| const enableLocationSharing = props.showLocationSharing === true | ||
|
|
||
| if (!enableLocationSharing) { | ||
| return <ChapterMap {...props} /> | ||
| } | ||
|
|
||
| const handleShareLocation = async () => { | ||
| if (userLocation) { | ||
| setUserLocation(null) | ||
| setSortedData(null) | ||
| return | ||
| } | ||
|
|
||
| try { | ||
| const location = await getUserLocationFromBrowser() | ||
|
|
||
| if (location) { | ||
| setUserLocation(location) | ||
| const sorted = sortChaptersByDistance(props.geoLocData, location) | ||
| setSortedData(sorted.map(({ _distance, ...chapter }) => chapter as unknown as Chapter)) | ||
|
anurag2787 marked this conversation as resolved.
|
||
| } | ||
| } catch (error) { | ||
| // eslint-disable-next-line no-console | ||
| console.error('Error detecting location:', error) | ||
| } | ||
| } | ||
|
|
||
| const mapData = sortedData ?? props.geoLocData | ||
|
|
||
| return ( | ||
| <div className="space-y-4"> | ||
| <ChapterMap | ||
| {...props} | ||
| geoLocData={mapData} | ||
| userLocation={userLocation} | ||
| onShareLocation={handleShareLocation} | ||
| /> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| export default ChapterMapWrapper | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| export interface UserLocation { | ||
| latitude: number | ||
| longitude: number | ||
| city?: string | ||
| country?: string | ||
| } | ||
|
anurag2787 marked this conversation as resolved.
|
||
|
|
||
| interface ChapterCoordinates { | ||
| lat: number | null | ||
| lng: number | null | ||
| } | ||
|
|
||
| export const calculateDistance = ( | ||
| lat1: number, | ||
| lon1: number, | ||
| lat2: number, | ||
| lon2: number | ||
| ): number => { | ||
| const R = 6371 | ||
| const dLat = ((lat2 - lat1) * Math.PI) / 180 | ||
| const dLon = ((lon2 - lon1) * Math.PI) / 180 | ||
|
|
||
| const a = | ||
| Math.sin(dLat / 2) ** 2 + | ||
| Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLon / 2) ** 2 | ||
|
|
||
| const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) | ||
| return R * c | ||
| } | ||
|
|
||
| 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 | ||
| } | ||
|
|
||
| /* Geolocation permission is required for the "Find chapters near you" feature. | ||
| The user must explicitly opt in by clicking a button. The location data never | ||
| leaves the client and is not sent to the backend. It is used only to calculate | ||
| distances between the user and nearby chapters. | ||
| */ | ||
| navigator.geolocation.getCurrentPosition( | ||
| (position) => { | ||
| resolve({ | ||
| latitude: position.coords.latitude, | ||
| longitude: position.coords.longitude, | ||
| }) | ||
| }, | ||
| (error) => { | ||
| // eslint-disable-next-line no-console | ||
| console.warn('Browser geolocation error:', error.message) | ||
| resolve(null) | ||
| }, | ||
| { | ||
| enableHighAccuracy: false, | ||
| timeout: 10000, | ||
| maximumAge: 0, | ||
| } | ||
| ) | ||
| }) | ||
| } | ||
|
|
||
| const extractChapterCoordinates = (chapter: Record<string, unknown>): ChapterCoordinates => { | ||
| const lat = | ||
| (chapter._geoloc as Record<string, unknown>)?.lat ?? | ||
| (chapter.geoLocation as Record<string, unknown>)?.lat ?? | ||
| null | ||
|
|
||
| const lng = | ||
| (chapter._geoloc as Record<string, unknown>)?.lng ?? | ||
| (chapter.geoLocation as Record<string, unknown>)?.lng ?? | ||
| null | ||
|
|
||
| return { lat: lat as number | null, lng: lng as number | null } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * Sort chapters by distance from user | ||
| */ | ||
| 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) | ||
| .sort((a, b) => a!.distance - b!.distance) | ||
|
Check warning on line 98 in frontend/src/utils/geolocationUtils.ts
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.