first backend frontend split

with docker compose file
nextjs something something
also includes migrations
This commit is contained in:
2025-06-20 14:06:00 +02:00
parent e7cc031588
commit dfc242084e
138 changed files with 1592 additions and 13 deletions
+103
View File
@@ -0,0 +1,103 @@
{% load static %}
<html>
<head>
{% include "head.html" %}
</head>
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<div class="window" id="main-window">
<div class="title-bar main-title-bar">
<div class="title-bar-text">
{% block window-title %}{% endblock %}
</div>
<div class="title-bar-controls">
<button aria-label="Minimize" onclick="resetWindow()"></button>
<button aria-label="Restore" onclick="resetWindow()"></button>
<form action="{% url 'oidc_logout' %}" method="post">
{% csrf_token %}
<button aria-label="Close" onclick="hideWindow()"></button>
</form>
</div>
</div>
<div class="window-body">
<menu role="tablist" hx-boost="true" class="multirows">
<li role="tab" {% if request.path == '/vote/' %}aria-selected="true" {% endif %}>
<a href="{% url 'vote' %}">Stemmen</a>
</li>
<li role="tab" {% if request.path == '/track/' %}aria-selected="true" {% endif %}>
<a href="{% url 'nominate' %}">Nomineren</a>
</li>
<li role="tab" {% if request.path == '/overview/' %}aria-selected="true" {% endif %}>
<a href="{% url 'overview' %}">Overzicht</a>
</li>
<li role="tab" {% if request.path == '/settings/' %}aria-selected="true" {% endif %}>
<a href="{% url 'settings' %}">Instellingen</a>
</li>
</menu>
{% block multi-row %}{% endblock %}
<div class="window" role="tabpanel">
<div class="window-body">
{% if message_type %}
<script>
function hidePopup() {
document.getElementById("info-window").classList.add("hide");
}
</script>
<div class="window popup" id="info-window">
<div class="title-bar">
<div class="title-bar-text">
{% if message_title %}{{message_title}}{% else %}{{message_type | title}}{% endif %}
</div>
<div class="title-bar-controls">
<button aria-label="Close" onclick="hidePopup()"></button>
</div>
</div>
<div class="window-body">
<div class="icon {{message_type}}"></div>
<div class="text">
<div>
{{message}}
</div>
<section class="field-row" style="justify-content: flex-end">
<button onclick="hidePopup()">Ok</button>
</section>
</div>
</div>
</div>
{% endif %}
{% block main-content %}{% endblock %}
</div>
</div>
</div>
{% block status-bar %}{% endblock%}
</div>
{% block clippy %}{% endblock %}
<script>
position.get_pos(); // Gets position from localStorage
clippy = document.getElementById("clippy")
window.addEventListener('load', function () {
interact('.main-title-bar').draggable({
listeners: {
move (event) {
position.x += event.dx
position.y += event.dy
event.target.parentElement.style.transform = `translate(${position.x}px, ${position.y}px)`
if (clippy) document.getElementById("clippy").style.transform = `translate(${position.x}px, ${position.y}px)`
},
}
})
})
function hideWindow() {
document.getElementById("main-window").classList.add("hide");
}
function resetWindow() {
position.reset()
document.getElementById("main-window").style.transform = '';
if (clippy) document.getElementById("clippy").style.transform = '';
}
</script>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
{% load static %}
{% if body %}
<script>
function closeclippy(e) {
console.log(e)
e.parentElement.classList.add("hide")
}
</script>
<div id="clippy" class="hide">
<div class="body">
<div class="content">{{ body }}</div>
<br /><button onclick="closeclippy(this)">Ok</button>
</div>
{% if img %}
<img src="{% static img %}" />
{% endif %}
</div>
{% endif %}
+66
View File
@@ -0,0 +1,66 @@
{% load static %}
<html>
<head>
{% include "head.html" %}
</head>
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<script>
a = new Audio("{% static 'startup.wav' %}");
a.volume = 0.5;
a.play();
</script>
<div class="window popup login" id="info-window">
<div class="title-bar">
<div class="title-bar-text">Welkom bij Muzak</div>
<div class="title-bar-controls">
<button aria-label="Close"></button>
</div>
</div>
<div class="window-body">
<div class="icon login"></div>
<div class="text">
<div>
<p>Gebruik onderstaande knop om in te loggen.</p>
<p>Als je geen inloggegevens hebt, meld je dan bij de beheerder van deze site.</p>
<div class="field-row">
<form action="{% url 'oidc_authentication_init' %}" method="get">
<input type="submit" value="Inloggen" class="default">
</form>
<button onclick="bsod()">Annuleren</button>
<script>
function bsod() {
var css = document.createElement("link")
css.setAttribute("rel", "stylesheet")
css.setAttribute("type", "text/css")
css.setAttribute("href", "{% static 'bsod.css' %}")
document.getElementsByTagName("head")[0].appendChild(css)
}
</script>
</div>
</div>
</div>
</div>
</div>
<div id="bsod">
<h1>Lekkerste recept voor suikerbrood</h1>
<div>* 500 gram bloem</div>
<div>* 1 zakje gist</div>
<div>* 1 theelepel kaneeel</div>
<div>* 3 eetlepels gembersiroop</div>
<div>* 200 ml melk</div>
<div>* 250 gram kandijsuiker</div>
<div>
Neem je bloem en zakje gist en meng dit samen met je kaneel in een kom.
Maak een kuiltje in het midden en schenk er gembersiroop in en lauwwarme, niet te hete, melk.
Meng dit tot deeg, voeg een snufje zout toe en laat dat 15 minuten staan.
Kneed het deeg tot een bal en laat het een uur rijzen onder een vochtige theedoek.
Maak van het gerezen deeg een dikke lap en verdeel er je suiker over.
Rol het geheel weer op en doe het over de lengte in een ingeboterde en ingesuikerde cakevorm.
Laat het nog een uurtje rijzen.
Bak het in ongeveer 30 minuten in een voorverwarmde oven op 200 graden.
Voilà. Suikerbrood. Eet smakelijk.
</div>
<div class="continue">Druk op F5 om verder te gaan..</div>
</div>
</body>
</html>
+112
View File
@@ -0,0 +1,112 @@
{% load static %}
<div id="embed"></div>
<div id="album">
{% if track.album.image %}
<img src="{{track.album.image.url}}" />
{% elif track.album.image_url %}
<img src="{{track.album.image_url}}" />
{% else %}
<img src="https://kagi.com/proxy/windows-95-logo-png-1.png?c=xjdP8sWN58YWflc6NcGN5ZvzREJyOwtrou-TSsaXxcx37J0VVJbSeycOcAptMJEUpzzvqmbKQ3JRuSmPN8HCK43OxjkRe_cQvncxVzlCblc%3D" />
{% endif %}
<h1 title="{{track.name}}">{{track.name}}</h1>
<h2 title="{{track.artist}}">{{track.artist}}</h2>
</div>
<input id="seek" type="range" min="0" step="0.1" max="100" value="0" />
<div id="media-control" class="waiting">
<button id="play"></button>
<button id="pause"></button>
<button id="stop"></button>
<div class="spacer"></div>
<button id="prev" class="dummy"></button>
<button id="back" class="dummy"></button>
<button id="forward" class="dummy"></button>
<button id="next" class="dummy"></button>
<div class="spacer"></div>
<button id="eject" class="dummy"></button>
<div class="spacer"></div>
<input id="volume" type="range" min="1" max="11" value="5" />
</div>
<audio id="kankerherrie" loop src="{% static 'kankerherrie.mp3' %}"></audio>
<script src="https://open.spotify.com/embed/iframe-api/v1"></script>
<script>
window.onSpotifyIframeApiReady = doSpotify;
doSpotify();
function doSpotify (IFrameAPI) {
if (IFrameAPI) {
ifa = IFrameAPI;
} else if (!ifa) return;
let element = document.getElementById('embed');
let playing = false;
let playingPosition = 0;
let duration = {{track.duration_ms}};
let options = {
uri: 'spotify:track:{{track.id}}',
height: 152
};
const callback = (EmbedController) => {
document.getElementById("play").addEventListener('click', () => {
EmbedController.play();
playing = true;
if (playingPosition) {
setTimeout(() => {
EmbedController.seek(parseInt(playingPosition/1000));
playingPosition = 0
}, 100)
}
});
function pause() {
if (playing) {
EmbedController.togglePlay();
playing = false;
}
}
document.getElementById("pause").addEventListener('click', pause);
document.getElementById("stop").addEventListener('click', () => {
setTimeout(() => {
playingPosition = 0;
EmbedController.seek(0);
document.getElementById("seek").value = 0;
}, 500);
pause();
});
EmbedController.addListener('ready', e => {
document.getElementById('media-control').classList.remove('waiting')
document.getElementById("seek").addEventListener('change', (e) => {
let percentile = parseFloat(e.target.value)/100;
EmbedController.seek(duration/1000*percentile);
});
});
EmbedController.addListener('playback_update', e => {
duration = e.data.duration;
if (playingPosition == 0) document.getElementById('seek').value = `${parseInt(e.data.position) / parseInt(e.data.duration) * 100}`;
if (e.data.isPaused) {
playingPosition = e.data.position;
}
else if (playingPosition != 0 && e.data.position != 0) {
EmbedController.seek(parseInt(playingPosition/1000));
playingPosition = 0;
}
})
};
ifa.createController(element, options, callback);
iframe = document.getElementsByTagName("iframe")[0]
iframe.addEventListener('load', (e) => {
console.log(e);
iframe.classList.add('hide');
});
};
document.getElementById("volume").addEventListener('input', (event) => {
let kankerherrie = document.getElementById("kankerherrie")
console.log(event);
let volume = event.target.value / 11;
event.target.classList.add("playing");
event.target.classList.remove("muted");
kankerherrie.play();
if (volume <= 1/11) {
kankerherrie.pause();
event.target.classList.remove("playing");
event.target.classList.add("muted");
}
kankerherrie.volume = volume;
})
</script>
+45
View File
@@ -0,0 +1,45 @@
{% extends "Base.html" %}
{% block window-title %}
Nomineren
{% endblock %}
{% block main-content %}
<form method="post">
{% csrf_token %}
{% if user.profile.can_nominate.0 %}
<p>Vul een spotify link in om een nummer te nomineren.</p>
<input type="text" id="spotify-input" name="spotify_link" placeholder="Spotify-link">
<input type="submit" value="Insturen" {% if user.profile.quota < 1%}disabled{%endif%}>
{% elif user.profile.can_nominate.1 == 'quota' %}
<p>Je quotum is op. Je zal helaas eventjes moeten wachten met nomineren.</p>
{% elif user.profile.can_nominate.1 == 'over' %}
<p>Het nomineren is voorbij voor dit jaar! Bedankt voor de inzendingen.</p>
{% endif %}
</form>
{% endblock %}
{% block status-bar%}
<div class="status-bar">
<div
class="status-bar-field progress-indicator segmented"
title="{{user.profile.quota | floatformat:2}} nominaties over."
>
<span class="progress-indicator-bar" style="width: calc({{user.profile.quota | floatformat:0}} * 18px);" />
</div>
<p class="status-bar-field">{{user.profile.quota_display}} nominaties over.</p>
</div>
<div class="status-bar" id="countdown">
<p class="status-bar-field"><span id="days"></span> dagen</p>
<p class="status-bar-field"><span id="hours"></span> uur</p>
<p class="status-bar-field"><span id="minutes"></span> minuten</p>
<p class="status-bar-field"><span id="seconds"></span> seconden</p>
</div>
<script>
document.body.addEventListener('htmx:load', function(evt) {
//startCountdown("2024-06-25T23:59:59+02:00");
startCountdown("{{user.profile.nominate_end}}");
});
</script>
{% endblock %}
+45
View File
@@ -0,0 +1,45 @@
{% extends "Base.html" %}
{% block window-title %}
Overzicht
{% endblock %}
{% block multi-row %}
{% comment %} <menu role="tablist" hx-boost="true" class="multirows">
<li role="tab" aria-selected="true">
<a href="{% url 'overview' %}">Tracks</a>
</li>
<li role="tab">
<a href="{% url 'playlist-list' %}">Playlists</a>
</li>
</menu> {% endcomment %}
{% endblock %}
{% block main-content %}
<p>Onderstaand alle nummers waar je al op gestemd hebt.</p>
<ul class="tree-view overview">
{% for artist, albums in artists.items %}
<li>
<details>
<summary>{{artist}}</summary>
<ul>
{% for album, tracks in albums.items %}
<li>
<details>
<summary>{{album}}</summary>
<ul>
{% for track in tracks %}
<li>
<a href="{{track.link}}" target="_blank">{{track.name}}</a>
</li>
{% endfor %}
</ul>
</details>
</li>
{% endfor %}
</ul>
</details>
</li>
{% endfor %}
</ul>
{% endblock %}
@@ -0,0 +1,22 @@
{% extends "Base.html" %}
{% block window-title %}
Playlist -
{% endblock %}
{% block main-content %}
{% if tracks %}
{% for track in tracks %}
<p>{{track}}: {{track.score}}</p>
{% endfor %}
{% else %}
<form method="post">
{% csrf_token %}
Hoe veel nummers wil je?
<input type="number" id="count" name="count">
<input type="submit" value="OK" >
</form>
{% endif %}
{% endblock %}
@@ -0,0 +1,9 @@
{% extends "Base.html" %}
{% block window-title %}
Overzicht - Playlists
{% endblock %}
{% block main-content %}
{% endblock %}
+17
View File
@@ -0,0 +1,17 @@
{% load static %}
<script>
function hidePopup() {
document.getElementById("info-window").classList.add("hide");
}
</script>
<div class="window popup" id="info-window">
<div class="title-bar">
<div class="title-bar-text">
{{title}}
<button aria-label="Close" onclick="hidePopup()"></button>
</div>
</div>
<div class="window-body">
{{body}}
</div>
</div>
+74
View File
@@ -0,0 +1,74 @@
{% extends "Base.html" %}
{% block window-title %}
Instellingen
{% endblock %}
{% block main-content %}
<p>Je bent ingelogd als <strong>{{ user.email }}</strong>.</p>
{% if user.is_staff %}
<form action="{% url 'admin:index' %}">
<div class="field-row settings">
<label>Administratieve achterkant:</label>
<input type="submit" value="" class="icon admin-panel" />
</div>
</form>
<form action="{% url 'playlist-import' %}" method="post">
{% csrf_token %}
<p>Geef een playlist url om oude nummers te importeren:</p>
<input type="text" id="spotify-input" name="playlist_link" placeholder="Spotify-link">
<input type="submit" value="Importeer">
</form>
{% comment %} {% if sptDisplayName %}
<form method="post">
{% csrf_token %}
<div class="field-row settings">
<label>Spotify gekoppeld:</label>
<span>{{sptDisplayName}}</span>
<input type="hidden" id="unlink-spotify" name="unlink-spotify" value="true" />
<input type="submit" id="spotify-cancel" value=""/>
</div>
</form>
{% else %}
<form action="{% url 'spotify-callback' %}">
<div class="field-row settings">
<label>Link je spotify:</label>
<input type="submit" value="" class="icon spotify" />
</div>
</form>
{% endif %} {% endcomment %}
{% endif %}
<form method="post">
{% csrf_token %}
{% if backgrounds %}
<div class="field-row settings">
<label for="wallpaper">Wallpaper:</label>
<select id="background" name="background" onchange="this.form.submit()">
{% for background in backgrounds %}
<option
value="{{background.id}}"
{% if background.id == currentBackground %} selected{% endif %}
>
{{background.name}}
</option>
{% endfor %}
</select>
</div>
{% endif %}
</form>
<div class="field-row settings">
<label>Nominatie quota:</label>
<span>{{user.profile.quota|floatformat:3}}</span>
</div>
<div id="average-stars" style="margin-top:10px;">
<fieldset>
<legend>Hoeveel sterren geef jij gemiddeld?</legend>
<div class="stars">
<div class="stars-overlay" style="width: calc({{average}} * 32px)">
</div>
</div>
</fieldset>
</div>
</div>
{% endblock %}
+95
View File
@@ -0,0 +1,95 @@
{% extends "Base.html" %}
{% block window-title %}
Stemmen{% if vote_left or skip_left %}: {{vote_left|add:skip_left}} te gaan.{% endif %}
{% endblock %}
{% block main-content %}
{% if user.profile.can_vote %}
{% if track %}
<div id="vote-box" hx-target="body">
{% if error %}<div class="error">{{ error }}</div>{% endif %}
{% include 'MediaPlayer.html' with track=track %}
<div
id="vote-stars"
hx-vals='{"spotify_id": "{{ track.id }}"}'
style="margin-top:10px;"
>
<fieldset>
<legend>Hoeveel sterren geef jij dit?</legend>
<div class="star-wrapper">
<a
hx-vals='{"vote": "1"}'
hx-post="{% url 'vote' %}"
></a>
<a
hx-vals='{"vote": "2"}'
hx-post="{% url 'vote' %}"
></a>
<a
hx-vals='{"vote": "3"}'
hx-post="{% url 'vote' %}"
></a>
<a
hx-vals='{"vote": "4"}'
hx-post="{% url 'vote' %}"
></a>
<a
hx-vals='{"vote": "5"}'
hx-post="{% url 'vote' %}"
></a>
</div>
</fieldset>
<div class="skip-wrapper">
{% if not skipped %}
{% csrf_token %}
<button class="skip" hx-post="{% url 'skip' %}" >Skip</button>
{% endif %}
</div>
<div class="undo-wrapper" hx-vals='{"spotify_id": "{{ undo.track.id }}"}' >
{% if undo %}
{% csrf_token %}
<p>Je hebt {{undo.track}} een score van {{undo.points}} sterren gegeven!</p>
<button class="undo" hx-post="{% url 'undo' %}" >Undo</button>
{% endif %}
</div>
</div>
</div>
{% else %}
<p hx-boost="true">
Je hebt niets meer om te stemmen. Lekker bezig!
<a href="{% url 'nominate' %}">Nomineer zelf iets</a>, als je nog quota hebt.
</p>
{% if undo %}
<!--<form class="undo-wrapper" method="post" action="{% url 'undo' %}" hx-vals='{"spotify_id": "{{ undo.track.id }}"}' >-->
<div class="undo-wrapper" hx-target="body" hx-vals='{"spotify_id": "{{ undo.track.id }}"}' >
{% csrf_token %}
<p>Je hebt {{undo.track}} een score van {{undo.points}} sterren gegeven!</p>
<button class="undo" hx-post="{% url 'undo' %}" >Undo</button>
</div>
{% endif %}
{% endif %}
{% else %}
<p> Stemmen is voorbij. Joe.</p>
{% endif %}
{% endblock %}
{% block clippy %}
{% include "Clippy.html" with body=track.banter img="img/draakje/draakje-headphones3.gif" %}
{% endblock %}
{% block status-bar%}
<div class="status-bar" id="countdown">
<p class="status-bar-field"><span id="days"></span> dagen</p>
<p class="status-bar-field"><span id="hours"></span> uur</p>
<p class="status-bar-field"><span id="minutes"></span> minuten</p>
<p class="status-bar-field"><span id="seconds"></span> seconden</p>
</div>
<script>
document.body.addEventListener('htmx:load', function(evt) {
//startCountdown("2024-08-25T23:59:59+02:00");
startCountdown("{{user.profile.vote_end}}");
});
</script>
{% endblock %}
+16
View File
@@ -0,0 +1,16 @@
{% load static %}
body {
{% if background.image %}
background-image: url('{{background.image.url}}');
{% endif %}
{% if not background.repeat %}
background-repeat: no-repeat;
{% endif %}
{% if background.cover %}
background-size: cover;
{% endif %}
{% if background.color %}
background-color: {{background.color}} !important;
{% endif %}
}
+59
View File
@@ -0,0 +1,59 @@
{% load static %}
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="{% static 'js/htmx.min.js' %}" defer></script>
<script src="{% static 'js/interact.js' %}" defer></script>
<script src="{% static 'js/countdown.js' %}" defer></script>
<link href="{% static '98.css/style.css' %}" rel="stylesheet" type="text/css" />
<link href="{% static 'style.css'%}" rel="stylesheet" type="text/css" />
<style>
{% include 'background.css' with background=user.profile.background %}
</style>
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'apple-touch-icon.png' %}">
<link rel="icon" type="image/png" sizes="32x32" href="{% static 'favicon-32x32.png' %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'favicon-16x16.png' %}">
<link rel="manifest" href="/site.webmanifest">
<title>HD Muzak</title>
<script>
//Global position object for cross-page window positioning
position = {
set x(val) {
this._x = val
localStorage.setItem("position", JSON.stringify({x: val, y: this._y}));
},
set y(val) {
this._y = val
localStorage.setItem("position", JSON.stringify({x: this._x, y: val}));
},
get x() { return this._x },
get y() { return this._y },
_x: 0, _y: 0,
get_pos() {
stored_position = JSON.parse(localStorage.getItem("position"));
if (stored_position == null) {
localStorage.removeItem("position")
position.x = 0;
position.y = 0;
} else {
position.x = stored_position.x
position.y = stored_position.y
}
document.getElementById("main-window").style.transform =
`translate(${position.x}px, ${position.y}px)`
clippy = document.getElementById("clippy")
if (clippy) {
clippy.style.transform =
`translate(${position.x}px, ${position.y}px)`
clippy.classList.remove('hide');
}
},
reset() {
this._x = 0
this._y = 0
localStorage.removeItem("position")
}
}
//global spotify IFrameAPI cache
ifa = null;
</script>