Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
277ecd1b55
|
|||
|
4e3a5ef682
|
|||
|
233f63f18e
|
|||
|
016f307240
|
|||
|
715acd6244
|
|||
|
0bc48d01a7
|
|||
|
c5646d0451
|
|||
|
710a0fc5bc
|
|||
|
1d0d16b4d4
|
|||
|
6b89bab0a6
|
|||
|
2bc2d98f88
|
|||
|
06096d471e
|
|||
|
40869e25f3
|
|||
|
4f0ac21ba3
|
|||
|
3801949fdb
|
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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" ]
|
||||||
|
|||||||
@@ -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/*
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -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 = "";
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -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,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,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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,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 |
@@ -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 |
@@ -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>
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,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,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>
|
||||||
|
|||||||
@@ -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,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,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>
|
||||||
|
|||||||
@@ -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
@@ -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,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>
|
||||||
@@ -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 %} <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 %} <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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+50
-37
@@ -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"
|
|
||||||
|
|||||||
@@ -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
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')
|
|
||||||
],
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user