From 2be0bf686d80603017353a69fc491e271d33f39e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20St=C3=BCckler?= <moritz.stueckler@gmail.com> Date: Fri, 4 Jun 2021 13:02:09 +0000 Subject: [PATCH] Feat/styling updates --- public/index.html | 4 +- src/components/App.tsx | 5 +++ src/components/Map/Map.tsx | 7 +++- src/components/Map/MapLayerControl.tsx | 43 ++++++++++++-------- src/components/PoiLoader.tsx | 21 ++++++++++ src/components/Select.tsx | 1 + src/components/Sidebar/CloseButton.tsx | 20 +++++++++ src/components/Sidebar/SidebarCreateView.tsx | 8 +--- src/components/Sidebar/SidebarListView.tsx | 39 +++++++++++++----- src/components/Sidebar/SidebarSingleView.tsx | 24 +++++++---- 10 files changed, 125 insertions(+), 47 deletions(-) create mode 100644 src/components/PoiLoader.tsx create mode 100644 src/components/Sidebar/CloseButton.tsx diff --git a/public/index.html b/public/index.html index 30016a1..92d157d 100644 --- a/public/index.html +++ b/public/index.html @@ -6,8 +6,8 @@ <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Fab City Hamburg Map</title> <!-- TODO: This should be imported from the leaflet npm package, not manually from the public folder. See https://github.com/snowpackjs/snowpack/discussions/1716 --> - <link rel="stylesheet" href="leaflet.css" /> - <script src="leaflet.js"></script> + <link rel="stylesheet" href="/leaflet.css" /> + <script src="/leaflet.js"></script> </head> <body class="h-full w-full"> <div id="root" class="h-full w-full"></div> diff --git a/src/components/App.tsx b/src/components/App.tsx index 2e552fb..d834c64 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -7,6 +7,7 @@ import Map from './Map/Map'; import SidebarCreateView from './Sidebar/SidebarCreateView'; import SidebarListView from './Sidebar/SidebarListView'; import SidebarSingleView from './Sidebar/SidebarSingleView'; +import PoiLoader from './PoiLoader'; const App: React.FC = () => { const selectedPoi = useStore((state) => state.selectedPoi); @@ -17,6 +18,10 @@ const App: React.FC = () => { <Notification /> <div className={'flex md:flex-row-reverse flex-col h-full'}> <Route path="/add">{({ match }) => <Map createMode={!!match} />}</Route> + <Route path="/poi/:poiId">{({ match }) => <PoiLoader poiId={match?.params?.poiId as string} />}</Route> + <Route exact path="/"> + <PoiLoader poiId={null} /> + </Route> <Switch> <Route exact path="/add"> <SidebarCreateView /> diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index cb97e5f..e397e68 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -6,6 +6,7 @@ import { useStore } from '../../hooks'; import MapViewController from './MapViewController'; import { useFilteredPoiData } from '../../hooks/useFilteredPoiData'; import MapLayerControl from './MapLayerControl'; +import { useHistory } from 'react-router-dom'; interface Props { createMode?: boolean; @@ -32,7 +33,7 @@ export const Map: React.FC<Props> = ({ createMode }) => { const hoveredPoi = useStore((state) => state.hoveredPoi); const setHoveredPoi = useStore((state) => state.setHoveredPoi); const selectedPoi = useStore((state) => state.selectedPoi); - const setSelectedPoi = useStore((state) => state.setSelectedPoi); + const history = useHistory(); const selectedLatlng: LatLngTuple | undefined = selectedPoi ? [selectedPoi?.lat, selectedPoi?.lng] : undefined; return ( @@ -71,7 +72,9 @@ export const Map: React.FC<Props> = ({ createMode }) => { key={poi.id} position={poiLatLng} eventHandlers={{ - click: () => setSelectedPoi(poi), + click: () => { + history.push(`/poi/${String(poi.id)}`); + }, mouseover: () => { setHoveredPoi(poi); }, diff --git a/src/components/Map/MapLayerControl.tsx b/src/components/Map/MapLayerControl.tsx index c3e9c05..285fc56 100644 --- a/src/components/Map/MapLayerControl.tsx +++ b/src/components/Map/MapLayerControl.tsx @@ -1,11 +1,13 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useFilteredPoiData } from '../../hooks/useFilteredPoiData'; import { usePoiData } from '../../hooks'; +import { ChevronDownOutline as DownIcon } from 'heroicons-react'; const MapLayerControl: React.FC = () => { const { data } = usePoiData(); const { filterCategories, setFilterCategories } = useFilteredPoiData(); const layers = Array.from(new Set(data?.map((poi) => poi.category))); + const [isOpen, setIsOpen] = useState(false); const onChangeCheckbox = (state: boolean, layer: string) => { if (state) { @@ -16,22 +18,29 @@ const MapLayerControl: React.FC = () => { }; return ( - <div className="border-2 border-black border-opacity-20 rounded-lg absolute right-0 bottom-0 mb-4 mr-4 bg-white p-4 z-10"> - {layers.map((layer) => { - return ( - <div key={layer}> - <label className="inline-flex items-center"> - <input - type="checkbox" - className="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-offset-0 focus:ring-indigo-200 focus:ring-opacity-50" - checked={!!filterCategories?.find((cat) => cat === layer)} - onChange={(e) => onChangeCheckbox(e.target.checked, layer)} - /> - <span className="ml-2">{layer}</span> - </label> - </div> - ); - })} + <div className="border-2 border-black w-48 border-opacity-20 rounded-lg absolute right-0 bottom-0 mb-4 mr-4 bg-white p-4 z-10"> + <div className={`flex justify-between cursor-pointer ${isOpen ? 'mb-2' : ''}`} onClick={() => setIsOpen(!isOpen)}> + <h3 className="text-base font-semibold text-gray-900">Kategorien:</h3> + <DownIcon + className={`w-6 h-6 text-gray-400 transform transition duration-300 ${isOpen ? 'rotate-180' : 'rotate-0'}`} + /> + </div> + {isOpen && + layers.map((layer) => { + return ( + <div key={layer}> + <label className="inline-flex items-center"> + <input + type="checkbox" + className="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-offset-0 focus:ring-indigo-200 focus:ring-opacity-50" + checked={!!filterCategories?.find((cat) => cat === layer)} + onChange={(e) => onChangeCheckbox(e.target.checked, layer)} + /> + <span className="ml-2">{layer}</span> + </label> + </div> + ); + })} </div> ); }; diff --git a/src/components/PoiLoader.tsx b/src/components/PoiLoader.tsx new file mode 100644 index 0000000..2255619 --- /dev/null +++ b/src/components/PoiLoader.tsx @@ -0,0 +1,21 @@ +import React, { useEffect } from 'react'; +import { usePoiData, useStore } from '../hooks'; + +interface Props { + poiId: string | null; +} + +const PoiLoader: React.FC<Props> = ({ poiId }) => { + const { data } = usePoiData(); + const setSelectedPoi = useStore((state) => state.setSelectedPoi); + + useEffect(() => { + if (data && poiId !== undefined) { + const poi = poiId ? data.find((poi) => String(poi.id) === poiId) : null; + if (poi !== undefined) setSelectedPoi(poi); + } + }, [data, poiId]); + return <></>; +}; + +export default PoiLoader; diff --git a/src/components/Select.tsx b/src/components/Select.tsx index 77ccdf9..8247b09 100644 --- a/src/components/Select.tsx +++ b/src/components/Select.tsx @@ -24,6 +24,7 @@ const Select = <OptionType, isMulti extends boolean>(props: NamedProps<OptionTyp primary25: '#C7D2FE', }, })} + placeholder={'Tags auswählen...'} styles={customStyles} name="pois" className="hover:border-opacity-40 rounded-lg w-full border-2 border-black border-opacity-20 focus-within:border-indigo-300 focus-within:ring focus-within:ring-indigo-200 focus-within:ring-opacity-50 mt-1" diff --git a/src/components/Sidebar/CloseButton.tsx b/src/components/Sidebar/CloseButton.tsx new file mode 100644 index 0000000..7ed9675 --- /dev/null +++ b/src/components/Sidebar/CloseButton.tsx @@ -0,0 +1,20 @@ +import React, { SyntheticEvent } from 'react'; +import { X as CloseIcon } from 'heroicons-react'; +interface Props { + onClick: (event: SyntheticEvent) => void; + absolute?: boolean; +} + +const CloseButton: React.FC<Props> = ({ onClick, absolute = false }) => { + return ( + <CloseIcon + size={32} + className={`${ + absolute ? 'absolute left-5 top-5' : '' + } p-1 text-gray-600 inline-block cursor-pointer bg-gray-200 bg-opacity-30 hover:bg-opacity-80 rounded-full`} + onClick={onClick} + /> + ); +}; + +export default CloseButton; diff --git a/src/components/Sidebar/SidebarCreateView.tsx b/src/components/Sidebar/SidebarCreateView.tsx index dc12838..1c4040b 100644 --- a/src/components/Sidebar/SidebarCreateView.tsx +++ b/src/components/Sidebar/SidebarCreateView.tsx @@ -1,18 +1,14 @@ -import { X as CloseIcon } from 'heroicons-react'; import React from 'react'; import { useHistory } from 'react-router-dom'; import AddPoiForm from '../Form/AddPoiForm'; +import CloseButton from './CloseButton'; import SidebarContainer from './SidebarContainer'; const SidebarCreateView: React.FC = () => { const history = useHistory(); return ( <SidebarContainer className="p-5"> - <CloseIcon - size={32} - className={`flex-shrink-0 left-5 top-5 p-1 text-gray-500 inline-block cursor-pointer hover:bg-gray-300 hover:bg-opacity-50 rounded-full`} - onClick={() => history.push('/')} - /> + <CloseButton onClick={() => history.push('/')} /> <h1 className="text-xl font-medium title-font text-gray-900 mt-2 mb-4">Neuen Ort anlegen:</h1> <AddPoiForm /> </SidebarContainer> diff --git a/src/components/Sidebar/SidebarListView.tsx b/src/components/Sidebar/SidebarListView.tsx index 7b102af..d2bf1d0 100644 --- a/src/components/Sidebar/SidebarListView.tsx +++ b/src/components/Sidebar/SidebarListView.tsx @@ -1,20 +1,23 @@ -import React from 'react'; +import React, { useState } from 'react'; import { usePoiData, useStore } from '../../hooks'; import ListElement from './ListElement'; import SidebarContainer from './SidebarContainer'; import { removeDuplicateObjects } from '../../util/array'; import { useFilteredPoiData } from '../../hooks/useFilteredPoiData'; import type { Tag } from '../../types/PointOfInterest'; +import { FilterOutline as FilterIcon } from 'heroicons-react'; +import { useHistory } from 'react-router-dom'; import Select from '../Select'; const SidebarListView: React.FC = () => { const tagsToSelectOptions = (tags?: Tag[]) => tags?.map((tag) => ({ label: tag.displayName, value: tag })); + const history = useHistory(); const { data } = usePoiData(); const { data: filteredData, filterTags, setFilterTags } = useFilteredPoiData(); const hoveredPoi = useStore((state) => state.hoveredPoi); const setHoveredPoi = useStore((state) => state.setHoveredPoi); - const setSelectedPoi = useStore((state) => state.setSelectedPoi); + const [filterInputIsOpen, setFilterInputIsOpen] = useState(false); const tags = data && @@ -26,21 +29,35 @@ const SidebarListView: React.FC = () => { return ( <SidebarContainer> - <div className="p-4"> - <Select - options={options} - isMulti={true} - value={filterTags && tagsToSelectOptions(filterTags)} - onChange={(selectedOptions) => setFilterTags(selectedOptions.map((opt) => opt.value))} - /> + <div className="flex-col m-4 mb-2 pb-2 border-black border-opacity-20 border-b-2"> + <div className="flex justify-between items-center"> + <h1 className="text-xl font-medium title-font text-gray-900">{filteredData?.length} Orte:</h1> + <FilterIcon + onClick={() => setFilterInputIsOpen(!filterInputIsOpen)} + className="text-black text-opacity-20 hover:text-opacity-60 w-5 h-5 cursor-pointer" + /> + </div> + + {filterInputIsOpen && ( + <div className="py-2"> + <Select + options={options} + isMulti={true} + value={filterTags && tagsToSelectOptions(filterTags)} + onChange={(selectedOptions) => setFilterTags(selectedOptions.map((opt) => opt.value))} + /> + </div> + )} </div> - <h1 className="text-xl font-medium title-font m-4 text-gray-900 mb-2">{filteredData?.length} Orte:</h1> + {filteredData?.map((poi) => ( <ListElement key={poi.id} onMouseEnter={() => setHoveredPoi(poi)} onMouseLeave={() => setHoveredPoi(null)} - onClick={() => setSelectedPoi(poi)} + onClick={() => { + history.push(`/poi/${String(poi.id)}`); + }} value={poi} hovered={hoveredPoi?.id === poi.id} /> diff --git a/src/components/Sidebar/SidebarSingleView.tsx b/src/components/Sidebar/SidebarSingleView.tsx index 4252c3f..64645a5 100644 --- a/src/components/Sidebar/SidebarSingleView.tsx +++ b/src/components/Sidebar/SidebarSingleView.tsx @@ -1,23 +1,24 @@ -import { HomeOutline as HomeIcon, LocationMarkerOutline as AddressIcon, X as CloseIcon } from 'heroicons-react'; +import { HomeOutline as HomeIcon, LocationMarkerOutline as AddressIcon } from 'heroicons-react'; import React from 'react'; import { useStore } from '../../hooks'; import SidebarContainer from './SidebarContainer'; import Tag from '../Tag'; +import CloseButton from './CloseButton'; +import { useHistory } from 'react-router-dom'; const SidebarSingleView: React.FC = () => { const selectedPoi = useStore((state) => state.selectedPoi); - const setSelectedPoi = useStore((state) => state.setSelectedPoi); const strippedUrl = selectedPoi?.website?.replace(/(^\w+:|^)\/\//, ''); + const history = useHistory(); return ( <SidebarContainer className={`relative p-0`}> <div className={`${selectedPoi?.image ? '' : 'pl-5 pt-5 '}`}> - <CloseIcon - size={32} - className={`${ - selectedPoi?.image ? 'absolute left-5 top-5 ' : '' - }p-1 text-gray-500 inline-block cursor-pointer hover:bg-gray-300 hover:bg-opacity-50 rounded-full`} - onClick={() => setSelectedPoi(null)} + <CloseButton + absolute + onClick={() => { + history.push('/'); + }} /> </div> {selectedPoi?.image && ( @@ -36,7 +37,12 @@ const SidebarSingleView: React.FC = () => { {selectedPoi?.website && ( <div className={'flex items-center'}> <HomeIcon size={18} className={'text-gray-500 mr-2'} /> - <a className={'text-sm text-gray-500 hover:underline'} href={selectedPoi?.website}> + <a + target="_blank" + rel="noopener noreferrer" + className={'text-sm text-gray-500 hover:underline'} + href={selectedPoi?.website} + > {strippedUrl} </a> </div> -- GitLab