From 5ac36230ba14e11e2fa06d350669d9d9a1a35241 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Moritz=20St=C3=BCckler?= <moritz.stueckler@gmail.com>
Date: Fri, 23 Apr 2021 14:30:17 +0000
Subject: [PATCH] Feat/add poi form

---
 .gitlab-ci.yml                                |   2 +-
 package-lock.json                             | 482 +++++++++++++++++-
 package.json                                  |   7 +-
 snowpack.config.js                            |   3 +-
 src/App.tsx                                   |  97 ----
 src/Sidebar/SidebarContainer.tsx              |  21 -
 src/Sidebar/SidebarListView.tsx               |  44 --
 src/Sidebar/SidebarSingleView.tsx             |  54 --
 src/components/App.tsx                        |  29 ++
 src/components/ErrorModal.tsx                 |  16 +
 src/{ => components}/Map/Map.tsx              |  59 ++-
 .../Map/MapViewController.tsx                 |   0
 src/{ => components}/Modal.tsx                |   0
 src/components/Sidebar/AddPoiForm.tsx         | 127 +++++
 .../Sidebar/ListElement.tsx}                  |   9 +-
 src/components/Sidebar/SidebarContainer.tsx   |  19 +
 src/components/Sidebar/SidebarCreateView.tsx  |  24 +
 src/components/Sidebar/SidebarListView.tsx    |  31 ++
 src/components/Sidebar/SidebarSingleView.tsx  |  62 +++
 src/components/Sidebar/Tag.tsx                |  19 +
 src/components/SwrWrapper.tsx                 |  29 ++
 src/hooks/index.ts                            |   2 +
 src/hooks/usePoiData.ts                       |  34 ++
 src/hooks/useStore.ts                         |  46 ++
 src/index.css                                 |   8 +
 src/index.tsx                                 |  14 +-
 src/testData.json                             |  55 --
 src/types/Error.ts                            |   5 +
 src/types/PointOfInterest.ts                  |  13 +-
 tailwind.config.js                            |   2 +-
 30 files changed, 988 insertions(+), 325 deletions(-)
 delete mode 100644 src/App.tsx
 delete mode 100644 src/Sidebar/SidebarContainer.tsx
 delete mode 100644 src/Sidebar/SidebarListView.tsx
 delete mode 100644 src/Sidebar/SidebarSingleView.tsx
 create mode 100644 src/components/App.tsx
 create mode 100644 src/components/ErrorModal.tsx
 rename src/{ => components}/Map/Map.tsx (52%)
 rename src/{ => components}/Map/MapViewController.tsx (100%)
 rename src/{ => components}/Modal.tsx (100%)
 create mode 100644 src/components/Sidebar/AddPoiForm.tsx
 rename src/{Sidebar/SidebarListElement.tsx => components/Sidebar/ListElement.tsx} (70%)
 create mode 100644 src/components/Sidebar/SidebarContainer.tsx
 create mode 100644 src/components/Sidebar/SidebarCreateView.tsx
 create mode 100644 src/components/Sidebar/SidebarListView.tsx
 create mode 100644 src/components/Sidebar/SidebarSingleView.tsx
 create mode 100644 src/components/Sidebar/Tag.tsx
 create mode 100644 src/components/SwrWrapper.tsx
 create mode 100644 src/hooks/index.ts
 create mode 100644 src/hooks/usePoiData.ts
 create mode 100644 src/hooks/useStore.ts
 delete mode 100644 src/testData.json
 create mode 100644 src/types/Error.ts

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6a3dd2c..2e644af 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -9,7 +9,7 @@ before_script:
   - apk add --no-cache lftp openssh
   - mkdir -p ~/.ssh
   - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
-  - echo "$FRONTEND_ENV" >> .env
+  - echo "$FRONTEND_ENV" > .env
 
 npm build:
   stage: build
diff --git a/package-lock.json b/package-lock.json
index 4fa0eba..2ad7b7e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,6 +5,7 @@
   "packages": {
     "": {
       "dependencies": {
+        "@tailwindcss/forms": "^0.3.2",
         "graphql": "^15.5.0",
         "graphql-request": "^3.4.0",
         "heroicons-react": "1.3.0",
@@ -12,10 +13,13 @@
         "react": "^17.0.0",
         "react-dom": "^17.0.0",
         "react-leaflet": "^3.1.0",
+        "react-router-dom": "^5.2.0",
         "swr": "^0.5.5",
-        "tailwindcss": "^2.0.3"
+        "tailwindcss": "^2.0.3",
+        "zustand": "^3.4.1"
       },
       "devDependencies": {
+        "@jadex/snowpack-plugin-tailwindcss-jit": "^0.2.0",
         "@snowpack/plugin-dotenv": "^2.1.0",
         "@snowpack/plugin-postcss": "^1.1.0",
         "@snowpack/plugin-react-refresh": "^2.4.0",
@@ -23,6 +27,7 @@
         "@types/leaflet": "^1.7.0",
         "@types/react": "^17.0.0",
         "@types/react-dom": "^17.0.0",
+        "@types/react-router-dom": "^5.1.7",
         "@types/snowpack-env": "^2.3.2",
         "autoprefixer": "^10.2.4",
         "postcss": "^8.2.6",
@@ -260,6 +265,14 @@
         "@babel/core": "^7.0.0-0"
       }
     },
+    "node_modules/@babel/runtime": {
+      "version": "7.13.10",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz",
+      "integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==",
+      "dependencies": {
+        "regenerator-runtime": "^0.13.4"
+      }
+    },
     "node_modules/@babel/template": {
       "version": "7.12.13",
       "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz",
@@ -305,6 +318,21 @@
         "purgecss": "^3.1.3"
       }
     },
+    "node_modules/@jadex/snowpack-plugin-tailwindcss-jit": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/@jadex/snowpack-plugin-tailwindcss-jit/-/snowpack-plugin-tailwindcss-jit-0.2.0.tgz",
+      "integrity": "sha512-iZfHqtLZzsFKAgMBVCGFJdQWWnXoLhWr7m98DJalu772SSMGXrWryW4nC/Hyk/1RDiL861BAVzXO6aZT3KLtgw==",
+      "dev": true,
+      "dependencies": {
+        "fs": "^0.0.1-security",
+        "micromatch": "^4.0.2",
+        "path": "^0.12.7"
+      },
+      "peerDependencies": {
+        "snowpack": "^3.2.2",
+        "tailwindcss": "^2.1.0"
+      }
+    },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.4",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz",
@@ -450,6 +478,7 @@
       "resolved": "https://registry.npmjs.org/@snowpack/plugin-postcss/-/plugin-postcss-1.2.2.tgz",
       "integrity": "sha512-XL4EyfDLcxCeo9bOm8ScNAFq9pU4bFkFq4GY6/Zokq/rxMh9lugOHTLWeT/p4IxLh4CX1o7pX8b61Eplcqlfqg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "postcss-load-config": "^3.0.1",
         "workerpool": "^6.1.2"
@@ -486,6 +515,17 @@
         "typescript": "*"
       }
     },
+    "node_modules/@tailwindcss/forms": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.3.2.tgz",
+      "integrity": "sha512-aj2/rJsGb2whAZ/BQWHWWQRSbhH0r/l1ozOByiv+ZNjBD84GMvb5dhAyfpeasFky+EJrAwX5eaqft8NQMZFWvA==",
+      "dependencies": {
+        "mini-svg-data-uri": "^1.2.3"
+      },
+      "peerDependencies": {
+        "tailwindcss": ">=2.0.0"
+      }
+    },
     "node_modules/@tootallnate/once": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@@ -501,6 +541,12 @@
       "integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==",
       "dev": true
     },
+    "node_modules/@types/history": {
+      "version": "4.7.8",
+      "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz",
+      "integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==",
+      "dev": true
+    },
     "node_modules/@types/leaflet": {
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.7.0.tgz",
@@ -542,6 +588,27 @@
         "@types/react": "*"
       }
     },
+    "node_modules/@types/react-router": {
+      "version": "5.1.13",
+      "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.13.tgz",
+      "integrity": "sha512-ZIuaO9Yrln54X6elg8q2Ivp6iK6p4syPsefEYAhRDAoqNh48C8VYUmB9RkXjKSQAJSJV0mbIFCX7I4vZDcHrjg==",
+      "dev": true,
+      "dependencies": {
+        "@types/history": "*",
+        "@types/react": "*"
+      }
+    },
+    "node_modules/@types/react-router-dom": {
+      "version": "5.1.7",
+      "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.7.tgz",
+      "integrity": "sha512-D5mHD6TbdV/DNHYsnwBTv+y73ei+mMjrkGrla86HthE4/PVvL1J94Bu3qABU+COXzpL23T1EZapVVpwHuBXiUg==",
+      "dev": true,
+      "dependencies": {
+        "@types/history": "*",
+        "@types/react": "*",
+        "@types/react-router": "*"
+      }
+    },
     "node_modules/@types/scheduler": {
       "version": "0.16.1",
       "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz",
@@ -1547,6 +1614,12 @@
         "node": "*"
       }
     },
+    "node_modules/fs": {
+      "version": "0.0.1-security",
+      "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
+      "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=",
+      "dev": true
+    },
     "node_modules/fs-extra": {
       "version": "9.1.0",
       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
@@ -1844,6 +1917,27 @@
         "react": ">=16.13"
       }
     },
+    "node_modules/history": {
+      "version": "4.10.1",
+      "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
+      "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
+      "dependencies": {
+        "@babel/runtime": "^7.1.2",
+        "loose-envify": "^1.2.0",
+        "resolve-pathname": "^3.0.0",
+        "tiny-invariant": "^1.0.2",
+        "tiny-warning": "^1.0.0",
+        "value-equal": "^1.0.1"
+      }
+    },
+    "node_modules/hoist-non-react-statics": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+      "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+      "dependencies": {
+        "react-is": "^16.7.0"
+      }
+    },
     "node_modules/hosted-git-info": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz",
@@ -2178,10 +2272,9 @@
       }
     },
     "node_modules/isarray": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
-      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
-      "dev": true
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+      "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
     },
     "node_modules/isexe": {
       "version": "2.0.0",
@@ -2451,6 +2544,24 @@
         "node": ">=6"
       }
     },
+    "node_modules/mini-create-react-context": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
+      "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
+      "dependencies": {
+        "@babel/runtime": "^7.12.1",
+        "tiny-warning": "^1.0.3"
+      },
+      "peerDependencies": {
+        "prop-types": "^15.0.0",
+        "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
+      }
+    },
+    "node_modules/mini-svg-data-uri": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.2.3.tgz",
+      "integrity": "sha512-zd6KCAyXgmq6FV1mR10oKXYtvmA9vRoB6xPSTUJTbFApCtkefDnYueVR1gkof3KcdLZo1Y8mjF2DFmQMIxsHNQ=="
+    },
     "node_modules/minimatch": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@@ -3049,6 +3160,16 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/path": {
+      "version": "0.12.7",
+      "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
+      "integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=",
+      "dev": true,
+      "dependencies": {
+        "process": "^0.11.1",
+        "util": "^0.10.3"
+      }
+    },
     "node_modules/path-is-absolute": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -3071,6 +3192,14 @@
       "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
       "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
     },
+    "node_modules/path-to-regexp": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
+      "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+      "dependencies": {
+        "isarray": "0.0.1"
+      }
+    },
     "node_modules/path-type": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -3379,6 +3508,15 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
     "node_modules/process-nextick-args": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -3404,6 +3542,16 @@
         "node": ">=10"
       }
     },
+    "node_modules/prop-types": {
+      "version": "15.7.2",
+      "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
+      "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
+      "dependencies": {
+        "loose-envify": "^1.4.0",
+        "object-assign": "^4.1.1",
+        "react-is": "^16.8.1"
+      }
+    },
     "node_modules/psl": {
       "version": "1.8.0",
       "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
@@ -3497,6 +3645,11 @@
         "react": "17.0.2"
       }
     },
+    "node_modules/react-is": {
+      "version": "16.13.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+    },
     "node_modules/react-leaflet": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-3.1.0.tgz",
@@ -3519,6 +3672,43 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/react-router": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
+      "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
+      "dependencies": {
+        "@babel/runtime": "^7.1.2",
+        "history": "^4.9.0",
+        "hoist-non-react-statics": "^3.1.0",
+        "loose-envify": "^1.3.1",
+        "mini-create-react-context": "^0.4.0",
+        "path-to-regexp": "^1.7.0",
+        "prop-types": "^15.6.2",
+        "react-is": "^16.6.0",
+        "tiny-invariant": "^1.0.2",
+        "tiny-warning": "^1.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=15"
+      }
+    },
+    "node_modules/react-router-dom": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz",
+      "integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==",
+      "dependencies": {
+        "@babel/runtime": "^7.1.2",
+        "history": "^4.9.0",
+        "loose-envify": "^1.3.1",
+        "prop-types": "^15.6.2",
+        "react-router": "5.2.0",
+        "tiny-invariant": "^1.0.2",
+        "tiny-warning": "^1.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=15"
+      }
+    },
     "node_modules/read-cache": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -3556,6 +3746,12 @@
         "util-deprecate": "~1.0.1"
       }
     },
+    "node_modules/readable-stream/node_modules/isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+      "dev": true
+    },
     "node_modules/readdirp": {
       "version": "3.5.0",
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
@@ -3581,6 +3777,11 @@
       "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
       "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="
     },
+    "node_modules/regenerator-runtime": {
+      "version": "0.13.7",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
+      "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
+    },
     "node_modules/request": {
       "version": "2.88.2",
       "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
@@ -3657,6 +3858,11 @@
         "node": ">=4"
       }
     },
+    "node_modules/resolve-pathname": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
+      "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
+    },
     "node_modules/retry": {
       "version": "0.12.0",
       "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
@@ -4130,6 +4336,16 @@
         "node": ">= 10"
       }
     },
+    "node_modules/tiny-invariant": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
+      "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
+    },
+    "node_modules/tiny-warning": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
+      "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
+    },
     "node_modules/to-fast-properties": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -4241,11 +4457,26 @@
         "punycode": "^2.1.0"
       }
     },
+    "node_modules/util": {
+      "version": "0.10.4",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+      "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
+      "dev": true,
+      "dependencies": {
+        "inherits": "2.0.3"
+      }
+    },
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
       "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
     },
+    "node_modules/util/node_modules/inherits": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+      "dev": true
+    },
     "node_modules/uuid": {
       "version": "3.4.0",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
@@ -4264,6 +4495,11 @@
         "builtins": "^1.0.3"
       }
     },
+    "node_modules/value-equal": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
+      "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
+    },
     "node_modules/verror": {
       "version": "1.10.0",
       "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
@@ -4509,6 +4745,14 @@
       "engines": {
         "node": ">=8"
       }
+    },
+    "node_modules/zustand": {
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.4.1.tgz",
+      "integrity": "sha512-Kb91vSjy5vwBQ/PQ1a5GE6naS3gCxCgpkujT9zqZSO85+gnvmzgqraMW3ao1I0jR4PwHBXtLTf26r9j7EXoUiQ==",
+      "peerDependencies": {
+        "react": ">=16.8"
+      }
     }
   },
   "dependencies": {
@@ -4721,6 +4965,14 @@
         "@babel/helper-plugin-utils": "^7.12.13"
       }
     },
+    "@babel/runtime": {
+      "version": "7.13.10",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz",
+      "integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==",
+      "requires": {
+        "regenerator-runtime": "^0.13.4"
+      }
+    },
     "@babel/template": {
       "version": "7.12.13",
       "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz",
@@ -4766,6 +5018,17 @@
         "purgecss": "^3.1.3"
       }
     },
+    "@jadex/snowpack-plugin-tailwindcss-jit": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/@jadex/snowpack-plugin-tailwindcss-jit/-/snowpack-plugin-tailwindcss-jit-0.2.0.tgz",
+      "integrity": "sha512-iZfHqtLZzsFKAgMBVCGFJdQWWnXoLhWr7m98DJalu772SSMGXrWryW4nC/Hyk/1RDiL861BAVzXO6aZT3KLtgw==",
+      "dev": true,
+      "requires": {
+        "fs": "^0.0.1-security",
+        "micromatch": "^4.0.2",
+        "path": "^0.12.7"
+      }
+    },
     "@nodelib/fs.scandir": {
       "version": "2.1.4",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz",
@@ -4911,6 +5174,14 @@
         "npm-run-path": "^4.0.1"
       }
     },
+    "@tailwindcss/forms": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.3.2.tgz",
+      "integrity": "sha512-aj2/rJsGb2whAZ/BQWHWWQRSbhH0r/l1ozOByiv+ZNjBD84GMvb5dhAyfpeasFky+EJrAwX5eaqft8NQMZFWvA==",
+      "requires": {
+        "mini-svg-data-uri": "^1.2.3"
+      }
+    },
     "@tootallnate/once": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@@ -4923,6 +5194,12 @@
       "integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==",
       "dev": true
     },
+    "@types/history": {
+      "version": "4.7.8",
+      "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz",
+      "integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==",
+      "dev": true
+    },
     "@types/leaflet": {
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.7.0.tgz",
@@ -4964,6 +5241,27 @@
         "@types/react": "*"
       }
     },
+    "@types/react-router": {
+      "version": "5.1.13",
+      "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.13.tgz",
+      "integrity": "sha512-ZIuaO9Yrln54X6elg8q2Ivp6iK6p4syPsefEYAhRDAoqNh48C8VYUmB9RkXjKSQAJSJV0mbIFCX7I4vZDcHrjg==",
+      "dev": true,
+      "requires": {
+        "@types/history": "*",
+        "@types/react": "*"
+      }
+    },
+    "@types/react-router-dom": {
+      "version": "5.1.7",
+      "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.7.tgz",
+      "integrity": "sha512-D5mHD6TbdV/DNHYsnwBTv+y73ei+mMjrkGrla86HthE4/PVvL1J94Bu3qABU+COXzpL23T1EZapVVpwHuBXiUg==",
+      "dev": true,
+      "requires": {
+        "@types/history": "*",
+        "@types/react": "*",
+        "@types/react-router": "*"
+      }
+    },
     "@types/scheduler": {
       "version": "0.16.1",
       "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz",
@@ -5750,6 +6048,12 @@
       "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.0.13.tgz",
       "integrity": "sha512-E1fz2Xs9ltlUp+qbiyx9wmt2n9dRzPsS11Jtdb8D2o+cC7wr9xkkKsVKJuBX0ST+LVS+LhLO+SbLJNtfWcJvXA=="
     },
+    "fs": {
+      "version": "0.0.1-security",
+      "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
+      "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=",
+      "dev": true
+    },
     "fs-extra": {
       "version": "9.1.0",
       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
@@ -5969,6 +6273,27 @@
       "integrity": "sha512-gz1XE6/BsuVfyBM/RiDMgapPmrf2ZrDcF8GxJ26PJTPQnEbDFs9TPzpJ5IDPQLpUor/qshPbVDv20yWIwhMwyA==",
       "requires": {}
     },
+    "history": {
+      "version": "4.10.1",
+      "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
+      "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
+      "requires": {
+        "@babel/runtime": "^7.1.2",
+        "loose-envify": "^1.2.0",
+        "resolve-pathname": "^3.0.0",
+        "tiny-invariant": "^1.0.2",
+        "tiny-warning": "^1.0.0",
+        "value-equal": "^1.0.1"
+      }
+    },
+    "hoist-non-react-statics": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+      "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+      "requires": {
+        "react-is": "^16.7.0"
+      }
+    },
     "hosted-git-info": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz",
@@ -6223,10 +6548,9 @@
       }
     },
     "isarray": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
-      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
-      "dev": true
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+      "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
     },
     "isexe": {
       "version": "2.0.0",
@@ -6452,6 +6776,20 @@
       "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
       "dev": true
     },
+    "mini-create-react-context": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
+      "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
+      "requires": {
+        "@babel/runtime": "^7.12.1",
+        "tiny-warning": "^1.0.3"
+      }
+    },
+    "mini-svg-data-uri": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.2.3.tgz",
+      "integrity": "sha512-zd6KCAyXgmq6FV1mR10oKXYtvmA9vRoB6xPSTUJTbFApCtkefDnYueVR1gkof3KcdLZo1Y8mjF2DFmQMIxsHNQ=="
+    },
     "minimatch": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@@ -6896,6 +7234,16 @@
         "lines-and-columns": "^1.1.6"
       }
     },
+    "path": {
+      "version": "0.12.7",
+      "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
+      "integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=",
+      "dev": true,
+      "requires": {
+        "process": "^0.11.1",
+        "util": "^0.10.3"
+      }
+    },
     "path-is-absolute": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -6912,6 +7260,14 @@
       "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
       "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
     },
+    "path-to-regexp": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
+      "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+      "requires": {
+        "isarray": "0.0.1"
+      }
+    },
     "path-type": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -7122,6 +7478,12 @@
       "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz",
       "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE="
     },
+    "process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=",
+      "dev": true
+    },
     "process-nextick-args": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -7144,6 +7506,16 @@
         "retry": "^0.12.0"
       }
     },
+    "prop-types": {
+      "version": "15.7.2",
+      "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
+      "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
+      "requires": {
+        "loose-envify": "^1.4.0",
+        "object-assign": "^4.1.1",
+        "react-is": "^16.8.1"
+      }
+    },
     "psl": {
       "version": "1.8.0",
       "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
@@ -7202,6 +7574,11 @@
         "scheduler": "^0.20.2"
       }
     },
+    "react-is": {
+      "version": "16.13.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+    },
     "react-leaflet": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-3.1.0.tgz",
@@ -7216,6 +7593,37 @@
       "integrity": "sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==",
       "dev": true
     },
+    "react-router": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
+      "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
+      "requires": {
+        "@babel/runtime": "^7.1.2",
+        "history": "^4.9.0",
+        "hoist-non-react-statics": "^3.1.0",
+        "loose-envify": "^1.3.1",
+        "mini-create-react-context": "^0.4.0",
+        "path-to-regexp": "^1.7.0",
+        "prop-types": "^15.6.2",
+        "react-is": "^16.6.0",
+        "tiny-invariant": "^1.0.2",
+        "tiny-warning": "^1.0.0"
+      }
+    },
+    "react-router-dom": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz",
+      "integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==",
+      "requires": {
+        "@babel/runtime": "^7.1.2",
+        "history": "^4.9.0",
+        "loose-envify": "^1.3.1",
+        "prop-types": "^15.6.2",
+        "react-router": "5.2.0",
+        "tiny-invariant": "^1.0.2",
+        "tiny-warning": "^1.0.0"
+      }
+    },
     "read-cache": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -7248,6 +7656,14 @@
         "safe-buffer": "~5.1.1",
         "string_decoder": "~1.1.1",
         "util-deprecate": "~1.0.1"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+          "dev": true
+        }
       }
     },
     "readdirp": {
@@ -7274,6 +7690,11 @@
         }
       }
     },
+    "regenerator-runtime": {
+      "version": "0.13.7",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
+      "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
+    },
     "request": {
       "version": "2.88.2",
       "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
@@ -7336,6 +7757,11 @@
       "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
       "dev": true
     },
+    "resolve-pathname": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
+      "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
+    },
     "retry": {
       "version": "0.12.0",
       "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
@@ -7678,6 +8104,16 @@
         "yallist": "^4.0.0"
       }
     },
+    "tiny-invariant": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
+      "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
+    },
+    "tiny-warning": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
+      "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
+    },
     "to-fast-properties": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -7764,6 +8200,23 @@
         "punycode": "^2.1.0"
       }
     },
+    "util": {
+      "version": "0.10.4",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+      "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3"
+      },
+      "dependencies": {
+        "inherits": {
+          "version": "2.0.3",
+          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+          "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
+          "dev": true
+        }
+      }
+    },
     "util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -7784,6 +8237,11 @@
         "builtins": "^1.0.3"
       }
     },
+    "value-equal": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
+      "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
+    },
     "verror": {
       "version": "1.10.0",
       "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
@@ -7970,6 +8428,12 @@
       "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz",
       "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==",
       "dev": true
+    },
+    "zustand": {
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.4.1.tgz",
+      "integrity": "sha512-Kb91vSjy5vwBQ/PQ1a5GE6naS3gCxCgpkujT9zqZSO85+gnvmzgqraMW3ao1I0jR4PwHBXtLTf26r9j7EXoUiQ==",
+      "requires": {}
     }
   }
 }
diff --git a/package.json b/package.json
index f829d35..d4e0b16 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
     "lint": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\""
   },
   "dependencies": {
+    "@tailwindcss/forms": "^0.3.2",
     "graphql": "^15.5.0",
     "graphql-request": "^3.4.0",
     "heroicons-react": "1.3.0",
@@ -13,10 +14,13 @@
     "react": "^17.0.0",
     "react-dom": "^17.0.0",
     "react-leaflet": "^3.1.0",
+    "react-router-dom": "^5.2.0",
     "swr": "^0.5.5",
-    "tailwindcss": "^2.0.3"
+    "tailwindcss": "^2.0.3",
+    "zustand": "^3.4.1"
   },
   "devDependencies": {
+    "@jadex/snowpack-plugin-tailwindcss-jit": "^0.2.0",
     "@snowpack/plugin-dotenv": "^2.1.0",
     "@snowpack/plugin-postcss": "^1.1.0",
     "@snowpack/plugin-react-refresh": "^2.4.0",
@@ -24,6 +28,7 @@
     "@types/leaflet": "^1.7.0",
     "@types/react": "^17.0.0",
     "@types/react-dom": "^17.0.0",
+    "@types/react-router-dom": "^5.1.7",
     "@types/snowpack-env": "^2.3.2",
     "autoprefixer": "^10.2.4",
     "postcss": "^8.2.6",
diff --git a/snowpack.config.js b/snowpack.config.js
index 829a854..09245c9 100644
--- a/snowpack.config.js
+++ b/snowpack.config.js
@@ -10,10 +10,11 @@ module.exports = {
     '@snowpack/plugin-typescript',
     '@snowpack/plugin-postcss',
     '@snowpack/plugin-dotenv',
+    '@jadex/snowpack-plugin-tailwindcss-jit',
   ],
   routes: [
     /* Enable an SPA Fallback in development: */
-    // {"match": "routes", "src": ".*", "dest": "/index.html"},
+    { match: 'routes', src: '.*', dest: '/index.html' },
   ],
   optimize: {
     /* Example: Bundle your final build: */
diff --git a/src/App.tsx b/src/App.tsx
deleted file mode 100644
index 938975d..0000000
--- a/src/App.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import useSWR from 'swr';
-import SidebarListView from './Sidebar/SidebarListView';
-import type { PointOfInterest } from './types/PointOfInterest';
-import SidebarSingleView from './Sidebar/SidebarSingleView';
-import Map from './Map/Map';
-import Modal from './Modal';
-import { ExclamationOutline as AlertIcon } from 'heroicons-react';
-
-function App() {
-  const [poiData, setPoiData] = useState<PointOfInterest[]>([]);
-  const [selectedPoi, setSelectedPoi] = useState<null | PointOfInterest>(null);
-  const [hoveredPoiId, setHoveredPoiId] = useState<null | number>(null);
-  const [showErrorModal, setShowErrorModal] = useState<boolean>(false);
-
-  const handlePoiClick = (id: number) => {
-    const newPoi = poiData.find((poi) => poi.id === id);
-    newPoi && setSelectedPoi(newPoi);
-  };
-
-  const handlePoiClose = () => {
-    setSelectedPoi(null);
-  };
-
-  const handlePoiHoverOn = (poiId: number) => {
-    setHoveredPoiId(poiId);
-  };
-
-  const handlePoiHoverOff = () => {
-    setHoveredPoiId(null);
-  };
-
-  const { data, error } = useSWR(
-    `{
-      pois {
-        id
-        name
-        description
-        website
-        address
-        lat
-        lng
-        image
-        category
-      }
-    }
-    `,
-  );
-
-  useEffect(() => {
-    data && console.log('Fetched new data', data);
-    data?.pois && setPoiData(data.pois);
-  }, [data]);
-
-  useEffect(() => {
-    if (error) {
-      console.error('Error while fetching', error);
-      setShowErrorModal(true);
-    }
-  }, [error]);
-
-  return (
-    <>
-      {showErrorModal && (
-        <Modal
-          title="API nicht erreichbar"
-          text="Kann die API nicht erreichen. Bitte später erneut probieren."
-          icon={<AlertIcon className="h-6 w-6 text-red-600" />}
-        />
-      )}
-      <div className={'flex md:flex-row-reverse flex-col h-full'}>
-        <Map
-          onMouseEnter={handlePoiHoverOn}
-          onMouseLeave={handlePoiHoverOff}
-          hoveredPoiId={hoveredPoiId}
-          values={poiData}
-          onSelect={handlePoiClick}
-          selectedEntry={selectedPoi}
-        />
-        {selectedPoi ? (
-          <SidebarSingleView className="sidebar" value={selectedPoi} onClose={handlePoiClose} />
-        ) : (
-          <SidebarListView
-            hoveredPoiId={hoveredPoiId}
-            onMouseEnter={handlePoiHoverOn}
-            onMouseLeave={handlePoiHoverOff}
-            className="sidebar"
-            values={poiData}
-            onClick={handlePoiClick}
-          />
-        )}
-      </div>
-    </>
-  );
-}
-
-export default App;
diff --git a/src/Sidebar/SidebarContainer.tsx b/src/Sidebar/SidebarContainer.tsx
deleted file mode 100644
index de02c07..0000000
--- a/src/Sidebar/SidebarContainer.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React, { CSSProperties } from 'react';
-
-interface Props {
-  style?: CSSProperties;
-  className?: string;
-}
-
-const SidebarContainer: React.FC<Props> = ({ style, className, children }) => {
-  return (
-    <div
-      style={style}
-      className={`flex flex-col shadow-2xl border-t-2 md:border-r-2 md:border-t-0 border-black border-opacity-20 ${
-        className ?? ''
-      }`}
-    >
-      {children}
-    </div>
-  );
-};
-
-export default SidebarContainer;
diff --git a/src/Sidebar/SidebarListView.tsx b/src/Sidebar/SidebarListView.tsx
deleted file mode 100644
index 2503578..0000000
--- a/src/Sidebar/SidebarListView.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React, { CSSProperties } from 'react';
-import SidebarContainer from './SidebarContainer';
-import SidebarListElement from './SidebarListElement';
-import type { PointOfInterest } from '../types/PointOfInterest';
-
-interface Props {
-  style?: CSSProperties;
-  values: PointOfInterest[];
-  onClick?: (id: number) => void;
-  onMouseEnter?: (id: number) => void;
-  onMouseLeave?: () => void;
-  className?: string;
-  hoveredPoiId?: number | null;
-}
-
-const SidebarListView: React.FC<Props> = ({
-  values,
-  onMouseEnter,
-  onMouseLeave,
-  onClick,
-  hoveredPoiId,
-  ...restProps
-}) => {
-  return (
-    values && (
-      <SidebarContainer {...restProps}>
-        <h1 className="text-xl font-medium title-font m-4 text-gray-900 mb-2">{values.length} Orte:</h1>
-        {values &&
-          values.map((poi) => (
-            <SidebarListElement
-              key={poi.id}
-              {...(onMouseLeave ? { onMouseLeave: () => onMouseLeave() } : {})}
-              {...(onMouseEnter ? { onMouseEnter: () => onMouseEnter(poi.id) } : {})}
-              {...(onClick ? { onClick: () => onClick(poi.id) } : {})}
-              value={poi}
-              hovered={hoveredPoiId === poi.id}
-            />
-          ))}
-      </SidebarContainer>
-    )
-  );
-};
-
-export default SidebarListView;
diff --git a/src/Sidebar/SidebarSingleView.tsx b/src/Sidebar/SidebarSingleView.tsx
deleted file mode 100644
index b5db3fe..0000000
--- a/src/Sidebar/SidebarSingleView.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import React, { CSSProperties } from 'react';
-import SidebarContainer from './SidebarContainer';
-import type { PointOfInterest } from '../types/PointOfInterest';
-import { X as CloseIcon, HomeOutline as HomeIcon, LocationMarkerOutline as AddressIcon } from 'heroicons-react';
-
-interface Props {
-  style?: CSSProperties;
-  value: PointOfInterest;
-  onClose?: () => void;
-  className?: string;
-}
-
-const SidebarSingleView: React.FC<Props> = ({ value, onClose, className, ...restProps }) => {
-  const strippedUrl = value?.website?.replace(/(^\w+:|^)\/\//, '');
-  return (
-    <SidebarContainer className={`relative p-0 ${className || ''}`} {...restProps}>
-      <div className={`${value.image ? '' : 'pl-5 pt-5'}`}>
-        <CloseIcon
-          size={32}
-          className={`${
-            value.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={onClose}
-        />
-      </div>
-      {value.image && (
-        <img className="lg:h-48 md:h-36 w-full object-cover object-center" src={value.image} alt="blog" />
-      )}
-      <div className="p-6">
-        <h2 className="tracking-widest uppercase text-xs title-font font-medium text-gray-400 mb-1">
-          {value.category}
-        </h2>
-        <h1 className="title-font text-lg font-medium text-gray-900 mb-3">{value.name}</h1>
-        <p className="leading-relaxed mb-6">{value.description}</p>
-        {value.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={value.website}>
-              {strippedUrl}
-            </a>
-          </div>
-        )}
-        {value.address && (
-          <div className={'flex items-center mt-3'}>
-            <AddressIcon size={18} className={'text-gray-500 mr-2'} />
-            <div className="text-sm text-gray-500">{value.address}</div>
-          </div>
-        )}
-      </div>
-    </SidebarContainer>
-  );
-};
-
-export default SidebarSingleView;
diff --git a/src/components/App.tsx b/src/components/App.tsx
new file mode 100644
index 0000000..f5aba53
--- /dev/null
+++ b/src/components/App.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { Route, Switch } from 'react-router-dom';
+import { useStore } from '../hooks';
+import ErrorModal from './ErrorModal';
+import Map from './Map/Map';
+import SidebarCreateView from './Sidebar/SidebarCreateView';
+import SidebarListView from './Sidebar/SidebarListView';
+import SidebarSingleView from './Sidebar/SidebarSingleView';
+
+const App = () => {
+  const selectedPoi = useStore((state) => state.selectedPoi);
+
+  return (
+    <>
+      <ErrorModal />
+      <div className={'flex md:flex-row-reverse flex-col h-full'}>
+        <Route path="/add" children={({ match }) => <Map hideAllPois={!!match} />} />
+        <Switch>
+          <Route exact path="/add">
+            <SidebarCreateView />
+          </Route>
+          <Route>{selectedPoi ? <SidebarSingleView /> : <SidebarListView />}</Route>
+        </Switch>
+      </div>
+    </>
+  );
+};
+
+export default App;
diff --git a/src/components/ErrorModal.tsx b/src/components/ErrorModal.tsx
new file mode 100644
index 0000000..7386e08
--- /dev/null
+++ b/src/components/ErrorModal.tsx
@@ -0,0 +1,16 @@
+import { ExclamationOutline as AlertIcon } from 'heroicons-react';
+import React from 'react';
+import { useStore } from '../hooks';
+import Modal from './Modal';
+
+interface Props {}
+
+const ErrorModal: React.FC<Props> = () => {
+  const error = useStore((state) => state.error);
+  const icons: { [index: string]: JSX.Element } = {
+    alert: <AlertIcon className="h-6 w-6 text-red-600" />,
+  };
+  return error && <Modal title={error.title} text={error.message} icon={icons[error.icon]} />;
+};
+
+export default ErrorModal;
diff --git a/src/Map/Map.tsx b/src/components/Map/Map.tsx
similarity index 52%
rename from src/Map/Map.tsx
rename to src/components/Map/Map.tsx
index 5f571c8..635bd42 100644
--- a/src/Map/Map.tsx
+++ b/src/components/Map/Map.tsx
@@ -1,34 +1,33 @@
-import { MapContainer, Marker, TileLayer } from 'react-leaflet';
-import React, { useMemo } from 'react';
-import { divIcon, DivIconOptions } from 'leaflet';
-import type { PointOfInterest } from '../types/PointOfInterest';
 import type { LatLngExpression } from 'leaflet';
+import { divIcon, DivIconOptions } from 'leaflet';
+import React, { useMemo } from 'react';
+import { MapContainer, Marker, TileLayer } from 'react-leaflet';
+import { usePoiData, useStore } from '../../hooks';
 import MapViewController from './MapViewController';
 
 interface Props {
-  values: PointOfInterest[];
-  onSelect: (id: number) => void;
-  selectedEntry?: PointOfInterest | null;
-  hoveredPoiId?: number | null;
-  onMouseEnter?: (id: number) => void;
-  onMouseLeave?: () => void;
+  hideAllPois?: boolean;
 }
 
 const DEFAULT_CENTER: LatLngExpression = [53.550359, 9.986701];
+const iconProps: DivIconOptions = {
+  className: 'marker',
+  // Source: https://fontawesome.com/icons/map-marker-alt
+  html: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M172.268 501.67C26.97 291.031 0 269.413 0 192 0 85.961 85.961 0 192 0s192 85.961 192 192c0 77.413-26.97 99.031-172.268 309.67-9.535 13.774-29.93 13.773-39.464 0zM192 272c44.183 0 80-35.817 80-80s-35.817-80-80-80-80 35.817-80 80 35.817 80 80 80z"/></svg>`,
+  iconSize: [24, 32],
+  iconAnchor: [12, 32],
+};
 
-export const Map: React.FC<Props> = (props) => {
-  const iconProps: DivIconOptions = {
-    className: 'marker',
-    // Source: https://fontawesome.com/icons/map-marker-alt
-    html: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M172.268 501.67C26.97 291.031 0 269.413 0 192 0 85.961 85.961 0 192 0s192 85.961 192 192c0 77.413-26.97 99.031-172.268 309.67-9.535 13.774-29.93 13.773-39.464 0zM192 272c44.183 0 80-35.817 80-80s-35.817-80-80-80-80 35.817-80 80 35.817 80 80 80z"/></svg>`,
-    iconSize: [24, 32],
-    iconAnchor: [12, 32],
-  };
+export const Map: React.FC<Props> = ({ hideAllPois = false }) => {
   const icon = useMemo(() => divIcon(iconProps), [iconProps]);
   const largeIcon = useMemo(() => divIcon({ ...iconProps, iconSize: [30, 40], iconAnchor: [15, 40] }), [iconProps]);
-  const selectedLatlng: LatLngExpression | undefined = props.selectedEntry
-    ? [props.selectedEntry?.lat, props.selectedEntry?.lng]
-    : undefined;
+
+  const { data } = usePoiData();
+  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 selectedLatlng: LatLngExpression | undefined = selectedPoi ? [selectedPoi?.lat, selectedPoi?.lng] : undefined;
 
   return (
     <MapContainer
@@ -50,25 +49,25 @@ export const Map: React.FC<Props> = (props) => {
       />
       <MapViewController center={selectedLatlng ?? DEFAULT_CENTER} zoom={13} />
       {/* Single marker when POI is selected */}
-      {!!(props.selectedEntry && selectedLatlng) && <Marker icon={largeIcon} position={selectedLatlng} />}
+      {!!(selectedPoi && selectedLatlng && !hideAllPois) && <Marker icon={largeIcon} position={selectedLatlng} />}
       {/* Multiple markers, when no POI is selected */}
-      {!props.selectedEntry &&
-        props.values &&
-        props.values.map((poi) => {
+      {!selectedPoi &&
+        !hideAllPois &&
+        data?.map((poi) => {
           const poiLatLng: LatLngExpression = [poi.lat, poi.lng];
           return (
             <Marker
-              icon={props.hoveredPoiId === poi.id ? largeIcon : icon}
-              opacity={props.hoveredPoiId === poi.id ? 1 : 0.7}
+              icon={hoveredPoi?.id === poi.id ? largeIcon : icon}
+              opacity={hoveredPoi?.id === poi.id ? 1 : 0.7}
               key={poi.id}
               position={poiLatLng}
               eventHandlers={{
-                click: () => props.onSelect(poi.id),
+                click: () => setSelectedPoi(poi),
                 mouseover: () => {
-                  props.onMouseEnter && props.onMouseEnter(poi.id);
+                  setHoveredPoi(poi);
                 },
                 mouseout: () => {
-                  props.onMouseLeave && props.onMouseLeave();
+                  setHoveredPoi(null);
                 },
               }}
             />
diff --git a/src/Map/MapViewController.tsx b/src/components/Map/MapViewController.tsx
similarity index 100%
rename from src/Map/MapViewController.tsx
rename to src/components/Map/MapViewController.tsx
diff --git a/src/Modal.tsx b/src/components/Modal.tsx
similarity index 100%
rename from src/Modal.tsx
rename to src/components/Modal.tsx
diff --git a/src/components/Sidebar/AddPoiForm.tsx b/src/components/Sidebar/AddPoiForm.tsx
new file mode 100644
index 0000000..a91d286
--- /dev/null
+++ b/src/components/Sidebar/AddPoiForm.tsx
@@ -0,0 +1,127 @@
+import { GraphQLClient, request, gql } from 'graphql-request';
+import React, { useEffect, useState } from 'react';
+
+interface Props {}
+
+const AddPoiForm: React.FC<Props> = () => {
+  const [formData, setFormData] = useState({
+    lat: 123,
+    lng: 123,
+    name: '',
+    address: '',
+    description: '',
+    website: '',
+    category: '',
+    image: '',
+    tags: '',
+  });
+
+  useEffect(() => {
+    console.log('Form data', formData);
+  }, [formData]);
+
+  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
+    setFormData({ ...formData, [e.target.name]: e.target.value });
+  };
+
+  // const mutation = gql`
+  //   mutation createPoiMutation(
+  //     $name: String!
+  //     $email: String!
+  //     $lat: Float!
+  //     $lng: Float!
+  //     $website: String
+  //     $description: String
+  //     $address: String!
+  //     $category: String!
+  //   ) {
+  //     createPoi(
+  //       poi: {
+  //         name: $name
+  //         email: $email
+  //         lat: $lat
+  //         lng: $lng
+  //         website: $website
+  //         description: $description
+  //         address: $address
+  //         category: $category
+  //         image: "ababababa"
+  //       }
+  //     )
+  //   }
+  // `;
+
+  // const variables = {
+  //   name: 'Inception',
+  //   email: 2010,
+  //   file: document.querySelector('input#avatar').files[0]
+  // };
+  // const data = await request(import.meta.env.SNOWPACK_PUBLIC_API_URL, mutation, formData);
+
+  return (
+    <form className="flex flex-col">
+      <p className="leading-relaxed mb-5 text-gray-600">Bitte auf der Karte einen Pin setzen.</p>
+      <label className="block mb-4">
+        <span className="form-label">Name</span>
+        <input
+          type="text"
+          name="name"
+          value={formData.name}
+          className="form-input"
+          placeholder="Musterspace"
+          onChange={handleInputChange}
+        />
+      </label>
+      <label className="block mb-4">
+        <span className="form-label">Adresse</span>
+        <input
+          type="text"
+          name="address"
+          value={formData.address}
+          className="form-input"
+          placeholder=""
+          onChange={handleInputChange}
+        />
+      </label>
+      <label className="block mb-4">
+        <span className="form-label">Webseite</span>
+        <input
+          type="text"
+          name="website"
+          value={formData.website}
+          className="form-input"
+          placeholder=""
+          onChange={handleInputChange}
+        />
+      </label>
+      <label className="block mb-4">
+        <span className="form-label">Beschreibung</span>
+        <textarea
+          name="description"
+          value={formData.description}
+          className="form-input"
+          rows={3}
+          onChange={handleInputChange}
+        ></textarea>
+      </label>
+      <label className="block mb-6">
+        <span className="form-label">Bild</span>
+        <input
+          className="block w-full focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
+          type="file"
+          id="avatar"
+          name="avatar"
+          accept="image/png, image/jpeg"
+        ></input>
+      </label>
+      <button
+        type="submit"
+        className="text-white bg-indigo-500 border-0 py-2 px-6 focus:outline-none hover:bg-indigo-600 rounded-lg text-lg"
+      >
+        Hinzufügen
+      </button>
+    </form>
+  );
+};
+
+export default AddPoiForm;
diff --git a/src/Sidebar/SidebarListElement.tsx b/src/components/Sidebar/ListElement.tsx
similarity index 70%
rename from src/Sidebar/SidebarListElement.tsx
rename to src/components/Sidebar/ListElement.tsx
index ac5e6bd..a088ece 100644
--- a/src/Sidebar/SidebarListElement.tsx
+++ b/src/components/Sidebar/ListElement.tsx
@@ -1,12 +1,15 @@
-import React from 'react';
+import React, { SyntheticEvent } from 'react';
 import type { PointOfInterest } from 'src/types/PointOfInterest';
 
 interface Props {
   value: PointOfInterest;
   hovered?: boolean;
+  onMouseEnter: (event: SyntheticEvent) => void;
+  onMouseLeave: (event: SyntheticEvent) => void;
+  onClick: (event: SyntheticEvent) => void;
 }
 
-const SidebarListElement: React.FC<Props> = ({ value, hovered, ...restProps }) => {
+const ListElement: React.FC<Props> = ({ value, hovered, ...restProps }) => {
   return (
     value && (
       <div
@@ -26,4 +29,4 @@ const SidebarListElement: React.FC<Props> = ({ value, hovered, ...restProps }) =
   );
 };
 
-export default SidebarListElement;
+export default ListElement;
diff --git a/src/components/Sidebar/SidebarContainer.tsx b/src/components/Sidebar/SidebarContainer.tsx
new file mode 100644
index 0000000..f00e3b1
--- /dev/null
+++ b/src/components/Sidebar/SidebarContainer.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+
+interface Props {
+  className?: string;
+}
+
+const SidebarContainer: React.FC<Props> = ({ className, children }) => {
+  return (
+    <aside
+      className={`sidebar box-border flex flex-col shadow-2xl border-t-2 md:border-r-2 md:border-t-0 border-black border-opacity-20 ${
+        className ?? ''
+      }`}
+    >
+      {children}
+    </aside>
+  );
+};
+
+export default SidebarContainer;
diff --git a/src/components/Sidebar/SidebarCreateView.tsx b/src/components/Sidebar/SidebarCreateView.tsx
new file mode 100644
index 0000000..85eeb5f
--- /dev/null
+++ b/src/components/Sidebar/SidebarCreateView.tsx
@@ -0,0 +1,24 @@
+import { X as CloseIcon } from 'heroicons-react';
+import React from 'react';
+import { useHistory } from 'react-router-dom';
+import AddPoiForm from './AddPoiForm';
+import SidebarContainer from './SidebarContainer';
+
+interface Props {}
+
+const SidebarCreateView: React.FC<Props> = () => {
+  const history = useHistory();
+  return (
+    <SidebarContainer className="p-5">
+      <CloseIcon
+        size={32}
+        className={`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('/')}
+      />
+      <h1 className="text-xl font-medium title-font text-gray-900 my-2">Ort anlegen:</h1>
+      <AddPoiForm />
+    </SidebarContainer>
+  );
+};
+
+export default SidebarCreateView;
diff --git a/src/components/Sidebar/SidebarListView.tsx b/src/components/Sidebar/SidebarListView.tsx
new file mode 100644
index 0000000..5d2460d
--- /dev/null
+++ b/src/components/Sidebar/SidebarListView.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { usePoiData, useStore } from '../../hooks';
+import ListElement from './ListElement';
+import SidebarContainer from './SidebarContainer';
+
+interface Props {}
+
+const SidebarListView: React.FC<Props> = () => {
+  const { data } = usePoiData();
+  const hoveredPoi = useStore((state) => state.hoveredPoi);
+  const setHoveredPoi = useStore((state) => state.setHoveredPoi);
+  const setSelectedPoi = useStore((state) => state.setSelectedPoi);
+
+  return (
+    <SidebarContainer>
+      <h1 className="text-xl font-medium title-font m-4 text-gray-900 mb-2">{data?.length} Orte:</h1>
+      {data?.map((poi) => (
+        <ListElement
+          key={poi.id}
+          onMouseEnter={() => setHoveredPoi(poi)}
+          onMouseLeave={() => setHoveredPoi(null)}
+          onClick={() => setSelectedPoi(poi)}
+          value={poi}
+          hovered={hoveredPoi?.id === poi.id}
+        />
+      ))}
+    </SidebarContainer>
+  );
+};
+
+export default SidebarListView;
diff --git a/src/components/Sidebar/SidebarSingleView.tsx b/src/components/Sidebar/SidebarSingleView.tsx
new file mode 100644
index 0000000..12c3070
--- /dev/null
+++ b/src/components/Sidebar/SidebarSingleView.tsx
@@ -0,0 +1,62 @@
+import { HomeOutline as HomeIcon, LocationMarkerOutline as AddressIcon, X as CloseIcon } from 'heroicons-react';
+import React from 'react';
+import { useStore } from '../../hooks';
+import SidebarContainer from './SidebarContainer';
+import Tag from './Tag';
+
+interface Props {}
+
+const SidebarSingleView: React.FC<Props> = () => {
+  const selectedPoi = useStore((state) => state.selectedPoi);
+  const setSelectedPoi = useStore((state) => state.setSelectedPoi);
+  const strippedUrl = selectedPoi?.website?.replace(/(^\w+:|^)\/\//, '');
+
+  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)}
+        />
+      </div>
+      {selectedPoi?.image && (
+        <img className="lg:h-48 md:h-36 w-full object-cover object-center" src={selectedPoi?.image} alt="blog" />
+      )}
+      <div className="p-6">
+        <h2 className="tracking-widest uppercase text-xs title-font font-medium text-gray-400 mb-1">
+          {selectedPoi?.category}
+        </h2>
+        <h1 className="title-font text-lg font-medium text-gray-900 mb-3">{selectedPoi?.name}</h1>
+        <p className="leading-relaxed mb-6">{selectedPoi?.description}</p>
+        {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}>
+              {strippedUrl}
+            </a>
+          </div>
+        )}
+        {selectedPoi?.address && (
+          <div className={'flex items-center mt-3'}>
+            <AddressIcon size={18} className={'text-gray-500 mr-2'} />
+            <div className="text-sm text-gray-500">{selectedPoi?.address}</div>
+          </div>
+        )}
+        {!!selectedPoi?.tags?.length && (
+          <div className={'flex items-center mt-3'}>
+            {selectedPoi?.tags.map((tag) => (
+              <Tag key={tag.id} color={tag.color}>
+                {tag.displayName}
+              </Tag>
+            ))}
+          </div>
+        )}
+      </div>
+    </SidebarContainer>
+  );
+};
+
+export default SidebarSingleView;
diff --git a/src/components/Sidebar/Tag.tsx b/src/components/Sidebar/Tag.tsx
new file mode 100644
index 0000000..7b730b4
--- /dev/null
+++ b/src/components/Sidebar/Tag.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+
+interface Props {
+  children: string | JSX.Element;
+  color?: string;
+}
+
+const Tag: React.FC<Props> = ({ children, color }) => {
+  return (
+    <span
+      className="inline-flex items-center px-3 py-0.5 rounded-full text-sm font-medium bg-indigo-100 text-indigo-800 mr-2"
+      style={color ? { backgroundColor: color } : {}}
+    >
+      {children}
+    </span>
+  );
+};
+
+export default Tag;
diff --git a/src/components/SwrWrapper.tsx b/src/components/SwrWrapper.tsx
new file mode 100644
index 0000000..96b19de
--- /dev/null
+++ b/src/components/SwrWrapper.tsx
@@ -0,0 +1,29 @@
+import { request } from 'graphql-request';
+import React from 'react';
+import { SWRConfig } from 'swr';
+import { useStore } from '../hooks';
+
+interface Props {}
+
+const SwrWrapper: React.FC<Props> = ({ children }) => {
+  const setError = useStore((state) => state.setError);
+  return (
+    <SWRConfig
+      value={{
+        fetcher: (query: string) => request(import.meta.env.SNOWPACK_PUBLIC_API_URL, query),
+        onError: (error) => {
+          setError({
+            title: 'API nicht erreichbar',
+            message: 'Kann die API nicht erreichen. Bitte später erneut probieren.',
+            icon: 'alert',
+          });
+          console.error('Error while fetching', error);
+        },
+      }}
+    >
+      {children}
+    </SWRConfig>
+  );
+};
+
+export default SwrWrapper;
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
new file mode 100644
index 0000000..df87068
--- /dev/null
+++ b/src/hooks/index.ts
@@ -0,0 +1,2 @@
+export * from './usePoiData';
+export * from './useStore';
diff --git a/src/hooks/usePoiData.ts b/src/hooks/usePoiData.ts
new file mode 100644
index 0000000..d23d3c7
--- /dev/null
+++ b/src/hooks/usePoiData.ts
@@ -0,0 +1,34 @@
+import { useEffect } from 'react';
+import type { PointsOfInterestDTO } from 'src/types/PointOfInterest';
+import useSWR from 'swr';
+
+export const usePoiData = () => {
+  const { data } = useSWR<PointsOfInterestDTO>(
+    `{
+      pois {
+        id
+        name
+        description
+        website
+        address
+        lat
+        lng
+        image
+        category
+        tags {
+          id
+          displayName
+          displayName
+          color
+        }
+      }
+    }
+    `,
+  );
+
+  useEffect(() => {
+    console.log('Got data', data);
+  }, [data]);
+
+  return { data: data?.pois };
+};
diff --git a/src/hooks/useStore.ts b/src/hooks/useStore.ts
new file mode 100644
index 0000000..4c01525
--- /dev/null
+++ b/src/hooks/useStore.ts
@@ -0,0 +1,46 @@
+import type { Error } from 'src/types/Error';
+import type { PointOfInterest } from 'src/types/PointOfInterest';
+import create, { GetState, SetState, State, StateCreator, StoreApi } from 'zustand';
+
+interface Store extends State {
+  selectedPoi: PointOfInterest | null;
+  setSelectedPoi: (poi: PointOfInterest | null) => void;
+  hoveredPoi: PointOfInterest | null;
+  setHoveredPoi: (poi: PointOfInterest | null) => void;
+  error: Error | null;
+  setError: (error: Error | null) => void;
+}
+
+const log = (config: StateCreator<Store>) => (set: SetState<Store>, get: GetState<Store>, api: StoreApi<Store>) =>
+  config(
+    (args) => {
+      console.group('Global state changed');
+      console.log('%cAction:', 'color: #00A7F7; font-weight: 700;', args);
+      set(args);
+      console.log('%cNext State:', 'color: #47B04B; font-weight: 700;', get());
+      console.groupEnd();
+    },
+    get,
+    api,
+  );
+
+export const useStore = create<Store>(
+  log((set) => ({
+    selectedPoi: null,
+    setSelectedPoi: (poi) => {
+      set({
+        selectedPoi: poi,
+      });
+    },
+    hoveredPoi: null,
+    setHoveredPoi: (poi) => {
+      set({
+        hoveredPoi: poi,
+      });
+    },
+    error: null,
+    setError: (error) => {
+      set({ error });
+    },
+  })),
+);
diff --git a/src/index.css b/src/index.css
index e8d6792..18a8368 100644
--- a/src/index.css
+++ b/src/index.css
@@ -45,3 +45,11 @@ code {
     display: initial;
   }
 }
+
+.form-input {
+  @apply mt-1 block w-full rounded-lg border-2 border-black border-opacity-20 hover:border-opacity-40 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50;
+}
+
+.form-label {
+  @apply leading-7 text-sm text-gray-600;
+}
diff --git a/src/index.tsx b/src/index.tsx
index 85d91b1..96a7b15 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,15 +1,17 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
-import App from './App';
-import { SWRConfig } from 'swr';
-import { request } from 'graphql-request';
+import { BrowserRouter as Router } from 'react-router-dom';
+import App from './components/App';
+import SwrWrapper from './components/SwrWrapper';
 import './index.css';
 
 ReactDOM.render(
   <React.StrictMode>
-    <SWRConfig value={{ fetcher: (query: string) => request(import.meta.env.SNOWPACK_PUBLIC_API_URL, query) }}>
-      <App />
-    </SWRConfig>
+    <SwrWrapper>
+      <Router>
+        <App />
+      </Router>
+    </SwrWrapper>
   </React.StrictMode>,
   document.getElementById('root'),
 );
diff --git a/src/testData.json b/src/testData.json
deleted file mode 100644
index d93ab54..0000000
--- a/src/testData.json
+++ /dev/null
@@ -1,55 +0,0 @@
-[
-  {
-    "id": 1,
-    "lat": 53.550359,
-    "lng": 9.986701,
-    "name": "Welcome Werkstatt e. V.",
-    "description": "Eine offene Stadtteilwerkstatt in Barmbek-Süd. Hier kann mit Holz, Metall und Elektronik gearbeitet werden.",
-    "address": "Bachstr. 98, 22083 Hamburg",
-    "category": "OFFENE WERKSTATT",
-    "website": "https://www.welcome-werkstatt.de/",
-    "image": "https://picsum.photos/720/400?random=1"
-  },
-  {
-    "id": 2,
-    "lat": 53.560359,
-    "lng": 9.976701,
-    "name": "Fabulous St. Pauli",
-    "description": "Photo booth fam kinfolk cold-pressed sriracha leggings jianbing microdosing tousled waistcoat.",
-    "address": "Mozartstr. 8, 22081 Hamburg",
-    "category": "OFFENE WERKSTATT",
-    "website": "http://www.fablab-hamburg.org/",
-    "image": "https://picsum.photos/720/400?random=2"
-  },
-  {
-    "id": 3,
-    "lat": 53.540359,
-    "lng": 9.996701,
-    "name": "HoFaLab Wilhelmsburg",
-    "description": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text.",
-    "address": "Langer-Straßen-Name. 128, 22089 Hamburg",
-    "category": "OFFENE WERKSTATT",
-    "website": "https://hofalab.de/de/home-de/",
-    "image": "https://picsum.photos/720/400?random=3"
-  },
-  {
-    "id": 4,
-    "lat": 53.570359,
-    "lng": 9.986701,
-    "name": "Fab City Haus",
-    "description": "Eine offene Stadtteilwerkstatt in Barmbek-Süd. Hier kann mit Holz, Metall und Elektronik gearbeitet werden.",
-    "address": "Jungfernstieg 1, 22083 Hamburg",
-    "category": "OFFENE WERKSTATT",
-    "image": "https://picsum.photos/720/400?random=4"
-  },
-  {
-    "id": 5,
-    "lat": 53.565359,
-    "lng": 9.966701,
-    "name": "Haus Drei e. V.",
-    "description": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text.",
-    "address": "Hein-Hoyer-Allee 36, 22083 Hamburg",
-    "category": "OFFENE WERKSTATT",
-    "image": "https://picsum.photos/720/400?random=5"
-  }
-]
diff --git a/src/types/Error.ts b/src/types/Error.ts
new file mode 100644
index 0000000..0979889
--- /dev/null
+++ b/src/types/Error.ts
@@ -0,0 +1,5 @@
+export interface Error {
+  title: string;
+  message: string;
+  icon: string;
+}
diff --git a/src/types/PointOfInterest.ts b/src/types/PointOfInterest.ts
index 81cd084..c612f81 100644
--- a/src/types/PointOfInterest.ts
+++ b/src/types/PointOfInterest.ts
@@ -1,5 +1,3 @@
-import type { LatLngExpression } from 'leaflet';
-
 export interface PointOfInterest {
   id: number;
   lat: number;
@@ -10,4 +8,15 @@ export interface PointOfInterest {
   website: string;
   category?: string;
   image?: string;
+  tags: Tag[];
+}
+
+export interface Tag {
+  id: string;
+  displayName: string;
+  color: string;
+}
+
+export interface PointsOfInterestDTO {
+  pois: PointOfInterest[];
 }
diff --git a/tailwind.config.js b/tailwind.config.js
index e6c84f1..d6669e9 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -8,5 +8,5 @@ module.exports = {
   variants: {
     extend: {},
   },
-  plugins: [],
+  plugins: [require('@tailwindcss/forms')],
 };
-- 
GitLab