22 Commits

Author SHA1 Message Date
a00b836f7b Use Amsterdam timezone as this is Dutch ^_^ 2024-12-27 15:56:28 +01:00
0727b4c9e2 Latest requirements 2024-12-27 15:56:04 +01:00
7912c5c897 Small cleanups 2024-11-24 21:51:33 +01:00
34db4e34e7 Use AlpineJS clipboard plugin, eliminating empty lines in copy 2024-11-24 21:40:48 +01:00
ba53552f40 Better async at the start 2024-11-24 21:05:39 +01:00
bb78abe50b Use the server-provided UTC-based countdown timer 2024-11-24 20:59:49 +01:00
8185ca21aa Game version as cache buster for CSS and game JS 2024-11-24 17:06:11 +01:00
9757c27cc9 Do not allow to peak ahead at future words 2024-11-24 17:02:23 +01:00
eab6605c83 Merge pull request #1 from aquatix/savegames
Savegames using Alpine.js persistence
2024-11-24 16:54:09 +01:00
0ae7b8618f Reset game state when new/different game has started 2024-11-24 16:49:00 +01:00
f52ae3cb8f Cleaned up non-Alpine.js persistence code 2024-11-24 16:45:23 +01:00
bbfa172e7a Persist the important game data in localStorage 2024-11-24 16:32:13 +01:00
795ee7aa16 Started work on saving game state 2024-11-23 22:16:47 +01:00
48ccb99492 Restored accidentally removed test-for-localStorage functions 2024-11-23 21:38:42 +01:00
4058dc3a7a Merge branch 'main' into savegames 2024-11-23 21:20:18 +01:00
e7a39becfc Latest requirements 2024-11-23 21:18:18 +01:00
cc20e80bf5 Initial support for server-sourced deadline 2024-11-23 14:05:46 +01:00
622d3d9681 Latest clipboard.js 2024-11-23 13:36:35 +01:00
28ff9368bb var-b-let 2024-11-23 13:24:45 +01:00
1ce340853f Use CSS to style centering the guess lists 2024-11-22 13:05:56 +01:00
20321f0bb4 Use UTC for calculating the current game 2024-11-22 13:05:52 +01:00
f8a3561d9a Use localStorage for restoring saveGame 2024-11-19 22:18:56 +01:00
6 changed files with 222 additions and 179 deletions

View File

@@ -2,26 +2,27 @@
# 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.7.0
# via # via
# httpx # httpx
# starlette # starlette
# watchfiles # watchfiles
certifi==2024.8.30 certifi==2024.12.14
# 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.6
# 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
@@ -33,14 +34,14 @@ httpcore==1.0.7
# 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.5
# via fastapi # via fastapi
markdown-it-py==3.0.0 markdown-it-py==3.0.0
# via rich # via rich
@@ -50,13 +51,13 @@ mdurl==0.1.2
# via markdown-it-py # via markdown-it-py
packaging==24.2 packaging==24.2
# via gunicorn # via gunicorn
pydantic==2.9.2 pydantic==2.10.4
# via # via
# fastapi # fastapi
# pydantic-settings # pydantic-settings
pydantic-core==2.23.4 pydantic-core==2.27.2
# via pydantic # via pydantic
pydantic-settings==2.6.1 pydantic-settings==2.7.0
# via -r requirements.in # via -r requirements.in
pygments==2.18.0 pygments==2.18.0
# via rich # via rich
@@ -64,35 +65,39 @@ python-dotenv==1.0.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.2
# via uvicorn # via uvicorn
rich==13.9.4 rich==13.9.4
# via typer # via
# rich-toolkit
# typer
rich-toolkit==0.12.0
# 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
# httpx
starlette==0.41.3 starlette==0.41.3
# via fastapi # via fastapi
typer==0.13.1 typer==0.15.1
# via fastapi-cli # via fastapi-cli
typing-extensions==4.12.2 typing-extensions==4.12.2
# via # via
# anyio
# fastapi # fastapi
# pydantic # pydantic
# pydantic-core # pydantic-core
# rich-toolkit
# typer # typer
uvicorn==0.32.0 uvicorn==0.34.0
# 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.3
# via uvicorn # via uvicorn
websockets==14.1 websockets==14.1
# via uvicorn # via uvicorn

View File

@@ -2,26 +2,27 @@
# 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.7.0
# via # via
# httpx # httpx
# starlette # starlette
# watchfiles # watchfiles
certifi==2024.8.30 certifi==2024.12.14
# 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.6
# 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.14.0
# via # via
@@ -31,14 +32,14 @@ httpcore==1.0.7
# 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.5
# via fastapi # via fastapi
markdown-it-py==3.0.0 markdown-it-py==3.0.0
# via rich # via rich
@@ -46,13 +47,13 @@ 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.9.2 pydantic==2.10.4
# via # via
# fastapi # fastapi
# pydantic-settings # pydantic-settings
pydantic-core==2.23.4 pydantic-core==2.27.2
# via pydantic # via pydantic
pydantic-settings==2.6.1 pydantic-settings==2.7.0
# via -r requirements.in # via -r requirements.in
pygments==2.18.0 pygments==2.18.0
# via rich # via rich
@@ -60,35 +61,39 @@ python-dotenv==1.0.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.2
# via uvicorn # via uvicorn
rich==13.9.4 rich==13.9.4
# via typer # via
# rich-toolkit
# typer
rich-toolkit==0.12.0
# 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
# httpx
starlette==0.41.3 starlette==0.41.3
# via fastapi # via fastapi
typer==0.13.1 typer==0.15.1
# via fastapi-cli # via fastapi-cli
typing-extensions==4.12.2 typing-extensions==4.12.2
# via # via
# anyio
# fastapi # fastapi
# pydantic # pydantic
# pydantic-core # pydantic-core
# rich-toolkit
# typer # typer
uvicorn==0.32.0 uvicorn==0.34.0
# 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.3
# via uvicorn # via uvicorn
websockets==14.1 websockets==14.1
# via uvicorn # via uvicorn

View File

@@ -1,9 +1,10 @@
"""Main alfagok API application.""" """Main alfagok API application."""
import logging import logging
from datetime import date from datetime import date, datetime, timezone
from typing import Union from typing import Union
from zoneinfo import ZoneInfo
from fastapi import FastAPI, Request from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
@@ -11,6 +12,11 @@ from pydantic import DirectoryPath, FilePath
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
VERSION = '0.3.1'
AMSTERDAM = ZoneInfo('Europe/Amsterdam')
class Settings(BaseSettings): class Settings(BaseSettings):
"""Configuration needed for alfagok to find its word list, using environment variables.""" """Configuration needed for alfagok to find its word list, using environment variables."""
@@ -51,11 +57,19 @@ 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 = date.today() 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():
"""Calculate the amount of time left for the current game."""
this_moment = datetime.now(tz=AMSTERDAM)
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)
return midnight - this_moment
def is_valid_dictionary_word(word: str) -> bool: def is_valid_dictionary_word(word: str) -> bool:
"""Verify if `word` is in the dictionary provided.""" """Verify if `word` is in the dictionary provided."""
# Either we: [ ] strip all the endlines during file load, or [x] use the endline to search here # Either we: [ ] strip all the endlines during file load, or [x] use the endline to search here
@@ -66,13 +80,13 @@ def is_valid_dictionary_word(word: str) -> bool:
async def index(request: Request): async def index(request: Request):
"""Generate the main HTML page of the game.""" """Generate the main HTML page of the game."""
language = 'nl' language = 'nl'
return templates.TemplateResponse(request=request, name='index.html', context={'language': language}) return templates.TemplateResponse(request=request, name='index.html', context={'language': language, 'version': VERSION})
@app.get('/api/game') @app.get('/api/game')
def what_game(): def what_game():
"""Handle incoming guess.""" """Which game is currently on?"""
return {'game': get_game_id()} return {'game': get_game_id(), 'deadline': get_game_deadline()}
@app.get('/api/guess/{word}') @app.get('/api/guess/{word}')
@@ -98,7 +112,10 @@ def handle_guess(word: Union[str, None] = None):
@app.get('/api/answer/{item_id}') @app.get('/api/answer/{item_id}')
def read_item(item_id: int, guess: Union[str, None] = None): def read_item(item_id: int):
"""Get the word for the current game.""" """Get the word for the game with ID `item_id`."""
current_game_id = get_game_id()
if item_id > current_game_id:
raise HTTPException(status_code=403, detail='No peaking!')
word = words[item_id].strip() word = words[item_id].strip()
return {'item_id': item_id, 'guess': guess, 'word': word} return {'item_id': item_id, 'word': word}

View File

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

View File

@@ -1,27 +1,41 @@
document.addEventListener('alpine:init', () => { document.addEventListener('alpine:init', () => {
Alpine.store('alfagok', { Alpine.store('alfagok', {
/* Main alfagok application, state etc */ /** Main alfagok application, state etc */
gameID: 0, gameID: Alpine.$persist(0).as('gameID'),
countingDown: '',
nextGameIn: 0,
gameFetchedAt: null,
loading: false, loading: false,
winTime: null, winTime: Alpine.$persist(null).as('winTime'),
startTime: null, startTime: Alpine.$persist(null).as('startTime'),
gaveUpTime: Alpine.$persist(null).as('gaveUpTime'), // not implemented yet
nrGuesses: 0, nrGuesses: Alpine.$persist(0).as('nrGuesses'),
guessesBefore: [], guessesBefore: Alpine.$persist([]).as('guessesBefore'),
guessesAfter: [], guessesAfter: Alpine.$persist([]).as('guessesAfter'),
guessValue: '', guessValue: Alpine.$persist('').as('guessValue'),
guessError: '', guessError: '',
resultGameID: '', resultGameID: Alpine.$persist('').as('resultGameID'),
resultGuesses: '', resultGuesses: Alpine.$persist('').as('resultGuesses'),
resultTimeTaken: '', resultTimeTaken: Alpine.$persist('').as('resultTimeTaken'),
resultsCopied: false,
async init() {
/** Initialise the application after loading */
await this.getGameID();
setInterval(() => {
// Update counter to next game (midnight UTC, fetched from API) every second
this.countDownTimer();
}, 1000);
},
async getGameID() { async getGameID() {
/* Get the game number from the backend */ /** Get the game number from the backend */
this.loading = true; this.loading = true;
console.log('Loading gameID...'); console.log('Loading gameID...');
let response = await fetch('/api/game'); let response = await fetch('/api/game');
@@ -29,11 +43,19 @@ document.addEventListener('alpine:init', () => {
console.log(result); console.log(result);
this.loading = false; this.loading = false;
if (result.game) { if (result.game) {
if (this.gameID !== result.game) {
this.setEmptyGameState();
}
this.nextGameIn = result.deadline;
this.gameFetchedAt = new Date();
if (this.countingDown === '') {
this.countDownTimer();
}
return this.gameID = result.game; return this.gameID = result.game;
} }
}, },
async doGuess() { async doGuess() {
/** 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 begin and end, just in case */
@@ -86,30 +108,31 @@ document.addEventListener('alpine:init', () => {
this.winTime = new Date(); this.winTime = new Date();
this.resultGameID = '🧩 Puzzel #' + this.gameID; this.resultGameID = '🧩 Puzzel #' + this.gameID;
this.resultGuesses = '🤔 '+ this.nrGuesses + ' gokken'; this.resultGuesses = '🤔 '+ this.nrGuesses + ' gokken';
this.resultTimeTaken = '⏱️ ' + getFormattedTime(this.winTime - this.startTime); let winTimeDate = new Date(this.winTime);
let startTimeDate = new Date(this.startTime);
this.resultTimeTaken = '⏱️ ' + this.getFormattedTime(winTimeDate - startTimeDate);
} }
}
}),
Alpine.store('darkMode', {
/* Different Alpine app, dark mode settings for the game */
init() {
this.on = window.matchMedia('(prefers-color-scheme: dark)').matches
}, },
setEmptyGameState() {
/** Clean slate for new game */
this.winTime = null;
this.startTime = null;
this.gaveUpTime = null;
on: false, this.nrGuesses = 0;
this.guessesBefore = [];
this.guessesAfter = [];
toggle() { this.guessValue = '';
this.on = ! this.on
}
})
}); this.guessError = '';
this.resultGameID = '';
/* Time formatting **/ this.resultGuesses = '';
this.resultTimeTaken = '';
function getFormattedTime(milliseconds) { },
getFormattedTime(milliseconds) {
/** Nicely format time for 'time played' */
if (!Number.isInteger(milliseconds)) { if (!Number.isInteger(milliseconds)) {
return ''; return '';
} }
@@ -131,50 +154,44 @@ function getFormattedTime(milliseconds) {
} }
return formattedTime.join(' ') || '0s'; return formattedTime.join(' ') || '0s';
} },
// # Countdown timer
/* Clipboard stuff **/ addZero(num){
/** Pad with 0 if needed */
var clip = new Clipboard('.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>';
});
/* Game timer, original from alphaguess.com **/
function go() {
window.timerID = window.setInterval(timer, 0);
}
function timer(){
var nextgame = document.getElementById('nextgame');
var now = new Date();
var midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate()+1, 0, 0, 0);
var diff = Math.floor((midnight - now)/1000);
var hoursRemain = Math.floor(diff/(60*60));
var minutesRemain = Math.floor((diff-hoursRemain*60*60)/60);
var secondsRemain = Math.floor(diff%60);
nextgame.innerHTML = '<span class="nextgame">'+addZero(hoursRemain)+':'+addZero(minutesRemain)+':'+addZero(secondsRemain)+' over</span>';
}
function addZero(num){
if (num <= 9) return '0' + num; if (num <= 9) return '0' + num;
else return num; else return num;
},
countDownTimer(){
/** Update counter to next game (midnight UTC, fetched from API) */
if (this.gameFetchedAt === null) { return; }
let now = new Date();
let gameDataFetched = new Date(this.gameFetchedAt);
let nextGameStart = gameDataFetched.setSeconds(gameDataFetched.getSeconds() + this.nextGameIn);
let diff = Math.floor((nextGameStart - now) / 1000);
let hoursRemain = Math.floor(diff / (60*60));
let minutesRemain = Math.floor((diff - hoursRemain*60*60) / 60);
let secondsRemain = Math.floor(diff % 60);
this.countingDown = this.addZero(hoursRemain) + ':' + this.addZero(minutesRemain) + ':' + this.addZero(secondsRemain) + ' over';
} }
go();
/* Get current gameID etc **/
document.addEventListener('alpine:initialized', () => {
/* On AlpineJS completely loaded, do all this */
Alpine.store('alfagok').getGameID();
}) })
Alpine.store('darkMode', {
/* Different Alpine app, dark mode settings for the game */
init() {
this.on = window.matchMedia('(prefers-color-scheme: dark)').matches
},
on: false,
toggle() {
this.on = ! this.on
}
})
});
// document.addEventListener('alpine:initialized', () => {
/* On AlpineJS completely loaded, do all this */
// Alpine.store('alfagok').getGameID();
// })

View File

@@ -5,27 +5,26 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Raad het woord van de dag. Elke gok geeft een hint over waar het woord zich in het alfabet bevindt. Iedereen speelt hetzelfde woord."> <meta name="description" content="Raad het woord van de dag. Elke gok geeft een hint over waar het woord zich in het alfabet bevindt. Iedereen speelt hetzelfde woord.">
<link rel="stylesheet" href="/static/game.css"> <link rel="stylesheet" href="/static/game.css?v={{ version }}">
<link id="favicon" rel="icon" type="image/x-icon" href="static/images/favicon.ico"> <link id="favicon" rel="icon" type="image/x-icon" href="/static/images/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="static/images/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="/static/images/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="static/images/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32x32.png">
<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/@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>
<div id="container" x-data=""> <div id="container" x-data="">
<a href="/" x-cloak class="title">alfagok</a> <span class="puzzleno">puzzel #<span x-text="$store.alfagok.gameID"></span><span id="nextgame"></span><span x-text="$store.alfagok.nrGuesses"></span> gokken</span> <a href="/" x-cloak class="title">alfagok</a> <span class="puzzleno">puzzel #<span x-text="$store.alfagok.gameID"></span><span x-text="$store.alfagok.countingDown"></span><span x-text="$store.alfagok.nrGuesses"></span> gokken</span>
<div x-cloak class="instructions" x-show="$store.alfagok.guessesBefore.length === 0 && $store.alfagok.guessesAfter.length === 0"> <div x-cloak class="instructions" x-show="$store.alfagok.guessesBefore.length === 0 && $store.alfagok.guessesAfter.length === 0">
<p>Raad het woord van de dag. Elke gok geeft een hint over waar het woord zich in het alfabet bevindt.</p> <p>Raad het woord van de dag. Elke gok geeft een hint over waar het woord zich in het alfabet bevindt.</p>
<p x-show="$store.alfagok.isLocalStorageAvailable"></p>
</div> </div>
<center>
<p class="guessesheading" x-cloak x-show="$store.alfagok.guessesBefore.length">Het woord van de dag komt <em>na</em>:</p> <p class="guessesheading" x-cloak x-show="$store.alfagok.guessesBefore.length">Het woord van de dag komt <em>na</em>:</p>
<ul class="guessesbefore"> <ul class="guessesbefore">
<template x-for="item in $store.alfagok.guessesBefore" :key="item"> <template x-for="item in $store.alfagok.guessesBefore" :key="item">
@@ -48,11 +47,11 @@
<p><b x-text="$store.alfagok.resultGameID"></b></p> <p><b x-text="$store.alfagok.resultGameID"></b></p>
<p x-text="$store.alfagok.resultGuesses"></p> <p x-text="$store.alfagok.resultGuesses"></p>
<p x-text="$store.alfagok.resultTimeTaken"></p> <p x-text="$store.alfagok.resultTimeTaken"></p>
<p>🔗 <span style="color:var(--blue)">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,16 +63,12 @@
</template> </template>
</ul> </ul>
</center>
</div> </div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.5.12/clipboard.min.js" rel=preload></script> <script src="/static/game.js?v={{ version }}"></script>
<script src="/static/game.js"></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>