14 Commits

Author SHA1 Message Date
diginaut 11a0d166a1 v0.3.4 2025-10-22 10:12:10 +02:00
diginaut e3fcfc1dd1 Latest requirements 2025-10-22 10:10:07 +02:00
diginaut c2bcff8c9a Merge pull request #2 from jcorbin/patch-1
Make. Input. Auto. Focus.
2025-10-22 10:08:43 +02:00
Joshua T Corbin 0ae5b53a54 Make. Input. Auto. Focus. 2025-10-21 20:40:33 -04:00
diginaut bea557127d Use unidecode for accent-less word comparison; latest requirements 2025-09-24 12:04:50 +02:00
diginaut 70f6f6546a Keep track of 'tab/window' shown events to see if game needs reloading 2025-05-20 22:20:10 +02:00
diginaut d71a35402d Latest requirements 2025-05-20 21:58:03 +02:00
diginaut 1adc7b0887 Make sure latest CSS is loaded 2025-02-12 12:04:28 +01:00
diginaut 4e914672e9 Make sure text stays on a line after floating elements 2025-02-12 12:02:15 +01:00
diginaut bf9a5320dc Latest requirements 2025-02-09 11:55:19 +01:00
diginaut a00b836f7b Use Amsterdam timezone as this is Dutch ^_^ 2024-12-27 15:56:28 +01:00
diginaut 0727b4c9e2 Latest requirements 2024-12-27 15:56:04 +01:00
diginaut 7912c5c897 Small cleanups 2024-11-24 21:51:33 +01:00
diginaut 34db4e34e7 Use AlpineJS clipboard plugin, eliminating empty lines in copy 2024-11-24 21:40:48 +01:00
7 changed files with 123 additions and 104 deletions
+46 -35
View File
@@ -2,97 +2,108 @@
# 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.11.0
# via # via
# httpx # httpx
# starlette # starlette
# watchfiles # watchfiles
certifi==2024.8.30 certifi==2025.10.5
# via # via
# httpcore # httpcore
# httpx # httpx
click==8.1.7 click==8.3.0
# via # via
# rich-toolkit
# typer # typer
# uvicorn # uvicorn
dnspython==2.7.0 dnspython==2.8.0
# via email-validator # via email-validator
email-validator==2.2.0 email-validator==2.3.0
# via fastapi # via fastapi
fastapi==0.115.5 fastapi==0.119.1
# via -r requirements.in # via -r requirements.in
fastapi-cli==0.0.5 fastapi-cli==0.0.14
# 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.7.1
# via uvicorn # via uvicorn
httpx==0.27.2 httpx==0.28.1
# via fastapi # via fastapi
idna==3.10 idna==3.11
# 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==4.0.0
# via rich # via rich
markupsafe==3.0.2 markupsafe==3.0.3
# 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.12.3
# via # via
# fastapi # fastapi
# pydantic-settings # pydantic-settings
pydantic-core==2.27.1 pydantic-core==2.41.4
# via pydantic # via pydantic
pydantic-settings==2.6.1 pydantic-settings==2.11.0
# via -r requirements.in # via -r requirements.in
pygments==2.18.0 pygments==2.19.2
# via rich # via rich
python-dotenv==1.0.1 python-dotenv==1.1.1
# 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.3
# via uvicorn # via uvicorn
rich==13.9.4 rich==14.2.0
# via typer # via
# rich-toolkit
# typer
rich-toolkit==0.15.1
# 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.48.0
# httpx
starlette==0.41.3
# via fastapi # via fastapi
typer==0.13.1 typer==0.20.0
# via fastapi-cli # via fastapi-cli
typing-extensions==4.12.2 typing-extensions==4.15.0
# via # via
# fastapi # fastapi
# pydantic # pydantic
# pydantic-core # pydantic-core
# rich-toolkit
# typer # typer
uvicorn==0.32.1 # typing-inspection
typing-inspection==0.4.2
# via
# pydantic
# pydantic-settings
unidecode==1.4.0
# via -r requirements.in
uvicorn==0.38.0
# via # via
# fastapi # fastapi
# fastapi-cli # fastapi-cli
uvloop==0.21.0 uvloop==0.22.1
# via uvicorn # via uvicorn
watchfiles==0.24.0 watchfiles==1.1.1
# via uvicorn # via uvicorn
websockets==14.1 websockets==15.0.1
# via uvicorn # via uvicorn
+4 -1
View File
@@ -1,2 +1,5 @@
fastapi[standard] fastapi[standard-no-fastapi-cloud-cli]
pydantic-settings pydantic-settings
# Normalise strings so we can compare without accents
unidecode
+45 -34
View File
@@ -2,93 +2,104 @@
# 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.11.0
# via # via
# httpx # httpx
# starlette # starlette
# watchfiles # watchfiles
certifi==2024.8.30 certifi==2025.10.5
# via # via
# httpcore # httpcore
# httpx # httpx
click==8.1.7 click==8.3.0
# via # via
# rich-toolkit
# typer # typer
# uvicorn # uvicorn
dnspython==2.7.0 dnspython==2.8.0
# via email-validator # via email-validator
email-validator==2.2.0 email-validator==2.3.0
# via fastapi # via fastapi
fastapi==0.115.5 fastapi==0.119.1
# via -r requirements.in # via -r requirements.in
fastapi-cli==0.0.5 fastapi-cli==0.0.14
# 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.7.1
# via uvicorn # via uvicorn
httpx==0.27.2 httpx==0.28.1
# via fastapi # via fastapi
idna==3.10 idna==3.11
# 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==4.0.0
# via rich # via rich
markupsafe==3.0.2 markupsafe==3.0.3
# 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.12.3
# via # via
# fastapi # fastapi
# pydantic-settings # pydantic-settings
pydantic-core==2.27.1 pydantic-core==2.41.4
# via pydantic # via pydantic
pydantic-settings==2.6.1 pydantic-settings==2.11.0
# via -r requirements.in # via -r requirements.in
pygments==2.18.0 pygments==2.19.2
# via rich # via rich
python-dotenv==1.0.1 python-dotenv==1.1.1
# 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.3
# via uvicorn # via uvicorn
rich==13.9.4 rich==14.2.0
# via typer # via
# rich-toolkit
# typer
rich-toolkit==0.15.1
# 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.48.0
# httpx
starlette==0.41.3
# via fastapi # via fastapi
typer==0.13.1 typer==0.20.0
# via fastapi-cli # via fastapi-cli
typing-extensions==4.12.2 typing-extensions==4.15.0
# via # via
# fastapi # fastapi
# pydantic # pydantic
# pydantic-core # pydantic-core
# rich-toolkit
# typer # typer
uvicorn==0.32.1 # typing-inspection
typing-inspection==0.4.2
# via
# pydantic
# pydantic-settings
unidecode==1.4.0
# via -r requirements.in
uvicorn==0.38.0
# via # via
# fastapi # fastapi
# fastapi-cli # fastapi-cli
uvloop==0.21.0 uvloop==0.22.1
# via uvicorn # via uvicorn
watchfiles==0.24.0 watchfiles==1.1.1
# via uvicorn # via uvicorn
websockets==14.1 websockets==15.0.1
# via uvicorn # via uvicorn
+9 -6
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
@@ -9,9 +10,11 @@ from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from pydantic import DirectoryPath, FilePath from pydantic import DirectoryPath, FilePath
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from unidecode import unidecode
VERSION = '0.3.4'
VERSION = '0.3.0' 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
@@ -97,9 +100,9 @@ def handle_guess(word: Union[str, None] = None):
return {'error': 'Word not in dictionary'} return {'error': 'Word not in dictionary'}
hint = 'it' hint = 'it'
if word_of_the_day < word: if word_of_the_day < unidecode(word):
hint = 'before' hint = 'before'
if word_of_the_day > word: if word_of_the_day > unidecode(word):
hint = 'after' hint = 'after'
logger.info('Guess: %s for game %d (%s), goal is %s', word, current_game_id, word_of_the_day, hint) logger.info('Guess: %s for game %d (%s), goal is %s', word, current_game_id, word_of_the_day, hint)
+3 -2
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"] {
+12 -21
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();
}
};
+4 -5
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>
@@ -32,7 +33,7 @@
</ul> </ul>
<div x-show="!$store.alfagok.winTime"> <div x-show="!$store.alfagok.winTime">
<input type="text" autocomplete="new-password" autocorrect="off" x-model="$store.alfagok.guessValue" @keyup.enter="$store.alfagok.doGuess()"> <input type="text" autocomplete="new-password" autocorrect="off" autofocus x-model="$store.alfagok.guessValue" @keyup.enter="$store.alfagok.doGuess()">
{# <p x-cloak>Je huidige gok is: <span x-text="$store.alfagok.guessValue"></span></p>#} {# <p x-cloak>Je huidige gok is: <span x-text="$store.alfagok.guessValue"></span></p>#}
<button @click="$store.alfagok.doGuess()">Doe een gok</button> <button @click="$store.alfagok.doGuess()">Doe een gok</button>
<p class="error" x-cloak x-show="$store.alfagok.guessError" x-text="$store.alfagok.guessError"></p> <p class="error" x-cloak x-show="$store.alfagok.guessError" x-text="$store.alfagok.guessError"></p>
@@ -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>