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