From 09182ee58d80840882091445fd3b6498a57b37b6 Mon Sep 17 00:00:00 2001 From: Jordy van Zeeland Date: Wed, 2 Nov 2022 13:28:02 +0100 Subject: [PATCH] Redesign components in frontend (DRY) --- ras/api/views.py | 2 +- ras/frontend/package-lock.json | 121 +++++ ras/frontend/package.json | 1 + ras/frontend/src/App.js | 554 ++------------------- ras/frontend/src/components/Books.js | 43 ++ ras/frontend/src/components/Charts.js | 298 +++++++++++ ras/frontend/src/components/Countries.js | 60 +++ ras/frontend/src/components/Data.js | 52 ++ ras/frontend/src/components/Genres.js | 43 ++ ras/frontend/src/components/Pages.js | 82 +++ ras/frontend/static/css/style.css | 262 ++++++++++ ras/frontend/static/js/main.js | 2 +- ras/frontend/static/js/main.js.LICENSE.txt | 232 +++++++++ ras/frontend/templates/index.html | 247 +-------- 14 files changed, 1231 insertions(+), 768 deletions(-) create mode 100644 ras/frontend/src/components/Books.js create mode 100644 ras/frontend/src/components/Charts.js create mode 100644 ras/frontend/src/components/Countries.js create mode 100644 ras/frontend/src/components/Genres.js create mode 100644 ras/frontend/src/components/Pages.js create mode 100644 ras/frontend/static/css/style.css diff --git a/ras/api/views.py b/ras/api/views.py index 1c6d019..411d70c 100644 --- a/ras/api/views.py +++ b/ras/api/views.py @@ -83,7 +83,7 @@ def books_per_genre_per_month(request): # Filter array on genre and date booksPerMonth = df.groupby(['genre','readed'])['genre'].count().reset_index(name="count") - booksPerMonth = booksPerMonth.sort_values(by=['readed', 'count'], ascending=False) + booksPerMonth = booksPerMonth.sort_values(by=['genre', 'readed', 'count'], ascending=False) for index, row in booksPerMonth.iterrows(): data.append({ diff --git a/ras/frontend/package-lock.json b/ras/frontend/package-lock.json index 5856981..da317c8 100644 --- a/ras/frontend/package-lock.json +++ b/ras/frontend/package-lock.json @@ -1266,6 +1266,11 @@ "@emotion/utils": "^1.2.0" } }, + "@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, "@emotion/unitless": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", @@ -1826,6 +1831,23 @@ "@babel/helper-define-polyfill-provider": "^0.3.2" } }, + "babel-plugin-styled-components": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz", + "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-module-imports": "^7.16.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "lodash": "^4.17.11", + "picomatch": "^2.3.0" + } + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -1865,6 +1887,11 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" }, + "camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==" + }, "caniuse-lite": { "version": "1.0.30001390", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001390.tgz", @@ -2039,6 +2066,21 @@ "which": "^2.0.1" } }, + "css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==" + }, + "css-to-react-native": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz", + "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==", + "requires": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "csstype": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", @@ -2460,6 +2502,11 @@ "p-locate": "^4.1.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -2620,6 +2667,11 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -2629,6 +2681,11 @@ "find-up": "^4.0.0" } }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, "prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -2678,6 +2735,23 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-loader-spinner": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-5.3.4.tgz", + "integrity": "sha512-G2vw4ssX+RDZ/vfaeva06yfNqyFViv/u+tVZ3kFLy5TKNlNx2DbuwreBSpRtPespQA+VxinxUJsigwLwG9erOg==", + "requires": { + "react-is": "^18.2.0", + "styled-components": "^5.3.5", + "styled-tools": "^1.7.2" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, "react-router": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", @@ -2856,6 +2930,11 @@ "kind-of": "^6.0.2" } }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2887,6 +2966,48 @@ "source-map": "^0.6.0" } }, + "styled-components": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.6.tgz", + "integrity": "sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + }, + "dependencies": { + "@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "styled-tools": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/styled-tools/-/styled-tools-1.7.2.tgz", + "integrity": "sha512-IjLxzM20RMwAsx8M1QoRlCG/Kmq8lKzCGyospjtSXt/BTIIcvgTonaxQAsKnBrsZNwhpHzO9ADx5te0h76ILVg==" + }, "stylis": { "version": "4.0.13", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz", diff --git a/ras/frontend/package.json b/ras/frontend/package.json index c52d42b..cbb47f0 100644 --- a/ras/frontend/package.json +++ b/ras/frontend/package.json @@ -30,6 +30,7 @@ "datatables.net": "^1.12.1", "datatables.net-dt": "^1.12.1", "jquery": "^3.6.1", + "react-loader-spinner": "^5.3.4", "react-router-dom": "^6.3.0" } } diff --git a/ras/frontend/src/App.js b/ras/frontend/src/App.js index 86bd147..f0472fb 100644 --- a/ras/frontend/src/App.js +++ b/ras/frontend/src/App.js @@ -1,6 +1,12 @@ import React, { Component } from "react"; import Challenge from "./components/Challenge"; import BookStats from "./components/Stats"; +import { ColorRing } from 'react-loader-spinner' +import Countries from "./components/Countries"; +import Pages from "./components/Pages"; +import Genres from "./components/Genres"; +import Books from "./components/Books"; +import { getReadingYears } from "./components/Data.js"; export default class App extends Component { constructor(props) { @@ -8,485 +14,33 @@ export default class App extends Component { this.state = { year: new Date().getFullYear(), readingYears: [], - countries: [], - pagesStats: [], } - - this.yearsArray = []; - } - - getGenres() { - fetch('/api/books/genres', { - "method": "GET", - "headers": { - "year": this.state.year - } - }) - .then(response => response.json()) - .then(books => { - this.initChart(books, this.state.year); - }) - } - - getCountries(init) { - fetch('/api/books/countries', { - "method": "GET", - "headers": { - "year": this.state.year - } - }) - .then(response => response.json()) - .then(data => { - this.setState({ - countries: data - }) - - if (init == true) { - $('#DataTable').DataTable({ - paging: false, - ordering: false, - info: false, - searching: false - }) - } - }) - } - - getShortestLongestBook(currentyear) { - fetch('/api/books/pages/stats', { - "method": "GET", - "headers": { - "year": currentyear - } - }) - .then(response => response.json()) - .then(bookstats => { - this.setState({ - pagesStats: bookstats - }) - }) } changeYear(event) { - this.setState({ year: event.target.value }) - - fetch('/api/books/countries', { - "method": "GET", - "headers": { - "year": event.target.value - } - }) - .then(response => response.json()) - .then(data => { - this.setState({ - countries: data - }) - - this.getCountries(false); - }) - - var $this = this; - - this.getShortestLongestBook(event.target.value); - - fetch('/api/books/genres/count', { - "method": "GET", - "headers": { - "year": event.target.value - } - }) - .then(response => response.json()) - .then(data => { - this.initDoughnut(data); - }) - - fetch('/api/books/genres', { - "method": "GET", - "headers": { - "year": event.target.value - } - }) - .then(response => response.json()) - .then(books => { - this.initChart(books, event.target.value); - }) - } - - initHorBar(data) { - - var countries = []; - var counts = []; - - data.forEach((count) => { - if (!countries.includes(count.country)) { - countries.push(count.country) - } - - counts.push(count.count) - }) - - $("canvas#countryChart").remove(); - $("div.books-per-country").append(''); - - var ctx = document.getElementById("countryChart"); - new Chart(ctx, { - type: 'bar', - options: { - indexAxis: 'y', - plugins: { - legend: { - display: false - }, - }, - scales: { - x: { - ticks: { - beginAtZero: true, - color: "white", - }, - stacked: true, - }, - y: { - ticks: { - beginAtZero: true, - stepSize: 1, - color: "white", - }, - stacked: true - } - }, - }, - data: { - labels: countries, - datasets: [ - { - label: "Boeken", - data: counts, - backgroundColor: '#696ffc', - }] - } - }); - } - - initDoughnut(data) { - - var labels = []; - var counts = []; - - data.forEach((count) => { - if (!labels.includes(count.genre)) { - labels.push(count.genre) - } - - counts.push(count.count) - }) - - const legendMargin = { - id: 'legendMargin', - beforeInit(chart, legend, options) { - const fitValue = chart.legend.fit; - - chart.legend.fit = function fit() { - fitValue.bind(chart.legend)(); - return this.height += 30; - } - } - }; - - $("canvas#chartGenres").remove(); - $("div.genresPercent").append(''); - - var ctx = document.getElementById("chartGenres"); - var myChart = new Chart(ctx, { - type: 'pie', - data: { - labels: labels, - datasets: [{ - label: '# of Tomatoes', - data: counts, - backgroundColor: [ - '#8066ee', '#58c8d6', '#fe4c62', '#49b8fd', '#ffbe0e' - ], - borderWidth: 0, - borderColor: '#1f2940', - tooltip: { - callbacks: { - label: function (context) { - let label = context.label; - let value = context.formattedValue; - - if (!label) - label = 'Unknown' - - let sum = 0; - let dataArr = context.chart.data.datasets[0].data; - dataArr.map(data => { - sum += Number(data); - }); - - let percentage = (value * 100 / sum).toFixed(1) + '%'; - return label + ": " + percentage; - } - } - } - }] - }, - options: { - cutout: '80%', - responsive: true, - plugins: { - legend: { - position: 'top', - labels: { - padding: 20, - usePointStyle: true, - // This more specific font property overrides the global property - color: "##101010", - font: { - size: 14, - family: 'Source Sans Pro' - } - } - } - } - }, - plugins: [{ - id: 'legendMargin', - beforeInit(chart, legend, options) { - const fitValue = chart.legend.fit; - - chart.legend.fit = function fit() { - fitValue.bind(chart.legend)(); - return this.height += 30; - } - } - }, { - afterDraw: chart => { - var ctx = chart.ctx; - ctx.save(); - var image = new Image(); - image.src = 'https://www.iconsdb.com/icons/preview/gray/book-xxl.png'; - var imageSize = 80; - ctx.drawImage(image, chart.width / 2 - imageSize / 2, chart.height / 2 - imageSize / 6, imageSize, imageSize); - ctx.restore(); - } - }], - }); - } - - initChart(data, year) { - - /* - ---------------------------------- - Books per month per genre - ---------------------------------- - */ - - var genres = []; - - var colors = [ - // '#696ffc', '#7596fa', '#92adfe', '#abc0ff' - '#8066ee', '#58c8d6', '#fe4c62', '#49b8fd', '#ffbe0e' - ] - - var dataSet = []; - - data.forEach(book => { - if (!genres.includes(book.genre)) { - genres.push(book.genre) - } - }); - - if (genres && genres.length > 0) { - genres.forEach((genre, index) => { - var genreData = []; - - for (var i = 0; i < 12; i++) { - - genreData[i] = 0; - - if ((i + 1) < 10) { - var month = "0" + (i + 1); - } else { - month = (i + 1); - } - - for (var j = 0; j < data.length; j++) { - if (data && data[j] && data[j].readed == month + '-' + year) { - if (data[j].genre == genre) { - genreData[i] = data[j].count; - } - - } - } - } - - dataSet.push({ - label: genre, - data: genreData, - backgroundColor: colors[index], - order: 2 - }) - }) - } - - /* - ---------------------------------- - Stacked bar chart - ---------------------------------- - */ - - $("canvas#chart").remove(); - $("div.books-per-month").append(''); - - const legendMargin = { - id: 'legendMargin', - beforeInit(chart, legend, options) { - const fitValue = chart.legend.fit; - - chart.legend.fit = function fit() { - fitValue.bind(chart.legend)(); - return this.height += 30; - } - } - }; - - new Chart(document.getElementById('chart'), { - type: 'bar', - data: { - labels: ["Januari", "Februari", "Maart", "April", "Mei", "Juni", "Juli", "Augustus", "September", "Oktober", "November", "December"], - datasets: dataSet - }, - options: { - maintainAspectRatio: false, - responsive: true, - showTooltips: true, - legend: { - display: true, - labels: { - usePointStyle: true, - } - }, - interaction: { - mode: 'index' - }, - scales: { - x: { - ticks: { - beginAtZero: true, - color: "#101010", - fontFamily: "Source Sans Pro", - }, - stacked: true, - }, - y: { - ticks: { - beginAtZero: true, - stepSize: 1, - color: "#101010", - fontFamily: "Source Sans Pro", - }, - stacked: true - } - }, - plugins: { - legend: { - position: 'top', - labels: { - usePointStyle: true, - color: "#101010", - padding: 20, - font: { - size: 14, - family: 'Source Sans Pro', - } - } - } - }, - tooltips: { - bodyFont: 'Source Sans Pro' - } - }, - plugins: [legendMargin], - }); } componentDidMount() { - var $this = this; + setTimeout(() =>{ + document.getElementById("loading-overlay").style.display = "none"; + }, 1000); - var currentyear = this.state.year ? this.state.year : new Date().getFullYear(); - - fetch('/api/books/genres', { - "method": "GET", - "headers": { - "year": currentyear - } - }) - .then(response => response.json()) - .then(books => { - this.initChart(books, currentyear); + getReadingYears().then(data => { + this.setState({ + readingYears: data }) - - this.getShortestLongestBook(this.state.year); - - fetch('/api/books/genres/count', { - "method": "GET", - "headers": { - "year": this.state.year - } }) - .then(response => response.json()) - .then(data => { - this.initDoughnut(data); - }) - - this.getCountries(true); - - fetch('/api/books/years', { - "method": "GET", - }) - .then(response => response.json()) - .then(data => { - this.setState({ - readingYears: data - }) - }) } render() { var url = window.location.href.split("/"); - var ratingshort = ''; - var ratinglong = ''; - - - if (this.state.pagesStats.shortestbook) { - for (var i = 0; i < this.state.pagesStats.shortestbook.rating; i++) { - ratingshort += ""; - } - } - - if (document.getElementById("shortest_rating") !== null) { - document.getElementById('shortest_rating').innerHTML = ratingshort; - } - - if (this.state.pagesStats.longestbook) { - for (var i = 0; i < this.state.pagesStats.longestbook.rating; i++) { - ratinglong += ""; - } - } - - if (document.getElementById("longest_rating") !== null) { - document.getElementById('longest_rating').innerHTML = ratinglong; - } return ( - -
@@ -499,6 +53,21 @@ export default class App extends Component {
+
+
+ + Data wordt geladen... +
+
+

Dashboard

Leesanalyse van Jordy van Zeeland

@@ -509,16 +78,9 @@ export default class App extends Component {
- this.changeYear(event)}> {this.state.readingYears.map((year, i) => { - - if (year === this.state.year) { - var selected = 'selected' - } else { - selected = '' - } - - return () + return () })} @@ -535,66 +97,16 @@ export default class App extends Component {
-
Boeken per maand per genre
-
-
-
- Kortste boek - -
{this.state.pagesStats.shortestbook ? this.state.pagesStats.shortestbook.pages : ''} pagina's
-
{this.state.pagesStats.shortestbook ? this.state.pagesStats.shortestbook.name : ''} - {this.state.pagesStats.shortestbook ? this.state.pagesStats.shortestbook.author : ''}
-
-
-
- -
-
- Langste boek - -
{this.state.pagesStats.longestbook ? this.state.pagesStats.longestbook.pages : ''} pagina's
-
{this.state.pagesStats.longestbook ? this.state.pagesStats.longestbook.name : ''} - {this.state.pagesStats.longestbook ? this.state.pagesStats.longestbook.author : ''}
-
-
-
-
+ +
-
- Landen - - - - - - - - - - {this.state.countries.map((country, i) => { - - var code = country.code.toLowerCase(); - return ( - - - - - - - - ) - - })} - -
#LandBoeken
{i + 1} {country.country}{country.count}
-
-
Genres
+ +
-
- -
) diff --git a/ras/frontend/src/components/Books.js b/ras/frontend/src/components/Books.js new file mode 100644 index 0000000..333d62e --- /dev/null +++ b/ras/frontend/src/components/Books.js @@ -0,0 +1,43 @@ +import React, { Component } from 'react'; +import { getBooksPerYearPerGenres } from "./Data.js"; +import { initChart } from "./Charts.js"; + +export default class Books extends Component { + constructor(props) { + super(props); + this.state = { + books: [] + } + } + + getComponentData() { + getBooksPerYearPerGenres(this.props.year).then(books => { + this.setState({ + books: books + }) + + initChart(books, this.props.year); + }) + } + + componentDidMount() { + this.getComponentData(); + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.year !== this.props.year) { + this.getComponentData(); + } + } + + render() { + return ( + +
+ Boeken per maand per genre + +
+
+ ) + } +} \ No newline at end of file diff --git a/ras/frontend/src/components/Charts.js b/ras/frontend/src/components/Charts.js new file mode 100644 index 0000000..3242f76 --- /dev/null +++ b/ras/frontend/src/components/Charts.js @@ -0,0 +1,298 @@ +export const initChart = (data, year) => { + + /* + ---------------------------------- + Books per month per genre + ---------------------------------- + */ + + var genres = []; + + var colors = [ + // '#696ffc', '#7596fa', '#92adfe', '#abc0ff' + '#8066ee', '#58c8d6', '#fe4c62', '#49b8fd', '#ffbe0e' + ] + + var dataSet = []; + + data.forEach(book => { + if (!genres.includes(book.genre)) { + genres.push(book.genre) + } + }); + + if (genres && genres.length > 0) { + genres.forEach((genre, index) => { + var genreData = []; + + for (var i = 0; i < 12; i++) { + + genreData[i] = 0; + + if ((i + 1) < 10) { + var month = "0" + (i + 1); + } else { + month = (i + 1); + } + + for (var j = 0; j < data.length; j++) { + if (data && data[j] && data[j].readed == month + '-' + year) { + if (data[j].genre == genre) { + genreData[i] = data[j].count; + } + + } + } + } + + dataSet.push({ + label: genre, + data: genreData, + backgroundColor: colors[index], + order: 2 + }) + }) + } + + /* + ---------------------------------- + Stacked bar chart + ---------------------------------- + */ + + $("canvas#chart").remove(); + $("div.books-per-month").append(''); + + const legendMargin = { + id: 'legendMargin', + beforeInit(chart, legend, options) { + const fitValue = chart.legend.fit; + + chart.legend.fit = function fit() { + fitValue.bind(chart.legend)(); + return this.height += 30; + } + } + }; + + new Chart(document.getElementById('chart'), { + type: 'bar', + data: { + labels: ["Januari", "Februari", "Maart", "April", "Mei", "Juni", "Juli", "Augustus", "September", "Oktober", "November", "December"], + datasets: dataSet + }, + options: { + maintainAspectRatio: false, + responsive: true, + showTooltips: true, + legend: { + display: true, + labels: { + usePointStyle: true, + } + }, + interaction: { + mode: 'index' + }, + scales: { + x: { + ticks: { + beginAtZero: true, + color: "#101010", + fontFamily: "Source Sans Pro", + }, + stacked: true, + }, + y: { + ticks: { + beginAtZero: true, + stepSize: 1, + color: "#101010", + fontFamily: "Source Sans Pro", + }, + stacked: true + } + }, + plugins: { + legend: { + position: 'top', + labels: { + usePointStyle: true, + color: "#101010", + padding: 20, + font: { + size: 14, + family: 'Source Sans Pro', + } + } + } + }, + tooltips: { + bodyFont: 'Source Sans Pro' + } + }, + plugins: [legendMargin], + }); +} + +export const initDoughnut = (data) => { + + var labels = []; + var counts = []; + + data.forEach((count) => { + if (!labels.includes(count.genre)) { + labels.push(count.genre) + } + + counts.push(count.count) + }) + + const legendMargin = { + id: 'legendMargin', + beforeInit(chart, legend, options) { + const fitValue = chart.legend.fit; + + chart.legend.fit = function fit() { + fitValue.bind(chart.legend)(); + return this.height += 30; + } + } + }; + + $("canvas#chartGenres").remove(); + $("div.genresPercent").append(''); + + var ctx = document.getElementById("chartGenres"); + var myChart = new Chart(ctx, { + type: 'pie', + data: { + labels: labels, + datasets: [{ + label: '# of Tomatoes', + data: counts, + backgroundColor: [ + '#8066ee', '#58c8d6', '#fe4c62', '#49b8fd', '#ffbe0e' + ], + borderWidth: 0, + borderColor: '#1f2940', + tooltip: { + callbacks: { + label: function (context) { + let label = context.label; + let value = context.formattedValue; + + if (!label) + label = 'Unknown' + + let sum = 0; + let dataArr = context.chart.data.datasets[0].data; + dataArr.map(data => { + sum += Number(data); + }); + + let percentage = (value * 100 / sum).toFixed(1) + '%'; + return label + ": " + percentage; + } + } + } + }] + }, + options: { + cutout: '80%', + responsive: true, + plugins: { + legend: { + position: 'top', + labels: { + padding: 20, + usePointStyle: true, + // This more specific font property overrides the global property + color: "##101010", + font: { + size: 14, + family: 'Source Sans Pro' + } + } + } + } + }, + plugins: [{ + id: 'legendMargin', + beforeInit(chart, legend, options) { + const fitValue = chart.legend.fit; + + chart.legend.fit = function fit() { + fitValue.bind(chart.legend)(); + return this.height += 30; + } + } + }, { + afterDraw: chart => { + var ctx = chart.ctx; + ctx.save(); + var image = new Image(); + image.src = 'https://www.iconsdb.com/icons/preview/gray/book-xxl.png'; + var imageSize = 80; + ctx.drawImage(image, chart.width / 2 - imageSize / 2, chart.height / 2 - imageSize / 6, imageSize, imageSize); + ctx.restore(); + } + }], + }); +} + +export const initHorBar = (data) => { + + var countries = []; + var counts = []; + + data.forEach((count) => { + if (!countries.includes(count.country)) { + countries.push(count.country) + } + + counts.push(count.count) + }) + + $("canvas#countryChart").remove(); + $("div.books-per-country").append(''); + + var ctx = document.getElementById("countryChart"); + new Chart(ctx, { + type: 'bar', + options: { + indexAxis: 'y', + plugins: { + legend: { + display: false + }, + }, + scales: { + x: { + ticks: { + beginAtZero: true, + color: "white", + }, + stacked: true, + }, + y: { + ticks: { + beginAtZero: true, + stepSize: 1, + color: "white", + }, + stacked: true + } + }, + }, + data: { + labels: countries, + datasets: [ + { + label: "Boeken", + data: counts, + backgroundColor: '#696ffc', + }] + } + }); +} \ No newline at end of file diff --git a/ras/frontend/src/components/Countries.js b/ras/frontend/src/components/Countries.js new file mode 100644 index 0000000..c4a0dc3 --- /dev/null +++ b/ras/frontend/src/components/Countries.js @@ -0,0 +1,60 @@ +import React, { Component } from 'react'; +import { getCountries } from "./Data.js"; + +export default class Countries extends Component { + constructor(props) { + super(props); + this.state = { + countries: [] + } + } + + getComponentData() { + getCountries(this.props.year).then(countries => { + this.setState({ + countries: countries + }) + }) + } + + componentDidMount() { + this.getComponentData(); + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.year !== this.props.year) { + this.getComponentData(); + } + } + + render() { + return ( + +
+ Landen + + + + + + + + + + {this.state.countries.map((country, i) => { + var code = country.code.toLowerCase(); + return ( + + + + + + ) + })} + +
#LandBoeken
{i + 1} {country.country}{country.count}
+
+
+ ) + } +} \ No newline at end of file diff --git a/ras/frontend/src/components/Data.js b/ras/frontend/src/components/Data.js index 676ca3b..adf3ac4 100644 --- a/ras/frontend/src/components/Data.js +++ b/ras/frontend/src/components/Data.js @@ -32,4 +32,56 @@ export const getReadingYears = () => { .then(data => { return data }) +} + +export const getCountries = (year) => { + return fetch('/api/books/countries', { + "method": "GET", + "headers": { + "year": year + } + }) + .then(response => response.json()) + .then(data => { + return data; + }) +} + +export const getShortestLongestBook = (year) => { + return fetch('/api/books/pages/stats', { + "method": "GET", + "headers": { + "year": year + } + }) + .then(response => response.json()) + .then(data => { + return data; + }) +} + +export const getBooksPerYearPerGenres = (year) => { + return fetch('/api/books/genres', { + "method": "GET", + "headers": { + "year": year + } + }) + .then(response => response.json()) + .then(data => { + return data; + }) +} + +export const getGenresCount = (year) => { + return fetch('/api/books/genres/count', { + "method": "GET", + "headers": { + "year": year + } + }) + .then(response => response.json()) + .then(data => { + return data; + }) } \ No newline at end of file diff --git a/ras/frontend/src/components/Genres.js b/ras/frontend/src/components/Genres.js new file mode 100644 index 0000000..33e99f0 --- /dev/null +++ b/ras/frontend/src/components/Genres.js @@ -0,0 +1,43 @@ +import React, { Component } from 'react'; +import { getGenresCount } from "./Data.js"; +import { initDoughnut } from "./Charts.js"; + +export default class Genres extends Component { + constructor(props) { + super(props); + this.state = { + genres: [] + } + } + + getComponentData() { + getGenresCount(this.props.year).then(genres => { + this.setState({ + genres: genres + }) + + initDoughnut(this.state.genres, this.props.year); + }) + } + + componentDidMount() { + this.getComponentData(); + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.year !== this.props.year) { + this.getComponentData(); + } + } + + render() { + return ( + +
+ Genres + +
+
+ ) + } +} \ No newline at end of file diff --git a/ras/frontend/src/components/Pages.js b/ras/frontend/src/components/Pages.js new file mode 100644 index 0000000..5d85511 --- /dev/null +++ b/ras/frontend/src/components/Pages.js @@ -0,0 +1,82 @@ +import React, { Component } from 'react'; +import { getShortestLongestBook } from "./Data.js"; + +export default class Pages extends Component { + constructor(props) { + super(props); + this.state = { + pagesStats: [] + } + } + + getComponentData() { + getShortestLongestBook(this.props.year).then(bookstats => { + this.setState({ + pagesStats: bookstats + }) + }) + } + + componentDidMount() { + this.getComponentData(); + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.year !== this.props.year) { + this.getComponentData(); + } + } + + render() { + + var ratingshort = ''; + var ratinglong = ''; + + + if (this.state.pagesStats.shortestbook) { + for (var i = 0; i < this.state.pagesStats.shortestbook.rating; i++) { + ratingshort += ""; + } + } + + if (document.getElementById("shortest_rating") !== null) { + document.getElementById('shortest_rating').innerHTML = ratingshort; + } + + if (this.state.pagesStats.longestbook) { + for (var i = 0; i < this.state.pagesStats.longestbook.rating; i++) { + ratinglong += ""; + } + } + + if (document.getElementById("longest_rating") !== null) { + document.getElementById('longest_rating').innerHTML = ratinglong; + } + + return ( + +
+
+
+ Kortste boek + +
{this.state.pagesStats.shortestbook ? this.state.pagesStats.shortestbook.pages : ''} pagina's
+
{this.state.pagesStats.shortestbook ? this.state.pagesStats.shortestbook.name : ''} - {this.state.pagesStats.shortestbook ? this.state.pagesStats.shortestbook.author : ''}
+
+
+
+ +
+
+ Langste boek + +
{this.state.pagesStats.longestbook ? this.state.pagesStats.longestbook.pages : ''} pagina's
+
{this.state.pagesStats.longestbook ? this.state.pagesStats.longestbook.name : ''} - {this.state.pagesStats.longestbook ? this.state.pagesStats.longestbook.author : ''}
+
+
+
+
+
+ ) + } +} \ No newline at end of file diff --git a/ras/frontend/static/css/style.css b/ras/frontend/static/css/style.css new file mode 100644 index 0000000..550d3d1 --- /dev/null +++ b/ras/frontend/static/css/style.css @@ -0,0 +1,262 @@ +html, body{ + background:#f8f8fa; + margin:0; + padding:0; + font-family: 'Source Sans Pro', sans-serif; + } + + h1{ + font-size: 30px; + font-weight: 500; + padding: 0 10px 0px 10px; + margin-bottom: 5px; + } + + h2{ + padding-left: 10px; + font-weight: 400; + font-size: 18px; + color: #a7adbd; + display: block; + margin-bottom: 50px; + } + + + .content{ + padding: 50px 50px 50px 110px; + } + + .filter{ + width:100%; + background:#1f2940; + padding: 20px 0; + } + + .books-per-month{ + height:700px; + } + + .books-per-month canvas{ + height:600px !important; + } + + .books-per-month, .genresPercent, .books-per-country, .book{ + background: #ffffff; + padding: 20px; + box-shadow: 0 2px 0px 1px rgb(0 0 0 / 3%); + margin-bottom: 20px; + border-radius: 10px; + } + + .book .book-icon{ + font-size: 60px; + float: left; + margin-right: 20px; + margin-bottom: 40px; + color: #808080; + } + + .book_rating{ + margin-top: 5px; + } + + .book_rating i{ + font-family: "Font Awesome 5 Free"; + color: #ffbe0e; + } + + .book .book_pages{ + font-size: 18px; + font-weight: 600; + } + + .book .book_title_author{ + font-size: 16px; + color: #808080; + } + + .sidebar{ + background: #363a53; + width: 70px; + height: 100vh; + position: fixed; + padding-top: 10px; + } + + .sidebar .menu-item{ + text-align: center; + padding: 15px 0; + } + + .sidebar .menu-item i{ + font-size: 25px; + color: #727794; + } + + .sidebar .menu-item.selected i{ + color:#fff; + } + + .sidebar svg{ + color:#ffffff; + } + + .books-stats{ + margin:20px 0; + } + + .books-icon{ + background:#000; + padding: 50px; + } + + .books-stats .stat-block, .stat-block{ + background: #ffffff; + box-shadow: 0 2px 0px 1px rgb(0 0 0 / 3%); + padding: 15px 5px; + color: #101010; + text-align: center; + border-radius: 10px; + } + + .books-stats .col-md-2:nth-child(1) i{ + background: #f8f5fc; + color: #8066ee; + } + .books-stats .col-md-2:nth-child(2) i{ + background: #f1fcf8; + color: #58c8d6; + } + .books-stats .col-md-2:nth-child(3) i{ + background: #fff5f6; + color: #fe4c62; + } + .books-stats .col-md-2:nth-child(4) i{ + background: #f2f9ff; + color: #49b8fd; + } + .books-stats .col-md-2:nth-child(5) i{ + background: #fffaee; + color: #ffbe0e; + } + .books-stats .col-md-2:nth-child(6) i{ + background: #f8f5fc; + color: #8066ee; + } + + .books-stats .stat-block i{ + font-weight: 900; + font-size: 25px; + border-radius: 50%; + padding: 17px; + width: 65px; + height: 65px; + line-height: 30px; + text-align: center; + background: #696ffc; + /* box-shadow: 2px 2px 0px 0px rgba(0, 0, 0, 0.3); */ + } + + .books-stats .stat-block .stats-number, .stats-number{ + font-weight: 600; + display: inline-block; + margin-left: 10px; + font-size: 20px; + margin-right: 10px; + } + + .books-stats .stat-block .stats-label, .stats-label{ + color: #a7adbd; + font-weight: 400; + font-size: 20px; + } + + .yearselector{ + display: inline-block; + width: auto; + background: #ffffff; + border: none; + color: #101010; + font-weight: 600; + } + + .container-fluid{ + margin-bottom:20px !important; + } + + .table{ + border-bottom: none !important; + } + + .table td{ + color: #101010; + border-bottom:none !important; + padding: 10px 10px !important; + } + + .table td img{ + margin-right:5px; + } + + #DataTable thead{ + display:none !important; + } + + span.block_name{ + color: #101010; + font-weight: 600; + border-bottom: solid 1px #f5f6fa; + width: 100%; + display: block; + padding-bottom: 10px; + margin-bottom: 10px; + } + + .progress{ + background: #f8f8fa; + height: 50px; + border: solid 2px #efefef; + padding: 5px; + border-radius: 0; + margin: 0 15px 15px 15px; + position: relative; + overflow: visible; + } + + .progress-bar{ + background-color: #8066ee; + position: relative; + overflow: visible; + border-right: solid 2px #333; + } + + .progress-bar-number{ + position: absolute; + right: 0; + background: #333; + border-radius: 50%; + padding: 10px; + top: -20px; + right: -20px; + } + + .loading-screen-overlay{ + background: #f8f8fa; + width:100%; + height:100vh; + position: fixed; + z-index: 1; + top:0; + } + + .loading-screen{ + width: 100%; + position: fixed; + background: #f8f8fa; + z-index: 1; + text-align: center; + top: 50%; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + } \ No newline at end of file diff --git a/ras/frontend/static/js/main.js b/ras/frontend/static/js/main.js index 4b89348..b08f84a 100644 --- a/ras/frontend/static/js/main.js +++ b/ras/frontend/static/js/main.js @@ -1,2 +1,2 @@ /*! For license information please see main.js.LICENSE.txt */ -(()=>{"use strict";var __webpack_modules__={"./src/App.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ "default": () => (/* binding */ App)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "./node_modules/react/index.js");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _components_Challenge__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./components/Challenge */ "./src/components/Challenge.js");\n/* harmony import */ var _components_Stats__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./components/Stats */ "./src/components/Stats.js");\n\n\n\nclass App extends react__WEBPACK_IMPORTED_MODULE_0__.Component {\n constructor(props) {\n super(props);\n this.state = {\n year: new Date().getFullYear(),\n readingYears: [],\n countries: [],\n pagesStats: []\n };\n this.yearsArray = [];\n }\n\n getGenres() {\n fetch(\'/api/books/genres\', {\n "method": "GET",\n "headers": {\n "year": this.state.year\n }\n }).then(response => response.json()).then(books => {\n this.initChart(books, this.state.year);\n });\n }\n\n getCountries(init) {\n fetch(\'/api/books/countries\', {\n "method": "GET",\n "headers": {\n "year": this.state.year\n }\n }).then(response => response.json()).then(data => {\n this.setState({\n countries: data\n });\n\n if (init == true) {\n $(\'#DataTable\').DataTable({\n paging: false,\n ordering: false,\n info: false,\n searching: false\n });\n }\n });\n }\n\n getShortestLongestBook(currentyear) {\n fetch(\'/api/books/pages/stats\', {\n "method": "GET",\n "headers": {\n "year": currentyear\n }\n }).then(response => response.json()).then(bookstats => {\n this.setState({\n pagesStats: bookstats\n });\n });\n }\n\n changeYear(event) {\n this.setState({\n year: event.target.value\n });\n fetch(\'/api/books/countries\', {\n "method": "GET",\n "headers": {\n "year": event.target.value\n }\n }).then(response => response.json()).then(data => {\n this.setState({\n countries: data\n });\n this.getCountries(false);\n });\n var $this = this;\n this.getShortestLongestBook(event.target.value);\n fetch(\'/api/books/genres/count\', {\n "method": "GET",\n "headers": {\n "year": event.target.value\n }\n }).then(response => response.json()).then(data => {\n this.initDoughnut(data);\n });\n fetch(\'/api/books/genres\', {\n "method": "GET",\n "headers": {\n "year": event.target.value\n }\n }).then(response => response.json()).then(books => {\n this.initChart(books, event.target.value);\n });\n }\n\n initHorBar(data) {\n var countries = [];\n var counts = [];\n data.forEach(count => {\n if (!countries.includes(count.country)) {\n countries.push(count.country);\n }\n\n counts.push(count.count);\n });\n $("canvas#countryChart").remove();\n $("div.books-per-country").append(\'\');\n var ctx = document.getElementById("countryChart");\n new Chart(ctx, {\n type: \'bar\',\n options: {\n indexAxis: \'y\',\n plugins: {\n legend: {\n display: false\n }\n },\n scales: {\n x: {\n ticks: {\n beginAtZero: true,\n color: "white"\n },\n stacked: true\n },\n y: {\n ticks: {\n beginAtZero: true,\n stepSize: 1,\n color: "white"\n },\n stacked: true\n }\n }\n },\n data: {\n labels: countries,\n datasets: [{\n label: "Boeken",\n data: counts,\n backgroundColor: \'#696ffc\'\n }]\n }\n });\n }\n\n initDoughnut(data) {\n var labels = [];\n var counts = [];\n data.forEach(count => {\n if (!labels.includes(count.genre)) {\n labels.push(count.genre);\n }\n\n counts.push(count.count);\n });\n const legendMargin = {\n id: \'legendMargin\',\n\n beforeInit(chart, legend, options) {\n const fitValue = chart.legend.fit;\n\n chart.legend.fit = function fit() {\n fitValue.bind(chart.legend)();\n return this.height += 30;\n };\n }\n\n };\n $("canvas#chartGenres").remove();\n $("div.genresPercent").append(\'\');\n var ctx = document.getElementById("chartGenres");\n var myChart = new Chart(ctx, {\n type: \'pie\',\n data: {\n labels: labels,\n datasets: [{\n label: \'# of Tomatoes\',\n data: counts,\n backgroundColor: [\'#8066ee\', \'#58c8d6\', \'#fe4c62\', \'#49b8fd\', \'#ffbe0e\'],\n borderWidth: 0,\n borderColor: \'#1f2940\',\n tooltip: {\n callbacks: {\n label: function (context) {\n let label = context.label;\n let value = context.formattedValue;\n if (!label) label = \'Unknown\';\n let sum = 0;\n let dataArr = context.chart.data.datasets[0].data;\n dataArr.map(data => {\n sum += Number(data);\n });\n let percentage = (value * 100 / sum).toFixed(1) + \'%\';\n return label + ": " + percentage;\n }\n }\n }\n }]\n },\n options: {\n cutout: \'80%\',\n responsive: true,\n plugins: {\n legend: {\n position: \'top\',\n labels: {\n padding: 20,\n usePointStyle: true,\n // This more specific font property overrides the global property\n color: "##101010",\n font: {\n size: 14,\n family: \'Source Sans Pro\'\n }\n }\n }\n }\n },\n plugins: [{\n id: \'legendMargin\',\n\n beforeInit(chart, legend, options) {\n const fitValue = chart.legend.fit;\n\n chart.legend.fit = function fit() {\n fitValue.bind(chart.legend)();\n return this.height += 30;\n };\n }\n\n }, {\n afterDraw: chart => {\n var ctx = chart.ctx;\n ctx.save();\n var image = new Image();\n image.src = \'https://www.iconsdb.com/icons/preview/gray/book-xxl.png\';\n var imageSize = 80;\n ctx.drawImage(image, chart.width / 2 - imageSize / 2, chart.height / 2 - imageSize / 6, imageSize, imageSize);\n ctx.restore();\n }\n }]\n });\n }\n\n initChart(data, year) {\n /*\r\n ----------------------------------\r\n Books per month per genre\r\n ----------------------------------\r\n */\n var genres = [];\n var colors = [// \'#696ffc\', \'#7596fa\', \'#92adfe\', \'#abc0ff\'\n \'#8066ee\', \'#58c8d6\', \'#fe4c62\', \'#49b8fd\', \'#ffbe0e\'];\n var dataSet = [];\n data.forEach(book => {\n if (!genres.includes(book.genre)) {\n genres.push(book.genre);\n }\n });\n\n if (genres && genres.length > 0) {\n genres.forEach((genre, index) => {\n var genreData = [];\n\n for (var i = 0; i < 12; i++) {\n genreData[i] = 0;\n\n if (i + 1 < 10) {\n var month = "0" + (i + 1);\n } else {\n month = i + 1;\n }\n\n for (var j = 0; j < data.length; j++) {\n if (data && data[j] && data[j].readed == month + \'-\' + year) {\n if (data[j].genre == genre) {\n genreData[i] = data[j].count;\n }\n }\n }\n }\n\n dataSet.push({\n label: genre,\n data: genreData,\n backgroundColor: colors[index],\n order: 2\n });\n });\n }\n /*\r\n ----------------------------------\r\n Stacked bar chart\r\n ----------------------------------\r\n */\n\n\n $("canvas#chart").remove();\n $("div.books-per-month").append(\'\');\n const legendMargin = {\n id: \'legendMargin\',\n\n beforeInit(chart, legend, options) {\n const fitValue = chart.legend.fit;\n\n chart.legend.fit = function fit() {\n fitValue.bind(chart.legend)();\n return this.height += 30;\n };\n }\n\n };\n new Chart(document.getElementById(\'chart\'), {\n type: \'bar\',\n data: {\n labels: ["Januari", "Februari", "Maart", "April", "Mei", "Juni", "Juli", "Augustus", "September", "Oktober", "November", "December"],\n datasets: dataSet\n },\n options: {\n maintainAspectRatio: false,\n responsive: true,\n showTooltips: true,\n legend: {\n display: true,\n labels: {\n usePointStyle: true\n }\n },\n interaction: {\n mode: \'index\'\n },\n scales: {\n x: {\n ticks: {\n beginAtZero: true,\n color: "#101010",\n fontFamily: "Source Sans Pro"\n },\n stacked: true\n },\n y: {\n ticks: {\n beginAtZero: true,\n stepSize: 1,\n color: "#101010",\n fontFamily: "Source Sans Pro"\n },\n stacked: true\n }\n },\n plugins: {\n legend: {\n position: \'top\',\n labels: {\n usePointStyle: true,\n color: "#101010",\n padding: 20,\n font: {\n size: 14,\n family: \'Source Sans Pro\'\n }\n }\n }\n },\n tooltips: {\n bodyFont: \'Source Sans Pro\'\n }\n },\n plugins: [legendMargin]\n });\n }\n\n componentDidMount() {\n var $this = this;\n var currentyear = this.state.year ? this.state.year : new Date().getFullYear();\n fetch(\'/api/books/genres\', {\n "method": "GET",\n "headers": {\n "year": currentyear\n }\n }).then(response => response.json()).then(books => {\n this.initChart(books, currentyear);\n });\n this.getShortestLongestBook(this.state.year);\n fetch(\'/api/books/genres/count\', {\n "method": "GET",\n "headers": {\n "year": this.state.year\n }\n }).then(response => response.json()).then(data => {\n this.initDoughnut(data);\n });\n this.getCountries(true);\n fetch(\'/api/books/years\', {\n "method": "GET"\n }).then(response => response.json()).then(data => {\n this.setState({\n readingYears: data\n });\n });\n }\n\n render() {\n var url = window.location.href.split("/");\n var ratingshort = \'\';\n var ratinglong = \'\';\n\n if (this.state.pagesStats.shortestbook) {\n for (var i = 0; i < this.state.pagesStats.shortestbook.rating; i++) {\n ratingshort += "";\n }\n }\n\n if (document.getElementById("shortest_rating") !== null) {\n document.getElementById(\'shortest_rating\').innerHTML = ratingshort;\n }\n\n if (this.state.pagesStats.longestbook) {\n for (var i = 0; i < this.state.pagesStats.longestbook.rating; i++) {\n ratinglong += "";\n }\n }\n\n if (document.getElementById("longest_rating") !== null) {\n document.getElementById(\'longest_rating\').innerHTML = ratinglong;\n }\n\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "sidebar"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: `menu-item ${url && url[3] == "" ? \'selected\' : \'\'}`\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {\n className: "fa fa-chart-bar"\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: `menu-item ${url && url[3] == "books" ? \'selected\' : \'\'}`\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {\n className: "fa fa-book"\n }))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "content"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h1", null, "Dashboard"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h2", null, "Leesanalyse van Jordy van Zeeland"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "books-stats"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "container-fluid"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "row"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "col-md-2"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "stat-block"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {\n className: "fa fa-calendar"\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "stats-number"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("select", {\n className: "yearselector",\n defaultValue: this.state.year,\n onChange: event => this.changeYear(event)\n }, this.state.readingYears.map((year, i) => {\n if (year === this.state.year) {\n var selected = \'selected\';\n } else {\n selected = \'\';\n }\n\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", {\n key: i,\n selected: selected,\n value: year\n }, year);\n }))))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_components_Stats__WEBPACK_IMPORTED_MODULE_2__["default"], {\n year: this.state.year\n })))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_components_Challenge__WEBPACK_IMPORTED_MODULE_1__["default"], {\n year: this.state.year\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "container-fluid"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "row"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "col-md-9"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "books-per-month"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "block_name"\n }, "Boeken per maand per genre"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("canvas", {\n id: "chart"\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "row"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "col-md-6"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "book shortest"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "block_name"\n }, "Kortste boek"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {\n className: "fa fa-book book-icon"\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "book_pages"\n }, this.state.pagesStats.shortestbook ? this.state.pagesStats.shortestbook.pages : \'\', " pagina\'s"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "book_title_author"\n }, this.state.pagesStats.shortestbook ? this.state.pagesStats.shortestbook.name : \'\', " - ", this.state.pagesStats.shortestbook ? this.state.pagesStats.shortestbook.author : \'\'), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n id: "shortest_rating",\n className: "book_rating"\n }))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "col-md-6"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "book longest"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "block_name"\n }, "Langste boek"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {\n className: "fa fa-book book-icon"\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "book_pages"\n }, this.state.pagesStats.longestbook ? this.state.pagesStats.longestbook.pages : \'\', " pagina\'s"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "book_title_author"\n }, this.state.pagesStats.longestbook ? this.state.pagesStats.longestbook.name : \'\', " - ", this.state.pagesStats.longestbook ? this.state.pagesStats.longestbook.author : \'\'), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n id: "longest_rating",\n className: "book_rating"\n }))))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "col-md-3"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "books-per-country"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "block_name"\n }, "Landen"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("table", {\n id: "DataTable",\n className: "showHead table responsive nowrap",\n width: "100%"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("thead", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("tr", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("th", null, "#"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("th", null, "Land"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("th", null, "Boeken"))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("tbody", null, this.state.countries.map((country, i) => {\n var code = country.code.toLowerCase();\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("tr", {\n key: "{i}"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("td", null, i + 1), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("td", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("img", {\n src: `https://flagcdn.com/32x24/${code}.png`\n }), " ", country.country), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("td", null, country.count)));\n })))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "genresPercent"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "block_name"\n }, "Genres"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("canvas", {\n id: "chartGenres"\n })))))));\n }\n\n}\n\n//# sourceURL=webpack://frontend/./src/App.js?')},"./src/components/Challenge.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ "default": () => (/* binding */ Challenge)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "./node_modules/react/index.js");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _Data_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./Data.js */ "./src/components/Data.js");\n\n\nclass Challenge extends react__WEBPACK_IMPORTED_MODULE_0__.Component {\n constructor(props) {\n super(props);\n this.state = {\n readingYears: [],\n challenge: 0\n };\n }\n\n getComponentData() {\n var $this = this;\n (0,_Data_js__WEBPACK_IMPORTED_MODULE_1__.getStats)(this.props.year).then(data => {\n $this.setState({\n totalbooks: data.totalbooks\n });\n });\n (0,_Data_js__WEBPACK_IMPORTED_MODULE_1__.getChallenge)(this.props.year).then(data => {\n this.setState({\n challenge: data && data.length > 0 ? data[0].nrofbooks : 0\n });\n });\n }\n\n componentDidMount() {\n this.getComponentData();\n }\n\n componentDidUpdate(prevProps, prevState) {\n if (prevProps.year !== this.props.year) {\n this.getComponentData();\n }\n }\n\n render() {\n var challengePercentage = this.state.totalbooks / this.state.challenge * 100;\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, this.state.challenge && this.state.challenge !== 0 ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "container-fluid"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "row"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "col-md-12"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "stat-block"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "block_name"\n }, "Book Challenge"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "progress"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "progress-bar progress-bar-striped",\n role: "progressbar",\n style: {\n width: challengePercentage + \'%\'\n },\n "aria-valuenow": challengePercentage,\n "aria-valuemin": "0",\n "aria-valuemax": "100"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "progress-bar-number"\n }, challengePercentage, "%"))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "stats-number"\n }, this.state.totalbooks), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "stats-label"\n }, "van de"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "stats-number"\n }, this.state.challenge), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "stats-label"\n }, "boeken gelezen"))))) : \'\');\n }\n\n}\n\n//# sourceURL=webpack://frontend/./src/components/Challenge.js?')},"./src/components/Data.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ "getChallenge": () => (/* binding */ getChallenge),\n/* harmony export */ "getReadingYears": () => (/* binding */ getReadingYears),\n/* harmony export */ "getStats": () => (/* binding */ getStats)\n/* harmony export */ });\nconst getStats = year => {\n return fetch(\'/api/books/stats\', {\n "method": "GET",\n "headers": {\n "year": year\n }\n }).then(response => response.json()).then(data => {\n return data;\n });\n};\nconst getChallenge = year => {\n return fetch(\'/api/books/challenge\', {\n "method": "GET",\n "headers": {\n "year": year\n }\n }).then(response => response.json()).then(data => {\n return data;\n });\n};\nconst getReadingYears = () => {\n return fetch(\'/api/books/years\', {\n "method": "GET"\n }).then(response => response.json()).then(data => {\n return data;\n });\n};\n\n//# sourceURL=webpack://frontend/./src/components/Data.js?')},"./src/components/Stats.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ "default": () => (/* binding */ BookStats)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "./node_modules/react/index.js");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _Data_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./Data.js */ "./src/components/Data.js");\n\n\nclass BookStats extends react__WEBPACK_IMPORTED_MODULE_0__.Component {\n constructor(props) {\n super(props);\n this.state = {\n readingYears: [],\n totalbooks: 0,\n totalpages: 0,\n totalauthors: 0,\n totalcountries: 0,\n totalgenres: 0\n };\n }\n\n getComponentData() {\n var $this = this;\n (0,_Data_js__WEBPACK_IMPORTED_MODULE_1__.getStats)(this.props.year).then(data => {\n $this.setState({\n totalbooks: data.totalbooks,\n totalpages: data.totalpages,\n totalauthors: data.totalauthors,\n totalcountries: data.totalcountries,\n totalgenres: data.totalgenres\n });\n });\n (0,_Data_js__WEBPACK_IMPORTED_MODULE_1__.getReadingYears)().then(data => {\n this.setState({\n readingYears: data\n });\n });\n }\n\n componentDidMount() {\n this.getComponentData();\n }\n\n componentDidUpdate(prevProps, prevState) {\n if (prevProps.year !== this.props.year) {\n this.getComponentData();\n }\n }\n\n render() {\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "col-md-2"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "stat-block"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {\n className: "fa fa-book"\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "stats-number"\n }, this.state.totalbooks), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "stats-label"\n }, "Boeken"))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "col-md-2"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "stat-block"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {\n className: "fa fa-book-open"\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "stats-number"\n }, this.state.totalpages), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "stats-label"\n }, "Bladzijdes"))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "col-md-2"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "stat-block"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {\n className: "fa fa-pen"\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "stats-number"\n }, this.state.totalauthors), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "stats-label"\n }, "Schrijvers"))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "col-md-2"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "stat-block"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {\n className: "fa fa-book"\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "stats-number"\n }, this.state.totalgenres), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "stats-label"\n }, "Genres"))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "col-md-2"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {\n className: "stat-block"\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {\n className: "fa fa-globe"\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "stats-number"\n }, this.state.totalcountries), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {\n className: "stats-label"\n }, "Landen"))));\n }\n\n}\n\n//# sourceURL=webpack://frontend/./src/components/Stats.js?')},"./src/index.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{eval('__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "./node_modules/react/index.js");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-dom */ "./node_modules/react-dom/index.js");\n/* harmony import */ var _App__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./App */ "./src/App.js");\n\n\n\nreact_dom__WEBPACK_IMPORTED_MODULE_1__.render( /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_App__WEBPACK_IMPORTED_MODULE_2__["default"], null), document.getElementById(\'app\'));\n\n//# sourceURL=webpack://frontend/./src/index.js?')},"./node_modules/react-dom/cjs/react-dom.development.js":(__unused_webpack_module,exports,__webpack_require__)=>{eval("/**\n * @license React\n * react-dom.development.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\n\nif (true) {\n (function() {\n\n 'use strict';\n\n/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */\nif (\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart ===\n 'function'\n) {\n __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());\n}\n var React = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\nvar Scheduler = __webpack_require__(/*! scheduler */ \"./node_modules/scheduler/index.js\");\n\nvar ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n\nvar suppressWarning = false;\nfunction setSuppressWarning(newSuppressWarning) {\n {\n suppressWarning = newSuppressWarning;\n }\n} // In DEV, calls to console.warn and console.error get replaced\n// by calls to these methods by a Babel plugin.\n//\n// In PROD (or in packages without access to React internals),\n// they are left as they are instead.\n\nfunction warn(format) {\n {\n if (!suppressWarning) {\n for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n args[_key - 1] = arguments[_key];\n }\n\n printWarning('warn', format, args);\n }\n }\n}\nfunction error(format) {\n {\n if (!suppressWarning) {\n for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {\n args[_key2 - 1] = arguments[_key2];\n }\n\n printWarning('error', format, args);\n }\n }\n}\n\nfunction printWarning(level, format, args) {\n // When changing this logic, you might want to also\n // update consoleWithStackDev.www.js as well.\n {\n var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;\n var stack = ReactDebugCurrentFrame.getStackAddendum();\n\n if (stack !== '') {\n format += '%s';\n args = args.concat([stack]);\n } // eslint-disable-next-line react-internal/safe-string-coercion\n\n\n var argsWithFormat = args.map(function (item) {\n return String(item);\n }); // Careful: RN currently depends on this prefix\n\n argsWithFormat.unshift('Warning: ' + format); // We intentionally don't use spread (or .apply) directly because it\n // breaks IE9: https://github.com/facebook/react/issues/13610\n // eslint-disable-next-line react-internal/no-production-logging\n\n Function.prototype.apply.call(console[level], console, argsWithFormat);\n }\n}\n\nvar FunctionComponent = 0;\nvar ClassComponent = 1;\nvar IndeterminateComponent = 2; // Before we know whether it is function or class\n\nvar HostRoot = 3; // Root of a host tree. Could be nested inside another node.\n\nvar HostPortal = 4; // A subtree. Could be an entry point to a different renderer.\n\nvar HostComponent = 5;\nvar HostText = 6;\nvar Fragment = 7;\nvar Mode = 8;\nvar ContextConsumer = 9;\nvar ContextProvider = 10;\nvar ForwardRef = 11;\nvar Profiler = 12;\nvar SuspenseComponent = 13;\nvar MemoComponent = 14;\nvar SimpleMemoComponent = 15;\nvar LazyComponent = 16;\nvar IncompleteClassComponent = 17;\nvar DehydratedFragment = 18;\nvar SuspenseListComponent = 19;\nvar ScopeComponent = 21;\nvar OffscreenComponent = 22;\nvar LegacyHiddenComponent = 23;\nvar CacheComponent = 24;\nvar TracingMarkerComponent = 25;\n\n// -----------------------------------------------------------------------------\n\nvar enableClientRenderFallbackOnTextMismatch = true; // TODO: Need to review this code one more time before landing\n// the react-reconciler package.\n\nvar enableNewReconciler = false; // Support legacy Primer support on internal FB www\n\nvar enableLazyContextPropagation = false; // FB-only usage. The new API has different semantics.\n\nvar enableLegacyHidden = false; // Enables unstable_avoidThisFallback feature in Fiber\n\nvar enableSuspenseAvoidThisFallback = false; // Enables unstable_avoidThisFallback feature in Fizz\n// React DOM Chopping Block\n//\n// Similar to main Chopping Block but only flags related to React DOM. These are\n// grouped because we will likely batch all of them into a single major release.\n// -----------------------------------------------------------------------------\n// Disable support for comment nodes as React DOM containers. Already disabled\n// in open source, but www codebase still relies on it. Need to remove.\n\nvar disableCommentsAsDOMContainers = true; // Disable javascript: URL strings in href for XSS protection.\n// and client rendering, mostly to allow JSX attributes to apply to the custom\n// element's object properties instead of only HTML attributes.\n// https://github.com/facebook/react/issues/11347\n\nvar enableCustomElementPropertySupport = false; // Disables children for