diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..1d953f4 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix diff --git a/.gitignore b/.gitignore index 983599a..d74ec92 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ db.sqlite3 dist/ .DS_Store .python-version +.direnv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 875403e..d0b56e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,13 @@ repos: -- repo: https://github.com/psf/black - rev: 24.3.0 - hooks: - - id: black +# disable due to incomaptible formatting between +# black and ruff +# TODO: replace with ruff when it works on NixOS +# - repo: https://github.com/psf/black +# rev: 24.8.0 +# hooks: +# - id: black - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort name: isort (python) @@ -12,4 +15,6 @@ repos: rev: v1.34.0 hooks: - id: djlint-reformat-django + args: ["--ignore", "H011"] - id: djlint-django + args: ["--ignore", "H011"] diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 0d70e04..743b006 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,8 +1,11 @@ { "recommendations": [ - "ms-python.black-formatter", + "charliermarsh.ruff", "ms-python.python", "ms-python.vscode-pylance", "ms-python.debugpy", + "batisteo.vscode-django", + "bradlc.vscode-tailwindcss", + "EditorConfig.EditorConfig" ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index fc1a347..3118271 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,7 +6,28 @@ "python.testing.pytestEnabled": true, "python.analysis.typeCheckingMode": "strict", "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter", - "editor.formatOnSave": true + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, }, + "ruff.path": ["/nix/store/jaibb3v0rrnlw5ib54qqq3452yhp1xcb-ruff-0.5.7/bin/ruff"], + "tailwind-fold.supportedLanguages": [ + "html", + "typescriptreact", + "javascriptreact", + "typescript", + "javascript", + "vue-html", + "vue", + "php", + "markdown", + "coffeescript", + "svelte", + "astro", + "erb", + "django-html" + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index d52a5ce..fe89cc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Add stats for dropped purchases, monthly playtimes * Allow deleting purchases * Add all-time stats +* Manage purchases ## Improved * mark refunded purchases red on game overview diff --git a/common/input.css b/common/input.css index 113775c..75e4442 100644 --- a/common/input.css +++ b/common/input.css @@ -70,9 +70,15 @@ form label { } @layer utilities { + .min-w-20char { + min-width: 20ch; + } .max-w-20char { max-width: 20ch; } + .min-w-30char { + min-width: 30ch; + } .max-w-30char { max-width: 30ch; } @@ -120,14 +126,6 @@ textarea:disabled { @apply mx-1; } -th { - @apply text-right; -} - -th label { - @apply mr-4; -} - .basic-button-container { @apply flex space-x-2 justify-center; } @@ -170,4 +168,4 @@ th label { @apply inline-block truncate max-w-20char transition-all 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; } -} */ \ No newline at end of file +} */ diff --git a/common/time.py b/common/time.py index a51f2ce..8e2d384 100644 --- a/common/time.py +++ b/common/time.py @@ -12,7 +12,7 @@ def _safe_timedelta(duration: timedelta | int | None): def format_duration( - duration: timedelta | int | None, format_string: str = "%H hours" + duration: timedelta | int | float | None, format_string: str = "%H hours" ) -> str: """ Format timedelta into the specified format_string. diff --git a/common/utils.py b/common/utils.py index ed82eb6..6a5ec38 100644 --- a/common/utils.py +++ b/common/utils.py @@ -1,3 +1,32 @@ +from random import choices +from string import ascii_lowercase +from typing import Any + +from django.template.loader import render_to_string +from django.utils.safestring import mark_safe + + +def Popover( + wrapped_content: str, + popover_content: str = "", +) -> str: + id = randomid() + if popover_content == "": + popover_content = wrapped_content + content = f"{wrapped_content}" + result = mark_safe( + str(content) + + render_to_string( + "cotton/popover.html", + { + "id": id, + "slot": popover_content, + }, + ) + ) + return result + + def safe_division(numerator: int | float, denominator: int | float) -> int | float: """ Divides without triggering division by zero exception. @@ -9,7 +38,7 @@ def safe_division(numerator: int | float, denominator: int | float) -> int | flo return 0 -def safe_getattr(obj, attr_chain, default=None): +def safe_getattr(obj: object, attr_chain: str, default: Any | None = None) -> object: """ Safely get the nested attribute from an object. @@ -28,3 +57,24 @@ def safe_getattr(obj, attr_chain, default=None): except AttributeError: return default return obj + + +def truncate(input_string: str, length: int = 30, ellipsis: str = "…") -> str: + return ( + (f"{input_string[:length-len(ellipsis)]}{ellipsis}") + if len(input_string) > 30 + else input_string + ) + + +def truncate_with_popover(input_string: str) -> str: + if (truncated := truncate(input_string)) != input_string: + print(f"Not the same after: {truncated=}") + return Popover(wrapped_content=truncated, popover_content=input_string) + else: + print("Strings are the same!") + return input_string + + +def randomid(seed: str = "", length: int = 10) -> str: + return seed + "".join(choices(ascii_lowercase, k=length)) diff --git a/games/forms.py b/games/forms.py index 1104d54..979d990 100644 --- a/games/forms.py +++ b/games/forms.py @@ -1,7 +1,7 @@ from django import forms from django.urls import reverse -from common.utils import safe_getattr +from common.utils import safe_getattr from games.models import Device, Edition, Game, Platform, Purchase, Session custom_date_widget = forms.DateInput(attrs={"type": "date"}) diff --git a/games/migrations/0035_alter_session_device.py b/games/migrations/0035_alter_session_device.py new file mode 100644 index 0000000..2f02601 --- /dev/null +++ b/games/migrations/0035_alter_session_device.py @@ -0,0 +1,25 @@ +# Generated by Django 5.1 on 2024-08-11 15:50 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("games", "0034_purchase_date_dropped_purchase_infinite"), + ] + + operations = [ + migrations.AlterField( + model_name="session", + name="device", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_DEFAULT, + to="games.device", + ), + ), + ] diff --git a/games/migrations/0036_alter_edition_platform.py b/games/migrations/0036_alter_edition_platform.py new file mode 100644 index 0000000..e101e0e --- /dev/null +++ b/games/migrations/0036_alter_edition_platform.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1 on 2024-08-11 16:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('games', '0035_alter_session_device'), + ] + + operations = [ + migrations.AlterField( + model_name='edition', + name='platform', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='games.platform'), + ), + ] diff --git a/games/models.py b/games/models.py index 3f5f3e5..15d6dd1 100644 --- a/games/models.py +++ b/games/models.py @@ -2,7 +2,7 @@ from datetime import timedelta from django.core.exceptions import ValidationError from django.db import models -from django.db.models import F, Manager, Sum +from django.db.models import F, Sum from django.utils import timezone from common.time import format_duration @@ -15,6 +15,18 @@ class Game(models.Model): wikidata = models.CharField(max_length=50, null=True, blank=True, default=None) created_at = models.DateTimeField(auto_now_add=True) + session_average: float | int | timedelta | None + session_count: int | None + + def __str__(self): + return self.name + + +class Platform(models.Model): + name = models.CharField(max_length=255) + group = models.CharField(max_length=255, null=True, blank=True, default=None) + created_at = models.DateTimeField(auto_now_add=True) + def __str__(self): return self.name @@ -23,11 +35,11 @@ class Edition(models.Model): class Meta: unique_together = [["name", "platform", "year_released"]] - game = models.ForeignKey("Game", on_delete=models.CASCADE) + game = models.ForeignKey(Game, on_delete=models.CASCADE) name = models.CharField(max_length=255) sort_name = models.CharField(max_length=255, null=True, blank=True, default=None) platform = models.ForeignKey( - "Platform", on_delete=models.CASCADE, null=True, blank=True, default=None + Platform, on_delete=models.SET_DEFAULT, null=True, blank=True, default=None ) year_released = models.IntegerField(null=True, blank=True, default=None) wikidata = models.CharField(max_length=50, null=True, blank=True, default=None) @@ -83,9 +95,9 @@ class Purchase(models.Model): objects = PurchaseQueryset().as_manager() - edition = models.ForeignKey("Edition", on_delete=models.CASCADE) + edition = models.ForeignKey(Edition, on_delete=models.CASCADE) platform = models.ForeignKey( - "Platform", on_delete=models.CASCADE, default=None, null=True, blank=True + Platform, on_delete=models.CASCADE, default=None, null=True, blank=True ) date_purchased = models.DateField() date_refunded = models.DateField(blank=True, null=True) @@ -100,7 +112,7 @@ class Purchase(models.Model): type = models.CharField(max_length=255, choices=TYPES, default=GAME) name = models.CharField(max_length=255, default="", null=True, blank=True) related_purchase = models.ForeignKey( - "Purchase", + "self", on_delete=models.SET_NULL, default=None, null=True, @@ -135,15 +147,6 @@ class Purchase(models.Model): super().save(*args, **kwargs) -class Platform(models.Model): - name = models.CharField(max_length=255) - group = models.CharField(max_length=255, null=True, blank=True, default=None) - created_at = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return self.name - - class SessionQuerySet(models.QuerySet): def total_duration_formatted(self): return format_duration(self.total_duration_unformatted()) @@ -172,14 +175,14 @@ class Session(models.Model): class Meta: get_latest_by = "timestamp_start" - purchase = models.ForeignKey("Purchase", on_delete=models.CASCADE) + purchase = models.ForeignKey(Purchase, on_delete=models.CASCADE) timestamp_start = models.DateTimeField() timestamp_end = models.DateTimeField(blank=True, null=True) duration_manual = models.DurationField(blank=True, null=True, default=timedelta(0)) duration_calculated = models.DurationField(blank=True, null=True) device = models.ForeignKey( "Device", - on_delete=models.CASCADE, + on_delete=models.SET_DEFAULT, null=True, blank=True, default=None, @@ -220,7 +223,7 @@ class Session(models.Model): def duration_sum(self) -> str: return Session.objects.all().total_duration_formatted() - def save(self, *args, **kwargs): + def save(self, *args, **kwargs) -> None: if self.timestamp_start != None and self.timestamp_end != None: self.duration_calculated = self.timestamp_end - self.timestamp_start else: diff --git a/games/static/base.css b/games/static/base.css index 7adf3d9..ba8af4f 100644 --- a/games/static/base.css +++ b/games/static/base.css @@ -1246,6 +1246,18 @@ input:checked + .toggle-bg { } } +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + .visible { visibility: visible; } @@ -1370,10 +1382,18 @@ input:checked + .toggle-bg { margin-bottom: 0.75rem; } +.mb-4 { + margin-bottom: 1rem; +} + .mb-8 { margin-bottom: 2rem; } +.me-2 { + margin-inline-end: 0.5rem; +} + .ml-1 { margin-left: 0.25rem; } @@ -1382,6 +1402,18 @@ input:checked + .toggle-bg { margin-right: 1rem; } +.ms-0 { + margin-inline-start: 0px; +} + +.ms-2 { + margin-inline-start: 0.5rem; +} + +.ms-2\.5 { + margin-inline-start: 0.625rem; +} + .mt-2 { margin-top: 0.5rem; } @@ -1390,6 +1422,14 @@ input:checked + .toggle-bg { margin-top: 1rem; } +.mb-5 { + margin-bottom: 1.25rem; +} + +.mb-6 { + margin-bottom: 1.5rem; +} + .block { display: block; } @@ -1427,12 +1467,16 @@ input:checked + .toggle-bg { height: 1.5rem; } +.h-10 { + height: 2.5rem; +} + .h-12 { height: 3rem; } -.h-24 { - height: 6rem; +.h-2\.5 { + height: 0.625rem; } .h-3 { @@ -1451,6 +1495,10 @@ input:checked + .toggle-bg { height: 1.5rem; } +.h-8 { + height: 2rem; +} + .h-9 { height: 2.25rem; } @@ -1463,6 +1511,14 @@ input:checked + .toggle-bg { width: 50%; } +.w-10 { + width: 2.5rem; +} + +.w-2\.5 { + width: 0.625rem; +} + .w-24 { width: 6rem; } @@ -1471,6 +1527,10 @@ input:checked + .toggle-bg { width: 1rem; } +.w-44 { + width: 11rem; +} + .w-5 { width: 1.25rem; } @@ -1487,10 +1547,6 @@ input:checked + .toggle-bg { width: 1.75rem; } -.w-auto { - width: auto; -} - .w-full { width: 100%; } @@ -1503,6 +1559,10 @@ input:checked + .toggle-bg { max-width: 1024px; } +.max-w-screen-xl { + max-width: 1280px; +} + .max-w-sm { max-width: 24rem; } @@ -1639,6 +1699,12 @@ input:checked + .toggle-bg { gap: 1.25rem; } +.-space-x-px > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(-1px * var(--tw-space-x-reverse)); + margin-left: calc(-1px * calc(1 - var(--tw-space-x-reverse))); +} + .space-x-1 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(0.25rem * var(--tw-space-x-reverse)); @@ -1651,6 +1717,23 @@ input:checked + .toggle-bg { margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); } +.space-x-3 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.75rem * var(--tw-space-x-reverse)); + margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse))); +} + +.divide-y > :not([hidden]) ~ :not([hidden]) { + --tw-divide-y-reverse: 0; + border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); +} + +.divide-gray-100 > :not([hidden]) ~ :not([hidden]) { + --tw-divide-opacity: 1; + border-color: rgb(243 244 246 / var(--tw-divide-opacity)); +} + .self-center { align-self: center; } @@ -1659,6 +1742,10 @@ input:checked + .toggle-bg { overflow: hidden; } +.overflow-x-auto { + overflow-x: auto; +} + .truncate { overflow: hidden; text-overflow: ellipsis; @@ -1673,6 +1760,10 @@ input:checked + .toggle-bg { text-wrap: wrap; } +.rounded { + border-radius: 0.25rem; +} + .rounded-full { border-radius: 9999px; } @@ -1717,6 +1808,10 @@ input:checked + .toggle-bg { border-width: 0px; } +.border-b { + border-bottom-width: 1px; +} + .border-blue-600 { --tw-border-opacity: 1; border-color: rgb(28 100 242 / var(--tw-border-opacity)); @@ -1747,6 +1842,11 @@ input:checked + .toggle-bg { border-color: rgb(220 215 254 / var(--tw-border-opacity)); } +.bg-blue-100 { + --tw-bg-opacity: 1; + background-color: rgb(225 239 254 / var(--tw-bg-opacity)); +} + .bg-blue-700 { --tw-bg-opacity: 1; background-color: rgb(26 86 219 / var(--tw-bg-opacity)); @@ -1762,6 +1862,16 @@ input:checked + .toggle-bg { background-color: rgb(229 231 235 / var(--tw-bg-opacity)); } +.bg-gray-400 { + --tw-bg-opacity: 1; + background-color: rgb(156 163 175 / var(--tw-bg-opacity)); +} + +.bg-gray-50 { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); +} + .bg-gray-800 { --tw-bg-opacity: 1; background-color: rgb(31 41 55 / var(--tw-bg-opacity)); @@ -1794,6 +1904,10 @@ input:checked + .toggle-bg { padding: 0.25rem; } +.p-2 { + padding: 0.5rem; +} + .p-2\.5 { padding: 0.625rem; } @@ -1807,6 +1921,11 @@ input:checked + .toggle-bg { padding-right: 0.5rem; } +.px-2\.5 { + padding-left: 0.625rem; + padding-right: 0.625rem; +} + .px-3 { padding-left: 0.75rem; padding-right: 0.75rem; @@ -1822,6 +1941,16 @@ input:checked + .toggle-bg { padding-right: 1.25rem; } +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.py-0\.5 { + padding-top: 0.125rem; + padding-bottom: 0.125rem; +} + .py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; @@ -1842,20 +1971,9 @@ input:checked + .toggle-bg { padding-bottom: 0.75rem; } -.pb-16 { - padding-bottom: 4rem; -} - -.pl-3 { - padding-left: 0.75rem; -} - -.pr-4 { - padding-right: 1rem; -} - -.pt-1 { - padding-top: 0.25rem; +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; } .pt-2 { @@ -1866,10 +1984,18 @@ input:checked + .toggle-bg { padding-top: 2rem; } +.text-left { + text-align: left; +} + .text-center { text-align: center; } +.text-right { + text-align: right; +} + .align-top { vertical-align: top; } @@ -1935,14 +2061,26 @@ input:checked + .toggle-bg { font-weight: 700; } +.font-extrabold { + font-weight: 800; +} + .font-medium { font-weight: 500; } +.font-normal { + font-weight: 400; +} + .font-semibold { font-weight: 600; } +.uppercase { + text-transform: uppercase; +} + .leading-6 { line-height: 1.5rem; } @@ -1951,11 +2089,33 @@ input:checked + .toggle-bg { line-height: 2.25rem; } +.leading-none { + line-height: 1; +} + +.leading-tight { + line-height: 1.25; +} + +.tracking-tight { + letter-spacing: -0.025em; +} + .text-blue-600 { --tw-text-opacity: 1; color: rgb(28 100 242 / var(--tw-text-opacity)); } +.text-blue-800 { + --tw-text-opacity: 1; + color: rgb(30 66 159 / var(--tw-text-opacity)); +} + +.text-gray-300 { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); +} + .text-gray-400 { --tw-text-opacity: 1; color: rgb(156 163 175 / var(--tw-text-opacity)); @@ -2256,14 +2416,6 @@ textarea:disabled:is(.dark *) { margin-right: 0.25rem; } -th { - text-align: right; -} - -th label { - margin-right: 1rem; -} - .basic-button-container { display: flex; justify-content: center; @@ -2366,11 +2518,26 @@ th label { } } */ +.odd\:bg-white:nth-child(odd) { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.even\:bg-gray-50:nth-child(even) { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); +} + .hover\:border-gray-300:hover { --tw-border-opacity: 1; border-color: rgb(209 213 219 / var(--tw-border-opacity)); } +.hover\:border-green-600:hover { + --tw-border-opacity: 1; + border-color: rgb(5 122 85 / var(--tw-border-opacity)); +} + .hover\:bg-blue-800:hover { --tw-bg-opacity: 1; background-color: rgb(30 66 159 / var(--tw-bg-opacity)); @@ -2381,9 +2548,14 @@ th label { background-color: rgb(243 244 246 / var(--tw-bg-opacity)); } -.hover\:bg-gray-400:hover { +.hover\:bg-gray-50:hover { --tw-bg-opacity: 1; - background-color: rgb(156 163 175 / var(--tw-bg-opacity)); + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); +} + +.hover\:bg-green-500:hover { + --tw-bg-opacity: 1; + background-color: rgb(14 159 110 / var(--tw-bg-opacity)); } .hover\:bg-green-700:hover { @@ -2396,6 +2568,11 @@ th label { background-color: rgb(253 232 232 / var(--tw-bg-opacity)); } +.hover\:bg-red-500:hover { + --tw-bg-opacity: 1; + background-color: rgb(240 82 82 / var(--tw-bg-opacity)); +} + .hover\:bg-violet-700:hover { --tw-bg-opacity: 1; background-color: rgb(109 40 217 / var(--tw-bg-opacity)); @@ -2421,13 +2598,19 @@ th label { color: rgb(75 85 99 / var(--tw-text-opacity)); } +.hover\:text-gray-700:hover { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); +} + .hover\:text-gray-900:hover { --tw-text-opacity: 1; color: rgb(17 24 39 / var(--tw-text-opacity)); } -.hover\:underline:hover { - text-decoration-line: underline; +.hover\:text-white:hover { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); } .focus\:z-10:focus { @@ -2476,6 +2659,11 @@ th label { --tw-ring-color: rgb(14 159 110 / var(--tw-ring-opacity)); } +.focus\:ring-green-700:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(4 108 78 / var(--tw-ring-opacity)); +} + .focus\:ring-violet-500:focus { --tw-ring-opacity: 1; --tw-ring-color: rgb(139 92 246 / var(--tw-ring-opacity)); @@ -2505,10 +2693,6 @@ th label { top: -2rem; } -.group:hover .group-hover\:block { - display: block; -} - .group:hover .group-hover\:min-w-60 { min-width: 15rem; } @@ -2557,6 +2741,11 @@ th label { outline-color: #AC94FA; } +.dark\:divide-gray-600:is(.dark *) > :not([hidden]) ~ :not([hidden]) { + --tw-divide-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-divide-opacity)); +} + .dark\:border-blue-500:is(.dark *) { --tw-border-opacity: 1; border-color: rgb(63 131 248 / var(--tw-border-opacity)); @@ -2581,6 +2770,11 @@ th label { border-color: transparent; } +.dark\:bg-blue-200:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(195 221 253 / var(--tw-bg-opacity)); +} + .dark\:bg-blue-600:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(28 100 242 / var(--tw-bg-opacity)); @@ -2624,6 +2818,16 @@ th label { color: rgb(63 131 248 / var(--tw-text-opacity)); } +.dark\:text-blue-800:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(30 66 159 / var(--tw-text-opacity)); +} + +.dark\:text-gray-200:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(229 231 235 / var(--tw-text-opacity)); +} + .dark\:text-gray-400:is(.dark *) { --tw-text-opacity: 1; color: rgb(156 163 175 / var(--tw-text-opacity)); @@ -2634,6 +2838,11 @@ th label { color: rgb(107 114 128 / var(--tw-text-opacity)); } +.dark\:text-gray-600:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity)); +} + .dark\:text-slate-400:is(.dark *) { --tw-text-opacity: 1; color: rgb(148 163 184 / var(--tw-text-opacity)); @@ -2654,6 +2863,26 @@ th label { color: rgb(255 255 255 / var(--tw-text-opacity)); } +.odd\:dark\:bg-gray-900:is(.dark *):nth-child(odd) { + --tw-bg-opacity: 1; + background-color: rgb(17 24 39 / var(--tw-bg-opacity)); +} + +.even\:dark\:bg-gray-800:is(.dark *):nth-child(even) { + --tw-bg-opacity: 1; + background-color: rgb(31 41 55 / var(--tw-bg-opacity)); +} + +.dark\:hover\:border-green-700:hover:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(4 108 78 / var(--tw-border-opacity)); +} + +.dark\:hover\:border-red-700:hover:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(200 30 30 / var(--tw-border-opacity)); +} + .dark\:hover\:bg-blue-700:hover:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(26 86 219 / var(--tw-bg-opacity)); @@ -2674,6 +2903,11 @@ th label { background-color: rgb(31 41 55 / var(--tw-bg-opacity)); } +.dark\:hover\:bg-green-600:hover:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(5 122 85 / var(--tw-bg-opacity)); +} + .dark\:hover\:bg-red-700:hover:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(200 30 30 / var(--tw-bg-opacity)); @@ -2704,11 +2938,22 @@ th label { --tw-ring-color: rgb(63 131 248 / var(--tw-ring-opacity)); } -@media (min-width: 640px) { - .sm\:inline { - display: inline; - } +.dark\:focus\:ring-blue-800:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(30 66 159 / var(--tw-ring-opacity)); +} +.dark\:focus\:ring-gray-600:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity)); +} + +.dark\:focus\:ring-green-500:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(14 159 110 / var(--tw-ring-opacity)); +} + +@media (min-width: 640px) { .sm\:table-cell { display: table-cell; } @@ -2717,19 +2962,23 @@ th label { max-width: 28rem; } + .sm\:max-w-screen-sm { + max-width: 640px; + } + .sm\:max-w-xl { max-width: 36rem; } + .sm\:rounded-lg { + border-radius: 0.5rem; + } + .sm\:px-4 { padding-left: 1rem; padding-right: 1rem; } - .sm\:pl-12 { - padding-left: 3rem; - } - .sm\:pl-2 { padding-left: 0.5rem; } @@ -2738,28 +2987,67 @@ th label { padding-left: 1rem; } - .sm\:pl-6 { - padding-left: 1.5rem; - } - .sm\:decoration-2 { text-decoration-thickness: 2px; } } @media (min-width: 768px) { + .md\:mb-0 { + margin-bottom: 0px; + } + + .md\:mt-0 { + margin-top: 0px; + } + .md\:block { display: block; } + .md\:inline { + display: inline; + } + + .md\:hidden { + display: none; + } + .md\:w-auto { width: auto; } + .md\:max-w-screen-md { + max-width: 768px; + } + .md\:flex-row { flex-direction: row; } + .md\:space-x-8 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(2rem * var(--tw-space-x-reverse)); + margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse))); + } + + .md\:border-0 { + border-width: 0px; + } + + .md\:bg-transparent { + background-color: transparent; + } + + .md\:bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); + } + + .md\:p-0 { + padding: 0px; + } + .md\:px-6 { padding-left: 1.5rem; padding-right: 1.5rem; @@ -2769,6 +3057,48 @@ th label { padding-top: 0.5rem; padding-bottom: 0.5rem; } + + .md\:text-5xl { + font-size: 3rem; + line-height: 1; + } + + .md\:text-blue-700 { + --tw-text-opacity: 1; + color: rgb(26 86 219 / var(--tw-text-opacity)); + } + + .md\:hover\:bg-transparent:hover { + background-color: transparent; + } + + .md\:hover\:text-blue-700:hover { + --tw-text-opacity: 1; + color: rgb(26 86 219 / var(--tw-text-opacity)); + } + + .md\:dark\:bg-gray-900:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(17 24 39 / var(--tw-bg-opacity)); + } + + .md\:dark\:bg-transparent:is(.dark *) { + background-color: transparent; + } + + .md\:dark\:text-blue-500:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(63 131 248 / var(--tw-text-opacity)); + } + + .md\:dark\:hover\:bg-transparent:hover:is(.dark *) { + background-color: transparent; + } + + .md\:dark\:hover\:text-blue-500:hover:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(63 131 248 / var(--tw-text-opacity)); + } } @media (min-width: 1024px) { @@ -2783,6 +3113,17 @@ th label { .lg\:max-w-lg { max-width: 32rem; } + + .lg\:text-6xl { + font-size: 3.75rem; + line-height: 1; + } +} + +@media (min-width: 1536px) { + .\32xl\:max-w-screen-2xl { + max-width: 1536px; + } } .rtl\:rotate-180:where([dir="rtl"], [dir="rtl"] *) { @@ -2792,4 +3133,22 @@ th label { .rtl\:space-x-reverse:where([dir="rtl"], [dir="rtl"] *) > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 1; -} \ No newline at end of file +} + +.rtl\:text-left:where([dir="rtl"], [dir="rtl"] *) { + text-align: left; +} + +.rtl\:text-right:where([dir="rtl"], [dir="rtl"] *) { + text-align: right; +} + +.\[\&\:first-of-type_button\]\:rounded-s-lg:first-of-type button { + border-start-start-radius: 0.5rem; + border-end-start-radius: 0.5rem; +} + +.\[\&\:last-of-type_button\]\:rounded-e-lg:last-of-type button { + border-start-end-radius: 0.5rem; + border-end-end-radius: 0.5rem; +} diff --git a/games/templates/add_purchase.html b/games/templates/add_purchase.html index 5a09d96..2fa0ec8 100644 --- a/games/templates/add_purchase.html +++ b/games/templates/add_purchase.html @@ -26,7 +26,9 @@ - Delete + Delete {% endif %} diff --git a/games/templates/base.html b/games/templates/base.html index bfc83de..98f33dc 100644 --- a/games/templates/base.html +++ b/games/templates/base.html @@ -16,8 +16,16 @@ {% django_htmx_script %} + - + loading indicator
- -
- {% block content %} - No content here. - {% endblock content %} -
- {% load version %} - {% version %} ({% version_date %}) + {% include "navbar.html" %} +
+ {% block content %} + No content here. + {% endblock content %}
- {% block scripts %} - {% endblock scripts %} - - + {% load version %} + {% version %} ({% version_date %}) +
+ {% block scripts %} + {% endblock scripts %} + + + diff --git a/games/templates/components.yml b/games/templates/components.yml deleted file mode 100644 index 4a5d35c..0000000 --- a/games/templates/components.yml +++ /dev/null @@ -1,3 +0,0 @@ -components: - gamelink: "components/game_link.html" - popover: "components/popover.html" \ No newline at end of file diff --git a/games/templates/components/edit_button.html b/games/templates/components/edit_button.html deleted file mode 100644 index 57997ee..0000000 --- a/games/templates/components/edit_button.html +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/games/templates/components/game_link.html b/games/templates/components/game_link.html deleted file mode 100644 index ae3fa4a..0000000 --- a/games/templates/components/game_link.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {% if children %} - {{ children }} - {% else %} - {{ name }} - {% endif %} - - \ No newline at end of file diff --git a/games/templates/components/popover.html b/games/templates/components/popover.html deleted file mode 100644 index 4bb218a..0000000 --- a/games/templates/components/popover.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/games/templates/cotton/button.html b/games/templates/cotton/button.html new file mode 100644 index 0000000..83f0695 --- /dev/null +++ b/games/templates/cotton/button.html @@ -0,0 +1,4 @@ + diff --git a/games/templates/cotton/button_group_button_sm.html b/games/templates/cotton/button_group_button_sm.html new file mode 100644 index 0000000..2ea43a9 --- /dev/null +++ b/games/templates/cotton/button_group_button_sm.html @@ -0,0 +1,20 @@ + + + {% if color == "gray" %} + + {% elif color == "red" %} + + {% elif color == "green" %} + + {% endif %} + diff --git a/games/templates/cotton/button_group_sm.html b/games/templates/cotton/button_group_sm.html new file mode 100644 index 0000000..987db77 --- /dev/null +++ b/games/templates/cotton/button_group_sm.html @@ -0,0 +1,6 @@ + +
+ {% for button in buttons %} + + {% endfor %} +
diff --git a/games/templates/components/button.html b/games/templates/cotton/button_old.html similarity index 100% rename from games/templates/components/button.html rename to games/templates/cotton/button_old.html diff --git a/games/templates/components/button_start.html b/games/templates/cotton/button_start.html similarity index 100% rename from games/templates/components/button_start.html rename to games/templates/cotton/button_start.html diff --git a/games/templates/cotton/gamelink.html b/games/templates/cotton/gamelink.html new file mode 100644 index 0000000..5be4acd --- /dev/null +++ b/games/templates/cotton/gamelink.html @@ -0,0 +1,10 @@ + + + {% if slot %} + {{ slot }} + {% else %} + {{ name }} + {% endif %} + + diff --git a/games/templates/cotton/h1.html b/games/templates/cotton/h1.html new file mode 100644 index 0000000..3b7ad35 --- /dev/null +++ b/games/templates/cotton/h1.html @@ -0,0 +1,8 @@ +

+ {{ slot }} + {% if badge %} + + {{ badge }} + + {% endif %} +

diff --git a/games/templates/cotton/popover.html b/games/templates/cotton/popover.html new file mode 100644 index 0000000..38b104c --- /dev/null +++ b/games/templates/cotton/popover.html @@ -0,0 +1,7 @@ + diff --git a/games/templates/cotton/simple_table.html b/games/templates/cotton/simple_table.html new file mode 100644 index 0000000..62c413f --- /dev/null +++ b/games/templates/cotton/simple_table.html @@ -0,0 +1,50 @@ +{% load param_utils %} +
+
+ + + + {% for column in columns %}{% endfor %} + + + + {% for row in rows %}{% endfor %} + +
{{ column }}
+
+ {% if page_obj and elided_page_range %} + + {% endif %} +
diff --git a/games/templates/cotton/table.html b/games/templates/cotton/table.html new file mode 100644 index 0000000..1376fdb --- /dev/null +++ b/games/templates/cotton/table.html @@ -0,0 +1,12 @@ +
+ + + + {% for column in columns %}{% endfor %} + + + + {{ slot }} + +
{{ column }}
+
diff --git a/games/templates/cotton/table_row.html b/games/templates/cotton/table_row.html new file mode 100644 index 0000000..4ca4622 --- /dev/null +++ b/games/templates/cotton/table_row.html @@ -0,0 +1,16 @@ + + {% if slot %} + {{ slot }} + {% else %} + {% for td in data %} + {% if forloop.first %} + {{ td }} + {% else %} + + {{ td }} + + {% endif %} + {% endfor %} + {% endif %} + diff --git a/games/templates/cotton/table_td.html b/games/templates/cotton/table_td.html new file mode 100644 index 0000000..47d2fca --- /dev/null +++ b/games/templates/cotton/table_td.html @@ -0,0 +1 @@ +{{ slot }} diff --git a/games/templates/list_purchases.html b/games/templates/list_purchases.html new file mode 100644 index 0000000..ac553d9 --- /dev/null +++ b/games/templates/list_purchases.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% load static %} +{% block title %} + {{ title }} +{% endblock title %} +{% block content %} +
+ +
+{% endblock content %} diff --git a/games/templates/list_sessions.html b/games/templates/list_sessions.html index 414261a..6aae78d 100644 --- a/games/templates/list_sessions.html +++ b/games/templates/list_sessions.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends "base.html" %} {% load static %} {% block title %} {{ title }} @@ -15,7 +15,7 @@ hx-target=".responsive-table tbody" onClick="document.querySelector('#last-session-start').classList.add('invisible')" class="{% if last.timestamp_end == null %}invisible{% endif %}"> - {% include 'components/button_start.html' with text=last.purchase title="Start session of last played game" only %} + {% include "components/button_start.html" with text=last.purchase title="Start session of last played game" only %} {% endif %} @@ -32,42 +32,43 @@ {% for session in dataset %} {% partialdef session-row inline=True %} - - - - - {{ session.purchase.edition.name }} - - - - - {{ session.timestamp_start | date:"d/m/Y H:i" }} - - - {% if not session.timestamp_end %} - {% url 'list_sessions_end_session' session.id as end_session_url %} - - Finish now? - - {% elif session.duration_manual %} - -- - {% else %} - {{ session.timestamp_end | date:"d/m/Y H:i" }} - {% endif %} - - {{ session.duration_formatted }} - - {% endpartialdef %} - {% endfor %} - - - {% else %} -
No sessions found.
- {% endif %} - + + + + + {{ session.purchase.edition.name }} + + + + + {{ session.timestamp_start | date:"d/m/Y H:i" }} + + + {% if not session.timestamp_end %} + {% url 'list_sessions_end_session' session.id as end_session_url %} + + Finish now? + + {% elif session.duration_manual %} + -- + {% else %} + {{ session.timestamp_end | date:"d/m/Y H:i" }} + {% endif %} + + {{ session.duration_formatted }} + + {% endpartialdef %} + {% endfor %} + + + {% else %} +
No sessions found.
+ {% endif %} + {% endblock content %} diff --git a/games/templates/navbar.html b/games/templates/navbar.html new file mode 100644 index 0000000..7b4b08f --- /dev/null +++ b/games/templates/navbar.html @@ -0,0 +1,136 @@ +{% load static %} + diff --git a/games/templates/registration/login.html b/games/templates/registration/login.html index 8df4f0d..94bb70a 100644 --- a/games/templates/registration/login.html +++ b/games/templates/registration/login.html @@ -1,6 +1,5 @@ {% extends "base.html" %} {% load static %} - {% block title %} Login {% endblock title %} diff --git a/games/templates/stats.html b/games/templates/stats.html index 567e03b..676b1c9 100644 --- a/games/templates/stats.html +++ b/games/templates/stats.html @@ -3,17 +3,15 @@ {{ title }} {% endblock title %} {% load static %} - {% partialdef purchase-name %} - {% if purchase.type != 'game' %} - {% #gamelink game_id=purchase.edition.game.id %} - {{ purchase.name }} ({{ purchase.edition.name }} {{ purchase.get_type_display }}) - {% /gamelink %} - {% else %} - {% gamelink game_id=purchase.edition.game.id name=purchase.edition.name %} - {% endif %} +{% if purchase.type != 'game' %} + + {{ purchase.name }} ({{ purchase.edition.name }} {{ purchase.get_type_display }}) + +{% else %} + +{% endif %} {% endpartialdef %} - {% block content %}
@@ -66,29 +64,36 @@ Longest session - {{ longest_session_time }} ({% gamelink game_id=longest_session_game.id name=longest_session_game.name %}) + + {{ longest_session_time }} () + Most sessions - {{ highest_session_count }} ({% gamelink game_id=highest_session_count_game.id name=highest_session_count_game.name %}) + + {{ highest_session_count }} () + Highest session average - {{ highest_session_average }} ({% gamelink game_id=highest_session_average_game.id name=highest_session_average_game.name %}) + {{ highest_session_average }} () First play - {% gamelink game_id=first_play_game.id name=first_play_game.name %} ({{ first_play_date }}) + + ({{ first_play_date }}) + Last play - {% gamelink game_id=last_play_game.id name=last_play_game.name %} ({{ last_play_date }}) + + ({{ last_play_date }}) + - {% if month_playtime %}

Playtime per month

@@ -102,7 +107,6 @@
{% endif %} -

Purchases

@@ -118,9 +122,7 @@ - + @@ -150,7 +152,7 @@ {% for game in top_10_games_by_playtime %} @@ -174,7 +176,6 @@ {% endfor %}
Dropped - {{ dropped_count }} ({{ dropped_percentage }}%) - {{ dropped_count }} ({{ dropped_percentage }}%)
Unfinished
- {% gamelink game_id=game.id name=game.name %} + {{ game.formatted_playtime }}
- {% if all_finished_this_year %}

Finished

@@ -187,16 +188,13 @@ {% for purchase in all_finished_this_year %} - + {% endfor %}
- {% partial purchase-name %} - {% partial purchase-name %} {{ purchase.date_finished | date:"d/m/Y" }}
{% endif %} - {% if this_year_finished_this_year %}

Finished ({{ year }} games)

@@ -209,16 +207,13 @@ {% for purchase in this_year_finished_this_year %} - + {% endfor %}
- {% partial purchase-name %} - {% partial purchase-name %} {{ purchase.date_finished | date:"d/m/Y" }}
{% endif %} - {% if purchased_this_year_finished_this_year %}

Bought and Finished ({{ year }})

@@ -231,16 +226,13 @@ {% for purchase in purchased_this_year_finished_this_year %} - + {% endfor %}
- {% partial purchase-name %} - {% partial purchase-name %} {{ purchase.date_finished | date:"d/m/Y" }}
{% endif %} - {% if purchased_unfinished %}

Unfinished Purchases

@@ -254,9 +246,7 @@ {% for purchase in purchased_unfinished %} - + @@ -264,7 +254,6 @@
- {% partial purchase-name %} - {% partial purchase-name %} {{ purchase.price }} {{ purchase.date_purchased | date:"d/m/Y" }}
{% endif %} - {% if all_purchased_this_year %}

All Purchases

@@ -278,9 +267,7 @@ {% for purchase in all_purchased_this_year %} - + diff --git a/games/templates/view_game.html b/games/templates/view_game.html index 7a3891e..c0f647d 100644 --- a/games/templates/view_game.html +++ b/games/templates/view_game.html @@ -10,151 +10,98 @@
{{ game.name }} {{ game.year_released }} - {% #popover id="popover-year" %} - Original release year - {% /popover %} + + Original release year +
- + {{ hours_sum }} - {% #popover id="popover-hours" %} - Total hours played - {% /popover %} + + Total hours played + - - + + {{ session_count }} - {% #popover id="popover-sessions" %} - Number of sessions - {% /popover %} + + Number of sessions + - + {{ session_average_without_manual }} - {% #popover id="popover-average" %} - Average playtime per session - {% /popover %} + + Average playtime per session + - - + + {{ playrange }} - {% #popover id="popover-playrange" %} - Earliest and latest dates played - {% /popover %} + + Earliest and latest dates played +
- -

- Editions ({{ edition_count }}) and Purchases ({{ purchase_count }}) -

-
    - {% for edition in editions %} -
  • - {{ edition.name }} ({{ edition.platform }}, {{ edition.year_released }}) - {% if edition.wikidata %} - - {% endif %} - {% url 'edit_edition' edition.id as edit_url %} - {% include 'components/edit_button.html' with edit_url=edit_url %} -
  • -
      - {% for purchase in edition.game_purchases %} -
    • - {{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }} - {% if purchase.price != 0 %}({{ purchase.price }} {{ purchase.price_currency }}){% endif %} - {% url 'edit_purchase' purchase.id as edit_url %} - {% include 'components/edit_button.html' with edit_url=edit_url %} -
    • -
        - {% for related_purchase in purchase.nongame_related_purchases %} -
      • - {{ related_purchase.name }} ({{ related_purchase.get_type_display }}, {{ purchase.platform }}, {{ related_purchase.date_purchased | date:"Y" }}, {{ related_purchase.price }} {{ related_purchase.price_currency }}) - {% url 'edit_purchase' related_purchase.id as edit_url %} - {% include 'components/edit_button.html' with edit_url=edit_url %} -
      • - {% endfor %} -
      - {% endfor %} -
    - {% endfor %} -
-

- Sessions - ({{ session_count }}) - {% if latest_session_id %} - {% url 'view_game_start_session_from_session' latest_session_id as add_session_link %} - New - {% endif %} - and Notes ({{ sessions_with_notes_count }}) -

-
    - {% for session in sessions %} - {% partialdef session-info inline=True %} -
  • - {{ session.timestamp_start | date:"d/m/Y H:i" }}{% if session.timestamp_end %}-{{ session.timestamp_end | date:"H:i" }}{% endif %} - ({{ session.device.get_type_display | default:"Unknown" }}, {{ session.duration_formatted }}) - {% url 'edit_session' session.id as edit_url %} - {% include 'components/edit_button.html' with edit_url=edit_url %} - {% if not session.timestamp_end %} - {% url 'view_game_end_session' session.id as end_session_url %} - - - - - - - {% endif %} -
  • -
  • {{ session.note|markdown }}
  • - - {% endpartialdef %} - {% endfor %} -
+ Editions +
+ +
+
+ Purchases + +
+
+ Sessions + +
- {% partial purchase-name %} - {% partial purchase-name %} {{ purchase.price }} {{ purchase.date_purchased | date:"d/m/Y" }}