9 Commits

6 changed files with 96 additions and 84 deletions

View File

@@ -2,45 +2,46 @@
# uv pip compile requirements-server.in # uv pip compile requirements-server.in
annotated-types==0.7.0 annotated-types==0.7.0
# via pydantic # via pydantic
anyio==4.6.2.post1 anyio==4.9.0
# via # via
# httpx # httpx
# starlette # starlette
# watchfiles # watchfiles
certifi==2024.8.30 certifi==2025.4.26
# via # via
# httpcore # httpcore
# httpx # httpx
click==8.1.7 click==8.1.8
# via # via
# rich-toolkit
# typer # typer
# uvicorn # uvicorn
dnspython==2.7.0 dnspython==2.7.0
# via email-validator # via email-validator
email-validator==2.2.0 email-validator==2.2.0
# via fastapi # via fastapi
fastapi==0.115.5 fastapi==0.115.12
# via -r requirements.in # via -r requirements.in
fastapi-cli==0.0.5 fastapi-cli==0.0.7
# via fastapi # via fastapi
gunicorn==23.0.0 gunicorn==23.0.0
# via -r requirements-server.in # via -r requirements-server.in
h11==0.14.0 h11==0.16.0
# via # via
# httpcore # httpcore
# uvicorn # uvicorn
httpcore==1.0.7 httpcore==1.0.9
# via httpx # via httpx
httptools==0.6.4 httptools==0.6.4
# via uvicorn # via uvicorn
httpx==0.27.2 httpx==0.28.1
# via fastapi # via fastapi
idna==3.10 idna==3.10
# via # via
# anyio # anyio
# email-validator # email-validator
# httpx # httpx
jinja2==3.1.4 jinja2==3.1.6
# via fastapi # via fastapi
markdown-it-py==3.0.0 markdown-it-py==3.0.0
# via rich # via rich
@@ -48,51 +49,59 @@ markupsafe==3.0.2
# via jinja2 # via jinja2
mdurl==0.1.2 mdurl==0.1.2
# via markdown-it-py # via markdown-it-py
packaging==24.2 packaging==25.0
# via gunicorn # via gunicorn
pydantic==2.10.1 pydantic==2.11.4
# via # via
# fastapi # fastapi
# pydantic-settings # pydantic-settings
pydantic-core==2.27.1 pydantic-core==2.33.2
# via pydantic # via pydantic
pydantic-settings==2.6.1 pydantic-settings==2.9.1
# via -r requirements.in # via -r requirements.in
pygments==2.18.0 pygments==2.19.1
# via rich # via rich
python-dotenv==1.0.1 python-dotenv==1.1.0
# via # via
# pydantic-settings # pydantic-settings
# uvicorn # uvicorn
python-multipart==0.0.17 python-multipart==0.0.20
# via fastapi # via fastapi
pyyaml==6.0.2 pyyaml==6.0.2
# via uvicorn # via uvicorn
rich==13.9.4 rich==14.0.0
# via typer # via
# rich-toolkit
# typer
rich-toolkit==0.14.6
# via fastapi-cli
shellingham==1.5.4 shellingham==1.5.4
# via typer # via typer
sniffio==1.3.1 sniffio==1.3.1
# via # via anyio
# anyio starlette==0.46.2
# httpx
starlette==0.41.3
# via fastapi # via fastapi
typer==0.13.1 typer==0.15.4
# via fastapi-cli # via fastapi-cli
typing-extensions==4.12.2 typing-extensions==4.13.2
# via # via
# fastapi # fastapi
# pydantic # pydantic
# pydantic-core # pydantic-core
# rich-toolkit
# typer # typer
uvicorn==0.32.1 # typing-inspection
typing-inspection==0.4.0
# via
# pydantic
# pydantic-settings
uvicorn==0.34.2
# via # via
# fastapi # fastapi
# fastapi-cli # fastapi-cli
uvloop==0.21.0 uvloop==0.21.0
# via uvicorn # via uvicorn
watchfiles==0.24.0 watchfiles==1.0.5
# via uvicorn # via uvicorn
websockets==14.1 websockets==15.0.1
# via uvicorn # via uvicorn

View File

@@ -2,43 +2,44 @@
# uv pip compile requirements.in # uv pip compile requirements.in
annotated-types==0.7.0 annotated-types==0.7.0
# via pydantic # via pydantic
anyio==4.6.2.post1 anyio==4.9.0
# via # via
# httpx # httpx
# starlette # starlette
# watchfiles # watchfiles
certifi==2024.8.30 certifi==2025.4.26
# via # via
# httpcore # httpcore
# httpx # httpx
click==8.1.7 click==8.1.8
# via # via
# rich-toolkit
# typer # typer
# uvicorn # uvicorn
dnspython==2.7.0 dnspython==2.7.0
# via email-validator # via email-validator
email-validator==2.2.0 email-validator==2.2.0
# via fastapi # via fastapi
fastapi==0.115.5 fastapi==0.115.12
# via -r requirements.in # via -r requirements.in
fastapi-cli==0.0.5 fastapi-cli==0.0.7
# via fastapi # via fastapi
h11==0.14.0 h11==0.16.0
# via # via
# httpcore # httpcore
# uvicorn # uvicorn
httpcore==1.0.7 httpcore==1.0.9
# via httpx # via httpx
httptools==0.6.4 httptools==0.6.4
# via uvicorn # via uvicorn
httpx==0.27.2 httpx==0.28.1
# via fastapi # via fastapi
idna==3.10 idna==3.10
# via # via
# anyio # anyio
# email-validator # email-validator
# httpx # httpx
jinja2==3.1.4 jinja2==3.1.6
# via fastapi # via fastapi
markdown-it-py==3.0.0 markdown-it-py==3.0.0
# via rich # via rich
@@ -46,49 +47,57 @@ markupsafe==3.0.2
# via jinja2 # via jinja2
mdurl==0.1.2 mdurl==0.1.2
# via markdown-it-py # via markdown-it-py
pydantic==2.10.1 pydantic==2.11.4
# via # via
# fastapi # fastapi
# pydantic-settings # pydantic-settings
pydantic-core==2.27.1 pydantic-core==2.33.2
# via pydantic # via pydantic
pydantic-settings==2.6.1 pydantic-settings==2.9.1
# via -r requirements.in # via -r requirements.in
pygments==2.18.0 pygments==2.19.1
# via rich # via rich
python-dotenv==1.0.1 python-dotenv==1.1.0
# via # via
# pydantic-settings # pydantic-settings
# uvicorn # uvicorn
python-multipart==0.0.17 python-multipart==0.0.20
# via fastapi # via fastapi
pyyaml==6.0.2 pyyaml==6.0.2
# via uvicorn # via uvicorn
rich==13.9.4 rich==14.0.0
# via typer # via
# rich-toolkit
# typer
rich-toolkit==0.14.6
# via fastapi-cli
shellingham==1.5.4 shellingham==1.5.4
# via typer # via typer
sniffio==1.3.1 sniffio==1.3.1
# via # via anyio
# anyio starlette==0.46.2
# httpx
starlette==0.41.3
# via fastapi # via fastapi
typer==0.13.1 typer==0.15.4
# via fastapi-cli # via fastapi-cli
typing-extensions==4.12.2 typing-extensions==4.13.2
# via # via
# fastapi # fastapi
# pydantic # pydantic
# pydantic-core # pydantic-core
# rich-toolkit
# typer # typer
uvicorn==0.32.1 # typing-inspection
typing-inspection==0.4.0
# via
# pydantic
# pydantic-settings
uvicorn==0.34.2
# via # via
# fastapi # fastapi
# fastapi-cli # fastapi-cli
uvloop==0.21.0 uvloop==0.21.0
# via uvicorn # via uvicorn
watchfiles==0.24.0 watchfiles==1.0.5
# via uvicorn # via uvicorn
websockets==14.1 websockets==15.0.1
# via uvicorn # via uvicorn

View File

@@ -2,6 +2,7 @@
import logging import logging
from datetime import date, datetime, timezone from datetime import date, datetime, timezone
from typing import Union from typing import Union
from zoneinfo import ZoneInfo
from fastapi import FastAPI, Request, HTTPException from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
@@ -11,7 +12,9 @@ from pydantic import DirectoryPath, FilePath
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
VERSION = '0.3.0' VERSION = '0.3.2'
AMSTERDAM = ZoneInfo('Europe/Amsterdam')
class Settings(BaseSettings): class Settings(BaseSettings):
@@ -54,15 +57,15 @@ if settings.debug:
def get_game_id(): def get_game_id():
"""Calculate the index for the game/word we are handling today.""" """Calculate the index for the game/word we are handling today."""
today = datetime.now(timezone.utc).date() today = datetime.now(tz=AMSTERDAM).date()
# Calculate the amount of days since the start of the games so we know which word is used today # Calculate the amount of days since the start of the games so we know which word is used today
return (today - settings.start_date).days return (today - settings.start_date).days
def get_game_deadline(): def get_game_deadline():
"""Calculate the amount of time left for the current game.""" """Calculate the amount of time left for the current game."""
this_moment = datetime.now(timezone.utc) this_moment = datetime.now(tz=AMSTERDAM)
midnight = datetime.now(timezone.utc).replace(hour=23, minute=59, second=59, microsecond=0) midnight = datetime.now(tz=AMSTERDAM).replace(hour=23, minute=59, second=59, microsecond=0)
# Calculate the amount of time left till midnight (and the start of the next game) # Calculate the amount of time left till midnight (and the start of the next game)
return midnight - this_moment return midnight - this_moment

View File

@@ -33,12 +33,13 @@ a.title {
padding: 2rem 0 0 0; padding: 2rem 0 0 0;
} }
.guessesheading { .guessesheading, .copied {
color: #CCC;; color: #CCC;
} }
.guessesheading, .guessesbefore, .guessesafter { .guessesheading, .guessesbefore, .guessesafter {
text-align: center; text-align: center;
clear: both;
} }
input[type="text"] { input[type="text"] {

View File

@@ -24,11 +24,13 @@ document.addEventListener('alpine:init', () => {
resultGuesses: Alpine.$persist('').as('resultGuesses'), resultGuesses: Alpine.$persist('').as('resultGuesses'),
resultTimeTaken: Alpine.$persist('').as('resultTimeTaken'), resultTimeTaken: Alpine.$persist('').as('resultTimeTaken'),
resultsCopied: false,
async init() { async init() {
/** Initialise the application after loading */ /** Initialise the application after loading */
await this.getGameID(); await this.getGameID();
setInterval(() => { setInterval(() => {
// Update counter to next game (midnight UTC, fetched from API) every second // Update counter to the next game (midnight UTC, fetched from API) every second
this.countDownTimer(); this.countDownTimer();
}, 1000); }, 1000);
}, },
@@ -56,7 +58,7 @@ document.addEventListener('alpine:init', () => {
/** Handle the newly entered guess */ /** Handle the newly entered guess */
this.guessError = null; this.guessError = null;
/* Normalise on lowercase, and strip whitespace from begin and end, just in case */ /* Normalise on lowercase, and strip whitespace from start and end, just in case */
this.guessValue = this.guessValue.toLowerCase().trim(); this.guessValue = this.guessValue.toLowerCase().trim();
if (this.guessValue === '') { if (this.guessValue === '') {
@@ -108,7 +110,6 @@ document.addEventListener('alpine:init', () => {
this.resultGuesses = '🤔 '+ this.nrGuesses + ' gokken'; this.resultGuesses = '🤔 '+ this.nrGuesses + ' gokken';
let winTimeDate = new Date(this.winTime); let winTimeDate = new Date(this.winTime);
let startTimeDate = new Date(this.startTime); let startTimeDate = new Date(this.startTime);
// this.resultTimeTaken = '⏱️ ' + getFormattedTime(this.winTime - this.startTime);
this.resultTimeTaken = '⏱️ ' + this.getFormattedTime(winTimeDate - startTimeDate); this.resultTimeTaken = '⏱️ ' + this.getFormattedTime(winTimeDate - startTimeDate);
} }
}, },
@@ -131,7 +132,7 @@ document.addEventListener('alpine:init', () => {
this.resultTimeTaken = ''; this.resultTimeTaken = '';
}, },
getFormattedTime(milliseconds) { getFormattedTime(milliseconds) {
/** Nicely format time for 'time played' */ /** Format the time for 'time played' nicely */
if (!Number.isInteger(milliseconds)) { if (!Number.isInteger(milliseconds)) {
return ''; return '';
} }
@@ -190,24 +191,14 @@ document.addEventListener('alpine:init', () => {
}); });
/* Clipboard stuff **/
let clip = new ClipboardJS('.copy');
clip.on("success", function(e) {
document.getElementById('copyresults').innerHTML = '<p style="font-size:var(--small);opacity:50%">Gekopieerd! Deel je resultaat.</p>';
e.clearSelection();
});
clip.on("error", function() {
document.getElementById('copyresults').innerHTML = '<p style="font-size:var(--small);opacity:50%">Fout. Graag handmatig kopi&euml;ren...</p>';
});
/* Get current gameID etc **/
// document.addEventListener('alpine:initialized', () => { // document.addEventListener('alpine:initialized', () => {
/* On AlpineJS completely loaded, do all this */ /* On AlpineJS completely loaded, do all this */
// Alpine.store('alfagok').getGameID(); // Alpine.store('alfagok').getGameID();
// }) // })
document.onvisibilitychange = () => {
if (!document.hidden) {
/* Tab/window is shown again, possibly after a long time, check if game is still current, otherwise re-load */
Alpine.store('alfagok').getGameID();
}
};

View File

@@ -12,6 +12,7 @@
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
<link rel="manifest" href="/static/images/site.webmanifest"> <link rel="manifest" href="/static/images/site.webmanifest">
<script src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@ryangjchandler/alpine-clipboard@2.x.x/dist/alpine-clipboard.js" defer></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head> </head>
<body> <body>
@@ -49,8 +50,8 @@
<p>🔗 <span class="link">alfagok.diginaut.net</span></p> <p>🔗 <span class="link">alfagok.diginaut.net</span></p>
</div> </div>
</div> </div>
<div id="copyresults"></div> <div class="copied" x-show="$store.alfagok.resultsCopied">Gekopieerd! Deel je resultaat.</div>
<button class="copy" data-clipboard-target="#results"> <button class="copy" @click="$clipboard($store.alfagok.resultGameID + '\n' + $store.alfagok.resultGuesses + '\n' + $store.alfagok.resultTimeTaken + '\n' + '🔗 alfagok.diginaut.net'); $store.alfagok.resultsCopied = true">
Tik om te kopi&euml;ren en te delen ❤️ Tik om te kopi&euml;ren en te delen ❤️
</button> </button>
</div> </div>
@@ -64,12 +65,10 @@
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/clipboard@2.0.11/dist/clipboard.min.js"></script>
<script src="/static/game.js?v={{ version }}"></script> <script src="/static/game.js?v={{ version }}"></script>
{# {#
<button x-data @click="$store.darkMode.toggle()">Toggle Dark Mode</button> <button x-data @click="$store.darkMode.toggle()">Toggle Dark Mode</button>
<div x-data :class="$store.darkMode.on && 'bg-black'">...</div> <div x-data :class="$store.darkMode.on && 'bg-black'">...</div>
#} #}
</body> </body>
</html> </html>