mirror of
https://github.com/apricote/Listory.git
synced 2026-01-13 21:21:02 +00:00
feat(frontend): render simple listen report
This commit is contained in:
parent
3828b841c2
commit
ebc079435d
8 changed files with 562 additions and 1 deletions
253
frontend/package-lock.json
generated
253
frontend/package-lock.json
generated
|
|
@ -1933,11 +1933,47 @@
|
||||||
"@babel/types": "^7.3.0"
|
"@babel/types": "^7.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/classnames": {
|
||||||
|
"version": "2.2.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.10.tgz",
|
||||||
|
"integrity": "sha512-1UzDldn9GfYYEsWWnn/P4wkTlkZDH7lDb0wBMGbtIQc9zXEQq7FlKBdZUn6OBqD8sKZZ2RQO2mAjGpXiDGoRmQ=="
|
||||||
|
},
|
||||||
"@types/color-name": {
|
"@types/color-name": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
||||||
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
|
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
|
||||||
},
|
},
|
||||||
|
"@types/d3-array": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-rGqfPVowNDTszSFvwoZIXvrPG7s/qKzm9piCRIH6xwTTRu7pPZ3ootULFnPkTt74B6i5lN0FpLQL24qGOw1uZA=="
|
||||||
|
},
|
||||||
|
"@types/d3-path": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-AZGHWslq/oApTAHu9+yH/Bnk63y9oFOMROtqPAtxl5uB6qm1x2lueWdVEjsjjV3Qc2+QfuzKIwIR5MvVBakfzA=="
|
||||||
|
},
|
||||||
|
"@types/d3-scale": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-oQFanN0/PiR2oySHfj+zAAkK1/p4LD32Nt1TMVmzk+bYHk7vgIg/iTXQWitp1cIkDw4LMdcgvO63wL+mNs47YA==",
|
||||||
|
"requires": {
|
||||||
|
"@types/d3-time": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/d3-shape": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-LtD8EaNYCaBRzHzaAiIPrfcL3DdIysc81dkGlQvv7WQP3+YXV7b0JJTtR1U3bzeRieS603KF4wUo+ZkJVenh8w==",
|
||||||
|
"requires": {
|
||||||
|
"@types/d3-path": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/d3-time": {
|
||||||
|
"version": "1.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-1.0.10.tgz",
|
||||||
|
"integrity": "sha512-aKf62rRQafDQmSiv1NylKhIMmznsjRN+MnXRXTqHoqm0U/UZzVpdrtRnSIfdiLS616OuC1soYeX1dBg2n1u8Xw=="
|
||||||
|
},
|
||||||
"@types/eslint-visitor-keys": {
|
"@types/eslint-visitor-keys": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
|
||||||
|
|
@ -2101,6 +2137,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz",
|
||||||
"integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA=="
|
"integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA=="
|
||||||
},
|
},
|
||||||
|
"@types/lodash": {
|
||||||
|
"version": "4.14.150",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.150.tgz",
|
||||||
|
"integrity": "sha512-kMNLM5JBcasgYscD9x/Gvr6lTAv2NVgsKtet/hm93qMyf/D1pt+7jeEZklKJKxMVmXjxbRVQQGfqDSfipYCO6w=="
|
||||||
|
},
|
||||||
"@types/minimatch": {
|
"@types/minimatch": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
|
||||||
|
|
@ -2422,6 +2463,126 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@vx/axis": {
|
||||||
|
"version": "0.0.196",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vx/axis/-/axis-0.0.196.tgz",
|
||||||
|
"integrity": "sha512-AtVjoDXCmecKRp671ATV9Qky/MpQY1pV/O1VWULxhq5dujKXWPj0GtnuRR7I7VgB0XCoKpEh12gVTi7dUq2FhQ==",
|
||||||
|
"requires": {
|
||||||
|
"@types/classnames": "^2.2.9",
|
||||||
|
"@types/react": "*",
|
||||||
|
"@vx/group": "0.0.196",
|
||||||
|
"@vx/point": "0.0.196",
|
||||||
|
"@vx/shape": "0.0.196",
|
||||||
|
"@vx/text": "0.0.196",
|
||||||
|
"classnames": "^2.2.5",
|
||||||
|
"prop-types": "^15.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@vx/curve": {
|
||||||
|
"version": "0.0.196",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vx/curve/-/curve-0.0.196.tgz",
|
||||||
|
"integrity": "sha512-uGucJuuAOa0ezrSTqQE8BHkyNPAwGV1bNB8BP1V5Ln1GA7o0LBkqwHOdzShkFzsVoeDadRRGy2mNga6gHDGh8A==",
|
||||||
|
"requires": {
|
||||||
|
"@types/d3-shape": "^1.3.1",
|
||||||
|
"d3-shape": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@vx/gradient": {
|
||||||
|
"version": "0.0.196",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vx/gradient/-/gradient-0.0.196.tgz",
|
||||||
|
"integrity": "sha512-euz0GRKIMXil7cx3Bx9lVmgfncz3M2Yv2+AEMyRiBe3Ln3Y04rTP2W6o94bvQ+TXWRD5U18LPQQg7Myr6cnFDA==",
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"prop-types": "^15.5.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@vx/grid": {
|
||||||
|
"version": "0.0.196",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vx/grid/-/grid-0.0.196.tgz",
|
||||||
|
"integrity": "sha512-u9zqpRA+k2qfickspWHCAgpqg/Sq55T3e3crPUvuAOsqiX/7doOd+LKbJJv0LRq6PVufdVRUBthTsW5F5qffrg==",
|
||||||
|
"requires": {
|
||||||
|
"@types/classnames": "^2.2.9",
|
||||||
|
"@types/react": "*",
|
||||||
|
"@vx/group": "0.0.196",
|
||||||
|
"@vx/point": "0.0.196",
|
||||||
|
"@vx/shape": "0.0.196",
|
||||||
|
"classnames": "^2.2.5",
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@vx/group": {
|
||||||
|
"version": "0.0.196",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vx/group/-/group-0.0.196.tgz",
|
||||||
|
"integrity": "sha512-neWYucGoyrWloEali12yEn//5pRVwSBtKpuzKlQ43yYTav8OLa869DtCSVeRlDG8TIcPvg9YWfWKKhGWKXRCtg==",
|
||||||
|
"requires": {
|
||||||
|
"@types/classnames": "^2.2.9",
|
||||||
|
"@types/react": "*",
|
||||||
|
"classnames": "^2.2.5",
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@vx/point": {
|
||||||
|
"version": "0.0.196",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vx/point/-/point-0.0.196.tgz",
|
||||||
|
"integrity": "sha512-dya1cwMwqQtRP+oQwC5x0N2bCiKENGqDd6Y4IuAKwkWMvxpjOAPoOlglZ9QKLDPzftUqLFxrjrsT5RlHTxyKnw=="
|
||||||
|
},
|
||||||
|
"@vx/scale": {
|
||||||
|
"version": "0.0.196",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vx/scale/-/scale-0.0.196.tgz",
|
||||||
|
"integrity": "sha512-s3lVbwEni0F9AGACaJG3m071uXVryoOgogiU6+yXv1O1gUoZMMm6AjzCndcz9jq1sCFDNcIrxyf5LomOA2tQaQ==",
|
||||||
|
"requires": {
|
||||||
|
"@types/d3-scale": "^2.1.1",
|
||||||
|
"d3-scale": "^2.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@vx/shape": {
|
||||||
|
"version": "0.0.196",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vx/shape/-/shape-0.0.196.tgz",
|
||||||
|
"integrity": "sha512-09s4yv0IKmUrB/z/bRLW936DzbhbsxExHv0ocowh2Zv3zgObAaeyf0MkkLyr1nDTqKDCZlfwOEM+1moOyBFQBA==",
|
||||||
|
"requires": {
|
||||||
|
"@types/classnames": "^2.2.9",
|
||||||
|
"@types/d3-path": "^1.0.8",
|
||||||
|
"@types/d3-shape": "^1.3.1",
|
||||||
|
"@types/react": "*",
|
||||||
|
"@vx/curve": "0.0.196",
|
||||||
|
"@vx/group": "0.0.196",
|
||||||
|
"classnames": "^2.2.5",
|
||||||
|
"d3-path": "^1.0.5",
|
||||||
|
"d3-shape": "^1.2.0",
|
||||||
|
"prop-types": "^15.5.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@vx/text": {
|
||||||
|
"version": "0.0.196",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vx/text/-/text-0.0.196.tgz",
|
||||||
|
"integrity": "sha512-KhhOtQyzqcZvO/cgYdMrsLLf0LYFT0u1cqJ4j8RVD8JcmTrhHmzFvL9lE6OXADroZxTSOFY3mNIbAPlshIabpw==",
|
||||||
|
"requires": {
|
||||||
|
"@types/classnames": "^2.2.9",
|
||||||
|
"@types/lodash": "^4.14.146",
|
||||||
|
"@types/react": "*",
|
||||||
|
"classnames": "^2.2.5",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"reduce-css-calc": "^1.3.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": {
|
||||||
|
"version": "0.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz",
|
||||||
|
"integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg="
|
||||||
|
},
|
||||||
|
"reduce-css-calc": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz",
|
||||||
|
"integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=",
|
||||||
|
"requires": {
|
||||||
|
"balanced-match": "^0.4.2",
|
||||||
|
"math-expression-evaluator": "^1.2.14",
|
||||||
|
"reduce-function-call": "^1.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@webassemblyjs/ast": {
|
"@webassemblyjs/ast": {
|
||||||
"version": "1.8.5",
|
"version": "1.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
|
||||||
|
|
@ -3934,6 +4095,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"classnames": {
|
||||||
|
"version": "2.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
||||||
|
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
|
||||||
|
},
|
||||||
"clean-css": {
|
"clean-css": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
|
||||||
|
|
@ -4603,6 +4769,80 @@
|
||||||
"type": "^1.0.1"
|
"type": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"d3-array": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw=="
|
||||||
|
},
|
||||||
|
"d3-collection": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A=="
|
||||||
|
},
|
||||||
|
"d3-color": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q=="
|
||||||
|
},
|
||||||
|
"d3-format": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-TWks25e7t8/cqctxCmxpUuzZN11QxIA7YrMbram94zMQ0PXjE4LVIMe/f6a4+xxL8HQ3OsAFULOINQi1pE62Aw=="
|
||||||
|
},
|
||||||
|
"d3-interpolate": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==",
|
||||||
|
"requires": {
|
||||||
|
"d3-color": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"d3-path": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
|
||||||
|
},
|
||||||
|
"d3-scale": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==",
|
||||||
|
"requires": {
|
||||||
|
"d3-array": "^1.2.0",
|
||||||
|
"d3-collection": "1",
|
||||||
|
"d3-format": "1",
|
||||||
|
"d3-interpolate": "1",
|
||||||
|
"d3-time": "1",
|
||||||
|
"d3-time-format": "2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"d3-shape": {
|
||||||
|
"version": "1.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
|
||||||
|
"integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
|
||||||
|
"requires": {
|
||||||
|
"d3-path": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"d3-time": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA=="
|
||||||
|
},
|
||||||
|
"d3-time-format": {
|
||||||
|
"version": "2.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.2.3.tgz",
|
||||||
|
"integrity": "sha512-RAHNnD8+XvC4Zc4d2A56Uw0yJoM7bsvOlJR33bclxq399Rak/b9bhvu/InjxdWhPtkgU53JJcleJTGkNRnN6IA==",
|
||||||
|
"requires": {
|
||||||
|
"d3-time": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"damerau-levenshtein": {
|
"damerau-levenshtein": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz",
|
||||||
|
|
@ -9689,6 +9929,11 @@
|
||||||
"object-visit": "^1.0.0"
|
"object-visit": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"math-expression-evaluator": {
|
||||||
|
"version": "1.2.22",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.22.tgz",
|
||||||
|
"integrity": "sha512-L0j0tFVZBQQLeEjmWOvDLoRciIY8gQGWahvkztXUal8jH8R5Rlqo9GCvgqvXcy9LQhEWdQCVvzqAbxgYNt4blQ=="
|
||||||
|
},
|
||||||
"md5.js": {
|
"md5.js": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
||||||
|
|
@ -12922,6 +13167,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"reduce-function-call": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==",
|
||||||
|
"requires": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"regenerate": {
|
"regenerate": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,21 @@
|
||||||
"@testing-library/jest-dom": "^5.5.0",
|
"@testing-library/jest-dom": "^5.5.0",
|
||||||
"@testing-library/react": "^10.0.4",
|
"@testing-library/react": "^10.0.4",
|
||||||
"@testing-library/user-event": "^10.1.0",
|
"@testing-library/user-event": "^10.1.0",
|
||||||
|
"@types/d3-array": "^2.0.0",
|
||||||
"@types/jest": "^25.2.1",
|
"@types/jest": "^25.2.1",
|
||||||
"@types/node": "^12.12.38",
|
"@types/node": "^12.12.38",
|
||||||
"@types/react": "^16.9.34",
|
"@types/react": "^16.9.34",
|
||||||
"@types/react-dom": "^16.9.7",
|
"@types/react-dom": "^16.9.7",
|
||||||
"@types/react-router-dom": "^5.1.5",
|
"@types/react-router-dom": "^5.1.5",
|
||||||
|
"@vx/axis": "0.0.196",
|
||||||
|
"@vx/curve": "0.0.196",
|
||||||
|
"@vx/gradient": "0.0.196",
|
||||||
|
"@vx/grid": "0.0.196",
|
||||||
|
"@vx/group": "0.0.196",
|
||||||
|
"@vx/scale": "0.0.196",
|
||||||
|
"@vx/shape": "0.0.196",
|
||||||
"autoprefixer": "^9.7.6",
|
"autoprefixer": "^9.7.6",
|
||||||
|
"d3-array": "^2.4.0",
|
||||||
"date-fns": "^2.13.0",
|
"date-fns": "^2.13.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"postcss-cli": "^7.1.1",
|
"postcss-cli": "^7.1.1",
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ import React from "react";
|
||||||
import { Route, Switch } from "react-router-dom";
|
import { Route, Switch } from "react-router-dom";
|
||||||
import { LoginFailure } from "./components/LoginFailure";
|
import { LoginFailure } from "./components/LoginFailure";
|
||||||
import { NavBar } from "./components/NavBar";
|
import { NavBar } from "./components/NavBar";
|
||||||
|
import { RecentListens } from "./components/RecentListens";
|
||||||
|
import { ReportListens } from "./components/ReportListens";
|
||||||
import { useAuth } from "./hooks/use-auth";
|
import { useAuth } from "./hooks/use-auth";
|
||||||
import "./tailwind/generated.css";
|
import "./tailwind/generated.css";
|
||||||
import { RecentListens } from "./components/RecentListens";
|
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const { isLoaded } = useAuth();
|
const { isLoaded } = useAuth();
|
||||||
|
|
@ -22,6 +23,7 @@ export function App() {
|
||||||
<Route path="/" exact />
|
<Route path="/" exact />
|
||||||
<Route path="/login/failure" exact component={LoginFailure} />
|
<Route path="/login/failure" exact component={LoginFailure} />
|
||||||
<Route path="/listens" exact component={RecentListens} />
|
<Route path="/listens" exact component={RecentListens} />
|
||||||
|
<Route path="/reports/listens" exact component={ReportListens} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
import { formatISO, parseISO } from "date-fns";
|
||||||
import { Listen } from "./entities/listen";
|
import { Listen } from "./entities/listen";
|
||||||
|
import { ListenReportItem } from "./entities/listen-report-item";
|
||||||
|
import { ListenReportOptions } from "./entities/listen-report-options";
|
||||||
import { Pagination } from "./entities/pagination";
|
import { Pagination } from "./entities/pagination";
|
||||||
import { PaginationOptions } from "./entities/pagination-options";
|
import { PaginationOptions } from "./entities/pagination-options";
|
||||||
import { User } from "./entities/user";
|
import { User } from "./entities/user";
|
||||||
|
|
@ -67,3 +70,33 @@ export const getRecentListens = async (
|
||||||
console.log("getRecentListens", { listens });
|
console.log("getRecentListens", { listens });
|
||||||
return listens;
|
return listens;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getListensReport = async (
|
||||||
|
options: ListenReportOptions
|
||||||
|
): Promise<ListenReportItem[]> => {
|
||||||
|
const { timeFrame, timeStart, timeEnd } = options;
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/v1/reports/listens?timeFrame=${timeFrame}&timeStart=${formatISO(
|
||||||
|
timeStart
|
||||||
|
)}&timeEnd=${formatISO(timeEnd)}`,
|
||||||
|
{
|
||||||
|
headers: getDefaultHeaders(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (res.status) {
|
||||||
|
case 200: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 401: {
|
||||||
|
throw new UnauthenticatedError(`No token or token expired`);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unable to getRecentListens: ${res.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawItems: { count: number; date: string }[] = (await res.json()).items;
|
||||||
|
return rawItems.map(({ count, date }) => ({ count, date: parseISO(date) }));
|
||||||
|
};
|
||||||
|
|
|
||||||
4
frontend/src/api/entities/listen-report-item.ts
Normal file
4
frontend/src/api/entities/listen-report-item.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface ListenReportItem {
|
||||||
|
date: Date;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
5
frontend/src/api/entities/listen-report-options.ts
Normal file
5
frontend/src/api/entities/listen-report-options.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface ListenReportOptions {
|
||||||
|
timeFrame: "day" | "week" | "month" | "year";
|
||||||
|
timeStart: Date;
|
||||||
|
timeEnd: Date;
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,9 @@ export const NavBar: React.FC = () => {
|
||||||
<Link to="/listens">
|
<Link to="/listens">
|
||||||
<NavItem>Your Listens</NavItem>
|
<NavItem>Your Listens</NavItem>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link to="/reports/listens">
|
||||||
|
<NavItem>Listens Report</NavItem>
|
||||||
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
252
frontend/src/components/ReportListens.tsx
Normal file
252
frontend/src/components/ReportListens.tsx
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
import { AxisBottom, AxisLeft } from "@vx/axis";
|
||||||
|
import { curveBasis } from "@vx/curve";
|
||||||
|
import { GradientLightgreenGreen } from "@vx/gradient";
|
||||||
|
import { Grid } from "@vx/grid";
|
||||||
|
import { Group } from "@vx/group";
|
||||||
|
import { scaleLinear, scaleTime } from "@vx/scale";
|
||||||
|
import { Area, Line, LinePath } from "@vx/shape";
|
||||||
|
import { extent } from "d3-array";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Redirect } from "react-router-dom";
|
||||||
|
import { getListensReport } from "../api/api";
|
||||||
|
import { ListenReportItem } from "../api/entities/listen-report-item";
|
||||||
|
import { ListenReportOptions } from "../api/entities/listen-report-options";
|
||||||
|
import { useAuth } from "../hooks/use-auth";
|
||||||
|
|
||||||
|
export const ReportListens: React.FC = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const [reportOptions, setReportOptions] = useState<ListenReportOptions>({
|
||||||
|
timeFrame: "day",
|
||||||
|
timeStart: new Date("2020-05-01"),
|
||||||
|
timeEnd: new Date(),
|
||||||
|
});
|
||||||
|
const [report, setReport] = useState<ListenReportItem[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const reportFromApi = await getListensReport(reportOptions);
|
||||||
|
setReport(reportFromApi);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error while fetching recent listens:", err);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [reportOptions, setReport, setIsLoading]);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return <Redirect to="/" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="md:flex md:justify-center p-4">
|
||||||
|
<div className="md:flex-shrink-0 min-w-full xl:min-w-0 xl:w-2/3 max-w-screen-lg">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<p className="text-2xl font-normal text-gray-700">Listen Report</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{isLoading && (
|
||||||
|
<div>
|
||||||
|
<span>Loading Listens</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{report.length === 0 && (
|
||||||
|
<div>
|
||||||
|
<p>Report is emtpy! :(</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{report.length > 0 && (
|
||||||
|
<div className="table-auto my-2 w-full text-gray-700">
|
||||||
|
<ReportGraph options={reportOptions} data={report} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReportGraph: React.FC<{
|
||||||
|
options: ListenReportOptions;
|
||||||
|
data: ListenReportItem[];
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
margin?: {
|
||||||
|
top: number;
|
||||||
|
right: number;
|
||||||
|
bottom: number;
|
||||||
|
left: number;
|
||||||
|
};
|
||||||
|
}> = ({
|
||||||
|
options,
|
||||||
|
data,
|
||||||
|
width = 900,
|
||||||
|
height = 500,
|
||||||
|
margin = { left: 70, right: 70, top: 20, bottom: 80 },
|
||||||
|
}) => {
|
||||||
|
// Then we'll create some bounds
|
||||||
|
const xMax = width - margin.left - margin.right;
|
||||||
|
const yMax = height - margin.top - margin.bottom;
|
||||||
|
|
||||||
|
// We'll make some helpers to get at the data we want
|
||||||
|
const x = (d: ListenReportItem) => d.date;
|
||||||
|
const y = (d: ListenReportItem) => d.count;
|
||||||
|
|
||||||
|
// responsive utils for axis ticks
|
||||||
|
const numTicksForHeight = (heightT: number): number => {
|
||||||
|
if (heightT <= 300) return 3;
|
||||||
|
if (300 < heightT && heightT <= 600) return 5;
|
||||||
|
return 10;
|
||||||
|
};
|
||||||
|
|
||||||
|
const numTicksForWidth = (widthT: number): number => {
|
||||||
|
if (widthT <= 300) return 2;
|
||||||
|
if (300 < widthT && widthT <= 400) return 5;
|
||||||
|
return 10;
|
||||||
|
};
|
||||||
|
|
||||||
|
// And then scale the graph by our data
|
||||||
|
const xScaleTime = scaleTime<number>({
|
||||||
|
range: [0, xMax],
|
||||||
|
domain: extent(data, x) as [Date, Date],
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
const xScaleBand = scaleBand({
|
||||||
|
range: [0, xMax],
|
||||||
|
domain: extent(data, x) as [Date, Date],
|
||||||
|
padding: 0.2,
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
const yScale = scaleLinear({
|
||||||
|
range: [yMax, 0],
|
||||||
|
domain: [0, Math.max(...data.map(y))],
|
||||||
|
nice: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compose together the scale and accessor functions to get point functions
|
||||||
|
const compose = (scale: any, accessor: any) => (d: ListenReportItem) =>
|
||||||
|
scale(accessor(d));
|
||||||
|
const xPoint = compose(xScaleTime, x);
|
||||||
|
const yPoint = compose(yScale, y);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg width={width} height={height}>
|
||||||
|
<GradientLightgreenGreen
|
||||||
|
id="linear"
|
||||||
|
vertical={false}
|
||||||
|
fromOpacity={0.8}
|
||||||
|
toOpacity={0.8}
|
||||||
|
/>
|
||||||
|
<Grid
|
||||||
|
top={margin.top}
|
||||||
|
left={margin.left}
|
||||||
|
xScale={xScaleTime}
|
||||||
|
yScale={yScale}
|
||||||
|
stroke="rgba(142, 32, 95, 0.9)"
|
||||||
|
width={xMax}
|
||||||
|
height={yMax}
|
||||||
|
numTicksRows={numTicksForHeight(height)}
|
||||||
|
numTicksColumns={numTicksForWidth(width)}
|
||||||
|
/>
|
||||||
|
<Group top={margin.top} left={margin.left}>
|
||||||
|
<Area
|
||||||
|
data={data}
|
||||||
|
x={(d) => xScaleTime(x(d))}
|
||||||
|
y0={(d) => yScale.range()[0]}
|
||||||
|
y1={(d) => yScale(y(d))}
|
||||||
|
strokeWidth={2}
|
||||||
|
stroke={"transparent"}
|
||||||
|
fill={"url(#linear)"}
|
||||||
|
curve={curveBasis}
|
||||||
|
/>
|
||||||
|
<LinePath
|
||||||
|
data={data}
|
||||||
|
x={(d) => xScaleTime(x(d))}
|
||||||
|
y={(d) => yScale(y(d))}
|
||||||
|
stroke={"url('#linear')"}
|
||||||
|
strokeWidth={2}
|
||||||
|
curve={curveBasis}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Group left={margin.left}>
|
||||||
|
<AxisLeft
|
||||||
|
top={margin.top}
|
||||||
|
left={0}
|
||||||
|
scale={yScale}
|
||||||
|
hideZero
|
||||||
|
numTicks={numTicksForHeight(height)}
|
||||||
|
label="Axis Left Label"
|
||||||
|
labelProps={{
|
||||||
|
fill: "#8e205f",
|
||||||
|
textAnchor: "middle",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
stroke="#1b1a1e"
|
||||||
|
tickStroke="#8e205f"
|
||||||
|
tickLabelProps={(value, index) => ({
|
||||||
|
fill: "#8e205f",
|
||||||
|
textAnchor: "end",
|
||||||
|
fontSize: 10,
|
||||||
|
dx: "-0.25em",
|
||||||
|
dy: "0.25em",
|
||||||
|
})}
|
||||||
|
tickComponent={({ formattedValue, ...tickProps }) => (
|
||||||
|
<text {...tickProps}>{formattedValue}</text>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
/>
|
||||||
|
<AxisBottom
|
||||||
|
top={height - margin.bottom}
|
||||||
|
left={0}
|
||||||
|
scale={xScaleTime}
|
||||||
|
numTicks={numTicksForWidth(width)}
|
||||||
|
label="Time"
|
||||||
|
>
|
||||||
|
{(axis) => {
|
||||||
|
const tickLabelSize = 10;
|
||||||
|
const tickRotate = 45;
|
||||||
|
const tickColor = "#8e205f";
|
||||||
|
const axisCenter = (axis.axisToPoint.x - axis.axisFromPoint.x) / 2;
|
||||||
|
return (
|
||||||
|
<g className="my-custom-bottom-axis">
|
||||||
|
{axis.ticks.map((tick, i) => {
|
||||||
|
const tickX = tick.to.x;
|
||||||
|
const tickY = tick.to.y + tickLabelSize + axis.tickLength;
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
key={`vx-tick-${tick.value}-${i}`}
|
||||||
|
className={"vx-axis-tick"}
|
||||||
|
>
|
||||||
|
<Line from={tick.from} to={tick.to} stroke={tickColor} />
|
||||||
|
<text
|
||||||
|
transform={`translate(${tickX}, ${tickY}) rotate(${tickRotate})`}
|
||||||
|
fontSize={tickLabelSize}
|
||||||
|
textAnchor="middle"
|
||||||
|
fill={tickColor}
|
||||||
|
>
|
||||||
|
{tick.formattedValue}
|
||||||
|
</text>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<text
|
||||||
|
textAnchor="middle"
|
||||||
|
transform={`translate(${axisCenter}, 50)`}
|
||||||
|
fontSize="8"
|
||||||
|
>
|
||||||
|
{axis.label}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</AxisBottom>
|
||||||
|
</Group>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue