diff --git a/.gitignore b/.gitignore
index 4d29575de80483b005c29bfcac5061cd2f45313e..b4e5410ec3cb7c4e6aca5c55bac2e84b413de53e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,9 @@
 # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 
+# IDE
+/.idea
+*.iml
+
 # dependencies
 /node_modules
 /.pnp
diff --git a/package-lock.json b/package-lock.json
index 6184b3a658ec590786ff00a568693d4c07318687..464c672915a5521ea0755536f98d882684414658 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1161,6 +1161,19 @@
       "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz",
       "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg=="
     },
+    "@emotion/is-prop-valid": {
+      "version": "0.7.3",
+      "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.7.3.tgz",
+      "integrity": "sha512-uxJqm/sqwXw3YPA5GXX365OBcJGFtxUVkB6WyezqFHlNe9jqUWH5ur2O2M8dGBz61kn1g3ZBlzUunFQXQIClhA==",
+      "requires": {
+        "@emotion/memoize": "0.7.1"
+      }
+    },
+    "@emotion/memoize": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.1.tgz",
+      "integrity": "sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg=="
+    },
     "@hapi/address": {
       "version": "2.1.4",
       "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz",
@@ -4054,6 +4067,16 @@
         }
       }
     },
+    "css-jss": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/css-jss/-/css-jss-10.4.0.tgz",
+      "integrity": "sha512-WA/MgZyQm6KAubnFlBn8d6jwNjpzv0kdRCZ9Qb81ylb3/EYTz8qyjGvhIMODa/MuRBUaIMgzr2qjkb1B7WXEkw==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "jss": "10.4.0",
+        "jss-preset-default": "10.4.0"
+      }
+    },
     "css-loader": {
       "version": "3.4.2",
       "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.4.2.tgz",
@@ -4120,6 +4143,15 @@
         }
       }
     },
+    "css-vendor": {
+      "version": "2.0.8",
+      "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz",
+      "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==",
+      "requires": {
+        "@babel/runtime": "^7.8.3",
+        "is-in-browser": "^1.0.2"
+      }
+    },
     "css-what": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.3.0.tgz",
@@ -6365,6 +6397,14 @@
         "minimalistic-crypto-utils": "^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": "2.8.8",
       "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
@@ -6563,6 +6603,11 @@
       "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
       "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM="
     },
+    "hyphenate-style-name": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
+      "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ=="
+    },
     "iconv-lite": {
       "version": "0.4.24",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -6603,9 +6648,9 @@
       "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="
     },
     "immer": {
-      "version": "1.10.0",
-      "resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz",
-      "integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg=="
+      "version": "7.0.9",
+      "resolved": "https://registry.npmjs.org/immer/-/immer-7.0.9.tgz",
+      "integrity": "sha512-Vs/gxoM4DqNAYR7pugIxi0Xc8XAun/uy7AQu4fLLqaTBHxjOP9pJ266Q9MWA/ly4z6rAFZbvViOtihxUZ7O28A=="
     },
     "import-cwd": {
       "version": "2.1.0",
@@ -6946,6 +6991,11 @@
         "is-extglob": "^2.1.1"
       }
     },
+    "is-in-browser": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz",
+      "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU="
+    },
     "is-negative-zero": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz",
@@ -7809,6 +7859,154 @@
         "verror": "1.10.0"
       }
     },
+    "jss": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/jss/-/jss-10.4.0.tgz",
+      "integrity": "sha512-l7EwdwhsDishXzqTc3lbsbyZ83tlUl5L/Hb16pHCvZliA9lRDdNBZmHzeJHP0sxqD0t1mrMmMR8XroR12JBYzw==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "csstype": "^3.0.2",
+        "is-in-browser": "^1.1.3",
+        "tiny-warning": "^1.0.2"
+      }
+    },
+    "jss-plugin-camel-case": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.4.0.tgz",
+      "integrity": "sha512-9oDjsQ/AgdBbMyRjc06Kl3P8lDCSEts2vYZiPZfGAxbGCegqE4RnMob3mDaBby5H9vL9gWmyyImhLRWqIkRUCw==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "hyphenate-style-name": "^1.0.3",
+        "jss": "10.4.0"
+      }
+    },
+    "jss-plugin-compose": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/jss-plugin-compose/-/jss-plugin-compose-10.4.0.tgz",
+      "integrity": "sha512-m1MKZQDH/48W2NHqgsfhYBAObVHzDzSCULLLqrc8nZh1fYGvEBUND82oqd6Jh95pJbMhTzx3E9st63MivEuvAw==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "jss": "10.4.0",
+        "tiny-warning": "^1.0.2"
+      }
+    },
+    "jss-plugin-default-unit": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.4.0.tgz",
+      "integrity": "sha512-BYJ+Y3RUYiMEgmlcYMLqwbA49DcSWsGgHpVmEEllTC8MK5iJ7++pT9TnKkKBnNZZxTV75ycyFCR5xeLSOzVm4A==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "jss": "10.4.0"
+      }
+    },
+    "jss-plugin-expand": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/jss-plugin-expand/-/jss-plugin-expand-10.4.0.tgz",
+      "integrity": "sha512-UiZ6D4Ud2Chg3GIzRGjgs3DLiueN4r+g1TkEgc7L/0J/L9wsvuFKOtkahdHn177+YUK5/+N05mIE9xsgREB4+Q==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "jss": "10.4.0"
+      }
+    },
+    "jss-plugin-extend": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/jss-plugin-extend/-/jss-plugin-extend-10.4.0.tgz",
+      "integrity": "sha512-TsgSmvWnpZWvXWpCDHl9Vj/n8wA/Awluutg/dnrfU7rwnM+BKkssHocGai8wQ5mtmIR+lYt+y7zAO+MOeigPiw==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "jss": "10.4.0",
+        "tiny-warning": "^1.0.2"
+      }
+    },
+    "jss-plugin-global": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.4.0.tgz",
+      "integrity": "sha512-b8IHMJUmv29cidt3nI4bUI1+Mo5RZE37kqthaFpmxf5K7r2aAegGliAw4hXvA70ca6ckAoXMUl4SN/zxiRcRag==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "jss": "10.4.0"
+      }
+    },
+    "jss-plugin-nested": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.4.0.tgz",
+      "integrity": "sha512-cKgpeHIxAP0ygeWh+drpLbrxFiak6zzJ2toVRi/NmHbpkNaLjTLgePmOz5+67ln3qzJiPdXXJB1tbOyYKAP4Pw==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "jss": "10.4.0",
+        "tiny-warning": "^1.0.2"
+      }
+    },
+    "jss-plugin-props-sort": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.4.0.tgz",
+      "integrity": "sha512-j/t0R40/2fp+Nzt6GgHeUFnHVY2kPGF5drUVlgkcwYoHCgtBDOhTTsOfdaQFW6sHWfoQYgnGV4CXdjlPiRrzwA==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "jss": "10.4.0"
+      }
+    },
+    "jss-plugin-rule-value-function": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.4.0.tgz",
+      "integrity": "sha512-w8504Cdfu66+0SJoLkr6GUQlEb8keHg8ymtJXdVHWh0YvFxDG2l/nS93SI5Gfx0fV29dO6yUugXnKzDFJxrdFQ==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "jss": "10.4.0",
+        "tiny-warning": "^1.0.2"
+      }
+    },
+    "jss-plugin-rule-value-observable": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-observable/-/jss-plugin-rule-value-observable-10.4.0.tgz",
+      "integrity": "sha512-Utnsvopa2Gg9Z/9rJ5uH0Gl5QRWlnx9Hd+K/rnAc7UyxbIpvTAWMv5hsnuCUbmUSyb9RKJPHlohJNwG8rFownQ==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "jss": "10.4.0",
+        "symbol-observable": "^1.2.0"
+      }
+    },
+    "jss-plugin-template": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/jss-plugin-template/-/jss-plugin-template-10.4.0.tgz",
+      "integrity": "sha512-bpRu56Dnas1+G/HvB0TdeC2907YujZ8F3pwLls7gNS6oJSYZD3iYbqsJuRVcAkhNINYVdcuW1SCo1aigCI7r/Q==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "jss": "10.4.0",
+        "tiny-warning": "^1.0.2"
+      }
+    },
+    "jss-plugin-vendor-prefixer": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.4.0.tgz",
+      "integrity": "sha512-DpF+/a+GU8hMh/948sBGnKSNfKkoHg2p9aRFUmyoyxgKjOeH9n74Ht3Yt8lOgdZsuWNJbPrvaa3U4PXKwxVpTQ==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "css-vendor": "^2.0.8",
+        "jss": "10.4.0"
+      }
+    },
+    "jss-preset-default": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/jss-preset-default/-/jss-preset-default-10.4.0.tgz",
+      "integrity": "sha512-WnmqDtQiK7bcw7yOxoW4iwf2ytVhgJfxqsb9R7V0gYOQi8TuApxs99nXgLVr3XN2HfVwk8hXlc9j50N5imozCQ==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "jss": "10.4.0",
+        "jss-plugin-camel-case": "10.4.0",
+        "jss-plugin-compose": "10.4.0",
+        "jss-plugin-default-unit": "10.4.0",
+        "jss-plugin-expand": "10.4.0",
+        "jss-plugin-extend": "10.4.0",
+        "jss-plugin-global": "10.4.0",
+        "jss-plugin-nested": "10.4.0",
+        "jss-plugin-props-sort": "10.4.0",
+        "jss-plugin-rule-value-function": "10.4.0",
+        "jss-plugin-rule-value-observable": "10.4.0",
+        "jss-plugin-template": "10.4.0",
+        "jss-plugin-vendor-prefixer": "10.4.0"
+      }
+    },
     "jsx-ast-utils": {
       "version": "2.4.1",
       "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz",
@@ -10598,6 +10796,11 @@
             "path-exists": "^4.0.0"
           }
         },
+        "immer": {
+          "version": "1.10.0",
+          "resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz",
+          "integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg=="
+        },
         "inquirer": {
           "version": "7.0.4",
           "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz",
@@ -10710,6 +10913,11 @@
         }
       }
     },
+    "react-display-name": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/react-display-name/-/react-display-name-0.2.5.tgz",
+      "integrity": "sha512-I+vcaK9t4+kypiSgaiVWAipqHRXYmZIuAiS8vzFvXHHXVigg/sMKwlRgLy6LH2i3rmP+0Vzfl5lFsFRwF1r3pg=="
+    },
     "react-dom": {
       "version": "16.13.1",
       "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",
@@ -10731,6 +10939,24 @@
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
       "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
     },
+    "react-jss": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/react-jss/-/react-jss-10.4.0.tgz",
+      "integrity": "sha512-q0P6GQrq0bm0ms/l6QpqNaPwPuJpYP5kcCXLSwcOQUnKACXh4NCcEsnqxD56TQmbAhE1p3RhtGD5xoBda1pjwQ==",
+      "requires": {
+        "@babel/runtime": "^7.3.1",
+        "@emotion/is-prop-valid": "^0.7.3",
+        "css-jss": "10.4.0",
+        "hoist-non-react-statics": "^3.2.0",
+        "is-in-browser": "^1.1.3",
+        "jss": "10.4.0",
+        "jss-preset-default": "10.4.0",
+        "prop-types": "^15.6.0",
+        "shallow-equal": "^1.2.0",
+        "theming": "^3.3.0",
+        "tiny-warning": "^1.0.2"
+      }
+    },
     "react-scripts": {
       "version": "3.4.3",
       "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.3.tgz",
@@ -10791,6 +11017,11 @@
         "workbox-webpack-plugin": "4.3.1"
       }
     },
+    "react-sweet-state": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/react-sweet-state/-/react-sweet-state-2.3.1.tgz",
+      "integrity": "sha512-dqTIK/rxr4j2TzSWl3GB2XBwTJ3Ux8i+B80T/uKVrLUib1LRjfweHi8ezKzCH48KWTUs+SGc26EZjWUtv7y6aA=="
+    },
     "read-pkg": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
@@ -11589,6 +11820,11 @@
         }
       }
     },
+    "shallow-equal": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
+      "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA=="
+    },
     "shebang-command": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@@ -12373,6 +12609,11 @@
         "util.promisify": "~1.0.0"
       }
     },
+    "symbol-observable": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
+      "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
+    },
     "symbol-tree": {
       "version": "3.2.4",
       "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -12550,6 +12791,17 @@
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
       "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ="
     },
+    "theming": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/theming/-/theming-3.3.0.tgz",
+      "integrity": "sha512-u6l4qTJRDaWZsqa8JugaNt7Xd8PPl9+gonZaIe28vAhqgHMIG/DOyFPqiKN/gQLQYj05tHv+YQdNILL4zoiAVA==",
+      "requires": {
+        "hoist-non-react-statics": "^3.3.0",
+        "prop-types": "^15.5.8",
+        "react-display-name": "^0.2.4",
+        "tiny-warning": "^1.0.2"
+      }
+    },
     "throat": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz",
@@ -12611,6 +12863,11 @@
       "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
       "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
     },
+    "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=="
+    },
     "tmp": {
       "version": "0.0.33",
       "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
diff --git a/package.json b/package.json
index 75ee36e4bccae791d6d7a331b817979e4eb19472..fb23f0cf898687aa489fef6968a8af0283a89df4 100644
--- a/package.json
+++ b/package.json
@@ -10,9 +10,12 @@
     "@types/node": "^12.12.62",
     "@types/react": "^16.9.49",
     "@types/react-dom": "^16.9.8",
+    "immer": "^7.0.9",
     "react": "^16.13.1",
     "react-dom": "^16.13.1",
+    "react-jss": "^10.4.0",
     "react-scripts": "3.4.3",
+    "react-sweet-state": "^2.3.1",
     "typescript": "^3.7.5"
   },
   "scripts": {
diff --git a/src/App.css b/src/App.css
deleted file mode 100644
index 74b5e053450a48a6bdb4d71aad648e7af821975c..0000000000000000000000000000000000000000
--- a/src/App.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.App {
-  text-align: center;
-}
-
-.App-logo {
-  height: 40vmin;
-  pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
-  .App-logo {
-    animation: App-logo-spin infinite 20s linear;
-  }
-}
-
-.App-header {
-  background-color: #282c34;
-  min-height: 100vh;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  font-size: calc(10px + 2vmin);
-  color: white;
-}
-
-.App-link {
-  color: #61dafb;
-}
-
-@keyframes App-logo-spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}
diff --git a/src/App.tsx b/src/App.tsx
index a53698aab3c66049c61980112dd0109dd2cd0845..24ce632278653305d1de2f750378cc1c6770d12c 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,26 +1,79 @@
-import React from 'react';
-import logo from './logo.svg';
-import './App.css';
+import React, {useEffect, useState} from 'react';
+import {createUseStyles} from "react-jss";
+import {useApiClient} from "./api/ApiClientContext";
+import {Title} from "./api/models/Title";
+import {Locale, LocalePriority, selectLocaleVersion} from "./locale/selectLocaleData";
+import {usePlayabilityRating} from "./mime/usePlayabilityRating";
+import {sortNumericallyDesc} from "./util/sortNumerically";
 
-function App() {
-  return (
-    <div className="App">
-      <header className="App-header">
-        <img src={logo} className="App-logo" alt="logo" />
-        <p>
-          Edit <code>src/App.tsx</code> and save to reload.
-        </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Learn React
-        </a>
-      </header>
-    </div>
-  );
+export default function App() {
+    const {apiClient} = useApiClient();
+    const playabilityRating = usePlayabilityRating();
+    const [data, setData] = useState<Title[]>();
+    useEffect(() => {
+        apiClient.listTitles().then(setData);
+    }, [apiClient]);
+    const locale: Locale = {
+        language: "de",
+        region: "DE",
+    };
+
+    const classes = useStyles();
+
+    return (
+        <div>
+            {data?.map((item: Title) => {
+                const title = selectLocaleVersion(locale, LocalePriority.REGION_BEFORE_LOCALE, item.titles);
+                const description = selectLocaleVersion(locale, LocalePriority.LOCALE_BEFORE_REGION, item.descriptions);
+                const poster = item.images.find(it => it.kind === "poster");
+                const backdrop = item.images.find(it => it.kind === "backdrop");
+                const mediaList = item.media.map(it => {
+                    return {
+                        item: it,
+                        value: playabilityRating(it),
+                    };
+                }).sort(sortNumericallyDesc(it => it.value));
+                const media = mediaList[0]?.item;
+
+                return (
+                    <div key={item.ids.uuid} className={classes.movie}>
+                        <h1>{title?.name}</h1>
+                        <strong>{description?.tagline}</strong>
+                        <p>{description?.overview}</p>
+                        {poster && (
+                            <img
+                                className={classes.poster}
+                                alt={`Movie poster for ${title?.name}`}
+                                src={poster.src}
+                            />
+                        )}
+                        {backdrop && (
+                            <img
+                                className={classes.poster}
+                                alt={`Movie backdrop for ${title?.name}`}
+                                src={backdrop.src}
+                            />
+                        )}
+                        {media && media.src}
+                        <pre>{JSON.stringify(item.subtitles, null, 2)}</pre>
+                        <pre>{JSON.stringify(item.preview, null, 2)}</pre>
+                    </div>
+                )
+            })}
+        </div>
+    );
 }
 
-export default App;
+const useStyles = createUseStyles({
+    movie: {
+        maxWidth: "40rem",
+        margin: {
+            left: "auto",
+            right: "auto",
+        }
+    },
+    poster: {
+        maxWidth: "20rem",
+        maxHeight: "20rem",
+    }
+});
diff --git a/src/api/ApiClient.ts b/src/api/ApiClient.ts
new file mode 100644
index 0000000000000000000000000000000000000000..065f352a42ed4a89af0b981ead876da4570fed15
--- /dev/null
+++ b/src/api/ApiClient.ts
@@ -0,0 +1,37 @@
+import {GenreWithTitles} from "./models/dto/GenreWithTitles";
+import {Episode} from "./models/Episode";
+import {Genre} from "./models/Genre";
+import {Title} from "./models/Title";
+import {RequestClient} from "../request/RequestClient";
+
+export class ApiClient extends RequestClient {
+    public async listGenres(): Promise<Genre[]> {
+        return await this.request(`api/v1/genres`, {
+            method: "GET"
+        });
+    }
+
+    public async getGenre(genreId: string): Promise<GenreWithTitles> {
+        return await this.request(`api/v1/genres/${genreId}`, {
+            method: "GET"
+        });
+    }
+
+    public async listTitles(): Promise<Title[]> {
+        return await this.request(`api/v1/titles`, {
+            method: "GET"
+        });
+    }
+
+    public async getTitle(titleId: string): Promise<Title> {
+        return await this.request(`api/v1/titles/${titleId}`, {
+            method: "GET"
+        });
+    }
+
+    public async listEpisodes(titleId: string): Promise<Episode> {
+        return await this.request(`api/v1/titles/${titleId}/episodes`, {
+            method: "GET"
+        });
+    }
+}
diff --git a/src/api/ApiClientContext.ts b/src/api/ApiClientContext.ts
new file mode 100644
index 0000000000000000000000000000000000000000..22e326d2e115c0423f60d26e8b0c67a2aa04820c
--- /dev/null
+++ b/src/api/ApiClientContext.ts
@@ -0,0 +1,19 @@
+import {createContext, useContext} from "react";
+import {ApiClient} from "./ApiClient";
+
+interface ApiClientContext {
+    apiClient: ApiClient
+}
+
+const ApiClientContext = createContext<ApiClientContext>({
+    apiClient: new ApiClient(
+        localStorage.getItem("API_ENDPOINT") ||
+        process.env.NODE_ENV === "development"
+            ? "http://localhost:8000/"
+            : new URL("/", window.location.href).toString()
+    )
+});
+
+export const ApiClientProvider = ApiClientContext.Provider;
+export const ApiClientConsumer = ApiClientContext.Consumer;
+export const useApiClient = () => useContext<ApiClientContext>(ApiClientContext);
diff --git a/src/api/models/Cast.ts b/src/api/models/Cast.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ee193ea5141da561f699e4125797a9102e510ff5
--- /dev/null
+++ b/src/api/models/Cast.ts
@@ -0,0 +1,8 @@
+import {Person} from "./Person";
+
+export interface Cast {
+    category: string | null,
+    characters: string[],
+    credit: string | null,
+    person: Person
+}
diff --git a/src/api/models/Episode.ts b/src/api/models/Episode.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4afedae6c81eb53b89790e8a6447cea595829fed
--- /dev/null
+++ b/src/api/models/Episode.ts
@@ -0,0 +1,8 @@
+import {Title} from "./Title";
+
+export interface Episode {
+    seasonNumber: string | null,
+    episodeNumber: string | null,
+    airDate: string | null,
+    title: Title
+}
diff --git a/src/api/models/Genre.ts b/src/api/models/Genre.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1ab15c2128f2128757faef896d967beac230fce8
--- /dev/null
+++ b/src/api/models/Genre.ts
@@ -0,0 +1,5 @@
+export interface Genre {
+    id: string,
+    tmdbId: number | null,
+    name: string
+}
diff --git a/src/api/models/Image.ts b/src/api/models/Image.ts
new file mode 100644
index 0000000000000000000000000000000000000000..14c89098c072dc96babc1b8554cd92be565be1ea
--- /dev/null
+++ b/src/api/models/Image.ts
@@ -0,0 +1,5 @@
+export interface Image {
+    kind: string,
+    mime: string,
+    src: string
+}
diff --git a/src/api/models/LocalizedData.ts b/src/api/models/LocalizedData.ts
new file mode 100644
index 0000000000000000000000000000000000000000..992a91e5ac62d417c16c6904c42d82d0def00dbc
--- /dev/null
+++ b/src/api/models/LocalizedData.ts
@@ -0,0 +1,5 @@
+export interface LocalizedData {
+    region: string | null,
+    languages: string[],
+    kind: string,
+}
diff --git a/src/api/models/Media.ts b/src/api/models/Media.ts
new file mode 100644
index 0000000000000000000000000000000000000000..715f4e0d80c0f7f2cd43aa1ffe761f6b2f0182c3
--- /dev/null
+++ b/src/api/models/Media.ts
@@ -0,0 +1,6 @@
+export interface Media {
+    mime: string,
+    codecs: string[],
+    languages: string[],
+    src: string
+}
diff --git a/src/api/models/Person.ts b/src/api/models/Person.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2bc063f01029d2d8837f1c584c06c8f96f1a75f3
--- /dev/null
+++ b/src/api/models/Person.ts
@@ -0,0 +1,5 @@
+export interface Person {
+    id: string,
+    imdbId: string | null,
+    name: string
+}
diff --git a/src/api/models/Rating.ts b/src/api/models/Rating.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6cef8e954a9eeaa140fc1beb74ddc0996f82153c
--- /dev/null
+++ b/src/api/models/Rating.ts
@@ -0,0 +1,4 @@
+export interface Rating {
+    region: string | null,
+    certification: string
+}
diff --git a/src/api/models/Subtitle.ts b/src/api/models/Subtitle.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1df9380ead8ee3abaaf6e84c6605703f45ffef52
--- /dev/null
+++ b/src/api/models/Subtitle.ts
@@ -0,0 +1,7 @@
+export interface Subtitle {
+    format: string,
+    language: string | null,
+    region: string | null,
+    specifier: string | null,
+    src: string
+}
diff --git a/src/api/models/Title.ts b/src/api/models/Title.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c22519a182c8e33dcab68706a61999c74456ae5b
--- /dev/null
+++ b/src/api/models/Title.ts
@@ -0,0 +1,28 @@
+import {Cast} from "./Cast";
+import {Genre} from "./Genre";
+import {Image} from "./Image";
+import {Media} from "./Media";
+import {Rating} from "./Rating";
+import {Subtitle} from "./Subtitle";
+import {TitleDescription} from "./TitleDescription";
+import {TitleId} from "./TitleId";
+import {TitleName} from "./TitleName";
+
+export interface Title {
+    ids: TitleId,
+    originalLanguage: string | null,
+    runtime: number | null,
+    yearStart: number | null,
+    yearEnd: number | null,
+    titles: TitleName[],
+    descriptions: TitleDescription[],
+    cast: Cast[],
+    genres: Genre[],
+    ratings: Rating[],
+    images: Image[],
+    media: Media[],
+    subtitles: Subtitle[],
+    preview: string | null,
+    createdAt: string,
+    updatedAt: string
+}
diff --git a/src/api/models/TitleDescription.ts b/src/api/models/TitleDescription.ts
new file mode 100644
index 0000000000000000000000000000000000000000..980640da329fd2f8219b61d5d66884dd11dd0ea9
--- /dev/null
+++ b/src/api/models/TitleDescription.ts
@@ -0,0 +1,6 @@
+import {LocalizedData} from "./LocalizedData";
+
+export interface TitleDescription extends LocalizedData {
+    overview: string,
+    tagline: string | null
+}
diff --git a/src/api/models/TitleId.ts b/src/api/models/TitleId.ts
new file mode 100644
index 0000000000000000000000000000000000000000..249afdec860f3d7f511f51cf6dd632784046d292
--- /dev/null
+++ b/src/api/models/TitleId.ts
@@ -0,0 +1,6 @@
+export interface TitleId {
+    uuid: string,
+    imdb: string | null,
+    tmdb: number | null,
+    tvdb: number | null
+}
diff --git a/src/api/models/TitleName.ts b/src/api/models/TitleName.ts
new file mode 100644
index 0000000000000000000000000000000000000000..914d7a42ad8b84db4700cbff1bf2fe8999450d19
--- /dev/null
+++ b/src/api/models/TitleName.ts
@@ -0,0 +1,5 @@
+import {LocalizedData} from "./LocalizedData";
+
+export interface TitleName extends LocalizedData {
+    name: string
+}
diff --git a/src/api/models/dto/GenreWithTitles.ts b/src/api/models/dto/GenreWithTitles.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2d93e32c773ec1d7bd4f12fe8653cb14dac99910
--- /dev/null
+++ b/src/api/models/dto/GenreWithTitles.ts
@@ -0,0 +1,7 @@
+import {Genre} from "../Genre";
+import {Title} from "../Title";
+
+export interface GenreWithTitles {
+    genre: Genre,
+    titles: Title[],
+}
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index ec2585e8c0bb8188184ed1e0703c4c8f2a8419b0..0000000000000000000000000000000000000000
--- a/src/index.css
+++ /dev/null
@@ -1,13 +0,0 @@
-body {
-  margin: 0;
-  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
-    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
-    sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-}
-
-code {
-  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
-    monospace;
-}
diff --git a/src/index.tsx b/src/index.tsx
index f5185c1ec7a5dccf30b55a8e3f89afc3eca764a1..08d504055369af3327e4d8e5f48650abb562fb5d 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,14 +1,19 @@
+import {produce} from 'immer'
 import React from 'react';
 import ReactDOM from 'react-dom';
-import './index.css';
+
+import {defaults} from 'react-sweet-state';
 import App from './App';
 import * as serviceWorker from './serviceWorker';
 
+defaults.devtools = process.env.NODE_ENV !== "production";
+defaults.mutator = (currentState, producer) => produce(currentState, producer)
+
 ReactDOM.render(
-  <React.StrictMode>
-    <App />
-  </React.StrictMode>,
-  document.getElementById('root')
+    <React.StrictMode>
+        <App/>
+    </React.StrictMode>,
+    document.getElementById('root')
 );
 
 // If you want your app to work offline and load faster, you can change
diff --git a/src/locale/selectLocaleData.ts b/src/locale/selectLocaleData.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b37640148c0c9e8ef116a7f71f816a7f22275884
--- /dev/null
+++ b/src/locale/selectLocaleData.ts
@@ -0,0 +1,35 @@
+import {LocalizedData} from "../api/models/LocalizedData";
+
+export interface Locale {
+    language: string,
+    region: string
+}
+
+export enum LocalePriority {
+    REGION_ONLY,
+    REGION_BEFORE_LOCALE,
+    LOCALE_BEFORE_REGION
+}
+
+function filterVersions<T extends LocalizedData>(locale: Locale, method: LocalePriority, data: T[]): T | undefined {
+    switch (method) {
+        case LocalePriority.REGION_ONLY:
+            return data.find((el) => {
+                return (el.region === locale.region);
+            });
+        case LocalePriority.REGION_BEFORE_LOCALE:
+            return data.sort((a, b) => (+b.languages.includes(locale.language)) - (+a.languages.includes(locale.language)))
+                .find((el) => el.region === locale.region);
+        case LocalePriority.LOCALE_BEFORE_REGION:
+            return data.sort((a, b) => (+(b.region === locale.region)) - (+(a.region === locale.region)))
+                .find((el) => el.languages.includes(locale.language));
+
+    }
+}
+
+export function selectLocaleVersion<T extends LocalizedData>(locale: Locale, method: LocalePriority, data: T[]): T | null {
+    return filterVersions(locale, method, data.filter(it => it.kind === "localized"))
+        || data.find(it => it.kind === "primary")
+        || data.find(it => it.kind === "original")
+        || null;
+}
diff --git a/src/logo.svg b/src/logo.svg
deleted file mode 100644
index 6b60c1042f58d9fabb75485aa3624dddcf633b5c..0000000000000000000000000000000000000000
--- a/src/logo.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
-    <g fill="#61DAFB">
-        <path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
-        <circle cx="420.9" cy="296.5" r="45.7"/>
-        <path d="M520.5 78.1z"/>
-    </g>
-</svg>
diff --git a/src/mime/PlayabilityRating.ts b/src/mime/PlayabilityRating.ts
new file mode 100644
index 0000000000000000000000000000000000000000..583eb57af315820a70fcc554b36849dbc5a3ae4f
--- /dev/null
+++ b/src/mime/PlayabilityRating.ts
@@ -0,0 +1,5 @@
+export enum PlayabilityRating {
+    UNLIKELY = 0,
+    MAYBE = 1,
+    PROBABLY = 2,
+}
diff --git a/src/mime/usePlayabilityRating.ts b/src/mime/usePlayabilityRating.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c3d61465c750ffd425f010b1a16ea7aa8eff9baa
--- /dev/null
+++ b/src/mime/usePlayabilityRating.ts
@@ -0,0 +1,18 @@
+import {useRef} from "react";
+import {Media} from "../api/models/Media";
+import {PlayabilityRating} from "./PlayabilityRating";
+import {videoMimeString} from "./videoMimeString";
+
+export const usePlayabilityRating = () => {
+    const mediaElement = useRef(document.createElement("video")).current;
+    return (media: Media) => {
+        switch (mediaElement.canPlayType(videoMimeString(media))) {
+            case "":
+                return PlayabilityRating.UNLIKELY;
+            case "maybe":
+                return PlayabilityRating.MAYBE;
+            case "probably":
+                return PlayabilityRating.PROBABLY;
+        }
+    }
+}
diff --git a/src/mime/videoMimeString.ts b/src/mime/videoMimeString.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1c8d61d7c57253bb34bca40f051a30de95816247
--- /dev/null
+++ b/src/mime/videoMimeString.ts
@@ -0,0 +1,4 @@
+import {Media} from "../api/models/Media";
+
+export const videoMimeString = (media: Media) =>
+    `${media.mime}; codecs="${media.codecs.join(", ")}"`;
diff --git a/src/request/RequestClient.ts b/src/request/RequestClient.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bb9d058cd60e037f590a07743fb1d22e565d751a
--- /dev/null
+++ b/src/request/RequestClient.ts
@@ -0,0 +1,25 @@
+import {RequestError} from "./RequestError";
+import {RequestErrorKind} from "./RequestErrorKind";
+
+export class RequestClient {
+    private baseUrl: string
+
+    constructor(baseUrl: string) {
+        this.baseUrl = baseUrl;
+    }
+
+    protected async request<T>(path: string, headers?: RequestInit): Promise<T> {
+        const url = new URL(path, this.baseUrl).toString();
+        const response = await fetch(url, headers);
+        if (response.ok) {
+            return await response.json();
+        } else {
+            if (response.status in RequestErrorKind) {
+                throw new RequestError(response.statusText, response.status)
+            } else {
+                throw new RequestError(response.statusText, RequestErrorKind.UnknownError)
+            }
+        }
+    }
+}
+
diff --git a/src/request/RequestError.ts b/src/request/RequestError.ts
new file mode 100644
index 0000000000000000000000000000000000000000..692046bfdc134a263da4ee43a2c848bae45a1c61
--- /dev/null
+++ b/src/request/RequestError.ts
@@ -0,0 +1,12 @@
+import {RequestErrorKind} from "./RequestErrorKind";
+
+export class RequestError extends Error {
+    public kind: RequestErrorKind;
+    public message: string;
+
+    constructor(message: string, kind: RequestErrorKind) {
+        super(`RequestError ${kind}: ${message}`);
+        this.kind = kind;
+        this.message = message;
+    }
+}
diff --git a/src/request/RequestErrorKind.ts b/src/request/RequestErrorKind.ts
new file mode 100644
index 0000000000000000000000000000000000000000..eb72e19f9a8bce4b1f7460733cbf7973f72fc129
--- /dev/null
+++ b/src/request/RequestErrorKind.ts
@@ -0,0 +1,42 @@
+export enum RequestErrorKind {
+    UnknownError = 0,
+    BadRequest = 400,
+    Unauthorized = 401,
+    PaymentRequired = 402,
+    Forbidden = 403,
+    NotFound = 404,
+    MethodNotAllowed = 405,
+    NotAcceptable = 406,
+    ProxyAuthenticationRequired = 407,
+    RequestTimeout = 408,
+    Conflict = 409,
+    Gone = 410,
+    LengthRequired = 411,
+    PreconditionFailed = 412,
+    PayloadTooLarge = 413,
+    UnsupportedMediaType = 415,
+    RangeNotSatisfiable = 416,
+    ExpectationFailed = 417,
+    IAmATeapot = 418,
+    MisdirectedRequest = 421,
+    UnprocessableEntity = 422,
+    Locked = 423,
+    FailedDeependency = 424,
+    TooEarly = 425,
+    UpgradeRequired = 426,
+    PreconditionRequired = 428,
+    TooManyRequests = 429,
+    RequestHeaderFieldsTooLarge = 431,
+    UnavailableForLegalReasons = 451,
+    InternalServerError = 500,
+    NotImplemented = 501,
+    BadGateway = 502,
+    ServiceUnavailable = 503,
+    GatewayTimeout = 504,
+    HttpVersionNotSupported = 505,
+    VariantAlsoNegotiates = 506,
+    InsufficientStorage = 507,
+    LoopDetected = 508,
+    NotExtended = 510,
+    NetworkAuthenticationRequired = 511
+}
diff --git a/src/util/sortLexically.ts b/src/util/sortLexically.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c765dee0744eabbc68b177469a7873d5e6493b4e
--- /dev/null
+++ b/src/util/sortLexically.ts
@@ -0,0 +1,5 @@
+export const sortLexicallyAsc = <T>(accessor: ((data: T) => string) = String):
+    (a: T, b: T) => number => (a: T, b: T) => accessor(a).localeCompare(accessor(b));
+
+export const sortLexicallyDesc = <T>(accessor: ((data: T) => string) = String):
+    (a: T, b: T) => number => (a: T, b: T) => accessor(b).localeCompare(accessor(a));
diff --git a/src/util/sortNumerically.ts b/src/util/sortNumerically.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0be7a978d5a20d5dcc56447858aab75f0aa745f4
--- /dev/null
+++ b/src/util/sortNumerically.ts
@@ -0,0 +1,5 @@
+export const sortNumericallyAsc = <T>(accessor: ((data: T) => number) = Number):
+    (a: T, b: T) => number => (a: T, b: T) => accessor(a) - accessor(b);
+
+export const sortNumericallyDesc = <T>(accessor: ((data: T) => number) = Number):
+    (a: T, b: T) => number => (a: T, b: T) => accessor(b) - accessor(a);