From cfb3290194ddf9cdf495071ab0b662d6caaef09c Mon Sep 17 00:00:00 2001 From: Jordy van Zeeland Date: Thu, 27 Oct 2022 11:17:03 +0200 Subject: [PATCH] Redesign components in frontend (DRY) --- ras/api/urls.py | 2 + ras/api/views.py | 46 ++++ ras/frontend/src/App.js | 288 +++++++++++---------- ras/frontend/src/components/Challenge.js | 65 +++++ ras/frontend/src/components/Data.js | 35 +++ ras/frontend/src/components/Stats.js | 88 +++++++ ras/frontend/static/js/main.js | 2 +- ras/frontend/static/js/main.js.LICENSE.txt | 12 + ras/frontend/templates/index.html | 63 ++++- 9 files changed, 454 insertions(+), 147 deletions(-) create mode 100644 ras/frontend/src/components/Challenge.js create mode 100644 ras/frontend/src/components/Data.js create mode 100644 ras/frontend/src/components/Stats.js diff --git a/ras/api/urls.py b/ras/api/urls.py index 329e372..7cdb1aa 100644 --- a/ras/api/urls.py +++ b/ras/api/urls.py @@ -2,6 +2,8 @@ from django.urls import path from .views import * urlpatterns = [ + path('books', getAllBooks), + path('books/challenge', getChallengeOfYear), path('books/years', getYears), path('books/stats', getStats), diff --git a/ras/api/views.py b/ras/api/views.py index ab369c2..1c6d019 100644 --- a/ras/api/views.py +++ b/ras/api/views.py @@ -17,6 +17,15 @@ def getBooksData(): return df +def getBookChallenge(year = None): + engine = create_engine('mysql+mysqldb://' + ras.settings.DATABASES['default']['USER'] + ':' + ras.settings.DATABASES['default']['PASSWORD'] + '@' + ras.settings.DATABASES['default']['HOST'] + ':3306/' + ras.settings.DATABASES['default']['NAME']) + if(year): + df = pd.read_sql('SELECT * FROM book_challenge where year = ' + year, engine) + else: + df = pd.read_sql('SELECT * FROM book_challenge', engine) + + return df + def filterData(df, datayear = None): df['readed'] = pd.to_datetime(df['readed'], format='%Y-%m-%d') df['readed'] = df['readed'].dt.strftime('%m-%Y') @@ -27,6 +36,43 @@ def filterData(df, datayear = None): return df +@api_view(['GET']) +def getAllBooks(request): + + data = [] + books = getBooksData() + + for index, row in books.iterrows(): + data.append({ + "id": row['id'], + "name": row['name'], + "author": row['author'], + "genre": row['genre'], + "author": row['author'], + "country": row['country'], + "country_code": row['country_code'], + "pages": row['pages'], + "readed": row['readed'], + "rating": row['rating'], + }) + + return Response(data) + +@api_view(['GET']) +def getChallengeOfYear(request): + if request.META.get('HTTP_YEAR'): + data = [] + df = getBookChallenge(request.META.get('HTTP_YEAR')) + + for index, row in df.iterrows(): + data.append({ + "year": row['year'], + "nrofbooks": row['nrofbooks'] + }) + + return Response(data) + else: + return Response("No year header included") @api_view(['GET']) def books_per_genre_per_month(request): diff --git a/ras/frontend/src/App.js b/ras/frontend/src/App.js index e4f4ba1..86bd147 100644 --- a/ras/frontend/src/App.js +++ b/ras/frontend/src/App.js @@ -1,4 +1,6 @@ import React, { Component } from "react"; +import Challenge from "./components/Challenge"; +import BookStats from "./components/Stats"; export default class App extends Component { constructor(props) { @@ -6,18 +8,14 @@ export default class App extends Component { this.state = { year: new Date().getFullYear(), readingYears: [], - totalbooks: 0, - totalpages: 0, - totalauthors: 0, - totalcountries: 0, - totalgenres: 0, - countries: [] + countries: [], + pagesStats: [], } this.yearsArray = []; } - getGenres(){ + getGenres() { fetch('/api/books/genres', { "method": "GET", "headers": { @@ -27,10 +25,10 @@ export default class App extends Component { .then(response => response.json()) .then(books => { this.initChart(books, this.state.year); - }) + }) } - getCountries(init){ + getCountries(init) { fetch('/api/books/countries', { "method": "GET", "headers": { @@ -43,7 +41,7 @@ export default class App extends Component { countries: data }) - if(init == true){ + if (init == true) { $('#DataTable').DataTable({ paging: false, ordering: false, @@ -54,33 +52,31 @@ export default class App extends Component { }) } + 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/stats', { - "method": "GET", - "headers": { - "year": event.target.value - } - }) - .then(response => response.json()) - .then(data => { - this.setState({ - totalbooks: data.totalbooks, - totalpages: data.totalpages, - totalauthors: data.totalauthors, - totalcountries: data.totalcountries, - totalgenres: data.totalgenres - }) - }) - fetch('/api/books/countries', { "method": "GET", "headers": { - "year": this.state.year + "year": event.target.value } }) .then(response => response.json()) @@ -94,19 +90,32 @@ export default class App extends Component { var $this = this; + this.getShortestLongestBook(event.target.value); + fetch('/api/books/genres/count', { "method": "GET", "headers": { - "year": this.state.year + "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){ + initHorBar(data) { var countries = []; var counts = []; @@ -177,10 +186,10 @@ export default class App extends Component { const legendMargin = { id: 'legendMargin', - beforeInit(chart, legend, options){ + beforeInit(chart, legend, options) { const fitValue = chart.legend.fit; - chart.legend.fit = function fit(){ + chart.legend.fit = function fit() { fitValue.bind(chart.legend)(); return this.height += 30; } @@ -205,19 +214,19 @@ export default class App extends Component { borderColor: '#1f2940', tooltip: { callbacks: { - label: function(context) { + 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; } @@ -246,24 +255,24 @@ export default class App extends Component { }, plugins: [{ id: 'legendMargin', - beforeInit(chart, legend, options){ + beforeInit(chart, legend, options) { const fitValue = chart.legend.fit; - - chart.legend.fit = function 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(); + 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(); - } + } }], }); } @@ -335,10 +344,10 @@ export default class App extends Component { const legendMargin = { id: 'legendMargin', - beforeInit(chart, legend, options){ + beforeInit(chart, legend, options) { const fitValue = chart.legend.fit; - chart.legend.fit = function fit(){ + chart.legend.fit = function fit() { fitValue.bind(chart.legend)(); return this.height += 30; } @@ -358,7 +367,7 @@ export default class App extends Component { legend: { display: true, labels: { - usePointStyle: true, + usePointStyle: true, } }, interaction: { @@ -405,15 +414,11 @@ export default class App extends Component { }); } - componentDidUpdate() { - this.getGenres(); - } - componentDidMount() { var $this = this; - var currentyear = this.state.year ? this.state.year : new Date().getFullYear() + var currentyear = this.state.year ? this.state.year : new Date().getFullYear(); fetch('/api/books/genres', { "method": "GET", @@ -426,6 +431,8 @@ export default class App extends Component { this.initChart(books, currentyear); }) + this.getShortestLongestBook(this.state.year); + fetch('/api/books/genres/count', { "method": "GET", "headers": { @@ -439,48 +446,56 @@ export default class App extends Component { this.getCountries(true); - fetch('/api/books/stats', { + fetch('/api/books/years', { "method": "GET", - "headers": { - "year": this.state.year - } }) .then(response => response.json()) .then(data => { - $this.setState({ - totalbooks: data.totalbooks, - totalpages: data.totalpages, - totalauthors: data.totalauthors, - totalcountries: data.totalcountries, - totalgenres: data.totalgenres + this.setState({ + readingYears: data }) }) - 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 ( + +
-
- +
+
-
- +
+
- +
@@ -492,105 +507,94 @@ export default class App extends Component {
- +
- -
-
- - {this.state.totalbooks} - Boeken -
-
-
-
- - {this.state.totalpages} - Bladzijdes -
-
-
-
- - {this.state.totalauthors} - Schrijvers -
-
-
-
- - {this.state.totalgenres} - Genres -
-
-
-
- - {this.state.totalcountries} - Landen -
-
+ +
+ +
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) => { +
#LandBoeken
+ + + + + + + + + {this.state.countries.map((country, i) => { - var code = country.code.toLowerCase(); - return( - - - - - - - - ) - - })} - + var code = country.code.toLowerCase(); + return ( + + + + + + + + ) + + })} +
#LandBoeken
{i+1} {country.country}{country.count}
{i + 1} {country.country}{country.count}
Genres
- +
- +
) diff --git a/ras/frontend/src/components/Challenge.js b/ras/frontend/src/components/Challenge.js new file mode 100644 index 0000000..8b91740 --- /dev/null +++ b/ras/frontend/src/components/Challenge.js @@ -0,0 +1,65 @@ +import React, { Component } from 'react'; +import { getChallenge, getStats } from "./Data.js"; + +export default class Challenge extends Component { + constructor(props) { + super(props); + this.state = { + readingYears: [], + challenge: 0 + } + } + + getComponentData(){ + var $this = this; + + getStats(this.props.year).then(data => { + $this.setState({ + totalbooks: data.totalbooks + }) + }); + + getChallenge(this.props.year).then(data => { + this.setState({ + challenge: data && data.length > 0 ? data[0].nrofbooks : 0 + }) + }); + } + + componentDidMount() { + this.getComponentData(); + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.year !== this.props.year) { + this.getComponentData(); + } + } + + render() { + var challengePercentage = (this.state.totalbooks / this.state.challenge) * 100 + + return ( + + {this.state.challenge && this.state.challenge !== 0 ? +
+
+
+
+ Book Challenge +
+
+
{challengePercentage}%
+
+
+ + {this.state.totalbooks}van de{this.state.challenge}boeken gelezen +
+
+
+
+ : ''} +
+ ) + } +} \ No newline at end of file diff --git a/ras/frontend/src/components/Data.js b/ras/frontend/src/components/Data.js new file mode 100644 index 0000000..676ca3b --- /dev/null +++ b/ras/frontend/src/components/Data.js @@ -0,0 +1,35 @@ +export const getStats = (year) => { + return fetch('/api/books/stats', { + "method": "GET", + "headers": { + "year": year + } + }) + .then(response => response.json()) + .then(data => { + return data; + }) +} + +export const getChallenge = (year) => { + return fetch('/api/books/challenge', { + "method": "GET", + "headers": { + "year": year + } + }) + .then(response => response.json()) + .then(data => { + return data + }) +} + +export const getReadingYears = () => { + return fetch('/api/books/years', { + "method": "GET", + }) + .then(response => response.json()) + .then(data => { + return data + }) +} \ No newline at end of file diff --git a/ras/frontend/src/components/Stats.js b/ras/frontend/src/components/Stats.js new file mode 100644 index 0000000..9f47f8c --- /dev/null +++ b/ras/frontend/src/components/Stats.js @@ -0,0 +1,88 @@ +import React, { Component } from 'react'; +import { getStats, getReadingYears } from "./Data.js"; + +export default class BookStats extends Component { + constructor(props) { + super(props); + this.state = { + readingYears: [], + totalbooks: 0, + totalpages: 0, + totalauthors: 0, + totalcountries: 0, + totalgenres: 0, + } + } + + getComponentData(){ + var $this = this; + + getStats(this.props.year).then(data => { + $this.setState({ + totalbooks: data.totalbooks, + totalpages: data.totalpages, + totalauthors: data.totalauthors, + totalcountries: data.totalcountries, + totalgenres: data.totalgenres + }) + }); + + getReadingYears().then(data => { + this.setState({ + readingYears: data + }) + }); + } + + componentDidMount() { + this.getComponentData(); + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.year !== this.props.year) { + this.getComponentData(); + } + } + + render() { + return ( + +
+
+ + {this.state.totalbooks} + Boeken +
+
+
+
+ + {this.state.totalpages} + Bladzijdes +
+
+
+
+ + {this.state.totalauthors} + Schrijvers +
+
+
+
+ + {this.state.totalgenres} + Genres +
+
+
+
+ + {this.state.totalcountries} + Landen +
+
+
+ ) + } +} \ No newline at end of file diff --git a/ras/frontend/static/js/main.js b/ras/frontend/static/js/main.js index af1b9f8..4b89348 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\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 totalbooks: 0,\n totalpages: 0,\n totalauthors: 0,\n totalcountries: 0,\n totalgenres: 0,\n countries: []\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 changeYear(event) {\n this.setState({\n year: event.target.value\n });\n fetch(\'/api/books/stats\', {\n "method": "GET",\n "headers": {\n "year": event.target.value\n }\n }).then(response => response.json()).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 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 this.getCountries(false);\n });\n var $this = this;\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 }\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 componentDidUpdate() {\n this.getGenres();\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 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/stats\', {\n "method": "GET",\n "headers": {\n "year": this.state.year\n }\n }).then(response => response.json()).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 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 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 class: "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 class: "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 class: "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 => {\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 selected: selected,\n value: year\n }, year);\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 class: "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 class: "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 class: "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 class: "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 class: "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")))))), /*#__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: "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 class: "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", null, /*#__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/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