Compare commits

...

66 commits

Author SHA1 Message Date
Yarne Coppens
b355680534 Dont' show players who haven't played game on the winrate per game graph 2025-02-13 10:30:57 +01:00
Yarne Coppens
33998097a7 Added winrate over time to single boardgame pages 2025-02-12 20:26:04 +01:00
Yarne Coppens
979870f41d Added weight bar to game info page 2025-02-12 14:20:22 +01:00
Yarne Coppens
dd575259b6 Added more info 2025-02-12 13:50:30 +01:00
Yarne Coppens
7512ed6257 Created info on individual board game pages 2025-02-12 13:30:27 +01:00
Yarne Coppens
07ae0d8396 Removed wrong closing tag 2025-02-12 12:38:15 +01:00
Yarne Coppens
6d27e5dc71 Removed console.log 2025-02-12 12:36:46 +01:00
Yarne Coppens
af654af1e4 Added custom game name search 2025-02-12 12:18:54 +01:00
Yarne Coppens
0ae0ce3cc7 Hide 'entries per page' 2025-02-06 16:50:22 +01:00
Yarne Coppens
60fe2662a5 Added player amount filtering 2025-02-06 16:49:22 +01:00
Yarne Coppens
1caa6da2f0 Displayed all games on one page 2025-02-06 15:30:58 +01:00
Yarne Coppens
99f2c94346 Merge branch 'bootstrap' of https://forgejo.yarnecoppens.com/Yarninator/boardgame_site_v2 into bootstrap 2024-10-17 09:57:19 +02:00
Yarne Coppens
e8c58e0f7f Added h-index statistic 2024-10-17 09:56:23 +02:00
fe77894c0c Changed to correct API url 2024-10-15 12:04:32 +02:00
f3a1ebca28 Added umami tracking 2024-10-15 12:00:21 +02:00
Yarne Coppens
5c2fdd202f Added boardgame playercount vote chart 2024-10-14 18:58:20 +02:00
Yarne Coppens
452eff32bf Started work on pre-ordered games page 2024-09-07 16:14:32 +02:00
Yarne Coppens
dba5076107 Added more important players 2024-09-07 16:13:41 +02:00
Yarne Coppens
ba35b25c60 Added favicon.ico 2024-09-07 16:12:41 +02:00
Yarne Coppens
57fa200f9b Added most bought designer & artist statistic 2024-08-26 15:02:04 +02:00
Yarne Coppens
56e420198b Made total collection cost round to 2 decimals 2024-08-25 19:06:53 +02:00
Yarne Coppens
dca6aa984e Added requirements.txt 2024-08-25 18:43:42 +02:00
Yarne Coppens
b2a6a0f7e1 Made changes to the board game statistic according to API change, no more attribute fidgeting 2024-08-25 17:20:01 +02:00
Yarne Coppens
d6b05ea986 Made statistic cards screen responsive 2024-08-25 11:38:18 +02:00
Yarne Coppens
3207920bac Created ability to size statistic cards 2024-08-25 11:21:49 +02:00
Yarne Coppens
f8067dba8f Made multi line start at different points 2024-08-25 10:58:01 +02:00
Yarne Coppens
08990e61dc Created ability to create a multi_line chart 2024-08-25 10:50:24 +02:00
Yarne Coppens
1d5b6a04bd Chart functions now accept statistic data instead of url, so that statistic data can be manipulated before being processed 2024-08-24 19:52:44 +02:00
Yarne Coppens
603a17b854 Made statistic chart creation more efficient 2024-08-22 22:38:48 +02:00
Yarne Coppens
050638ae43 Use new refactored API 2024-08-22 14:10:17 +02:00
Yarne Coppens
1bd2f84d91 Made owned collection work with new refactored classes 2024-08-21 23:48:54 +02:00
Yarne Coppens
0fa86b0755 Made boardgame cards overflow with scrollbar to show more than 6 games (e.g. shelf of shame) 2024-08-16 10:14:39 +02:00
Yarne Coppens
c4c803cfad Added chart.umd.js.map 2024-08-15 19:36:57 +02:00
Yarne Coppens
57950adbf0 Added shelf of shame statistic. Cards will now align properly 2024-08-15 13:48:19 +02:00
Yarne Coppens
7ab74205a1 Created more generic functions to re-use for charts 2024-08-15 09:29:57 +02:00
Yarne Coppens
8d415846e6 Changed the nav to an actual navbar 2024-08-14 16:35:33 +02:00
Yarne Coppens
92600f8649 Made play page prettier 2024-08-14 16:26:02 +02:00
Yarne Coppens
597510da52 Made statistics prettier 2024-08-14 16:16:28 +02:00
Yarne Coppens
ed066180bc Removed unused class 2024-08-14 15:06:01 +02:00
Yarne Coppens
50710b33b4 Started work on board game grid statistic 2024-08-14 15:05:40 +02:00
Yarne Coppens
938b6e98d2 Added bar chart for games played per year 2024-08-14 13:51:07 +02:00
Yarne Coppens
fb5f5a5bc0 Made navbar be responsive to current page 2024-08-14 11:03:19 +02:00
Yarne Coppens
1239995080 Added navbar 2024-08-14 10:55:43 +02:00
Yarne Coppens
8e10d5fa1d Added loading spinner on plays page 2024-08-14 10:55:31 +02:00
Yarne Coppens
26a1c0422f Split JS in files per page 2024-08-14 10:16:54 +02:00
Yarne Coppens
e0443811b1 Added a 'plays' page to list all plays 2024-08-14 08:49:46 +02:00
Yarne Coppens
41441db75d Created line and bar multi chart for games over time 2024-08-12 09:17:32 +02:00
Yarne Coppens
06e1599461 Filtered out expansions in owned game list. Owned game statistic uses day steps 2024-08-12 09:09:39 +02:00
Yarne Coppens
8036276187 Self hosting chart.js 2024-08-12 08:27:10 +02:00
Yarne Coppens
141f533af6 Implemented chart.js 2024-08-11 23:04:30 +02:00
Yarne Coppens
a3af3aea05 Added bootstrap map 2024-08-11 22:52:46 +02:00
Yarne Coppens
af9196bca0 Made game thumbnails fluid 2024-08-11 17:48:50 +02:00
Yarne Coppens
d88ca6d216 Created more board game table columns 2024-08-11 16:20:53 +02:00
Yarne Coppens
49b97ef0c2 Conformed with the API change of the boardgame ID path -> GET 2024-08-11 16:03:22 +02:00
Yarne Coppens
76497e21c9 Self hosting all css & javascript 2024-08-11 15:17:18 +02:00
Yarne Coppens
51de22aa67 Added wishlist page 2024-08-11 12:07:16 +02:00
Yarne Coppens
d3fd938da3 Added board game card 2024-08-11 10:49:59 +02:00
Yarne Coppens
d9fd74f422 Made datatable rows clickable 2024-08-11 10:29:40 +02:00
Yarne Coppens
ecbc5f9162 Turned table into a bootstrap datatable 2024-08-11 10:06:27 +02:00
Yarne Coppens
08e6bd431d Added route to request specific board game 2024-08-10 22:45:08 +02:00
Yarne Coppens
2fabfb60f0 Created base for Flask application 2024-08-10 22:07:47 +02:00
Yarne Coppens
5a9360d10a Make board games clickable 2024-08-09 15:29:29 +02:00
Yarne Coppens
581adb64a3 Added weight column 2024-08-09 14:42:52 +02:00
Yarne Coppens
bc494a6133 Table now loads boardgame thumbnails 2024-08-08 16:32:05 +02:00
Yarne Coppens
f944e320ef Table now loads owned board games 2024-08-08 16:14:55 +02:00
Yarne Coppens
204f8a4f22 Created base files 2024-08-08 15:43:49 +02:00
27 changed files with 1425 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
__pycache__/
venv/
.vscode/

28
app.py Normal file
View file

@ -0,0 +1,28 @@
from flask import Flask, render_template, request
app = Flask(__name__)
@app.get("/")
def get_owned():
return render_template('owned.jinja')
@app.get("/wishlist")
def get_wishlist():
return render_template('wishlist.jinja')
@app.get("/boardgame")
def get_boardgame():
return render_template('boardgame.jinja')
@app.get('/statistics')
def get_statistics():
return render_template('statistics.jinja')
@app.get('/incoming')
def get_incoming():
return render_template('incoming.jinja')
@app.get('/plays')
def get_plays():
return render_template('plays.jinja')

7
requirements.txt Normal file
View file

@ -0,0 +1,7 @@
blinker==1.8.2
click==8.1.7
Flask==3.0.3
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==2.1.5
Werkzeug==3.0.3

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

27
static/css/main.css Normal file
View file

@ -0,0 +1,27 @@
body{
padding-top: 100px;
padding-bottom: 100px;
}
@media screen and (max-width: 576px) {
body {
padding-top: 230px;
}
}
.boardgame_statistic_card_image{
width: 100%;
height: 100px;
object-fit: cover;
}
.boardgame_play_card_image{
width: 100%;
height: 400px;
object-fit: cover;
}
.card-body{
max-height: 550px;
}

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

View file

@ -0,0 +1,221 @@
document.body.onload=loadGame()
const important_player_name_colors = {'Yarne':'black', 'Lore':'green', 'Lucas':'brown','Louize':'blue','Ruben':'purple', 'Ina':'orange', 'Matthias':'yellow','Kelly':'darkorange','Keanu':'darkblue'}
async function create_info_block(block_title, block_value){
var title = document.createElement('h2')
title.textContent = block_title
var info_container = document.createElement('p')
info_container.textContent = block_value
$('#info_block').append(title)
$('#info_block').append(info_container)
}
async function create_info_meter(meter_title, min, max, value){
var title = document.createElement('h2')
title.textContent = meter_title
var meter_holder = document.createElement('div')
meter_holder.classList.add('progress')
var meter = document.createElement('div')
meter.classList.add('progress-bar')
meter.role = 'progressbar'
meter.style = `width: ${(value/max) * 100}%`
meter.setAttribute('aria-valuenow', value)
meter.setAttribute('aria-valuemin', min)
meter.setAttribute('aria-valuemax', max)
if (value < max * 0.40){
meter.classList.add('bg-success')
}else if (value < max * 0.60){
meter.classList.add('bg-warning')
}else {
meter.classList.add('bg-danger')
}
meter_holder.appendChild(meter)
// meter.value = value
// meter.min = min
// meter.max = max
$('#info_block').append(title)
$('#info_block').append(meter_holder)
}
async function create_game_block(requested_game){
$('#boardgame_image').attr('src', requested_game.image_url)
$('#boardgame_name').text(requested_game.name)
$('#boardgame_description').text(requested_game.description)
$('#boardgame_link').attr('href', 'https://boardgamegeek.com/boardgame/' + requested_game.id)
}
async function create_playercount_vote_chart(requested_game){
const playercount_votes_chart = $('#playercount_votes_chart')
const playercountVotes = requested_game.playercount_votes
const labels = playercountVotes.map(vote => vote.playercount);
const bestData = playercountVotes.map(vote => vote.best);
const recommendedData = playercountVotes.map(vote => vote.recommended);
const notRecommendedData = playercountVotes.map(vote => vote.not_recommended);
const data = {
labels: labels,
datasets: [
{
label: 'Not Recommended',
data: notRecommendedData,
backgroundColor: 'rgba(255, 99, 132, 0.6)',
},
{
label: 'Recommended',
data: recommendedData,
backgroundColor: 'rgba(75, 192, 192, 0.6)',
},
{
label: 'Best',
data: bestData,
backgroundColor: 'rgba(50, 205, 50, 0.6)',
},
],
};
const config = {
type: 'bar',
data: data,
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: 'Player Count Votes'
}
},
scales: {
x: {
stacked: true,
},
y: {
stacked: true,
}
}
},
};
new Chart(playercount_votes_chart, config);
}
async function create_player_winrate_chart(requested_game){
const playercount_votes_chart = $('#player_winrate_chart')
const playercountVotes = requested_game.playercount_votes
// const labels = playercountVotes.map(vote => vote.playercount);
// const bestData = playercountVotes.map(vote => vote.best);
// const recommendedData = playercountVotes.map(vote => vote.recommended);
// const notRecommendedData = playercountVotes.map(vote => vote.not_recommended);
const data_url = api_url + `/statistics/winrate_over_time?day_step=30&boardgame_id=${requested_game.id}`
const data_request = await makeRequest(data_url)
let datasets = []
let players_who_played_game = [];
for (const play of requested_game.plays){
for (const player of play.players){
if (!players_who_played_game.includes(player.name)){
players_who_played_game.push(player.name)
}
}
}
for (const [playername, color] of Object.entries(important_player_name_colors)){
if (players_who_played_game.includes(playername)){
let dataset = {
label: playername,
data: Object.values(data_request[playername].result),
borderColor: color
}
datasets.push(dataset)
}
}
const labels = Object.keys(data_request[Object.keys(data_request)[0]].result)
const data = {
labels: labels,
datasets: datasets
};
const config = {
type: 'line',
data: data,
options: {
scales: {
y: {
beginAtZero: true,
offset: true,
ticks:{
format:{
style: 'percent'
}
}
},
x: {
offset: true
},
}
},
};
new Chart(playercount_votes_chart, config);
}
async function loadGame() {
let params = new URLSearchParams(document.location.search);
let boardgame_id = params.get("id");
var loadGameURL = api_url + '/boardgame?id=' + boardgame_id
var requested_game = await makeRequest(loadGameURL)
create_game_block(requested_game)
create_info_block('Spelers', requested_game.min_players != requested_game.max_players ? `${requested_game.min_players} - ${requested_game.max_players}` : requested_game.min_players)
create_info_meter('Moeilijkheid', 1, 5, requested_game.weight)
create_info_block(requested_game.designers.length > 1 ? 'Designers' : 'Designer', requested_game.designers.map(designer => designer.name).join(' - '))
create_info_block(requested_game.artists.length > 1 ? 'Artiesten' : 'Artiest', requested_game.artists.map(artist => artist.name).join(' - '))
if (requested_game.owned_info != null){
let acquisition_date = new Date(requested_game.owned_info.acquisition_date)
const date_formatter = new Intl.DateTimeFormat('nl-BE', { dateStyle: 'short' });
acquisition_date = date_formatter.format(acquisition_date)
create_info_block('Datum gekocht', acquisition_date)
}
create_info_block('Aantal keer gespeeld', requested_game.plays.length)
create_playercount_vote_chart(requested_game)
create_player_winrate_chart(requested_game)
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

File diff suppressed because one or more lines are too long

14
static/javascript/main.js Normal file
View file

@ -0,0 +1,14 @@
const api_url = "https://api.bordspellen.yarnecoppens.com"
var all_owned_games
async function makeRequest(url) {
try {
const url_request = new Request(url)
const response = await fetch(url_request);
const result = await response.json();
return result
} catch (error) {
console.error("Error:", error);
}
}

109
static/javascript/owned.js Normal file
View file

@ -0,0 +1,109 @@
document.body.onload=loadOwnedGames()
document.getElementById('owned_nav').classList.add('active')
async function loadOwnedGames() {
const name_input = document.getElementById("name_input")
const player_amount_input = document.getElementById("player_amount_input")
var boardgame_datatable = new DataTable('.boardgame_table', {
"pageLength":-1,
"bLengthChange": false,
"bPaginate": false,
"info": false,
ajax: {
url: api_url + '/owned?filter_expansions_out=true',
dataSrc: ''
},
columns: [
{
data: 'thumbnail_url',
render: function (data,type){
return '<img src="' + data + '" class="img-fluid" />'
}
},
{
data: 'name'
},
{
data: 'min_players',
render: function(data,type,row){
if (row.min_players != row.max_players){
return row.min_players + '-' + row.max_players
}else{
return row.min_players
}
}
},
{
data: 'min_playing_time',
render: function(data,type,row){
if (row.min_playing_time != row.max_playing_time){
return row.min_playing_time + '-' + row.max_playing_time
}else{
return row.min_playing_time
}
}
},
{
data: 'weight'
}
],
columnDefs: [
{
targets: 'no-sort', orderable: false
},
{
targets: '_all',
orderSequence: [ 'asc', 'desc' ]
}
],
order: [[1, 'asc']]
});
// Custom range filtering function
boardgame_datatable.search.fixed('range', function (searchStr, data, index) {
function isCorrectPlayerAmount(player_amount_wanted, min_players, max_players){
if ((player_amount_wanted >= min_players && player_amount_wanted <= max_players) || isNaN(player_amount_wanted)){
return true;
}
}
function isCorrectName(game_name_wanted, game_name){
if (game_name.includes(game_name_wanted) || game_name_wanted == ""){
return true;
}
}
var player_amount_wanted = parseInt(player_amount_input.value, 10);
var min_players = parseFloat(data['min_players'])
var max_players = parseFloat(data['max_players'])
var game_name_wanted = name_input.value.toLowerCase()
var game_name = data['name'].toLowerCase()
if (isCorrectPlayerAmount(player_amount_wanted, min_players, max_players) && isCorrectName(game_name_wanted, game_name)){
return true;
}
return false;
});
player_amount_input.addEventListener('input', function () {
boardgame_datatable.draw();
});
name_input.addEventListener('input', function () {
boardgame_datatable.draw();
});
$('.boardgame_table').on('click', 'tbody tr', function() {
var boardgame_id = boardgame_datatable.row(this).data().id;
window.location.href = "/boardgame?id=" + boardgame_id
})
}

101
static/javascript/plays.js Normal file
View file

@ -0,0 +1,101 @@
document.body.onload=loadPlays()
document.getElementById('plays_nav').classList.add('active')
async function loadPlays() {
function createPlayCard(play){
const card_div = document.createElement('div')
card_div.classList.add("card")
card_div.classList.add('h-100')
const boardgame_image_linked = document.createElement('a')
boardgame_image_linked.setAttribute('href', '/boardgame?id=' + play.boardgame.id)
const card_image = document.createElement('img')
card_image.src = play.boardgame.image_url
card_image.classList.add('card-img-top')
card_image.classList.add('boardgame_play_card_image')
boardgame_image_linked.appendChild(card_image)
const card_body = document.createElement('div')
card_body.classList.add('card-body')
const card_title = document.createElement('h4')
card_title.innerHTML = play.boardgame.name
card_title.classList.add('card-title')
const player_names = document.createElement('p')
player_names.classList.add('card-text')
for (let player_index in play.players){
const player = play.players[player_index]
const player_div = document.createElement('div')
player_div.innerHTML = player.name
if (player.has_won) {
player_div.style.color = "green"
}
player_names.appendChild(player_div)
}
card_body.appendChild(card_title)
card_body.appendChild(player_names)
card_div.appendChild(boardgame_image_linked)
card_div.appendChild(card_body)
return card_div
}
const all_plays = await makeRequest(api_url + '/plays?filter_expansions_out=true')
const row = document.createElement('row')
row.classList.add('row')
row.classList.add('row-cols-1')
row.classList.add('row-cols-md-4')
for (let play_index in all_plays){
let current_play = all_plays[play_index]
const column = document.createElement('div')
column.classList.add('col')
let card = createPlayCard(current_play)
column.appendChild(card)
row.appendChild(column)
}
// for (let row_number = 0; row_number < rows_needed; row_number++){
// const row_div = document.createElement('div')
// row_div.className = 'row'
// for (let column_number = 0; column_number < MAX_COLUMNS; column_number++) {
// if(((row_number * MAX_COLUMNS) + column_number) == all_plays.length){
// break
// }
// const column_div = document.createElement('div')
// column_div.className = "col-sm-" + column_width
// const current_play = all_plays[(row_number * MAX_COLUMNS) + column_number]
// const play_card = createPlayCard(current_play)
// column_div.appendChild(play_card)
// row_div.appendChild(column_div)
// }
// document.body.appendChild(row_div)
// }
document.body.append(row)
document.getElementById('plays_loading_spinner').remove()
}

View file

@ -0,0 +1,524 @@
document.body.onload=loadStatistics()
document.getElementById('statistics_nav').classList.add('active')
const important_player_name_colors = {'Yarne':'black', 'Lore':'green', 'Lucas':'brown','Louize':'blue','Ruben':'purple', 'Ina':'orange', 'Matthias':'yellow','Kelly':'darkorange','Keanu':'darkblue'}
function create_statistic_card(col_number = null){
function create_statistic_card_col(){
const col = document.createElement('col')
if (col_number == null){
col.classList.add('col-12')
col.classList.add('col-md-6')
}else{
col.classList.add('col-' + col_number * 2)
col.classList.add('col-md-'+col_number)
}
const card = document.createElement('div')
card.classList.add('card')
card.classList.add('h-100')
col.appendChild(card)
return col
}
let statistic_row = document.getElementById('statistic_row')
const statistic_card_col = create_statistic_card_col()
const statistic_card = statistic_card_col.firstChild
statistic_row.appendChild(statistic_card_col)
return statistic_card
}
async function create_games_over_time_chart(){
let games_over_time_statistic = await makeRequest(api_url + '/statistics/amount_of_games_over_time?day_step=30')
let games_over_time_statistic_no_expanions = await makeRequest(api_url + '/statistics/amount_of_games_over_time?day_step=30&filter_expansions_out=true')
let games_over_time_statistic_only_expanions = await makeRequest(api_url + '/statistics/amount_of_games_over_time?day_step=30&only_expansions=true')
const card_to_fill = create_statistic_card(12)
const chart_canvas_container = document.createElement('div')
chart_canvas_container.classList.add('card-body')
chart_canvas_container.classList.add('card-img-top')
const chart_canvas = document.createElement('canvas')
chart_canvas.classList.add('chart_visual')
chart_canvas_container.appendChild(chart_canvas)
new Chart(chart_canvas, {
type: 'bar',
data: {
labels: Object.keys(games_over_time_statistic.result),
datasets: [{
label: games_over_time_statistic.name,
data: Object.values(games_over_time_statistic.result),
borderWidth: 1,
type: 'line'
},
{
label: "Base games",
data: Object.values(games_over_time_statistic_no_expanions.result),
borderWidth: 1
},
{
label: "Expansions",
data: Object.values(games_over_time_statistic_only_expanions.result),
borderWidth: 1
}
]
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
});
const card_footer = document.createElement('div')
card_footer.classList.add('card-header')
card_footer.classList.add('text-muted')
card_footer.innerHTML = games_over_time_statistic.name
card_to_fill.appendChild(card_footer)
card_to_fill.appendChild(chart_canvas_container)
}
async function create_line_chart(statistic_data, name=''){
if (name == ""){
statistic_name = statistic_data.name
}else{
statistic_name = name
}
const card_to_fill = create_statistic_card()
const chart_canvas_container = document.createElement('div')
chart_canvas_container.classList.add('card-body')
chart_canvas_container.classList.add('card-img-top')
const chart_canvas = document.createElement('canvas')
chart_canvas.classList.add('chart_visual')
chart_canvas_container.appendChild(chart_canvas)
const highest_bar_value = Math.max(...Object.values(statistic_data.result))
let grid_offset = 0
if (highest_bar_value > 10000){
grid_offset = 25000
}else if(highest_bar_value > 1000){
grid_offset = 2500
}else if(highest_bar_value > 100){
grid_offset = 250
}else if(highest_bar_value > 10){
grid_offset = 25
}else if(highest_bar_value > 2){
grid_offset = 2.5
}else{
grid_offset = 1.25
}
new Chart(chart_canvas, {
type: 'line',
data: {
labels: Object.keys(statistic_data.result),
datasets: [{
label: statistic_data.name,
data: Object.values(statistic_data.result),
borderWidth: 1
}
]
},
options: {
scales: {
y: {
beginAtZero: true,
max: Math.ceil((highest_bar_value) / grid_offset) * grid_offset
}
}
}
});
const card_footer = document.createElement('div')
card_footer.classList.add('card-header')
card_footer.classList.add('text-muted')
card_footer.innerHTML = statistic_name
card_to_fill.appendChild(card_footer)
card_to_fill.appendChild(chart_canvas_container)
}
async function create_multi_line_chart(statistic_data, name=''){
if (name == ""){
statistic_name = statistic_data.name
}else{
statistic_name = name
}
const card_to_fill = create_statistic_card(12)
const chart_canvas_container = document.createElement('div')
chart_canvas_container.classList.add('card-body')
chart_canvas_container.classList.add('card-img-top')
const chart_canvas = document.createElement('canvas')
chart_canvas.classList.add('chart_visual')
chart_canvas_container.appendChild(chart_canvas)
let own_datasets = []
let all_results = []
let all_dates = []
for (key in statistic_data){
for (date in statistic_data[key].result){
if (statistic_data[key].result[date] == 0){
statistic_data[key].result[date] = null
}
}
if (!Object.keys(important_player_name_colors).includes(key)){
dataset = {
label: key,
data: Object.values(statistic_data[key].result)
}
}else{
dataset = {
label: key,
data: Object.values(statistic_data[key].result),
borderColor: important_player_name_colors[key]
}
}
own_datasets.push(dataset)
for (result_value in Object.values(statistic_data[key].result)){
all_results.push(result_value)
}
for (date in statistic_data[key].result){
if (!all_dates.includes(date)){
all_dates.push(date)
}
}
}
new Chart(chart_canvas, {
type: 'line',
data: {
labels: all_dates,
datasets: own_datasets
},
options: {
scales: {
y: {
beginAtZero: true,
offset: true
},
x: {
offset: true
}
}
}
});
const card_footer = document.createElement('div')
card_footer.classList.add('card-header')
card_footer.classList.add('text-muted')
card_footer.innerHTML = statistic_name
card_to_fill.appendChild(card_footer)
card_to_fill.appendChild(chart_canvas_container)
}
async function create_bar_chart(statistic_data, name=''){
if (name == ""){
statistic_name = statistic_data.name
}else{
statistic_name = name
}
const card_to_fill = create_statistic_card()
const chart_canvas_container = document.createElement('div')
chart_canvas_container.classList.add('card-body')
chart_canvas_container.classList.add('card-img-top')
const chart_canvas = document.createElement('canvas')
chart_canvas.classList.add('chart_visual')
chart_canvas_container.appendChild(chart_canvas)
const highest_bar_value = Math.max(...Object.values(statistic_data.result))
let grid_offset = 0
if (highest_bar_value > 10000){
grid_offset = 25000
}else if(highest_bar_value > 1000){
grid_offset = 2500
}else if(highest_bar_value > 100){
grid_offset = 250
}else{
grid_offset = 25
}
new Chart(chart_canvas, {
plugins: [ChartDataLabels],
type: 'bar',
data: {
labels: Object.keys(statistic_data.result),
datasets: [{
label: statistic_data.name,
data: Object.values(statistic_data.result),
borderWidth: 1
}
]
},
options: {
scales: {
y: {
beginAtZero: true,
max: Math.ceil((highest_bar_value) / grid_offset) * grid_offset
}
},
plugins: {
datalabels: {
anchor: 'end',
align: 'end'
}
}
}
});
const card_footer = document.createElement('div')
card_footer.classList.add('card-header')
card_footer.classList.add('text-muted')
card_footer.innerHTML = statistic_name
card_to_fill.appendChild(card_footer)
card_to_fill.appendChild(chart_canvas_container)
}
async function create_multiple_boardgame_chart(statistic_data, name = '', include_footer = false, footer_preamble = ''){
function create_boardgame_image_grid_row(boardgames, include_footer, footer_data, footer_preamble){
const row_container = document.createElement('div')
row_container.classList.add('container')
const row = document.createElement('div')
row.classList.add('row')
row.classList.add('row-cols-1')
row.classList.add('row-cols-md-3')
row.classList.add('g-4')
for (let boardgame_index in boardgames){
let current_boardgame = boardgames[boardgame_index]
const column = document.createElement('div')
column.classList.add('col')
const boardgame_card = document.createElement('div')
boardgame_card.classList.add('card')
boardgame_card.classList.add('h-100')
const boardgame_imaged_linked = document.createElement('a')
boardgame_imaged_linked.setAttribute('href', '/boardgame?id=' + current_boardgame.id)
const boardgame_image = document.createElement('img')
boardgame_image.src = current_boardgame.thumbnail_url
boardgame_image.classList.add('card-img-top')
boardgame_image.classList.add('boardgame_statistic_card_image')
boardgame_imaged_linked.appendChild(boardgame_image)
const boardgame_card_body = document.createElement('div')
boardgame_card_body.classList.add('card-body')
const boardgame_title = document.createElement('h6')
boardgame_title.innerHTML = current_boardgame.name
boardgame_title.classList.add('card-title')
boardgame_card_body.appendChild(boardgame_title)
if (include_footer){
const boardgame_footer = document.createElement('p')
boardgame_footer.innerHTML = footer_preamble + footer_data[current_boardgame.id]
boardgame_footer.classList.add('card-text')
boardgame_card_body.appendChild(boardgame_footer)
}
boardgame_card.append(boardgame_imaged_linked)
boardgame_card.append(boardgame_card_body)
column.appendChild(boardgame_card)
row.appendChild(column)
}
return row
}
if (name == ""){
statistic_name = statistic_data.name
}else{
statistic_name = name
}
const card_to_fill = create_statistic_card()
const card_header = document.createElement('div')
card_header.classList.add('card-header')
const card_footer_text = document.createElement('div')
card_footer_text.classList.add('text-muted')
card_footer_text.innerHTML = statistic_name
card_header.appendChild(card_footer_text)
card_to_fill.appendChild(card_header)
const boardgame_image_container = document.createElement('div')
boardgame_image_container.classList.add('card-body')
boardgame_image_container.classList.add('chart_visual')
boardgame_image_container.classList.add('container-fluid')
boardgame_image_container.classList.add('overflow-auto')
const boardgames_to_grid = statistic_data.games
const footer_data = statistic_data.result
// if (footer_attribute != ''){
// for (boardgame_index in boardgames_to_grid){
// let footer_string = footer_attribute_to_value(boardgames_to_grid[boardgame_index], footer_attribute)
// if (footer_preamble != ''){
// footer_string = footer_preamble + footer_string
// }
// footer_data.push(footer_string)
// }
// }
const row = create_boardgame_image_grid_row(boardgames_to_grid, include_footer, footer_data, footer_preamble)
boardgame_image_container.appendChild(row)
card_to_fill.appendChild(boardgame_image_container)
}
async function create_basic_statistic_chart(statistic_data, name='', preamble=''){
if (name == ""){
statistic_name = statistic_data.name
}else{
statistic_name = name
}
const card_to_fill = create_statistic_card(3)
const card_header = document.createElement('div')
card_header.classList.add('card-header')
card_header.classList.add('text-muted')
card_header.innerHTML = statistic_name
card_to_fill.appendChild(card_header)
const card_body = document.createElement('div')
card_body.classList.add('card-body')
const card_title = document.createElement('h5')
card_title.classList.add('card-title')
card_title.innerHTML = preamble + statistic_data.result
card_body.appendChild(card_title)
card_to_fill.appendChild(card_header)
card_to_fill.appendChild(card_body)
}
async function loadStatistics(){
const amount_of_games_statistic_data = await makeRequest(api_url+'/statistics/amount_of_games')
create_basic_statistic_chart(amount_of_games_statistic_data, 'Spellen in bezit')
const total_collection_cost_statistic_data = await makeRequest(api_url+'/statistics/total_collection_cost')
total_collection_cost_statistic_data.result = total_collection_cost_statistic_data.result.toFixed(2)
create_basic_statistic_chart(total_collection_cost_statistic_data, 'Totale kost van spellen in bezit', '\u20AC ')
const h_index_statistic_data = await makeRequest(api_url+'/statistics/h_index')
create_basic_statistic_chart(h_index_statistic_data, 'H-index')
//Seperate because of multiple data
await create_games_over_time_chart()
const winrate_over_time_statistic_data = await makeRequest(api_url + '/statistics/winrate_over_time?day_step=30')
for (let player_name in winrate_over_time_statistic_data){
if (!Object.keys(important_player_name_colors).includes(player_name)){
delete winrate_over_time_statistic_data[player_name]
}else{
for (date_key in winrate_over_time_statistic_data[player_name].result)
winrate_over_time_statistic_data[player_name].result[date_key] *= 100
}
}
create_multi_line_chart(winrate_over_time_statistic_data, 'Winrate van spelers over tijd')
const games_played_per_year_statistic_data = await makeRequest(api_url + '/statistics/games_played_per_year?filter_expansions_out=true')
create_bar_chart(games_played_per_year_statistic_data, 'Spellen gespeeld per jaar')
const winrate_statistic_data = await makeRequest(api_url + '/statistics/winrate')
for (let player_name in winrate_statistic_data.result){
if (!Object.keys(important_player_name_colors).includes(player_name)){
delete winrate_statistic_data.result[player_name]
}else{
winrate_statistic_data.result[player_name] *= 100
winrate_statistic_data.result[player_name] = winrate_statistic_data.result[player_name].toFixed(2)
}
}
create_bar_chart(winrate_statistic_data, 'Winrate van spelers')
const most_expensive_games_statistic_data = await makeRequest(api_url+'/statistics/most_expensive_games?top_amount=6')
create_multiple_boardgame_chart(most_expensive_games_statistic_data, 'Duurste spellen', true, '\u20AC ')
const cheapest_per_play_games_statistic_data = await makeRequest(api_url+'/statistics/cheapest_per_play?top_amount=6')
for (let boardgame_id_result in cheapest_per_play_games_statistic_data.result){
cheapest_per_play_games_statistic_data.result[boardgame_id_result] = cheapest_per_play_games_statistic_data.result[boardgame_id_result].toFixed(2)
}
create_multiple_boardgame_chart(cheapest_per_play_games_statistic_data, 'Goedkoopste per sessie', true, '\u20AC ')
const shelf_of_shame_statistic_data = await makeRequest(api_url + '/statistics/shelf_of_shame')
create_multiple_boardgame_chart(shelf_of_shame_statistic_data, 'Shelf of Shame', false)
const most_bought_from_designer_statistic_data = await makeRequest(api_url + '/statistics/most_bought_designer?filter_expansions_out=true')
create_multiple_boardgame_chart(most_bought_from_designer_statistic_data, 'Designer waarvan we het meeste hebben gekocht', true)
const most_bought_from_artist_statistic_data = await makeRequest(api_url + '/statistics/most_bought_artist?filter_expansions_out=true')
create_multiple_boardgame_chart(most_bought_from_artist_statistic_data, 'Artiest waarvan we het meeste hebben gekocht', true)
}

View file

@ -0,0 +1,69 @@
document.body.onload=loadWishlistedGames()
document.getElementById('wishlist_nav').classList.add('active')
async function loadWishlistedGames() {
var wishlist_priorities = [1,2,3,4]
jQuery.each(wishlist_priorities, function(index, item){
var boardgame_datatable = new DataTable('#wishlist_table'+item, {
ajax: {
url: api_url + '/wishlist?priority='+item,
dataSrc: ''
},
columns: [
{
data: 'wishlist_info.wishlist_priority'
},
{
data: 'thumbnail_url',
render: function (data,type){
return '<img src="' + data + '" class="img-fluid" />'
}
},
{
data: 'name'
},
{
data: 'min_players',
render: function(data,type,row){
if (row.min_players != row.max_players){
return row.min_players + '-' + row.max_players
}else{
return row.min_players
}
}
},
{
data: 'min_playing_time',
render: function(data,type,row){
if (row.min_playing_time != row.max_playing_time){
return row.min_playing_time + '-' + row.max_playing_time
}else{
return row.min_playing_time
}
}
},
{
data: 'weight'
}
],
columnDefs: [
{
target: 0,
visible: false
},
{ targets: 'no-sort', orderable: false }
],
order: [[2, 'asc']]
});
$('#wishlist_table'+item).on('click', 'tbody tr', function() {
var boardgame_id = boardgame_datatable.row(this).data().id;
window.location.href = "/boardgame?id=" + boardgame_id
})
});
}

59
templates/base.jinja Normal file
View file

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html data-theme="light">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
Boardgame Site
</title>
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link href="{{ url_for('static', filename='css/bootstrap/bootstrap.min.css') }}" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link href="{{ url_for('static', filename='css/datatables/datatables.min.css') }}" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
<script src="{{ url_for('static', filename='javascript/jquery/jquery-3.7.1.min.js') }}" defer></script>
<script src="{{ url_for('static', filename='javascript/datatables/datatables.min.js') }}" defer></script>
<script src="{{ url_for('static', filename='javascript/chart/chart.js') }}" defer></script>
<script src="{{ url_for('static', filename='javascript/chart/chartjs-plugin-datalabels.min.js') }}" defer></script>
<script defer src="https://umami.yarnecoppens.com/script.js" data-website-id="fe668e9a-4434-48ed-bffb-6d1001f494c8"></script>
</head>
<body>
<nav class="navbar navbar-expand-sm bg-dark navbar-dark fixed-top">
<div class="container-fluid">
<ul class="navbar-nav">
<li class="nav-item">
<a id="owned_nav" class="nav-link" href="{{ url_for('get_owned') }}">Collectie</a>
</li>
<li class="nav-item">
<a id="wishlist_nav" class="nav-link" href="{{ url_for('get_wishlist') }}">Wishlist</a>
</li>
<li class="nav-item">
<a id="statistics_nav" class="nav-link" href="{{ url_for('get_statistics') }}">Statistieken</a>
</li>
<li class="nav-item">
<a id="plays_nav" class="nav-link" href="{{ url_for('get_plays') }}">Gespeeld</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#">Disabled</a>
</li>
</ul>
</div>
</nav>
{% block body_block %}
{% endblock body_block %}
<script src="{{ url_for('static', filename='javascript/main.js') }}" defer></script>
{% block extra_js_files %}
{% endblock extra_js_files%}
</body>
</html>

31
templates/boardgame.jinja Normal file
View file

@ -0,0 +1,31 @@
{% extends "base.jinja" %}
{% block body_block %}
<div class="row">
<div class="col-sm-3">
<div class="card">
<img id="boardgame_image">
<div class="card-body">
<h4 class="card-title" id="boardgame_name">John Doe</h4>
<p class="card-text" id="boardgame_description">Some example text.</p>
<a href="#" id="boardgame_link" class="btn btn-primary">Bekijk op BGG</a>
</div>
</div>
</div>
<div class="col-sm-3" id="info_block">
<h1>Info</h1>
</div>
<div class="col-sm-6">
<canvas id="playercount_votes_chart"></canvas>
<canvas id="player_winrate_chart"></canvas>
</div>
</div>
{% endblock body_block %}
{% block extra_js_files %}
<script src="{{ url_for('static', filename='javascript/boardgame.js') }}" defer></script>
{% endblock extra_js_files %}

14
templates/incoming.jinja Normal file
View file

@ -0,0 +1,14 @@
{% extends "base.jinja" %}
{% block body_block %}
{% endblock body_block %}
{% block extra_js_files %}
<script src="{{ url_for('static', filename='javascript/incoming.js') }}" defer></script>
{% endblock extra_js_files %}

31
templates/owned.jinja Normal file
View file

@ -0,0 +1,31 @@
{% extends "base.jinja" %}
{% block body_block %}
<div class="form-group">
<input class="form-control" id="name_input" placeholder="Spelnaam">
<input type="number" class="form-control" id="player_amount_input" placeholder="Aantal spelers">
</div>
<div class="table-responsive">
<table class="table table-striped boardgame_table">
<thead>
<tr>
<th scope="col" class="no-sort">Thumbnail</th>
<th scope="col">Naam</th>
<th scope="col">Spelers</th>
<th scope="col">Duratie</th>
<th scope="col">Moeilijkheid</th>
</tr>
</thead>
</table>
</div>
{% endblock body_block %}
{% block extra_js_files %}
<script src="{{ url_for('static', filename='javascript/owned.js') }}" defer></script>
{% endblock extra_js_files %}

11
templates/plays.jinja Normal file
View file

@ -0,0 +1,11 @@
{% extends "base.jinja" %}
{% block body_block %}
<div id="plays_loading_spinner" class="spinner-border"></div>
{% endblock body_block %}
{% block extra_js_files %}
<script src="{{ url_for('static', filename='javascript/plays.js') }}" defer></script>
{% endblock extra_js_files %}

View file

@ -0,0 +1,17 @@
{% extends "base.jinja" %}
{% block body_block %}
<div class="container">
<div id="statistic_row" class="row g-4 ">
</div>
</div>
{% endblock body_block %}
{% block extra_js_files %}
<script src="{{ url_for('static', filename='javascript/statistics.js') }}" defer></script>
{% endblock extra_js_files %}

81
templates/wishlist.jinja Normal file
View file

@ -0,0 +1,81 @@
{% extends "base.jinja" %}
{% block body_block %}
<body onload="loadWishlistedGames()">
<div class="table-responsive">
<table id="wishlist_table1" class="table table-striped boardgame_table">
<thead>
<tr>
<th scope="col">wishlist_priority</th>
<th scope="col" class="no-sort">Thumbnail</th>
<th scope="col">Naam</th>
<th scope="col">Spelers</th>
<th scope="col">Duratie</th>
<th scope="col">Moeilijkheid</th>
</tr>
</thead>
</table>
</div>
<div class="table-responsive">
<table id="wishlist_table2" class="table table-striped boardgame_table">
<thead>
<tr>
<th scope="col">wishlist_priority</th>
<th scope="col" class="no-sort">Thumbnail</th>
<th scope="col">Naam</th>
<th scope="col">Spelers</th>
<th scope="col">Duratie</th>
<th scope="col">Moeilijkheid</th>
</tr>
</tr>
</thead>
</table>
</div>
<div class="table-responsive">
<table id="wishlist_table3" class="table table-striped boardgame_table">
<thead>
<tr>
<th scope="col">wishlist_priority</th>
<th scope="col" class="no-sort">Thumbnail</th>
<th scope="col">Naam</th>
<th scope="col">Spelers</th>
<th scope="col">Duratie</th>
<th scope="col">Moeilijkheid</th>
</tr>
</tr>
</thead>
</table>
</div>
<div class="table-responsive">
<table id="wishlist_table4" class="table table-striped boardgame_table">
<thead>
<tr>
<th scope="col">wishlist_priority</th>
<th scope="col" class="no-sort">Thumbnail</th>
<th scope="col">Naam</th>
<th scope="col">Spelers</th>
<th scope="col">Duratie</th>
<th scope="col">Moeilijkheid</th>
</tr>
</tr>
</thead>
</table>
</div>
</body>
{% endblock body_block %}
{% block extra_js_files %}
<script src="{{ url_for('static', filename='javascript/wishlist.js') }}" defer></script>
{% endblock extra_js_files %}