From 628632ff5092f1e3cf6c968d9bdfbd9a24f59541 Mon Sep 17 00:00:00 2001 From: Marcin Zelent Date: Fri, 8 Jan 2021 19:47:37 +0100 Subject: Initial commit --- components/Gallery/Gallery.module.css | 63 ++++++++++++++++++ components/Gallery/Gallery.tsx | 80 ++++++++++++++++++++++ components/Map/Map.module.css | 28 ++++++++ components/Map/Map.tsx | 121 ++++++++++++++++++++++++++++++++++ components/Sidebar/Sidebar.module.css | 54 +++++++++++++++ components/Sidebar/Sidebar.tsx | 71 ++++++++++++++++++++ 6 files changed, 417 insertions(+) create mode 100644 components/Gallery/Gallery.module.css create mode 100644 components/Gallery/Gallery.tsx create mode 100644 components/Map/Map.module.css create mode 100644 components/Map/Map.tsx create mode 100644 components/Sidebar/Sidebar.module.css create mode 100644 components/Sidebar/Sidebar.tsx (limited to 'components') diff --git a/components/Gallery/Gallery.module.css b/components/Gallery/Gallery.module.css new file mode 100644 index 0000000..01e8037 --- /dev/null +++ b/components/Gallery/Gallery.module.css @@ -0,0 +1,63 @@ +.galleryContainer { + padding: 20px; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 99999; + background: rgba(0, 0, 0, 0.8); +} + +.photoContainer { + width: 100%; + height: 100%; + padding: 20px; + position: relative; +} + +.photo { + max-width: 100%; + max-height: 100%; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.btn { + position: absolute; + z-index: 99999; + font-size: 2rem; + color: #eee; + background: none; + border: none; + cursor: pointer; + outline: currentcolor none medium; +} + +.btn:hover { + color: #fff; +} + +.arrowLeft { + top: 50%; + left: 0; + transform: translateY(-50%); +} + +.arrowRight { + top: 50%; + right: 0; + transform: translateY(-50%); +} + +.closeBtn { + width: 64px; + height: 64px; + top: 0; + right: 0; + font-size: 4rem; + line-height: 4rem; + font-weight: 300; +} diff --git a/components/Gallery/Gallery.tsx b/components/Gallery/Gallery.tsx new file mode 100644 index 0000000..dfadbc0 --- /dev/null +++ b/components/Gallery/Gallery.tsx @@ -0,0 +1,80 @@ +import { useEffect, useRef } from 'react'; + +import { Photo } from 'models'; +import useEvent from 'lib/useEvent'; +import styles from './Gallery.module.css'; + +interface Props { + currentPhoto: Photo; + handleClose: () => void; + handlePhotoChange: (direction: boolean) => void; +} + +export default function Gallery({ + currentPhoto, + handleClose, + handlePhotoChange, +}: Props): JSX.Element { + const wrapperRef = useRef(null); + + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if ( + wrapperRef.current && + !wrapperRef.current.contains(e.target) && + (e.target as Node).nodeName !== 'BUTTON' + ) { + handleClose(); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [wrapperRef]); + + useEvent('keydown', (e: KeyboardEvent) => { + if (e.key === 'ArrowLeft') { + handlePhotoChange(false); + } else if (e.key === 'ArrowRight') { + handlePhotoChange(true); + } else if (e.key === 'Escape') { + handleClose(); + } + }); + + return ( +
+ +
+ {currentPhoto.name} +
+ + +
+ ); +} diff --git a/components/Map/Map.module.css b/components/Map/Map.module.css new file mode 100644 index 0000000..b13f5d2 --- /dev/null +++ b/components/Map/Map.module.css @@ -0,0 +1,28 @@ +.popup { + width: auto; + min-height: 90%; +} + +.popup .leaflet-popup-content { + margin: 0; +} + +.markerIcon { + border: 2px solid #fff; + border-radius: 2px; + overflow: hidden; +} + +.markerItemCount { + width: 18px; + height: 18px; + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 10px; + background: #fff; + text-align: center; + font-weight: bold; +} diff --git a/components/Map/Map.tsx b/components/Map/Map.tsx new file mode 100644 index 0000000..44f8ea6 --- /dev/null +++ b/components/Map/Map.tsx @@ -0,0 +1,121 @@ +import { LatLngBounds, DivIcon, Icon } from 'leaflet'; +import { useEffect, useState } from 'react'; +import ReactDom from 'react-dom/server'; +import { MapContainer, Marker, TileLayer, GeoJSON, ZoomControl } from 'react-leaflet'; +import MarkerClusterGroup from 'react-leaflet-markercluster'; + +import { Trip, Photo } from 'models'; +import { distanceBetween } from 'lib/util'; + +import 'leaflet/dist/leaflet.css'; +import styles from './Map.module.css'; + +interface Props { + trip: Trip; + handleMarkerClick: (photo: string) => void; +} + +export default function Map({ trip, handleMarkerClick }: Props): JSX.Element { + const [map, setMap] = useState(null); + + const bounds = trip + ? new LatLngBounds( + [trip.track.bbox[1], trip.track.bbox[0]], + [trip.track.bbox[3], trip.track.bbox[2]], + ) + : new LatLngBounds([75, -145], [-52, 145]); + + function createMarkers(photos: Photo[]): JSX.Element[] { + // cluster photos that are close to each other + const clusters: Photo[][] = []; + for (let i = 0; i < photos.length; i += 1) { + if (clusters.filter((c) => c.includes(photos[i])).length === 0) { + const cluster = [photos[i]]; + for (let j = 0; j < photos.length; j += 1) { + if (photos[i] !== photos[j]) { + const a = [photos[i].latitude, photos[i].longitude]; + const b = [photos[j].latitude, photos[j].longitude]; + const distance = distanceBetween(a, b); + if (distance < 10) cluster.push(photos[j]); + } + } + clusters.push(cluster); + } + } + + // create React elements based on the clusters + const markers = clusters.map((cluster) => { + let Wrapper = ({ children }) => <>{children}; + if (cluster.length > 1) { + Wrapper = ({ children }) => ( + + new DivIcon({ + html: ReactDom.renderToString( + <> + + {markerCluster.getChildCount()} + , + ), + iconSize: [36, 36], + className: styles.markerIcon, + }) + } + > + {children} + + ); + } + + const children = cluster.map((photo) => ( + handleMarkerClick(photo.name) }} + /> + )); + + return {children}; + }); + + return markers; + } + + useEffect(() => { + if (map) { + map.setView(bounds.getCenter()); + map.fitBounds(bounds); + } + }, [trip]); + + return ( + + + + {trip && } + {trip && createMarkers(trip.photos)} + + ); +} diff --git a/components/Sidebar/Sidebar.module.css b/components/Sidebar/Sidebar.module.css new file mode 100644 index 0000000..a3c8a1a --- /dev/null +++ b/components/Sidebar/Sidebar.module.css @@ -0,0 +1,54 @@ +.aside { + min-width: 272px; + height: 100%; + padding: 5px; + display: flex; + flex-direction: column; + border-right: 1px solid #e5e5e5; + box-shadow: -2px 0 10px #000; + z-index: 9999; + background-color: #fff; +} + +.aside h2 { + padding-left: 1.5rem; +} + +.list { + margin: 0; + padding: 0; + list-style: none; + overflow-y: auto; +} + +.listItem { + margin-bottom: 5px; + padding: 1rem 2rem 1rem 1.5rem; + display: block; + border-radius: 10px; + cursor: pointer; + user-select: none; + outline: currentcolor none medium; +} + +.listItem:hover { + background-color: #f5f5f5; +} + +.listItemActive { + background-color: #e9f3ff !important; +} + +@media only screen and (max-width: 500px) { + .aside { + position: absolute; + top: 0; + bottom: 0; + left: -100vw; + transition: left 0.3s ease-in; + } + + .asideOpen { + left: 0; + } +} diff --git a/components/Sidebar/Sidebar.tsx b/components/Sidebar/Sidebar.tsx new file mode 100644 index 0000000..6b0473a --- /dev/null +++ b/components/Sidebar/Sidebar.tsx @@ -0,0 +1,71 @@ +import { useEffect, useRef } from 'react'; + +import { Trip } from 'models'; +import { secondsToTimeString } from 'lib/util'; + +import styles from './Sidebar.module.css'; + +interface Props { + trips: Trip[]; + currentTrip: Trip; + asideOpen: boolean; + handleClose: () => void; + setCurrentTrip: (trip: Trip) => void; +} + +export default function Sidebar({ + trips, + currentTrip, + asideOpen, + handleClose, + setCurrentTrip, +}: Props): JSX.Element { + const wrapperRef = useRef(null); + + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (wrapperRef.current && !wrapperRef.current.contains(e.target)) { + handleClose(); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [wrapperRef]); + + function handleTripChange(trip: Trip): void { + setCurrentTrip(trip); + handleClose(); + } + + return ( + + ); +} -- cgit v1.2.3