Skip to content

Commit

Permalink
Map page cross-linking (#310)
Browse files Browse the repository at this point in the history
* Focus on Canopeum if use location unavailable

* Zoom to center of all sites by default

* Select pins from clicking map + visit site link

* Fix map-page scrolling

* fix missing scrollbar in general
  • Loading branch information
Samuel-Therrien-Beslogic authored Nov 28, 2024
1 parent 6c62881 commit 26f5606
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 86 deletions.
8 changes: 3 additions & 5 deletions canopeum_frontend/src/assets/styles/GlobalStyles.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@use '../../../node_modules/bootstrap/scss/bootstrap.scss';
@use 'Variables.scss' as *;

// TODO (Sam. T): Move this file to a GlobalStyles/_index.scss and imported files into same folder
Expand All @@ -10,16 +9,15 @@
@use 'Transitions.scss';

::-webkit-scrollbar {
width: 10px;
height: 10px;
width: 0.5em;
height: 0.5em;
}

::-webkit-scrollbar-track {
background-color: transparent;
}

::-webkit-scrollbar-thumb {
background-color: map-get(bootstrap.$colors, dark-gray);
background-color: $gray-600; // map-get($colors, gray)
border-radius: 5px;
}

Expand Down
11 changes: 6 additions & 5 deletions canopeum_frontend/src/components/auth/AuthPageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ const AuthPageLayout = ({ children }: Props) => {
overflow-y-auto
'>
{children}
<Link className='mt-4 d-flex align-items-center link-inner-underline' to={appRoutes.map}>
<span className='material-symbols-outlined text-primary text-decoration-none'>
<Link
className='mt-4 link-inner-underline text-primary '
to={appRoutes.map}
>
<span className='material-symbols-outlined text-decoration-none align-top'>
arrow_back
</span>
<span className='text-primary'>
{translate('auth.back-to-map')}
</span>
<span className='ms-1'>{translate('auth.back-to-map')}</span>
</Link>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions canopeum_frontend/src/components/social/PostCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const PostCard = ({ post, showActions }: Props) => {
<div className='card'>
<div className='card-body d-flex flex-column gap-3'>
<div className='d-flex justify-content-start align-items-center gap-2'>
<Link to={appRoutes.site(post.site.id)}>
<Link to={appRoutes.siteSocial(post.site.id)}>
<img
alt='site'
className='rounded-circle'
Expand All @@ -100,7 +100,7 @@ const PostCard = ({ post, showActions }: Props) => {
</Link>
<div className='d-flex flex-column'>
<h6 className='text-uppercase fw-bold mb-1'>
<Link to={appRoutes.site(post.site.id)}>{post.site.name}</Link>
<Link to={appRoutes.siteSocial(post.site.id)}>{post.site.name}</Link>
</h6>
<Link className='text-muted initialism' to={appRoutes.postDetail(post.id)}>
{formatDate(post.createdAt, { dateStyle: 'short' })}
Expand Down
1 change: 1 addition & 0 deletions canopeum_frontend/src/locale/en/social.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default {
unfollow: 'Unfollow',
public: 'Public',
},
'visit-site': 'Visit the Site',
comments: {
'leave-a-comment': 'Leave a Comment',
word_one: 'word',
Expand Down
1 change: 1 addition & 0 deletions canopeum_frontend/src/locale/fr/social.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default {
unfollow: 'Ne plus suivre',
public: 'Publique',
},
'visit-site': 'Visiter le Site',
comments: {
'leave-a-comment': 'Laisser un Commentaire',
word_one: 'mot',
Expand Down
40 changes: 10 additions & 30 deletions canopeum_frontend/src/pages/MapPage.scss
Original file line number Diff line number Diff line change
@@ -1,51 +1,31 @@
@use '../assets/styles/Variables.scss' as *;

#map-page-row-container {
padding: 0 2rem;
}

#map-sites-list-container {
padding: 0;
}

.map-site-image {
border-radius: $border-radius $border-radius 0 0;
width: 100%;
}

@media screen and (min-width: $medium-width) {
#map-page-row-container {
padding: 0 4rem;
}
#map-container {
padding: 0;
}
#map-page-row-container {
height: calc(100vh - 62px);
}
#map-container {
height: calc(100vh - 62px);
}

@media screen and (max-width: $large-width) {
#map-sites-list-container,
#map-container {
height: calc(100vh - 3.3rem);
margin-top: 2rem;
padding: 0 3rem;
}
}

@media screen and (min-width: $large-width) {
#map-sites-list-container {
overflow-y: auto;
padding: 1rem;
}

#map-page-row-container {
padding: 0;
}

#map-container {
height: 100%;
margin: 0;
margin-top: 0;
}

#map-page-row-container {
height: calc(100vh - 4rem);
}

.map-site-image {
border-radius: $border-radius 0 0 $border-radius;
width: unset;
Expand Down
159 changes: 118 additions & 41 deletions canopeum_frontend/src/pages/MapPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import './MapPage.scss'

import { useCallback, useEffect, useState } from 'react'
import type { Marker as MarkerInstance } from 'maplibre-gl'
import { type CSSProperties, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { MarkerEvent } from 'react-map-gl/dist/esm/types'
import ReactMap, { GeolocateControl, Marker, NavigationControl, ScaleControl, type ViewState } from 'react-map-gl/maplibre'
import { Link } from 'react-router-dom'

Expand All @@ -11,64 +14,123 @@ import { getSiteTypeIconKey, type SiteTypeID } from '@models/SiteType'
import type { SiteMap } from '@services/api'
import { getApiBaseUrl } from '@services/apiSettings'

type MarkerEvent = {
target: {
_lngLat: {
lat: number,
lng: number,
const PIN_FOCUS_ZOOM_LEVEL = 15
const MAP_DISTANCE_ZOOM_MULTIPLIER = 20

/**
* The initial map location if the user doesn't provide location
*/
const initialMapLocation = (sites: SiteMap[]) => {
// eslint-disable-next-line unicorn/no-array-reduce -- Find the middle point between all sites
const { minLat, maxLat, minLong, maxLong } = sites.reduce(
(previous, current) => {
// Unset or invalid coordinate should be ignored when trying to pin the center of all sites
if (!current.coordinates.latitude || !current.coordinates.longitude) return previous

return {
minLat: Math.min(previous.minLat, current.coordinates.latitude),
maxLat: Math.max(previous.maxLat, current.coordinates.latitude),
minLong: Math.min(previous.minLong, current.coordinates.longitude),
maxLong: Math.max(previous.maxLong, current.coordinates.longitude),
}
},
},
{ minLat: 90, maxLat: -90, minLong: 180, maxLong: -180 },
)

return {
// Center the map to the middle point between all sites
latitude: (maxLat + minLat) / 2,
longitude: (maxLong + minLong) / 2,
// min to take the most zoomed out between latitude or longitude
zoom: Math.min(
// 0 is max zoomed out, so we use an "inverse" (1 / x)
// The bigger the distance (max - min), the lower the zoom
(1 / (maxLat - minLat)) * MAP_DISTANCE_ZOOM_MULTIPLIER,
(1 / (maxLong - minLong)) * MAP_DISTANCE_ZOOM_MULTIPLIER,
),
}
}

const MapPage = () => {
const { getApiClient } = useApiClient()

const { t } = useTranslation()
const [sites, setSites] = useState<SiteMap[]>([])
const [selectedSiteId, setSelectedSiteId] = useState<number | undefined>()

const [mapViewState, setMapViewState] = useState({
longitude: -100,
latitude: 40,
zoom: 5,
longitude: 0,
latitude: 0,
zoom: PIN_FOCUS_ZOOM_LEVEL,
})

const fetchData = useCallback(async () => {
const response = await getApiClient().siteClient.map()
setSites(response)
return response
}, [getApiClient])

const onMarkerClick = (event: MarkerEvent, site: SiteMap) => {
const { lat, lng } = event.target._lngLat
const onSelectSite = (
site: SiteMap,
mapMarkerEvent?: MarkerEvent<MarkerInstance, MouseEvent>,
) => {
const latitude = mapMarkerEvent?.target._lngLat.lat ?? site.coordinates.latitude
const longitude = mapMarkerEvent?.target._lngLat.lng ?? site.coordinates.longitude
if (!latitude || !longitude) return

setMapViewState({
latitude: lat,
longitude: lng,
zoom: 15,
latitude,
longitude,
zoom: PIN_FOCUS_ZOOM_LEVEL,
})
setSelectedSiteId(site.id)
document.getElementById(`${site.id}`)?.scrollIntoView({ behavior: 'smooth' })
if (mapMarkerEvent) {
// Clicked from map, scroll card into view
document.getElementById(`site-card-${site.id}`)?.scrollIntoView({ behavior: 'smooth' })
} else {
// Clicked from card, scroll to top for mobile
window.scrollTo({ behavior: 'smooth', top: 0 })
}
}

const onMapMove = (viewState: ViewState) => {
setMapViewState(viewState)
setSelectedSiteId(undefined)
}

useEffect(() => {
void fetchData()

/* eslint-disable-next-line sonarjs/no-intrusive-permissions
-- We only ask when the map is rendered */
navigator.geolocation.getCurrentPosition(position => {
const { latitude, longitude } = position.coords
setMapViewState(mvs => ({ ...mvs, latitude, longitude }))
})
}, [fetchData])
useEffect(() =>
void Promise.all([
fetchData(),
new Promise(
(
resolve: (position: GeolocationPosition | GeolocationPositionError) => void,
_reject,
): void => {
/* eslint-disable-next-line sonarjs/no-intrusive-permissions
-- We only ask when the map is rendered */
navigator.geolocation.getCurrentPosition(resolve, resolve)
},
),
]).then(([fetchedSites, position]) =>
'code' in position
// If there's an error obtaining the user position, use our default position instead
// Note that getCurrentPosition always error code 2 in http
? setMapViewState(mvs => ({
...mvs,
...initialMapLocation(fetchedSites),
}))
// Otherwise focus on the user's position
: setMapViewState(mvs => ({
...mvs,
latitude: position.coords.latitude,
longitude: position.coords.longitude,
}))
), [fetchData])

return (
<div className='container-fluid p-0'>
<div className='container-fluid p-0 h-100'>
<div className='row flex-row-reverse m-0' id='map-page-row-container'>
<div
className='col-12 col-lg-8 d-flex p-0'
className='col-12 col-lg-8 d-flex '
id='map-container'
>
<ReactMap
Expand All @@ -90,7 +152,7 @@ const MapPage = () => {
key={`${site.id}-${latitude}-${longitude}`}
latitude={latitude}
longitude={longitude}
onClick={event => onMarkerClick(event, site)}
onClick={event => onSelectSite(site, event)}
style={{ cursor: 'pointer' }}
>
<SiteTypePin siteTypeId={site.siteType.id as SiteTypeID} />
Expand All @@ -100,24 +162,23 @@ const MapPage = () => {
</ReactMap>
</div>

<div
className='col-12 col-lg-4 h-100'
id='map-sites-list-container'
>
<div className='py-3 d-flex flex-column gap-3'>
<div className='col-12 col-lg-4 h-100 py-3' id='map-sites-list-container'>
<div className='d-flex flex-column gap-3'>
{sites.map(site => (
<div
className={`card ${
selectedSiteId === site.id
? 'border border-secondary border-5'
? 'shadow '
: ''
}`}
id={`site-card-${site.id}`}
key={site.id}
style={{ '--bs-box-shadow': '0 0 0 0.25rem var(--bs-secondary)' } as CSSProperties}
>
<Link
className='stretched-link list-group-item-action'
id={`${site.id}`}
to={appRoutes.siteSocial(site.id)}
<button
className='stretched-link list-group-item-action unstyled-button rounded'
onClick={() => onSelectSite(site)}
type='button'
>
<div className='row g-0 h-100'>
<div className='col-lg-4'>
Expand All @@ -143,10 +204,26 @@ const MapPage = () => {
<span className='material-symbols-outlined fill-icon'>location_on</span>
<span className='ms-1'>{site.coordinates.address}</span>
</h6>

<Link
className='fw-bold text-secondary link-inner-underline'
// special styles needed for link-in-button hover
style={{ position: 'relative', zIndex: 2 }}
to={appRoutes.siteSocial(site.id)}
>
<span className='me-1'>{t('social.visit-site')}</span>
<span className='
material-symbols-outlined
align-top
text-decoration-none
'>
arrow_forward
</span>
</Link>
</div>
</div>
</div>
</Link>
</button>
</div>
))}
</div>
Expand Down
4 changes: 2 additions & 2 deletions canopeum_frontend/src/pages/PostDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ const PostDetailsPage = () => {
return (
<div className='container py-5'>
<Link
className='mb-3 d-flex align-items-center text-light link-inner-underline'
className='d-inline-block mb-3 text-light link-inner-underline'
to={appRoutes.siteSocial(postDetail.site.id)}
>
<span className='material-symbols-outlined text-decoration-none'>arrow_back</span>
<span className='material-symbols-outlined text-decoration-none align-top'>arrow_back</span>
<span className='ms-1'>{translate('posts.back-to-social')}</span>
</Link>

Expand Down
2 changes: 1 addition & 1 deletion pyrightconfig.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Must be set at root for pylance to pickup. Can't be in cannopeum_backend/pyproject.toml. bleh
// Must be set at root for pylance to pickup. Can't be in canopeum_backend/pyproject.toml. bleh
// https://github.com/microsoft/pyright/blob/main/docs/configuration.md#sample-pyprojecttoml-file
{
"pythonVersion": "3.12",
Expand Down

0 comments on commit 26f5606

Please sign in to comment.