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);