Compare commits

..

15 Commits

Author SHA1 Message Date
lukas 277ecd1b55 Update to 1.6.1
Django CI/CD / test (push) Successful in 24s
Django CI/CD / build-and-push (push) Has been skipped
2026-01-30 11:49:39 +01:00
lukas 4e3a5ef682 Make buttons use pointer cursor 2026-01-30 11:45:42 +01:00
lukas 233f63f18e Update Django et al
Django CI/CD / test (push) Successful in 27s
Django CI/CD / build-and-push (push) Successful in 1m25s
2026-01-29 16:53:45 +01:00
lukas 016f307240 Upgrade to Tailwind v4 2026-01-29 13:17:04 +01:00
lukas 715acd6244 Finish poetry migration 2026-01-29 12:56:45 +01:00
lukas 0bc48d01a7 Fix search field icon misalignment
Django CI/CD / test (push) Successful in 16s
Django CI/CD / build-and-push (push) Successful in 1m0s
2026-01-29 12:17:40 +01:00
lukas c5646d0451 Make sure Dockerfile is consistent with entrypoint.sh
Django CI/CD / test (push) Successful in 23s
Django CI/CD / build-and-push (push) Successful in 48s
2026-01-27 21:39:30 +01:00
lukas 710a0fc5bc Update entrypoint.sh
Django CI/CD / test (push) Successful in 23s
Django CI/CD / build-and-push (push) Successful in 57s
2026-01-27 21:30:04 +01:00
lukas 1d0d16b4d4 Disable cache
Django CI/CD / test (push) Successful in 21s
Django CI/CD / build-and-push (push) Successful in 49s
2026-01-27 21:15:39 +01:00
lukas 6b89bab0a6 Switch from poetry to uv
Django CI/CD / test (push) Successful in 9m34s
Django CI/CD / build-and-push (push) Failing after 1m55s
2026-01-27 20:03:39 +01:00
lukas 2bc2d98f88 Fix purchase form logic 2026-01-27 19:30:07 +01:00
lukas 06096d471e Improve dark/light mode 2026-01-27 19:28:05 +01:00
lukas 40869e25f3 Pre-calculate playevent time from last playevent 2026-01-27 18:39:09 +01:00
lukas 4f0ac21ba3 Fill up 2026-01-27 18:39:09 +01:00
lukas 3801949fdb Keep calculate_price_per_game stub
Django CI/CD / test (push) Successful in 24s
Django CI/CD / build-and-push (push) Successful in 2m1s
2026-01-16 12:32:32 +01:00
43 changed files with 4666 additions and 5095 deletions
+28 -14
View File
@@ -9,28 +9,42 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with: with:
python-version: 3.12 enable-cache: false
- run: | python-version: "3.14"
python -m pip install poetry
poetry install - name: Install dependencies
poetry env info run: uv sync --frozen
poetry run python manage.py migrate
# PROD=1 poetry run pytest - name: Run Migrations
run: uv run python manage.py migrate
# - name: Run Tests
# run: PROD=1 uv run pytest
build-and-push: build-and-push:
needs: test needs: test
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5 - name: Set Version
run: echo "VERSION_NUMBER=1.6.1" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v5
with: with:
context: . context: .
push: true push: true
tags: | tags: |
registry.kucharczyk.xyz/timetracker:latest registry.kucharczyk.xyz/timetracker:latest
registry.kucharczyk.xyz/timetracker:${{ env.VERSION_NUMBER }} registry.kucharczyk.xyz/timetracker:${{ env.VERSION_NUMBER }}
env: # cache-from: type=gha
VERSION_NUMBER: 1.5.1 # cache-to: type=gha,mode=max
+8
View File
@@ -1,3 +1,11 @@
## 1.6.1 / 2026-01-30 11:48+01:00
### New
* Pre-fill time played into new playevent, also tracks time since last playevent
* Improve light theme and fix light/dark theme switcher
* Fix purchase form logic
* Update dependencies
## 1.6.0 / 2025-01-15 23:13+01:00 ## 1.6.0 / 2025-01-15 23:13+01:00
### New ### New
+30 -34
View File
@@ -1,45 +1,41 @@
FROM python:3.12.0-slim-bullseye FROM ghcr.io/astral-sh/uv:python3.14-bookworm-slim AS builder
ENV VERSION_NUMBER=1.6.0 \ ENV UV_LINK_MODE=copy \
PROD=1 \ UV_COMPILE_BYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /home/timetracker/app
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project --no-dev
COPY . .
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
FROM python:3.14-slim-bookworm
ENV PROD=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
PYTHONFAULTHANDLER=1 \ PATH="/home/timetracker/app/.venv/bin:$PATH"
PYTHONHASHSEED=random \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_DEFAULT_TIMEOUT=100 \
PIP_ROOT_USER_ACTION=ignore \
POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_CREATE=false \
POETRY_CACHE_DIR='/var/cache/pypoetry' \
POETRY_HOME='/usr/local'
RUN apt-get update && apt-get upgrade -y \
&& apt-get install --no-install-recommends -y \
bash \
curl \
&& curl -sSL 'https://install.python-poetry.org' | python - \
&& poetry --version \
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
&& apt-get clean -y && rm -rf /var/lib/apt/lists/*
RUN useradd -m --uid 1000 timetracker \ RUN useradd -m --uid 1000 timetracker \
&& mkdir -p '/var/www/django/static' \ && mkdir -p /var/www/django/static \
&& chown timetracker:timetracker '/var/www/django/static' && chown timetracker:timetracker /var/www/django/static
WORKDIR /home/timetracker/app WORKDIR /home/timetracker/app
COPY . /home/timetracker/app/
RUN chown -R timetracker:timetracker /home/timetracker/app
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
RUN --mount=type=cache,target="$POETRY_CACHE_DIR" \ COPY --from=builder --chown=timetracker:timetracker /home/timetracker/app /home/timetracker/app
echo "$PROD" \
&& poetry version \ COPY --chown=timetracker:timetracker entrypoint.sh /
&& poetry run pip install -U pip \ RUN chmod +x /entrypoint.sh
&& poetry install --only main --no-interaction --no-ansi --sync
USER timetracker USER timetracker
ENV VERSION_NUMBER=1.6.1
EXPOSE 8000 EXPOSE 8000
CMD [ "/entrypoint.sh" ] CMD [ "/entrypoint.sh" ]
+26 -22
View File
@@ -9,64 +9,68 @@ npm:
npm install npm install
css: common/input.css css: common/input.css
npx tailwindcss -i ./common/input.css -o ./games/static/base.css npx @tailwindcss/cli -i ./common/input.css -o ./games/static/base.css
makemigrations: makemigrations:
poetry run python manage.py makemigrations uv run python manage.py makemigrations
migrate: makemigrations migrate: makemigrations
poetry run python manage.py migrate uv run python manage.py migrate
init: init:
pyenv install -s $(PYTHON_VERSION) uv install $(PYTHON_VERSION)
pyenv local $(PYTHON_VERSION) uv sync
pip install poetry
poetry install
npm install npm install
$(MAKE) sethookdir
$(MAKE) loadplatforms
sethookdir:
git config core.hooksPath .githooks
chmod +x .githooks/*
dev: dev:
@npx concurrently \ @npx concurrently \
--names "Django,Tailwind" \ --names "Django,Tailwind" \
--prefix-colors "blue,green" \ --prefix-colors "blue,green" \
"poetry run python -Wa manage.py runserver" \ "uv run python -Wa manage.py runserver" \
"npx tailwindcss -i ./common/input.css -o ./games/static/base.css --watch" "npx @tailwindcss/cli -i ./common/input.css -o ./games/static/base.css --watch"
caddy: caddy:
caddy run --watch caddy run --watch
dev-prod: migrate collectstatic dev-prod: migrate collectstatic
PROD=1 poetry run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker PROD=1 uv run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker
dumpgames: dumpgames:
poetry run python manage.py dumpdata --format yaml games --output tracker_fixture.yaml uv run python manage.py dumpdata --format yaml games --output tracker_fixture.yaml
loadplatforms: loadplatforms:
poetry run python manage.py loaddata platforms.yaml uv run python manage.py loaddata platforms.yaml
loadall: loadall:
poetry run python manage.py loaddata data.yaml uv run python manage.py loaddata data.yaml
loadsample: loadsample:
poetry run python manage.py loaddata sample.yaml uv run python manage.py loaddata sample.yaml
createsuperuser: createsuperuser:
poetry run python manage.py createsuperuser uv run python manage.py createsuperuser
shell: shell:
poetry run python manage.py shell uv run python manage.py shell
collectstatic: collectstatic:
poetry run python manage.py collectstatic --clear --no-input uv run python manage.py collectstatic --clear --no-input
poetry.lock: pyproject.toml uv.lock: pyproject.toml
poetry install uv sync
test: poetry.lock test: uv.lock
poetry run pytest uv run pytest
date: date:
poetry run python -c 'import datetime; from zoneinfo import ZoneInfo; print(datetime.datetime.isoformat(datetime.datetime.now(ZoneInfo("Europe/Prague")), timespec="minutes", sep=" "))' uv run python -c 'import datetime; from zoneinfo import ZoneInfo; print(datetime.datetime.isoformat(datetime.datetime.now(ZoneInfo("Europe/Prague")), timespec="minutes", sep=" "))'
cleanstatic: cleanstatic:
rm -r static/* rm -r static/*
+7 -1
View File
@@ -142,7 +142,13 @@ def Button(
): ):
return Component( return Component(
template="cotton/button.html", template="cotton/button.html",
attributes=attributes + [("size", size), ("icon", icon), ("color", color)], attributes=attributes
+ [
("size", size),
("icon", icon),
("color", color),
("class", "hover:cursor-pointer"),
],
children=children, children=children,
) )
+111 -66
View File
@@ -1,92 +1,137 @@
@tailwind base; @import 'tailwindcss';
@tailwind components;
@tailwind utilities;
@font-face { @plugin '@tailwindcss/typography';
font-family: "IBM Plex Mono"; @plugin '@tailwindcss/forms';
src: url("fonts/IBMPlexMono-Regular.woff2") format("woff2"); @plugin 'flowbite/plugin';
font-weight: 400;
font-style: normal; @source '../node_modules/flowbite/**/*.js';
@custom-variant dark (&:is(.dark *));
@theme {
--font-sans:
IBM Plex Sans, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-mono:
IBM Plex Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
--font-serif:
IBM Plex Serif, ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
--font-condensed:
IBM Plex Sans Condensed, ui-sans-serif, system-ui, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--color-accent: #7c3aed;
--color-background: #1f2937;
} }
@font-face { /*
font-family: "IBM Plex Sans"; The default border color has changed to `currentcolor` in Tailwind CSS v4,
src: url("fonts/IBMPlexSans-Regular.woff2") format("woff2"); so we've added these compatibility styles to make sure everything still
font-weight: 400; looks the same as it did with Tailwind CSS v3.
font-style: normal;
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
} }
@font-face { @utility min-w-20char {
font-family: "IBM Plex Serif"; min-width: 20ch;
src: url("fonts/IBMPlexSerif-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
} }
@font-face { @utility max-w-20char {
font-family: "IBM Plex Serif"; max-width: 20ch;
src: url("fonts/IBMPlexSerif-Bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
} }
@font-face { @utility min-w-30char {
font-family: "IBM Plex Sans Condensed"; min-width: 30ch;
src: url("fonts/IBMPlexSansCondensed-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
} }
@utility max-w-30char {
max-width: 30ch;
}
/* a:hover { @utility max-w-35char {
max-width: 35ch;
}
@utility max-w-40char {
max-width: 40ch;
}
@layer utilities {
@font-face {
font-family: 'IBM Plex Mono';
src: url('fonts/IBMPlexMono-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'IBM Plex Sans';
src: url('fonts/IBMPlexSans-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'IBM Plex Serif';
src: url('fonts/IBMPlexSerif-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'IBM Plex Serif';
src: url('fonts/IBMPlexSerif-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'IBM Plex Sans Condensed';
src: url('fonts/IBMPlexSansCondensed-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
/* a:hover {
text-decoration-color: #ff4400; text-decoration-color: #ff4400;
color: rgb(254, 185, 160); color: rgb(254, 185, 160);
transition: all 0.2s ease-out; transition: all 0.2s ease-out;
} */ } */
/* form label { /* form label {
@apply dark:text-slate-400; @apply dark:text-slate-400;
} */ } */
.responsive-table { .responsive-table {
@apply dark:text-white mx-auto table-fixed; @apply dark:text-white mx-auto table-fixed;
}
.responsive-table tr:nth-child(even) {
@apply bg-slate-800
}
.responsive-table tbody tr:nth-child(odd) {
@apply bg-slate-900
}
.responsive-table thead th {
@apply text-left border-b-2 border-b-slate-500 text-xl;
}
.responsive-table thead th:not(:first-child),
.responsive-table td:not(:first-child) {
@apply border-l border-l-slate-500;
}
@layer utilities {
.min-w-20char {
min-width: 20ch;
} }
.max-w-20char {
max-width: 20ch; .responsive-table tr:nth-child(even) {
@apply bg-indigo-100 dark:bg-slate-800;
} }
.min-w-30char {
min-width: 30ch; .responsive-table tbody tr:nth-child(odd) {
@apply bg-indigo-200 dark:bg-slate-900;
} }
.max-w-30char {
max-width: 30ch; .responsive-table thead th {
@apply text-left border-b-2 border-b-slate-500 text-xl;
} }
.max-w-35char {
max-width: 35ch; .responsive-table thead th:not(:first-child),
} .responsive-table td:not(:first-child) {
.max-w-40char { @apply border-l border-l-slate-500;
max-width: 40ch;
} }
} }
@@ -131,7 +176,7 @@ textarea:disabled {
} }
.basic-button { .basic-button {
@apply inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out; @apply inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-sm shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-hidden focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out;
} }
.markdown-content ul { .markdown-content ul {
+4 -4
View File
@@ -2,10 +2,10 @@
# Apply database migrations # Apply database migrations
set -euo pipefail set -euo pipefail
echo "Apply database migrations" echo "Apply database migrations"
poetry run python manage.py migrate python manage.py migrate
echo "Collect static files" echo "Collect static files"
poetry run python manage.py collectstatic --clear --no-input python manage.py collectstatic --clear --no-input
_term() { _term() {
echo "Caught SIGTERM signal!" echo "Caught SIGTERM signal!"
@@ -15,9 +15,9 @@ _term() {
trap _term SIGTERM trap _term SIGTERM
echo "Starting Django-Q cluster" echo "Starting Django-Q cluster"
poetry run python manage.py qcluster & django_q_pid=$! python manage.py qcluster & django_q_pid=$!
echo "Starting app" echo "Starting app"
poetry run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile - & gunicorn_pid=$! python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile - & gunicorn_pid=$!
wait "$gunicorn_pid" "$django_q_pid" wait "$gunicorn_pid" "$django_q_pid"
+3152 -3385
View File
File diff suppressed because it is too large Load Diff
+3 -14
View File
@@ -25,18 +25,7 @@ function setupElementHandlers() {
document.addEventListener("DOMContentLoaded", setupElementHandlers); document.addEventListener("DOMContentLoaded", setupElementHandlers);
document.addEventListener("htmx:afterSwap", setupElementHandlers); document.addEventListener("htmx:afterSwap", setupElementHandlers);
getEl("#id_type").onchange = () => { getEl("#id_type").addEventListener("change", () => {
setupElementHandlers(); setupElementHandlers();
}; }
);
document.body.addEventListener("htmx:beforeRequest", function (event) {
// Assuming 'Purchase1' is the element that triggers the HTMX request
if (event.target.id === "id_games") {
var idEditionValue = document.getElementById("id_games").value;
// Condition to check - replace this with your actual logic
if (idEditionValue != "") {
event.preventDefault(); // This cancels the HTMX request
}
}
});
+5
View File
@@ -43,6 +43,7 @@ function syncSelectInputUntilChanged(syncData, parentSelector = document) {
const targetElement = document.querySelector(syncItem.target); const targetElement = document.querySelector(syncItem.target);
if (targetElement && valueToSync !== null) { if (targetElement && valueToSync !== null) {
console.log(`Changing value of ${syncItem.target} to ${valueToSync}`)
targetElement[syncItem.target_value] = valueToSync; targetElement[syncItem.target_value] = valueToSync;
} }
} }
@@ -184,13 +185,17 @@ function disableElementsWhenValueNotEqual(
function disableElementsWhenTrue(targetSelect, targetValue, elementList) { function disableElementsWhenTrue(targetSelect, targetValue, elementList) {
return conditionalElementHandler([ return conditionalElementHandler([
() => { () => {
console.log(`${disableElementsWhenTrue.name}: triggered on ${targetSelect}`)
console.log(`Value of ${targetSelect} is ${targetValue}: ${getEl(targetSelect).value == targetValue}`)
return getEl(targetSelect).value == targetValue; return getEl(targetSelect).value == targetValue;
}, },
elementList, elementList,
(el) => { (el) => {
console.log(`${disableElementsWhenTrue.name}: disabling ${el.id}`)
el.disabled = "disabled"; el.disabled = "disabled";
}, },
(el) => { (el) => {
console.log(`${disableElementsWhenTrue.name}: enabling ${el.id}`)
el.disabled = ""; el.disabled = "";
}, },
]); ]);
+13
View File
@@ -75,3 +75,16 @@ def convert_prices():
floatformat(purchase.price * exchange_rate.rate, 0), floatformat(purchase.price * exchange_rate.rate, 0),
currency_to, currency_to,
) )
def calculate_price_per_game():
"""
This task is deprecated because price_per_game is now a GeneratedField.
It is kept here to prevent errors from lingering scheduled tasks.
"""
try:
from django_q.models import Schedule
Schedule.objects.filter(func="games.tasks.calculate_price_per_game").delete()
except Exception:
pass
+1 -1
View File
@@ -1,6 +1,6 @@
<c-vars color="blue" size="base" type="button" /> <c-vars color="blue" size="base" type="button" />
<button type="{{ type }}" <button type="{{ type }}"
title="{{ title }}" title="{{ title }}"
class="{{ class }} {% if color == "blue" %} bg-blue-700 dark:bg-blue-600 dark:focus:ring-blue-800 dark:hover:bg-blue-700 focus:ring-blue-300 hover:bg-blue-800 text-white {% elif color == "red" %} bg-red-700 dark:bg-red-600 dark:focus:ring-red-900 dark:hover:bg-red-700 focus:ring-red-300 hover:bg-red-800 text-white {% elif color == "gray" %} bg-white border-gray-200 dark:bg-gray-800 dark:border-gray-600 dark:focus:ring-gray-700 dark:hover:bg-gray-700 dark:hover:text-white dark:text-gray-400 focus:ring-gray-100 hover:bg-gray-100 hover:text-blue-700 text-gray-900 border {% elif color == "green" %} bg-green-700 dark:bg-green-600 dark:focus:ring-green-800 dark:hover:bg-green-700 focus:ring-green-300 hover:bg-green-800 text-white {% endif %} focus:outline-none focus:ring-4 font-medium mb-2 me-2 rounded-lg {% if size == "xs" %} px-3 py-2 text-xs {% elif size == "sm" %} px-3 py-2 text-sm {% elif size == "base" %} px-5 py-2.5 text-sm {% elif size == "lg" %} px-5 py-3 text-base {% elif size == "xl" %} px-6 py-3.5 text-base {% endif %} {% if icon %} inline-flex text-center items-center gap-2 {% else %} {% endif %} "> class="{{ class }} {% if color == "blue" %} bg-blue-700 dark:bg-blue-600 dark:focus:ring-blue-800 dark:hover:bg-blue-700 focus:ring-blue-300 hover:bg-blue-800 text-white {% elif color == "red" %} bg-red-700 dark:bg-red-600 dark:focus:ring-red-900 dark:hover:bg-red-700 focus:ring-red-300 hover:bg-red-800 text-white {% elif color == "gray" %} bg-white border-gray-200 dark:bg-gray-800 dark:border-gray-600 dark:focus:ring-gray-700 dark:hover:bg-gray-700 dark:hover:text-white dark:text-gray-400 focus:ring-gray-100 hover:bg-gray-100 hover:text-blue-700 text-gray-900 border {% elif color == "green" %} bg-green-700 dark:bg-green-600 dark:focus:ring-green-800 dark:hover:bg-green-700 focus:ring-green-300 hover:bg-green-800 text-white {% endif %} focus:outline-hidden focus:ring-4 font-medium mb-2 me-2 rounded-lg {% if size == "xs" %} px-3 py-2 text-xs {% elif size == "sm" %} px-3 py-2 text-sm {% elif size == "base" %} px-5 py-2.5 text-sm {% elif size == "lg" %} px-5 py-3 text-base {% elif size == "xl" %} px-6 py-3.5 text-base {% endif %} {% if icon %} inline-flex text-center items-center gap-2 {% else %} {% endif %} ">
{{ slot }} {{ slot }}
</button> </button>
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="inline-flex rounded-md shadow-sm" role="group"> <div class="inline-flex rounded-md shadow-xs" role="group">
{% if slot %}{{ slot }}{% endif %} {% if slot %}{{ slot }}{% endif %}
{% for button in buttons %} {% for button in buttons %}
{% if button.slot %} {% if button.slot %}
@@ -4,19 +4,19 @@
{% if color == "gray" %} {% if color == "gray" %}
<button type="button" <button type="button"
title="{{ title }}" title="{{ title }}"
class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white"> class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white hover:cursor-pointer">
{{ slot }} {{ slot }}
</button> </button>
{% elif color == "red" %} {% elif color == "red" %}
<button type="button" <button type="button"
title="{{ title }}" title="{{ title }}"
class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 hover:bg-red-500 hover:text-white focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:border-red-700 dark:hover:bg-red-700 dark:focus:ring-blue-500 dark:focus:text-white"> class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 hover:bg-red-500 hover:text-white focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:border-red-700 dark:hover:bg-red-700 dark:focus:ring-blue-500 dark:focus:text-white hover:cursor-pointer">
{{ slot }} {{ slot }}
</button> </button>
{% elif color == "green" %} {% elif color == "green" %}
<button type="button" <button type="button"
title="{{ title }}" title="{{ title }}"
class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 hover:bg-green-500 hover:border-green-600 hover:text-white focus:z-10 focus:ring-2 focus:ring-green-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:border-green-700 dark:hover:bg-green-600 dark:focus:ring-green-500 dark:focus:text-white"> class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 hover:bg-green-500 hover:border-green-600 hover:text-white focus:z-10 focus:ring-2 focus:ring-green-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:border-green-700 dark:hover:bg-green-600 dark:focus:ring-green-500 dark:focus:text-white hover:cursor-pointer">
{{ slot }} {{ slot }}
</button> </button>
{% endif %} {% endif %}
+1 -1
View File
@@ -4,7 +4,7 @@ text
{% endcomment %} {% endcomment %}
<a href="{{ link }}" <a href="{{ link }}"
title="{{ title }}" title="{{ title }}"
class="truncate max-w-xs py-1 px-2 text-xs bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-sm"> class="truncate max-w-xs py-1 px-2 text-xs bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center font-semibold shadow-md focus:outline-hidden focus:ring-2 focus:ring-offset-2 rounded-xs">
{% comment %} <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="self-center w-6 h-6 inline"> {% comment %} <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="self-center w-6 h-6 inline">
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
</svg> </svg>
+1 -1
View File
@@ -5,7 +5,7 @@ text
<button type="button" <button type="button"
title="{{ title }}" title="{{ title }}"
autofocus autofocus
class="truncate max-w-xs sm:max-w-md lg:max-w-lg py-1 px-2 bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg"> class="truncate max-w-xs sm:max-w-md lg:max-w-lg py-1 px-2 bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-hidden focus:ring-2 focus:ring-offset-2 rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
+1 -1
View File
@@ -1,7 +1,7 @@
<h1 class="{% if badge %}flex items-center {% endif %}mb-4 text-3xl font-extrabold leading-none tracking-tight text-gray-900 dark:text-white"> <h1 class="{% if badge %}flex items-center {% endif %}mb-4 text-3xl font-extrabold leading-none tracking-tight text-gray-900 dark:text-white">
{{ slot }} {{ slot }}
{% if badge %} {% if badge %}
<span class="bg-blue-100 text-blue-800 text-2xl font-semibold me-2 px-2.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800 ms-2"> <span class="bg-blue-100 text-blue-800 text-2xl font-semibold me-2 px-2.5 py-0.5 rounded-sm dark:bg-blue-200 dark:text-blue-800 ms-2">
{{ badge }} {{ badge }}
</span> </span>
{% endif %} {% endif %}
@@ -0,0 +1,3 @@
<svg class="dark:text-white w-3" viewBox="5 8 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 9L12 15L18 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 284 B

+1 -1
View File
@@ -2,7 +2,7 @@
x="0px" x="0px"
y="0px" y="0px"
viewBox="0 0 48 48" viewBox="0 0 48 48"
class="text-black dark:text-white w-4 h-4"> class="w-4 h-4">
<path fill="currentColor" d="M 11.396484 4.1113281 C 9.1042001 4.2020187 7 6.0721788 7 8.5917969 L 7 39.408203 C 7 42.767694 10.742758 44.971891 13.681641 43.34375 L 41.490234 27.935547 C 44.513674 26.260259 44.513674 21.739741 41.490234 20.064453 L 13.681641 4.65625 C 12.94692 4.2492148 12.160579 4.0810979 11.396484 4.1113281 z M 11.431641 7.0664062 C 11.690234 7.0652962 11.961284 7.1323321 12.226562 7.2792969 L 40.037109 22.6875 C 41.13567 23.296212 41.13567 24.703788 40.037109 25.3125 L 12.226562 40.720703 C 11.165446 41.308562 10 40.620712 10 39.408203 L 10 8.5917969 C 10 7.9855423 10.290709 7.5116121 10.714844 7.2617188 C 10.926911 7.136772 11.173048 7.0675163 11.431641 7.0664062 z"> <path fill="currentColor" d="M 11.396484 4.1113281 C 9.1042001 4.2020187 7 6.0721788 7 8.5917969 L 7 39.408203 C 7 42.767694 10.742758 44.971891 13.681641 43.34375 L 41.490234 27.935547 C 44.513674 26.260259 44.513674 21.739741 41.490234 20.064453 L 13.681641 4.65625 C 12.94692 4.2492148 12.160579 4.0810979 11.396484 4.1113281 z M 11.431641 7.0664062 C 11.690234 7.0652962 11.961284 7.1323321 12.226562 7.2792969 L 40.037109 22.6875 C 41.13567 23.296212 41.13567 24.703788 40.037109 25.3125 L 12.226562 40.720703 C 11.165446 41.308562 10 40.620712 10 39.408203 L 10 8.5917969 C 10 7.9855423 10.290709 7.5116121 10.714844 7.2617188 C 10.926911 7.136772 11.173048 7.0675163 11.431641 7.0664062 z">
</path> </path>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 861 B

After

Width:  |  Height:  |  Size: 834 B

+51 -38
View File
@@ -39,46 +39,59 @@
<span class="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span> <span class="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span>
</div> </div>
{{ scripts }} {{ scripts }}
<script> <script type="module">
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon'); document.addEventListener('DOMContentLoaded', () => {
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon'); if (window.mountCrownIcon) {
window.mountCrownIcon('#crown-icon-mount-point', {
// Change the icons inside the button based on previous settings mastered: {{ game.mastered|yesno:"true,false" }}
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { });
themeToggleLightIcon.classList.remove('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
}
var themeToggleBtn = document.getElementById('theme-toggle');
themeToggleBtn.addEventListener('click', function () {
// toggle icons inside button
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
// if set via local storage previously
if (localStorage.getItem('color-theme')) {
if (localStorage.getItem('color-theme') === 'light') {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
}
// if NOT set via local storage previously
} else {
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
} }
// Theme toggle logic
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
const themeToggleBtn = document.getElementById('theme-toggle');
// Ensure all elements are found before proceeding
if (themeToggleDarkIcon && themeToggleLightIcon && themeToggleBtn) {
// Initial state of icons based on current theme
// The FOUC script in <head> already set document.documentElement.classList.add/remove('dark')
// So we just need to set the icon visibility based on that.
if (document.documentElement.classList.contains('dark')) {
themeToggleLightIcon.classList.remove('hidden');
themeToggleDarkIcon.classList.add('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
themeToggleLightIcon.classList.add('hidden');
}
themeToggleBtn.addEventListener('click', function () {
// toggle icons inside button
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
// if set via local storage previously
if (localStorage.getItem('color-theme')) {
if (localStorage.getItem('color-theme') === 'light') {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
} else { // current theme is dark, switch to light
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
}
// if NOT set via local storage previously
} else { // no theme in local storage, use system preference
if (document.documentElement.classList.contains('dark')) { // currently dark, switch to light
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else { // currently light, switch to dark
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
}
});
}
}); });
</script> </script>
</body> </body>
+1 -1
View File
@@ -2,7 +2,7 @@
<div data-popover <div data-popover
id="{{ id }}" id="{{ id }}"
role="tooltip" role="tooltip"
class="absolute z-10 invisible inline-block text-sm text-white transition-opacity duration-300 bg-white border border-purple-200 rounded-lg shadow-sm opacity-0 dark:text-white dark:border-purple-600 dark:bg-purple-800"> class="absolute z-10 invisible inline-block text-sm text-white transition-opacity duration-300 bg-white border border-purple-200 rounded-lg shadow-xs opacity-0 dark:text-white dark:border-purple-600 dark:bg-purple-800">
<div class="px-3 py-2">{{ popover_content }}</div> <div class="px-3 py-2">{{ popover_content }}</div>
<div data-popper-arrow></div> <div data-popper-arrow></div>
<!-- for Tailwind CSS to generate decoration-dotted CSS from Python component --> <!-- for Tailwind CSS to generate decoration-dotted CSS from Python component -->
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="pb-4 bg-white dark:bg-gray-900"> <div class="pb-4 bg-white dark:bg-gray-900">
<label for="table-search" class="sr-only">Search</label> <label for="table-search" class="sr-only">Search</label>
<div class="relative mt-1"> <div class="relative mt-1">
<div class="absolute inset-y-0 rtl:inset-r-0 start-0 flex items-center ps-3 pointer-events-none"> <div class="absolute inset-y-3 rtl:inset-r-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20"> <svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
</svg> </svg>
+2 -2
View File
@@ -7,12 +7,12 @@
{{ header_action }} {{ header_action }}
</c-table-header> </c-table-header>
{% endif %} {% endif %}
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 [&_th:not(:first-child):not(:last-child)]:max-sm:hidden"> <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 max-sm:[&_th:not(:first-child):not(:last-child)]:hidden">
<tr> <tr>
{% for column in columns %}<th scope="col" class="px-6 py-3">{{ column }}</th>{% endfor %} {% for column in columns %}<th scope="col" class="px-6 py-3">{{ column }}</th>{% endfor %}
</tr> </tr>
</thead> </thead>
<tbody class="dark:divide-y [&_td:not(:first-child):not(:last-child)]:max-sm:hidden"> <tbody class="dark:divide-y max-sm:[&_td:not(:first-child):not(:last-child)]:hidden">
{% for row in rows %}<c-table-row :data=row />{% endfor %} {% for row in rows %}<c-table-row :data=row />{% endfor %}
</tbody> </tbody>
</table> </table>
+1 -1
View File
@@ -1,4 +1,4 @@
<tr class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 [&_a]:underline [&_a]:underline-offset-4 [&_a]:decoration-2 [&_td:last-child]:text-right"> <tr class="odd:bg-white dark:odd:bg-gray-900 even:bg-gray-50 dark:even:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 [&_a]:underline [&_a]:underline-offset-4 [&_a]:decoration-2 [&_td:last-child]:text-right">
{% if slot %} {% if slot %}
{{ slot }} {{ slot }}
{% else %} {% else %}
@@ -1,6 +1,6 @@
<c-layouts.base> <c-layouts.base>
{% load static %} {% load static %}
<div class="2xl:max-w-screen-2xl xl:max-w-screen-xl md:max-w-screen-md sm:max-w-screen-sm self-center"> <div class="2xl:max-w-(--breakpoint-2xl) xl:max-w-(--breakpoint-xl) md:max-w-(--breakpoint-md) sm:max-w-(--breakpoint-sm) self-center">
<form method="post" class="dark:text-white"> <form method="post" class="dark:text-white">
{% csrf_token %} {% csrf_token %}
<div> <div>
+1 -1
View File
@@ -1,6 +1,6 @@
<c-layouts.base> <c-layouts.base>
{% load static %} {% load static %}
<div class="2xl:max-w-screen-2xl xl:max-w-screen-xl md:max-w-screen-md sm:max-w-screen-sm self-center"> <div class="2xl:max-w-(--breakpoint-2xl) xl:max-w-(--breakpoint-xl) md:max-w-(--breakpoint-md) sm:max-w-(--breakpoint-sm) self-center">
<c-simple-table :columns=["Test"] :rows=data.rows :page_obj=page_obj :elided_page_range=elided_page_range :header_action=data.header_action /> <c-simple-table :columns=["Test"] :rows=data.rows :page_obj=page_obj :elided_page_range=elided_page_range :header_action=data.header_action />
</div> </div>
</c-layouts.base> </c-layouts.base>
+1 -1
View File
@@ -3,7 +3,7 @@
{{ title }} {{ title }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<div class="text-slate-300 mx-auto max-w-screen-lg text-center"> <div class="text-slate-300 mx-auto max-w-(--breakpoint-lg) text-center">
{% if session_count > 0 %} {% if session_count > 0 %}
You have played a total of {{ session_count }} sessions for a total of {{ total_duration_formatted }}. You have played a total of {{ session_count }} sessions for a total of {{ total_duration_formatted }}.
{% elif not game_available or not platform_available %} {% elif not game_available or not platform_available %}
+1 -1
View File
@@ -1,6 +1,6 @@
<c-layouts.base> <c-layouts.base>
{% load static %} {% load static %}
<div class="2xl:max-w-screen-2xl xl:max-w-screen-xl md:max-w-screen-md sm:max-w-screen-sm self-center"> <div class="2xl:max-w-(--breakpoint-2xl) xl:max-w-(--breakpoint-xl) md:max-w-(--breakpoint-md) sm:max-w-(--breakpoint-sm) self-center">
<c-simple-table :columns=data.columns :rows=data.rows :page_obj=page_obj :elided_page_range=elided_page_range :header_action=data.header_action /> <c-simple-table :columns=data.columns :rows=data.rows :page_obj=page_obj :elided_page_range=elided_page_range :header_action=data.header_action />
</div> </div>
</c-layouts.base> </c-layouts.base>
+1 -1
View File
@@ -1,6 +1,6 @@
<c-layouts.base> <c-layouts.base>
{% load static %} {% load static %}
<div class="2xl:max-w-screen-2xl xl:max-w-screen-xl md:max-w-screen-md sm:max-w-screen-sm self-center"> <div class="2xl:max-w-(--breakpoint-2xl) xl:max-w-(--breakpoint-xl) md:max-w-(--breakpoint-md) sm:max-w-(--breakpoint-sm) self-center">
<c-simple-table :columns=data.columns :rows=data.rows :page_obj=page_obj :elided_page_range=elided_page_range :header_action=data.header_action /> <c-simple-table :columns=data.columns :rows=data.rows :page_obj=page_obj :elided_page_range=elided_page_range :header_action=data.header_action />
</div> </div>
</c-layouts.base> </c-layouts.base>
+1 -1
View File
@@ -35,7 +35,7 @@
<tr> <tr>
<td class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name relative align-top w-24 h-12 group"> <td class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name relative align-top w-24 h-12 group">
<span class="inline-block relative"> <span class="inline-block relative">
<a class="underline decoration-slate-500 sm:decoration-2 inline-block truncate max-w-20char group-hover:absolute group-hover:max-w-none group-hover:-top-8 group-hover:-left-6 group-hover:min-w-60 group-hover:px-6 group-hover:py-3.5 group-hover:bg-purple-600 group-hover:rounded-sm group-hover:outline-dashed group-hover:outline-purple-400 group-hover:outline-4 group-hover:decoration-purple-900 group-hover:text-purple-100" <a class="underline decoration-slate-500 sm:decoration-2 inline-block truncate max-w-20char group-hover:absolute group-hover:max-w-none group-hover:-top-8 group-hover:-left-6 group-hover:min-w-60 group-hover:px-6 group-hover:py-3.5 group-hover:bg-purple-600 group-hover:rounded-xs group-hover:outline-dashed group-hover:outline-purple-400 group-hover:outline-4 group-hover:decoration-purple-900 group-hover:text-purple-100"
href="{% url 'view_game' session.game.id %}"> href="{% url 'view_game' session.game.id %}">
{{ session.game.name }} {{ session.game.name }}
</a> </a>
+22 -12
View File
@@ -1,6 +1,6 @@
{% load static %} {% load static %}
<nav class="bg-white border-gray-200 dark:bg-gray-900 dark:border-gray-700"> <nav class="bg-white border-gray-200 dark:bg-gray-900 dark:border-gray-700">
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4"> <div class="max-w-(--breakpoint-xl) flex flex-wrap items-center justify-between mx-auto p-4">
<a href="{% url 'index' %}" <a href="{% url 'index' %}"
class="flex items-center space-x-3 rtl:space-x-reverse"> class="flex items-center space-x-3 rtl:space-x-reverse">
<img src="{% static 'icons/schedule.png' %}" <img src="{% static 'icons/schedule.png' %}"
@@ -12,7 +12,7 @@
</a> </a>
<button data-collapse-toggle="navbar-dropdown" <button data-collapse-toggle="navbar-dropdown"
type="button" type="button"
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600" class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-hidden focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
aria-controls="navbar-dropdown" aria-controls="navbar-dropdown"
aria-expanded="false"> aria-expanded="false">
<span class="sr-only">Open main menu</span> <span class="sr-only">Open main menu</span>
@@ -26,19 +26,29 @@
</button> </button>
<div class="hidden w-full md:block md:w-auto" id="navbar-dropdown"> <div class="hidden w-full md:block md:w-auto" id="navbar-dropdown">
<ul class="items-center flex flex-col font-medium p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700"> <ul class="items-center flex flex-col font-medium p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
<li class="text-white flex flex-col items-center text-xs"> <li class="flex items-center">
<span class="flex uppercase gap-1">Today<span class="text-gray-400">·</span>Last 7 days</span> <button id="theme-toggle" type="button" class="p-2 text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-hidden focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm hover:cursor-pointer">
<span class="flex items-center gap-1">{{ today_played }}<span class="text-gray-400">·</span>{{ last_7_played }}</span> <svg id="theme-toggle-dark-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M3.32031 11.6835C3.32031 16.6541 7.34975 20.6835 12.3203 20.6835C16.1075 20.6835 19.3483 18.3443 20.6768 15.032C19.6402 15.4486 18.5059 15.6834 17.3203 15.6834C12.3497 15.6834 8.32031 11.654 8.32031 6.68342C8.32031 5.50338 8.55165 4.36259 8.96453 3.32996C5.65605 4.66028 3.32031 7.89912 3.32031 11.6835Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg id="theme-toggle-light-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 3V4M12 20V21M4 12H3M6.31412 6.31412L5.5 5.5M17.6859 6.31412L18.5 5.5M6.31412 17.69L5.5 18.5001M17.6859 17.69L18.5 18.5001M21 12H20M16 12C16 14.2091 14.2091 16 12 16C9.79086 16 8 14.2091 8 12C8 9.79086 9.79086 8 12 8C14.2091 8 16 9.79086 16 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</li>
<li class="dark:text-white flex flex-col items-center text-xs">
<span class="flex uppercase gap-1">Today<span class="dark:text-gray-400">·</span>Last 7 days</span>
<span class="flex items-center gap-1">{{ today_played }}<span class="dark:text-gray-400">·</span>{{ last_7_played }}</span>
</li> </li>
<li> <li>
<a href="#" <a href="#"
class="block py-2 px-3 text-white bg-blue-700 rounded md:bg-transparent md:text-blue-700 md:p-0 md:dark:text-blue-500 dark:bg-blue-600 md:dark:bg-transparent" class="block py-2 px-3 text-white bg-blue-700 rounded-sm md:bg-transparent md:text-blue-700 md:p-0 md:dark:text-blue-500 dark:bg-blue-600 md:dark:bg-transparent"
aria-current="page">Home</a> aria-current="page">Home</a>
</li> </li>
<li> <li>
<button id="dropdownNavbarNewLink" <button id="dropdownNavbarNewLink"
data-dropdown-toggle="dropdownNavbarNew" data-dropdown-toggle="dropdownNavbarNew"
class="flex items-center justify-between w-full py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 md:w-auto dark:text-white md:dark:hover:text-blue-500 dark:focus:text-white dark:border-gray-700 dark:hover:bg-gray-700 md:dark:hover:bg-transparent"> class="flex items-center justify-between w-full py-2 px-3 text-gray-900 rounded-sm hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 md:w-auto dark:text-white md:dark:hover:text-blue-500 dark:focus:text-white dark:border-gray-700 dark:hover:bg-gray-700 md:dark:hover:bg-transparent hover:cursor-pointer">
New New
<svg class="w-2.5 h-2.5 ms-2.5" <svg class="w-2.5 h-2.5 ms-2.5"
aria-hidden="true" aria-hidden="true"
@@ -50,7 +60,7 @@
</button> </button>
<!-- Dropdown menu --> <!-- Dropdown menu -->
<div id="dropdownNavbarNew" <div id="dropdownNavbarNew"
class="z-10 hidden font-normal bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700 dark:divide-gray-600"> class="z-10 hidden font-normal bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700 dark:divide-gray-600">
<ul class="py-2 text-sm text-gray-700 dark:text-gray-400" <ul class="py-2 text-sm text-gray-700 dark:text-gray-400"
aria-labelledby="dropdownLargeButton"> aria-labelledby="dropdownLargeButton">
<li> <li>
@@ -79,7 +89,7 @@
<li> <li>
<button id="dropdownNavbarManageLink" <button id="dropdownNavbarManageLink"
data-dropdown-toggle="dropdownNavbarManage" data-dropdown-toggle="dropdownNavbarManage"
class="flex items-center justify-between w-full py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 md:w-auto dark:text-white md:dark:hover:text-blue-500 dark:focus:text-white dark:border-gray-700 dark:hover:bg-gray-700 md:dark:hover:bg-transparent"> class="flex items-center justify-between w-full py-2 px-3 text-gray-900 rounded-sm hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 md:w-auto dark:text-white md:dark:hover:text-blue-500 dark:focus:text-white dark:border-gray-700 dark:hover:bg-gray-700 md:dark:hover:bg-transparent hover:cursor-pointer">
Manage Manage
<svg class="w-2.5 h-2.5 ms-2.5" <svg class="w-2.5 h-2.5 ms-2.5"
aria-hidden="true" aria-hidden="true"
@@ -91,7 +101,7 @@
</button> </button>
<!-- Dropdown menu --> <!-- Dropdown menu -->
<div id="dropdownNavbarManage" <div id="dropdownNavbarManage"
class="z-10 hidden font-normal bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700 dark:divide-gray-600"> class="z-10 hidden font-normal bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700 dark:divide-gray-600">
<ul class="py-2 text-sm text-gray-700 dark:text-gray-400" <ul class="py-2 text-sm text-gray-700 dark:text-gray-400"
aria-labelledby="dropdownLargeButton"> aria-labelledby="dropdownLargeButton">
<li> <li>
@@ -123,11 +133,11 @@
</li> </li>
<li> <li>
<a href="{% url 'stats_by_year' global_current_year %}" <a href="{% url 'stats_by_year' global_current_year %}"
class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Stats</a> class="block py-2 px-3 text-gray-900 rounded-sm hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Stats</a>
</li> </li>
<li> <li>
<a href="{% url 'logout' %}" <a href="{% url 'logout' %}"
class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Log class="block py-2 px-3 text-gray-900 rounded-sm hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Log
out</a> out</a>
</li> </li>
</ul> </ul>
@@ -22,22 +22,20 @@
} }
}" }"
> >
<div class="inline-flex rounded-md shadow-xs" role="group" @click.outside="open = false"> <div class="inline-flex rounded-md shadow-2xs" role="group" @click.outside="open = false">
<button type="button" @click="open = !open" class="relative px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white align-middle"> <button type="button" @click="open = !open" class="relative px-4 py-2 text-sm font-medium bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white align-middle hover:cursor-pointer">
<span class="flex flex-row gap-4 justify-between items-center"> <span class="flex flex-row gap-4 justify-between items-center">
{% for status_value, status_label in game_statuses %} {% for status_value, status_label in game_statuses %}
<template x-if="status == '{{ status_value }}'"> <template x-if="status == '{{ status_value }}'">
<c-gamestatus display="flex" status="{{ status_value }}" class="text-slate-300">{{ status_label }}</c-gamestatus> <c-gamestatus display="flex" status="{{ status_value }}">{{ status_label }}</c-gamestatus>
</template> </template>
{% endfor %} {% endfor %}
<svg class="text-white w-3" viewBox="5 8 14 8" fill="none" xmlns="http://www.w3.org/2000/svg"> <c-icon.arrowdown />
<path d="M6 9L12 15L18 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span> </span>
<div class="absolute top-[105%] left-0 w-full whitespace-nowrap z-10 text-sm font-medium bg-gray-800/20 backdrop-blur-lg rounded-md rounded-t-none border border-gray-200 dark:border-gray-700" x-show="open" style="display: none;"> <div class="absolute top-[105%] left-0 w-full whitespace-nowrap z-10 text-sm font-medium bg-gray-800/20 backdrop-blur-lg rounded-md rounded-t-none border border-gray-200 dark:border-gray-700" x-show="open" style="display: none;">
<ul class="[&_li:first-of-type_a]:rounded-none [&_li:last-of-type_a]:rounded-t-none"> <ul class="[&_li:first-of-type_a]:rounded-none [&_li:last-of-type_a]:rounded-t-none">
{% for status_value, status_label in game_statuses %} {% for status_value, status_label in game_statuses %}
<li><a href="#" @click.prevent.stop="setStatus('{{ status_value }}', '{{ status_label }}'); open = false;" class="block px-4 py-2 dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white rounded !no-underline !border-0" :class="{ 'font-bold': status === '{{ status_value }}' }"><c-gamestatus display="flex" status="{{ status_value }}" class="text-slate-300">{{ status_label }}</c-gamestatus></a></li> <li><a href="#" @click.prevent.stop="setStatus('{{ status_value }}', '{{ status_label }}'); open = false;" class="block px-4 py-2 dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white rounded-sm no-underline! border-0!" :class="{ 'font-bold': status === '{{ status_value }}' }"><c-gamestatus display="flex" status="{{ status_value }}" class="text-slate-300">{{ status_label }}</c-gamestatus></a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
+1 -1
View File
@@ -1,6 +1,6 @@
<ul class="list-disc list-inside"> <ul class="list-disc list-inside">
{% for change in statuschanges %} {% for change in statuschanges %}
<li class="text-slate-500"> <li class="text-slate-500">
{% if change.timestamp %}{{ change.timestamp | date:"d/m/Y H:i" }}: Changed{% else %}At some point changed{% endif %} status from <c-gamestatus :status="change.old_status" class="text-white">{{ change.get_old_status_display }}</c-gamestatus> to <c-gamestatus :status="change.new_status" class="text-white">{{ change.get_new_status_display }}</c-gamestatus> (<a href="{% url 'edit_statuschange' change.id %}">Edit</a>, <a href="{% url 'delete_statuschange' change.id %}">Delete</a>)</li> {% if change.timestamp %}{{ change.timestamp | date:"d/m/Y H:i" }}: Changed{% else %}At some point changed{% endif %} status from <c-gamestatus :status="change.old_status">{{ change.get_old_status_display }}</c-gamestatus> to <c-gamestatus :status="change.new_status">{{ change.get_new_status_display }}</c-gamestatus> (<a href="{% url 'edit_statuschange' change.id %}">Edit</a>, <a href="{% url 'delete_statuschange' change.id %}">Delete</a>)</li>
{% endfor %} {% endfor %}
</ul> </ul>
+12 -14
View File
@@ -2,7 +2,7 @@
<div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto"> <div class="dark:text-white max-w-sm sm:max-w-xl lg:max-w-3xl mx-auto">
<div id="game-info" class="mb-10"> <div id="game-info" class="mb-10">
<div class="flex gap-5 mb-3"> <div class="flex gap-5 mb-3">
<span class="text-balance max-w-[30rem] text-4xl"> <span class="text-balance max-w-120 text-4xl">
<span class="font-bold font-serif">{{ game.name }}</span>{% if game.year_released %}&nbsp;<c-popover id="popover-year" popover_content="Original release year" class="text-slate-500 text-2xl">{{ game.year_released }}</c-popover>{% endif %} <span class="font-bold font-serif">{{ game.name }}</span>{% if game.year_released %}&nbsp;<c-popover id="popover-year" popover_content="Original release year" class="text-slate-500 text-2xl">{{ game.year_released }}</c-popover>{% endif %}
</span> </span>
</div> </div>
@@ -52,10 +52,10 @@
{{ playrange }} {{ playrange }}
</c-popover> </c-popover>
</div> </div>
<div class="flex flex-col mb-6 text-slate-400 gap-y-4"> <div class="flex flex-col mb-6 text-gray-600 dark:text-slate-400 gap-y-4">
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<span class="uppercase">Original year</span> <span class="uppercase">Original year</span>
<span class="text-slate-300">{{ game.original_year_released }}</span> <span class="text-black dark:text-slate-300">{{ game.original_year_released }}</span>
</div> </div>
<div class="flex gap-2 items-center" <div class="flex gap-2 items-center"
> >
@@ -67,18 +67,16 @@
x-data="{ open: false }" x-data="{ open: false }"
> >
<span class="uppercase">Played</span> <span class="uppercase">Played</span>
<div class="inline-flex rounded-md shadow-xs" role="group" x-data="{ played: {{ game.playevents.count }} }"> <div class="inline-flex rounded-md shadow-2xs" role="group" x-data="{ played: {{ game.playevents.count }} }">
<a href="{% url 'add_playevent' %}"> <a href="{% url 'add_playevent' %}">
<button type="button" class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-s-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white"> <button type="button" class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-s-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white hover:cursor-pointer">
<span x-text="played"></span> times <span x-text="played"></span> times
</button> </button>
</a> </a>
<button type="button" x-on:click="open = !open" @click.outside="open = false" class="relative px-4 py-2 text-sm font-medium text-gray-900 bg-white border-e border-b border-t border-gray-200 rounded-e-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white align-middle"> <button type="button" x-on:click="open = !open" @click.outside="open = false" class="relative px-4 py-2 text-sm font-medium text-gray-900 bg-white border-e border-b border-t border-gray-200 rounded-e-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white align-middle hover:cursor-pointer">
<svg class="text-white w-3" viewBox="5 8 14 8" fill="none" xmlns="http://www.w3.org/2000/svg"> <c-icon.arrowdown />
<path d="M6 9L12 15L18 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div <div
class="absolute top-[100%] -left-[1px] w-auto whitespace-nowrap z-10 text-sm font-medium bg-gray-800/20 backdrop-blur-lg rounded-md rounded-tl-none border border-gray-200 dark:border-gray-700" class="absolute top-full -left-px w-auto whitespace-nowrap z-10 text-sm font-medium bg-gray-800/20 backdrop-blur-lg rounded-md rounded-tl-none border border-gray-200 dark:border-gray-700"
x-show="open" x-show="open"
> >
<ul <ul
@@ -110,19 +108,19 @@
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<span class="uppercase">Platform</span> <span class="uppercase">Platform</span>
<span class="text-slate-300">{{ game.platform }}</span> <span class="text-black dark:text-slate-300">{{ game.platform }}</span>
</div> </div>
</div> </div>
<div class="inline-flex rounded-md shadow-sm mb-3" role="group"> <div class="inline-flex rounded-md shadow-xs mb-3" role="group">
<a href="{% url 'edit_game' game.id %}"> <a href="{% url 'edit_game' game.id %}">
<button type="button" <button type="button"
class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-s-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white"> class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-s-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white hover:cursor-pointer">
Edit Edit
</button> </button>
</a> </a>
<a href="{% url 'delete_game' game.id %}"> <a href="{% url 'delete_game' game.id %}">
<button type="button" <button type="button"
class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-e-lg hover:bg-red-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-red-700 dark:focus:ring-blue-500 dark:focus:text-white"> class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-e-lg hover:bg-red-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-red-700 dark:focus:ring-blue-500 dark:focus:text-white hover:cursor-pointer">
Delete Delete
</button> </button>
</a> </a>
+2 -2
View File
@@ -9,12 +9,12 @@
{{ purchase.name }} {{ purchase.name }}
{% endif %} {% endif %}
</div> </div>
<span class="text-balance max-w-[30rem] text-4xl"> <span class="text-balance max-w-120 text-4xl">
<span class="font-bold font-serif"> <span class="font-bold font-serif">
{{ purchase.date_purchased }} ({{ purchase.num_purchases }} game{{ purchase.num_purchases|pluralize}}) {{ purchase.date_purchased }} ({{ purchase.num_purchases }} game{{ purchase.num_purchases|pluralize}})
</span> </span>
</span> </span>
<div class="inline-flex rounded-md shadow-sm mb-3" role="group"> <div class="inline-flex rounded-md shadow-xs mb-3" role="group">
<a href="{% url 'edit_purchase' purchase.id %}"> <a href="{% url 'edit_purchase' purchase.id %}">
<button type="button" <button type="button"
class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-s-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white"> class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-s-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white">
+75 -4
View File
@@ -1,4 +1,5 @@
import logging import logging
from datetime import datetime, timedelta
from typing import Any, Callable, TypedDict from typing import Any, Callable, TypedDict
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
@@ -11,9 +12,9 @@ from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
from common.components import A, Button, Icon from common.components import A, Button, Icon
from common.time import dateformat, local_strftime from common.time import dateformat, format_duration, local_strftime
from games.forms import PlayEventForm from games.forms import PlayEventForm
from games.models import Game, PlayEvent from games.models import Game, PlayEvent, Session
logger = logging.getLogger("games") logger = logging.getLogger("games")
@@ -83,6 +84,39 @@ def create_playevent_tabledata(
} }
def _get_formatted_playtime_for_game_sessions_in_range(
game: Game,
start_timestamp: datetime | None = None,
end_timestamp: datetime | None = None,
) -> str:
"""
Calculates and formats the total playtime for a game's sessions
between specified start and end timestamps. If timestamps are not provided,
it uses the earliest and latest session start times for the game.
Returns "0h 00m" if no sessions exist for the game or if the range is invalid.
"""
sessions_queryset = game.sessions.all()
if not sessions_queryset.exists():
return "0h 00m"
actual_start_ts = (
start_timestamp
if start_timestamp is not None
else sessions_queryset.earliest("timestamp_start").timestamp_start
)
actual_end_ts = (
end_timestamp
if end_timestamp is not None
else sessions_queryset.latest("timestamp_start").timestamp_start
)
sessions_in_range = sessions_queryset.filter(
timestamp_start__gte=actual_start_ts, timestamp_start__lte=actual_end_ts
)
return format_duration(sessions_in_range.total_duration_unformatted(), "%Hh %mm")
@login_required @login_required
def list_playevents(request: HttpRequest) -> HttpResponse: def list_playevents(request: HttpRequest) -> HttpResponse:
page_number = request.GET.get("page", 1) page_number = request.GET.get("page", 1)
@@ -115,8 +149,45 @@ def add_playevent(request: HttpRequest, game_id: int = 0) -> HttpResponse:
# coming from add_playevent_for_game url path # coming from add_playevent_for_game url path
game = get_object_or_404(Game, id=game_id) game = get_object_or_404(Game, id=game_id)
initial["game"] = game initial["game"] = game
initial["started"] = game.sessions.earliest().timestamp_start try:
initial["ended"] = game.sessions.latest().timestamp_start # First, try to get the latest session. If no sessions, then no playtime.
latest_session = game.sessions.latest("timestamp_start")
latest_session_ts = latest_session.timestamp_start
# Now, determine the start date for the new playevent.
# This will be either the day after the last playevent ended, or the earliest session.
try:
latest_playevent = game.playevents.latest("ended")
# Start date for the new PlayEvent form
new_playevent_form_start_date = latest_playevent.ended + timedelta(
days=1
)
initial["started"] = new_playevent_form_start_date
# Start timestamp for playtime calculation
playtime_calc_start_ts = datetime.combine(
new_playevent_form_start_date, datetime.min.time()
)
except PlayEvent.DoesNotExist:
# No previous playevents, so the new playevent starts from the earliest session.
earliest_session_ts = game.sessions.earliest(
"timestamp_start"
).timestamp_start
initial["started"] = earliest_session_ts.date()
playtime_calc_start_ts = earliest_session_ts
# The end date for the new PlayEvent form and playtime calculation is the latest session's start date.
initial["ended"] = latest_session_ts.date()
playtime_calc_end_ts = latest_session_ts
initial["note"] = _get_formatted_playtime_for_game_sessions_in_range(
game, playtime_calc_start_ts, playtime_calc_end_ts
)
except Session.DoesNotExist:
initial["started"] = None
initial["ended"] = None
initial["note"] = "0h 00m"
form = PlayEventForm(request.POST or None, initial=initial) form = PlayEventForm(request.POST or None, initial=initial)
if form.is_valid(): if form.is_valid():
form.save() form.save()
+10 -5
View File
@@ -140,7 +140,7 @@ def add_purchase(request: HttpRequest, game_id: int = 0) -> HttpResponse:
context["form"] = form context["form"] = form
context["title"] = "Add New Purchase" context["title"] = "Add New Purchase"
# context["script_name"] = "add_purchase.js" context["script_name"] = "add_purchase.js"
return render(request, "add_purchase.html", context) return render(request, "add_purchase.html", context)
@@ -156,7 +156,7 @@ def edit_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
context["title"] = "Edit Purchase" context["title"] = "Edit Purchase"
context["form"] = form context["form"] = form
context["purchase_id"] = str(purchase_id) context["purchase_id"] = str(purchase_id)
# context["script_name"] = "add_purchase.js" context["script_name"] = "add_purchase.js"
return render(request, "add_purchase.html", context) return render(request, "add_purchase.html", context)
@@ -208,7 +208,12 @@ def related_purchase_by_game(request: HttpRequest) -> HttpResponse:
if isinstance(games, int) or isinstance(games, str): if isinstance(games, int) or isinstance(games, str):
games = [games] games = [games]
form = PurchaseForm() form = PurchaseForm()
form.fields["related_purchase"].queryset = Purchase.objects.filter( qs = Purchase.objects.filter(games__in=games, type=Purchase.GAME).order_by(
games__in=games, type=Purchase.GAME "games__sort_name"
).order_by("games__sort_name") )
form.fields["related_purchase"].queryset = qs
first_option = qs.first()
if first_option:
form.fields["related_purchase"].initial = first_option.id
return render(request, "partials/related_purchase_field.html", {"form": form}) return render(request, "partials/related_purchase_field.html", {"form": form})
+2 -1
View File
@@ -4,9 +4,10 @@
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.13",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"npm-check-updates": "^16.14.20", "npm-check-updates": "^16.14.20",
"tailwindcss": "^3.4.14" "tailwindcss": "^4.1.18"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/cli": "^4.1.18",
"flowbite": "^2.4.1" "flowbite": "^2.4.1"
} }
} }
Generated
-1385
View File
File diff suppressed because it is too large Load Diff
+50 -37
View File
@@ -1,46 +1,59 @@
[tool.poetry] [project]
name = "timetracker" name = "timetracker"
version = "1.6.0" version = "1.6.1"
description = "A simple time tracker." description = "A simple time tracker."
authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"] authors = [{ name = "Lukáš Kucharczyk", email = "lukas@kucharczyk.xyz" }]
license = "GPL" requires-python = ">=3.13,<4"
readme = "README.md" readme = "README.md"
packages = [{include = "timetracker"}] license = "AGPL-3.0-or-later"
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
dependencies = [
"django>6.0",
"gunicorn>=23.0.0,<24",
"uvicorn>=0.30.1,<0.31",
"graphene-django>=3.2.0,<4",
"django-htmx>=1.18.0,<2",
"django-template-partials>=24.2,<25",
"markdown>=3.6,<4",
"django-cotton==2.3",
"django-q2>=1.7.4,<2",
"croniter>=5.0.1,<6",
"requests>=2.32.3,<3",
"pyyaml>=6.0.2,<7",
"django-ninja>1.5",
]
[tool.poetry.group.dev.dependencies] [project.scripts]
mypy = "^1.10.1" timetracker-import = "common.import_data:import_from_file"
pyyaml = "^6.0.1"
pytest = "^8.2.2"
django-extensions = "^3.2.3"
djhtml = "^3.0.6"
djlint = "^1.34.1"
isort = "^5.13.2"
pre-commit = "^3.7.1"
django-debug-toolbar = "^4.4.2"
[dependency-groups]
dev = [
"mypy>=1.10.1,<2",
"pyyaml>=6.0.1,<7",
"pytest>=8.2.2,<9",
"django-extensions>=3.2.3,<4",
"djhtml>=3.0.6,<4",
"djlint>=1.34.1,<2",
"isort>=5.13.2,<6",
"pre-commit>=3.7.1,<4",
"django-debug-toolbar>=4.4.2,<5",
"ruff"
]
[tool.poetry.dependencies] [tool.uv]
python = "^3.11"
django = "^5.0.6"
gunicorn = "^23.0.0"
uvicorn = "^0.30.1"
graphene-django = "^3.2.0"
django-htmx = "^1.18.0"
django-template-partials = "^24.2"
markdown = "^3.6"
django-cotton = "^1.2.1"
django-q2 = "^1.7.4" [tool.uv.build-backend]
croniter = "^5.0.1" module-name = ["timetracker"]
requests = "^2.32.3" module-root = ""
pyyaml = "^6.0.2"
django-ninja = "^1.3.0"
[tool.isort]
profile = "black"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["uv_build>=0.9.26,<0.10.0"]
build-backend = "poetry.core.masonry.api" build-backend = "uv_build"
[tool.isort]
[tool.poetry.scripts] profile = "black"
timetracker-import = "common.import_data:import_from_file"
+3 -3
View File
@@ -6,13 +6,13 @@ pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
nodejs nodejs
python3 python3
poetry uv
ruff ruff
]; ];
shellHook = '' shellHook = ''
python -m venv .venv uv venv --clear
. .venv/bin/activate . .venv/bin/activate
poetry install uv sync
''; '';
} }
-26
View File
@@ -1,26 +0,0 @@
const defaultTheme = require('tailwindcss/defaultTheme')
const colors = require('tailwindcss/colors');
module.exports = {
darkMode: 'class',
content: ["./games/**/*.{html,js}", './node_modules/flowbite/**/*.js'],
theme: {
extend: {
fontFamily: {
'sans': ['IBM Plex Sans', ...defaultTheme.fontFamily.sans],
'mono': ['IBM Plex Mono', ...defaultTheme.fontFamily.mono],
'serif': ['IBM Plex Serif', ...defaultTheme.fontFamily.serif],
'condensed': ['IBM Plex Sans Condensed', ...defaultTheme.fontFamily.sans],
},
colors: {
'accent': colors.violet[600],
'background': colors.gray[800],
}
},
},
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms'),
require('flowbite/plugin')
],
}
Generated
+1023
View File
File diff suppressed because it is too large Load Diff