Compare commits
199 Commits
d6f77c0c19
...
fb1f6d2a33
Author | SHA1 | Date |
---|---|---|
Lukáš Kucharczyk | fb1f6d2a33 | |
Lukáš Kucharczyk | b219e3f6bc | |
Lukáš Kucharczyk | eff598f475 | |
Lukáš Kucharczyk | a3be509893 | |
Lukáš Kucharczyk | 6af754afa6 | |
Lukáš Kucharczyk | c99743701e | |
Lukáš Kucharczyk | da0a04e0c6 | |
Lukáš Kucharczyk | e4c6e9e414 | |
Lukáš Kucharczyk | 2eaccc57b0 | |
Lukáš Kucharczyk | 865ecd1ee0 | |
Lukáš Kucharczyk | fed1bfa053 | |
Lukáš Kucharczyk | dd92148db5 | |
Lukáš Kucharczyk | 8bf2c32eb5 | |
Lukáš Kucharczyk | d303039b1c | |
Lukáš Kucharczyk | 67b9cbb048 | |
Lukáš Kucharczyk | 5d36ad386e | |
Lukáš Kucharczyk | 42bc391e57 | |
Lukáš Kucharczyk | 850ca382ad | |
Lukáš Kucharczyk | d2e0bcfb12 | |
Lukáš Kucharczyk | b773d9df58 | |
Lukáš Kucharczyk | dc6c295ee7 | |
Lukáš Kucharczyk | d272915ef6 | |
Lukáš Kucharczyk | cbc8062d92 | |
Lukáš Kucharczyk | 02c04badac | |
Lukáš Kucharczyk | 5756b736d8 | |
Lukáš Kucharczyk | d4c0d47712 | |
Lukáš Kucharczyk | 6f62b2026b | |
Lukáš Kucharczyk | e6640a4083 | |
Lukáš Kucharczyk | 81fbcc9281 | |
Lukáš Kucharczyk | 2e891fc166 | |
Lukáš Kucharczyk | b95d5dfb98 | |
Lukáš Kucharczyk | 0c564ef146 | |
Lukáš Kucharczyk | 048401b20a | |
Lukáš Kucharczyk | aecf0d6a6e | |
Lukáš Kucharczyk | a4ec5d0dbc | |
Lukáš Kucharczyk | bec4e3716a | |
Lukáš Kucharczyk | 03142bc3c3 | |
Lukáš Kucharczyk | b0ef975c2b | |
Lukáš Kucharczyk | 555608d8c6 | |
Lukáš Kucharczyk | a7293c659d | |
Lukáš Kucharczyk | f36e692361 | |
Lukáš Kucharczyk | fe97f540a0 | |
Lukáš Kucharczyk | c35b539c42 | |
Lukáš Kucharczyk | bbe5e072b2 | |
Lukáš Kucharczyk | 6fc2f623dc | |
Lukáš Kucharczyk | 9481bd5fef | |
Lukáš Kucharczyk | 4083165123 | |
Lukáš Kucharczyk | 45bb2681c7 | |
Lukáš Kucharczyk | dbb8ec3f9a | |
Lukáš Kucharczyk | 206b5f6d46 | |
Lukáš Kucharczyk | b7e14ecc83 | |
Lukáš Kucharczyk | 912e010729 | |
Lukáš Kucharczyk | a485237456 | |
Lukáš Kucharczyk | f5faf92ee0 | |
Lukáš Kucharczyk | 07452d8c43 | |
Lukáš Kucharczyk | 229a79d266 | |
Lukáš Kucharczyk | c6ed577fe3 | |
Lukáš Kucharczyk | 171e4779a3 | |
Lukáš Kucharczyk | 79f94e5984 | |
Lukáš Kucharczyk | ccebcb89c6 | |
Lukáš Kucharczyk | fe0a6b39e3 | |
Lukáš Kucharczyk | 6a495f951f | |
Lukáš Kucharczyk | c8646d0a0c | |
Lukáš Kucharczyk | f2bb15e669 | |
Lukáš Kucharczyk | c49177d63c | |
Lukáš Kucharczyk | bd8d30eac1 | |
Lukáš Kucharczyk | c44d8bf427 | |
Lukáš Kucharczyk | 3f037b4c7c | |
Lukáš Kucharczyk | 8783d1fc8e | |
Lukáš Kucharczyk | 9a1d24dbfd | |
Lukáš Kucharczyk | 4720660cff | |
Lukáš Kucharczyk | e158bc0623 | |
Lukáš Kucharczyk | 8982fc5086 | |
Lukáš Kucharczyk | 729e1d939b | |
Lukáš Kucharczyk | 2b4683e489 | |
Lukáš Kucharczyk | cce810e8cf | |
Lukáš Kucharczyk | 62cd17f702 | |
Lukáš Kucharczyk | f31280c682 | |
Lukáš Kucharczyk | a745d16ec3 | |
Lukáš Kucharczyk | ae079e36ec | |
Lukáš Kucharczyk | c8a3212b77 | |
Lukáš Kucharczyk | d211326c3f | |
Lukáš Kucharczyk | 270a291f05 | |
Lukáš Kucharczyk | 13b750ca92 | |
Lukáš Kucharczyk | 015b6db2f7 | |
Lukáš Kucharczyk | 667b161fff | |
Lukáš Kucharczyk | 5958cbf4a6 | |
Lukáš Kucharczyk | 3b37f2c3f0 | |
Lukáš Kucharczyk | 4517ff2b5a | |
Lukáš Kucharczyk | 884ce13e26 | |
Lukáš Kucharczyk | dd219bae9d | |
Lukáš Kucharczyk | 60d29090a1 | |
Lukáš Kucharczyk | 1bc3ca057b | |
Lukáš Kucharczyk | c2c0886451 | |
Lukáš Kucharczyk | b0be7b5887 | |
Lukáš Kucharczyk | 099d989f16 | |
Lukáš Kucharczyk | a879360ebd | |
Lukáš Kucharczyk | 866f2526e6 | |
Lukáš Kucharczyk | ce3c4b55f0 | |
Lukáš Kucharczyk | c52cd822ae | |
Lukáš Kucharczyk | cdc6ca1324 | |
Lukáš Kucharczyk | e7ed349356 | |
Lukáš Kucharczyk | 5052ca7dbf | |
Lukáš Kucharczyk | f408bfd927 | |
Lukáš Kucharczyk | 666dee33ba | |
Lukáš Kucharczyk | e0b09e051a | |
Lukáš Kucharczyk | 4552cf7616 | |
Lukáš Kucharczyk | a614b51d29 | |
Lukáš Kucharczyk | e67aa3fda1 | |
Lukáš Kucharczyk | 8423fd02b4 | |
Lukáš Kucharczyk | 2bd07e5f2d | |
Lukáš Kucharczyk | 058b83522c | |
Lukáš Kucharczyk | f13ed8a078 | |
Lukáš Kucharczyk | 02d5adcb3c | |
Lukáš Kucharczyk | d6fb16bb74 | |
Lukáš Kucharczyk | 71b90b8202 | |
Lukáš Kucharczyk | 3ee36932c3 | |
Lukáš Kucharczyk | 391fcc79a8 | |
Lukáš Kucharczyk | 57d4fd7212 | |
Lukáš Kucharczyk | a5b2854bf6 | |
Lukáš Kucharczyk | 518c0ecd56 | |
Lukáš Kucharczyk | a6cd7a3430 | |
Lukáš Kucharczyk | dba8414fd9 | |
Lukáš Kucharczyk | 0e2113eefd | |
Lukáš Kucharczyk | c4b0347f3b | |
Lukáš Kucharczyk | c6ed21167c | |
Lukáš Kucharczyk | 4ce15c44fc | |
Lukáš Kucharczyk | c814b4c2cb | |
Lukáš Kucharczyk | 11b9c602de | |
Lukáš Kucharczyk | 9a332593f4 | |
Lukáš Kucharczyk | 22935721ca | |
Lukáš Kucharczyk | a2ecdcf44a | |
Lukáš Kucharczyk | 3c958c4a13 | |
Lukáš Kucharczyk | 3db1724e22 | |
Lukáš Kucharczyk | d2a9630b04 | |
Lukáš Kucharczyk | e3ee832d3f | |
Lukáš Kucharczyk | 7467e2732d | |
Lukáš Kucharczyk | 787ee8640f | |
Lukáš Kucharczyk | ab41222f3c | |
Lukáš Kucharczyk | 29bf3b1946 | |
Lukáš Kucharczyk | 3f7ccea2e2 | |
Lukáš Kucharczyk | b5ffb3586b | |
Lukáš Kucharczyk | 26d57a238e | |
Lukáš Kucharczyk | 2d5ad3182c | |
Lukáš Kucharczyk | 49cc3ea0cc | |
Lukáš Kucharczyk | 440e1cfb71 | |
Lukáš Kucharczyk | 1cbd8c5c55 | |
Lukáš Kucharczyk | bc81a0ee8e | |
Lukáš Kucharczyk | c5653977ff | |
Lukáš Kucharczyk | f151730ab6 | |
Lukáš Kucharczyk | f469a67d94 | |
Lukáš Kucharczyk | 104ffc9d03 | |
Lukáš Kucharczyk | a4b13eb247 | |
Lukáš Kucharczyk | 2307fac83a | |
Lukáš Kucharczyk | 6b52c0d4c4 | |
Lukáš Kucharczyk | ff5d8c215d | |
Lukáš Kucharczyk | cdb3b89b08 | |
Lukáš Kucharczyk | ffa8198540 | |
Lukáš Kucharczyk | 0b7da3550c | |
Lukáš Kucharczyk | e1655d6cfa | |
Lukáš Kucharczyk | 29c41865d0 | |
Lukáš Kucharczyk | d21b461726 | |
Lukáš Kucharczyk | 95489cfb78 | |
Lukáš Kucharczyk | fa4f1c4810 | |
Lukáš Kucharczyk | 366c25a1ff | |
Lukáš Kucharczyk | a3042caa20 | |
Lukáš Kucharczyk | 7997f9bbb2 | |
Lukáš Kucharczyk | b78c4ba9c5 | |
Lukáš Kucharczyk | 1df889c45d | |
Lukáš Kucharczyk | 468d05a9e2 | |
Lukáš Kucharczyk | 2640a49734 | |
Lukáš Kucharczyk | 65c175afb2 | |
Lukáš Kucharczyk | 0814071a26 | |
Lukáš Kucharczyk | 5f845f866e | |
Lukáš Kucharczyk | c3d4697470 | |
Lukáš Kucharczyk | 77293f03e9 | |
Lukáš Kucharczyk | 1fa364e2ec | |
Lukáš Kucharczyk | 4a6f4a2f9a | |
Lukáš Kucharczyk | 9590988b6a | |
Lukáš Kucharczyk | 938c82a395 | |
Lukáš Kucharczyk | 33939f631c | |
Lukáš Kucharczyk | ac8cd6534a | |
Lukáš Kucharczyk | 51d8e953c0 | |
Lukáš Kucharczyk | 2eec677f41 | |
Lukáš Kucharczyk | f2eb14d3ef | |
Lukáš Kucharczyk | c337d2200f | |
Lukáš Kucharczyk | 8a8b05b0bd | |
Lukáš Kucharczyk | 9446065271 | |
Lukáš Kucharczyk | 755093845d | |
Lukáš Kucharczyk | d4ab0596da | |
Lukáš Kucharczyk | 8dcbe2f0ad | |
Lukáš Kucharczyk | 25bc74eff1 | |
Lukáš Kucharczyk | 8a7d083fb2 | |
Lukáš Kucharczyk | 8296ebcf31 | |
Lukáš Kucharczyk | 04a4f2e0be | |
Lukáš Kucharczyk | 4070b4e46e | |
Lukáš Kucharczyk | 4892218c83 | |
Lukáš Kucharczyk | 6b00a950ce | |
Lukáš Kucharczyk | feee9d6dac |
|
@ -3,12 +3,10 @@ name: Django CI/CD
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
pull_request:
|
paths-ignore: [ 'README.md' ]
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
needs: test
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
35
CHANGELOG.md
35
CHANGELOG.md
|
@ -1,17 +1,5 @@
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
## Improved
|
|
||||||
* game overview: improve how editions and purchases are displayed
|
|
||||||
* add purchase: only allow choosing purchases of selected edition
|
|
||||||
|
|
||||||
## 1.5.1 / 2023-11-14 21:10+01:00
|
|
||||||
|
|
||||||
## Improved
|
|
||||||
* Disallow choosing non-game purchase as related purchase
|
|
||||||
* Improve display of purchases
|
|
||||||
|
|
||||||
## 1.5.0 / 2023-11-14 19:27+01:00
|
|
||||||
|
|
||||||
## New
|
## New
|
||||||
* Add stat for finished this year's games
|
* Add stat for finished this year's games
|
||||||
* Add purchase types:
|
* Add purchase types:
|
||||||
|
@ -20,9 +8,6 @@
|
||||||
* Season Pass
|
* Season Pass
|
||||||
* Battle Pass
|
* Battle Pass
|
||||||
|
|
||||||
## Fixed
|
|
||||||
* Order purchases by date on game view
|
|
||||||
|
|
||||||
## 1.4.0 / 2023-11-09 21:01+01:00
|
## 1.4.0 / 2023-11-09 21:01+01:00
|
||||||
|
|
||||||
### New
|
### New
|
||||||
|
@ -110,24 +95,22 @@
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
* Improve form appearance
|
* Improve form appearance
|
||||||
* Focus important fields on forms
|
* Add helper buttons next to datime fields
|
||||||
* Use the same form when editing a session as when adding a session
|
|
||||||
* Change recent session view to current year instead of last 30 days
|
* Change recent session view to current year instead of last 30 days
|
||||||
* Add a hacky way not to reload a page when starting or ending a session (https://git.kucharczyk.xyz/lukas/timetracker/issues/52)
|
|
||||||
* Improve session list (https://git.kucharczyk.xyz/lukas/timetracker/issues/53)
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
* Fix session being wrongly considered in progress if it had a certain amount of manual hours (https://git.kucharczyk.xyz/lukas/timetracker/issues/58)
|
|
||||||
* Fix bug when filtering only manual sessions (https://git.kucharczyk.xyz/lukas/timetracker/issues/51)
|
* Fix bug when filtering only manual sessions (https://git.kucharczyk.xyz/lukas/timetracker/issues/51)
|
||||||
|
* Add copy button on Add session page to copy times between fields
|
||||||
|
* Use the same form when editing a session as when adding a session
|
||||||
|
* Add a hacky way not to reload a page when starting or ending a session (https://git.kucharczyk.xyz/lukas/timetracker/issues/52)
|
||||||
|
* Focus important fields on forms
|
||||||
|
* Improve session list (https://git.kucharczyk.xyz/lukas/timetracker/issues/53)
|
||||||
|
* Change fonts to IBM Plex
|
||||||
|
* Only use local WOFF2 font files
|
||||||
|
|
||||||
## 1.0.3 / 2023-02-20 17:16+01:00
|
## 1.0.3 / 2023-02-20 17:16+01:00
|
||||||
|
|
||||||
* Add wikidata ID and year for editions
|
* Add wikidata ID and year for editions
|
||||||
* Add icons for game, edition, purchase filters
|
|
||||||
* Allow filtering by game, edition, purchase from the session list
|
* Allow filtering by game, edition, purchase from the session list
|
||||||
* Allow editing filtered entities from session list
|
* Add icons for the above
|
||||||
|
|
||||||
## 1.0.2 / 2023-02-18 21:48+01:00
|
## 1.0.2 / 2023-02-18 21:48+01:00
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,11 @@ custom_datetime_widget = forms.DateTimeInput(
|
||||||
)
|
)
|
||||||
autofocus_input_widget = forms.TextInput(attrs={"autofocus": "autofocus"})
|
autofocus_input_widget = forms.TextInput(attrs={"autofocus": "autofocus"})
|
||||||
|
|
||||||
|
custom_date_widget = forms.DateInput(attrs={"type": "date"})
|
||||||
|
custom_datetime_widget = forms.DateTimeInput(
|
||||||
|
attrs={"type": "datetime-local"}, format="%Y-%m-%d %H:%M"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SessionForm(forms.ModelForm):
|
class SessionForm(forms.ModelForm):
|
||||||
# purchase = forms.ModelChoiceField(
|
# purchase = forms.ModelChoiceField(
|
||||||
|
@ -82,7 +87,6 @@ class PurchaseForm(forms.ModelForm):
|
||||||
widgets = {
|
widgets = {
|
||||||
"date_purchased": custom_date_widget,
|
"date_purchased": custom_date_widget,
|
||||||
"date_refunded": custom_date_widget,
|
"date_refunded": custom_date_widget,
|
||||||
"date_finished": custom_date_widget,
|
|
||||||
}
|
}
|
||||||
model = Purchase
|
model = Purchase
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -147,7 +151,7 @@ class EditionForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Edition
|
model = Edition
|
||||||
fields = ["game", "name", "sort_name", "platform", "year_released", "wikidata"]
|
fields = ["game", "name", "platform", "year_released", "wikidata"]
|
||||||
|
|
||||||
|
|
||||||
class GameForm(forms.ModelForm):
|
class GameForm(forms.ModelForm):
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
# Generated by Django 4.1.5 on 2023-11-14 08:41
|
# Generated by Django 4.1.5 on 2023-11-14 08:41
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("games", "0026_purchase_type"),
|
("games", "0026_purchase_type"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# Generated by Django 4.1.5 on 2023-11-14 11:05
|
# Generated by Django 4.1.5 on 2023-11-14 11:05
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
from games.models import Purchase
|
from games.models import Purchase
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ from datetime import timedelta
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import F, Manager, Sum
|
from django.db.models import F, Manager, Sum
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
@ -38,13 +39,9 @@ class Edition(models.Model):
|
||||||
|
|
||||||
game = models.ForeignKey("Game", on_delete=models.CASCADE)
|
game = models.ForeignKey("Game", on_delete=models.CASCADE)
|
||||||
name = models.CharField(max_length=255)
|
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)
|
||||||
platform = models.ForeignKey(
|
year_released = models.IntegerField(default=datetime.today().year)
|
||||||
"Platform", on_delete=models.CASCADE, 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)
|
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.sort_name
|
return self.sort_name
|
||||||
|
@ -124,25 +121,14 @@ class Purchase(models.Model):
|
||||||
type = models.CharField(max_length=255, choices=TYPES, default=GAME)
|
type = models.CharField(max_length=255, choices=TYPES, default=GAME)
|
||||||
name = models.CharField(max_length=255, default="", null=True, blank=True)
|
name = models.CharField(max_length=255, default="", null=True, blank=True)
|
||||||
related_purchase = models.ForeignKey(
|
related_purchase = models.ForeignKey(
|
||||||
"Purchase",
|
"Purchase", on_delete=models.SET_NULL, default=None, null=True, blank=True
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
default=None,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
related_name="related_purchases",
|
|
||||||
)
|
)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
additional_info = [
|
platform_info = self.platform
|
||||||
self.get_type_display() if self.type != Purchase.GAME else "",
|
if self.platform != self.edition.platform:
|
||||||
f"{self.edition.platform} version on {self.platform}"
|
platform_info = f"{self.edition.platform} version on {self.platform}"
|
||||||
if self.platform != self.edition.platform
|
return f"{self.edition} ({platform_info}, {self.edition.year_released}, {self.get_ownership_type_display()})"
|
||||||
else self.platform,
|
|
||||||
self.edition.year_released,
|
|
||||||
self.get_ownership_type_display(),
|
|
||||||
]
|
|
||||||
return f"{self.edition} ({', '.join(filter(None, map(str, additional_info)))})"
|
|
||||||
|
|
||||||
def is_game(self):
|
def is_game(self):
|
||||||
return self.type == self.GAME
|
return self.type == self.GAME
|
||||||
|
|
|
@ -755,10 +755,6 @@ select {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.relative {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-2 {
|
.bottom-2 {
|
||||||
bottom: 0.5rem;
|
bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
@ -775,55 +771,20 @@ select {
|
||||||
top: 0.75rem;
|
top: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx-2 {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx-auto {
|
.mx-auto {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.my-2 {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.my-4 {
|
.my-4 {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.my-6 {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-1 {
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-10 {
|
|
||||||
margin-bottom: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-2 {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-4 {
|
.mb-4 {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ml-1 {
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ml-2 {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mr-4 {
|
.mr-4 {
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -836,10 +797,6 @@ select {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-block {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline {
|
.inline {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
@ -856,14 +813,6 @@ select {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-4 {
|
|
||||||
height: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-5 {
|
|
||||||
height: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-6 {
|
.h-6 {
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
@ -872,22 +821,10 @@ select {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-5 {
|
|
||||||
width: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-6 {
|
.w-6 {
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-7 {
|
|
||||||
width: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-auto {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -896,10 +833,6 @@ select {
|
||||||
max-width: 1024px;
|
max-width: 1024px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.max-w-sm {
|
|
||||||
max-width: 24rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.max-w-xs {
|
.max-w-xs {
|
||||||
max-width: 20rem;
|
max-width: 20rem;
|
||||||
}
|
}
|
||||||
|
@ -926,18 +859,10 @@ select {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.justify-center {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.justify-between {
|
.justify-between {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gap-2 {
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.self-center {
|
.self-center {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
@ -960,35 +885,16 @@ select {
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rounded-sm {
|
|
||||||
border-radius: 0.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.border-gray-200 {
|
.border-gray-200 {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(229 231 235 / var(--tw-border-opacity));
|
border-color: rgb(229 231 235 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-slate-500 {
|
|
||||||
--tw-border-opacity: 1;
|
|
||||||
border-color: rgb(100 116 139 / var(--tw-border-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-gray-200 {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-green-600 {
|
.bg-green-600 {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
|
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-violet-600 {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(124 58 237 / var(--tw-bg-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-white {
|
.bg-white {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||||
|
@ -1003,11 +909,6 @@ select {
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.px-4 {
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.py-1 {
|
.py-1 {
|
||||||
padding-top: 0.25rem;
|
padding-top: 0.25rem;
|
||||||
padding-bottom: 0.25rem;
|
padding-bottom: 0.25rem;
|
||||||
|
@ -1026,10 +927,6 @@ select {
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pt-1 {
|
|
||||||
padding-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -1038,9 +935,12 @@ select {
|
||||||
font-family: IBM Plex Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
font-family: IBM Plex Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-3xl {
|
.font-serif {
|
||||||
font-size: 1.875rem;
|
font-family: IBM Plex Serif, ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||||
line-height: 2.25rem;
|
}
|
||||||
|
|
||||||
|
.font-sans {
|
||||||
|
font-family: IBM Plex Sans, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-4xl {
|
.text-4xl {
|
||||||
|
@ -1048,21 +948,11 @@ select {
|
||||||
line-height: 2.5rem;
|
line-height: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-5xl {
|
|
||||||
font-size: 3rem;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-base {
|
.text-base {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-lg {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-xl {
|
.text-xl {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
line-height: 1.75rem;
|
line-height: 1.75rem;
|
||||||
|
@ -1073,17 +963,55 @@ select {
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-2xl {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-9xl {
|
||||||
|
font-size: 8rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-8xl {
|
||||||
|
font-size: 6rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-7xl {
|
||||||
|
font-size: 4.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-6xl {
|
||||||
|
font-size: 3.75rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-5xl {
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-lg {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
line-height: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.font-semibold {
|
.font-semibold {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.italic {
|
.font-bold {
|
||||||
font-style: italic;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-gray-700 {
|
.capitalize {
|
||||||
--tw-text-opacity: 1;
|
text-transform: capitalize;
|
||||||
color: rgb(55 65 81 / var(--tw-text-opacity));
|
}
|
||||||
|
|
||||||
|
.italic {
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-slate-300 {
|
.text-slate-300 {
|
||||||
|
@ -1101,14 +1029,6 @@ select {
|
||||||
color: rgb(253 224 71 / var(--tw-text-opacity));
|
color: rgb(253 224 71 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.underline {
|
|
||||||
text-decoration-line: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.decoration-slate-500 {
|
|
||||||
text-decoration-color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shadow-md {
|
.shadow-md {
|
||||||
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||||
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
|
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
|
||||||
|
@ -1208,10 +1128,12 @@ select {
|
||||||
}
|
}
|
||||||
|
|
||||||
.responsive-table thead th:not(:first-child),
|
.responsive-table thead th:not(:first-child),
|
||||||
.responsive-table td:not(:first-child) {
|
td:not(:first-child) {
|
||||||
border-left-width: 1px;
|
border-left-width: 1px;
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-left-color: rgb(100 116 139 / var(--tw-border-opacity));
|
border-left-color: rgb(100 116 139 / var(--tw-border-opacity));
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.dark form input),:is(.dark
|
:is(.dark form input),:is(.dark
|
||||||
|
@ -1343,21 +1265,11 @@ th label {
|
||||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover\:bg-gray-400:hover {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(156 163 175 / var(--tw-bg-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover\:bg-green-700:hover {
|
.hover\:bg-green-700:hover {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(21 128 61 / var(--tw-bg-opacity));
|
background-color: rgb(21 128 61 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover\:bg-violet-700:hover {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(109 40 217 / var(--tw-bg-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover\:underline:hover {
|
.hover\:underline:hover {
|
||||||
text-decoration-line: underline;
|
text-decoration-line: underline;
|
||||||
}
|
}
|
||||||
|
@ -1378,11 +1290,6 @@ th label {
|
||||||
--tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity));
|
--tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.focus\:ring-violet-500:focus {
|
|
||||||
--tw-ring-opacity: 1;
|
|
||||||
--tw-ring-color: rgb(139 92 246 / var(--tw-ring-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.focus\:ring-offset-2:focus {
|
.focus\:ring-offset-2:focus {
|
||||||
--tw-ring-offset-width: 2px;
|
--tw-ring-offset-width: 2px;
|
||||||
}
|
}
|
||||||
|
@ -1391,14 +1298,6 @@ th label {
|
||||||
--tw-ring-offset-color: #bfdbfe;
|
--tw-ring-offset-color: #bfdbfe;
|
||||||
}
|
}
|
||||||
|
|
||||||
.focus\:ring-offset-violet-200:focus {
|
|
||||||
--tw-ring-offset-color: #ddd6fe;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group:hover .group-hover\:block {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(.dark .dark\:bg-gray-800) {
|
:is(.dark .dark\:bg-gray-800) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
|
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
|
||||||
|
@ -1409,16 +1308,6 @@ th label {
|
||||||
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
|
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.dark .dark\:text-slate-400) {
|
|
||||||
--tw-text-opacity: 1;
|
|
||||||
color: rgb(148 163 184 / var(--tw-text-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(.dark .dark\:text-slate-500) {
|
|
||||||
--tw-text-opacity: 1;
|
|
||||||
color: rgb(100 116 139 / var(--tw-text-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(.dark .dark\:text-slate-600) {
|
:is(.dark .dark\:text-slate-600) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(71 85 105 / var(--tw-text-opacity));
|
color: rgb(71 85 105 / var(--tw-text-opacity));
|
||||||
|
@ -1430,10 +1319,6 @@ th label {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
.sm\:inline {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm\:table-cell {
|
.sm\:table-cell {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
}
|
}
|
||||||
|
@ -1442,19 +1327,11 @@ th label {
|
||||||
max-width: 28rem;
|
max-width: 28rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm\:max-w-xl {
|
|
||||||
max-width: 36rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm\:px-4 {
|
.sm\:px-4 {
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm\:pl-12 {
|
|
||||||
padding-left: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm\:pl-2 {
|
.sm\:pl-2 {
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
@ -1509,10 +1386,6 @@ th label {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lg\:max-w-3xl {
|
|
||||||
max-width: 48rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lg\:max-w-lg {
|
.lg\:max-w-lg {
|
||||||
max-width: 32rem;
|
max-width: 32rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,29 @@
|
||||||
import { syncSelectInputUntilChanged } from './utils.js';
|
/**
|
||||||
|
* @description Sync select field with input field until user focuses it.
|
||||||
|
* @param {HTMLSelectElement} sourceElementSelector
|
||||||
|
* @param {HTMLInputElement} targetElementSelector
|
||||||
|
*/
|
||||||
|
function syncSelectInputUntilChanged(
|
||||||
|
sourceElementSelector,
|
||||||
|
targetElementSelector
|
||||||
|
) {
|
||||||
|
const sourceElement = document.querySelector(sourceElementSelector);
|
||||||
|
const targetElement = document.querySelector(targetElementSelector);
|
||||||
|
function sourceElementHandler(event) {
|
||||||
|
let selected = event.target.value;
|
||||||
|
let selectedValue = document.querySelector(
|
||||||
|
`#id_game option[value='${selected}']`
|
||||||
|
).textContent;
|
||||||
|
targetElement.value = selectedValue;
|
||||||
|
}
|
||||||
|
function targetElementHandler(event) {
|
||||||
|
sourceElement.removeEventListener("change", sourceElementHandler);
|
||||||
|
}
|
||||||
|
|
||||||
let syncData = [
|
sourceElement.addEventListener("change", sourceElementHandler);
|
||||||
{
|
targetElement.addEventListener("focus", targetElementHandler);
|
||||||
"source": "#id_game",
|
}
|
||||||
"source_value": "dataset.name",
|
|
||||||
"target": "#id_name",
|
|
||||||
"target_value": "value"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": "#id_game",
|
|
||||||
"source_value": "textContent",
|
|
||||||
"target": "#id_sort_name",
|
|
||||||
"target_value": "value"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": "#id_game",
|
|
||||||
"source_value": "dataset.year",
|
|
||||||
"target": "#id_year_released",
|
|
||||||
"target_value": "value"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
syncSelectInputUntilChanged(syncData, "form");
|
window.addEventListener("load", () => {
|
||||||
|
syncSelectInputUntilChanged("#id_game", "#id_name");
|
||||||
|
});
|
||||||
|
|
|
@ -1,9 +1,4 @@
|
||||||
import {
|
import { syncSelectInputUntilChanged, getEl, conditionalElementHandler } from "./utils.js";
|
||||||
syncSelectInputUntilChanged,
|
|
||||||
getEl,
|
|
||||||
disableElementsWhenTrue,
|
|
||||||
disableElementsWhenFalse,
|
|
||||||
} from "./utils.js";
|
|
||||||
|
|
||||||
let syncData = [
|
let syncData = [
|
||||||
{
|
{
|
||||||
|
@ -16,28 +11,21 @@ let syncData = [
|
||||||
|
|
||||||
syncSelectInputUntilChanged(syncData, "form");
|
syncSelectInputUntilChanged(syncData, "form");
|
||||||
|
|
||||||
function setupElementHandlers() {
|
|
||||||
disableElementsWhenTrue("#id_type", "game", [
|
|
||||||
"#id_name",
|
|
||||||
"#id_related_purchase",
|
|
||||||
]);
|
|
||||||
disableElementsWhenFalse("#id_type", "game", ["#id_date_finished"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", setupElementHandlers);
|
let myConfig = [
|
||||||
document.addEventListener("htmx:afterSwap", setupElementHandlers);
|
() => {
|
||||||
getEl("#id_type").onchange = () => {
|
return getEl("#id_type").value == "game";
|
||||||
setupElementHandlers();
|
},
|
||||||
};
|
["#id_name", "#id_related_purchase"],
|
||||||
|
(el) => {
|
||||||
document.body.addEventListener('htmx:beforeRequest', function(event) {
|
el.disabled = "disabled";
|
||||||
// Assuming 'Purchase1' is the element that triggers the HTMX request
|
},
|
||||||
if (event.target.id === 'id_edition') {
|
(el) => {
|
||||||
var idEditionValue = document.getElementById('id_edition').value;
|
el.disabled = "";
|
||||||
|
|
||||||
// Condition to check - replace this with your actual logic
|
|
||||||
if (idEditionValue != '') {
|
|
||||||
event.preventDefault(); // This cancels the HTMX request
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
]
|
||||||
|
|
||||||
|
document.DOMContentLoaded = conditionalElementHandler(...myConfig)
|
||||||
|
getEl("#id_type").onchange = () => {
|
||||||
|
conditionalElementHandler(...myConfig)
|
||||||
|
}
|
||||||
|
|
|
@ -8,9 +8,6 @@ for (let button of document.querySelectorAll("[data-target]")) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (type == "now") {
|
if (type == "now") {
|
||||||
targetElement.value = toISOUTCString(new Date);
|
targetElement.value = toISOUTCString(new Date);
|
||||||
} else if (type == "copy") {
|
|
||||||
const oppositeName = targetElement.name == "timestamp_start" ? "timestamp_end" : "timestamp_start";
|
|
||||||
document.querySelector(`[name='${oppositeName}']`).value = targetElement.value;
|
|
||||||
} else if (type == "toggle") {
|
} else if (type == "toggle") {
|
||||||
if (targetElement.type == "datetime-local") targetElement.type = "text";
|
if (targetElement.type == "datetime-local") targetElement.type = "text";
|
||||||
else targetElement.type = "datetime-local";
|
else targetElement.type = "datetime-local";
|
||||||
|
|
|
@ -3,16 +3,9 @@
|
||||||
* @param {Date} date
|
* @param {Date} date
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function toISOUTCString(date) {
|
export function toISOUTCString(date) {
|
||||||
function stringAndPad(number) {
|
let month = (date.getMonth() + 1).toString().padStart(2, 0);
|
||||||
return number.toString().padStart(2, 0);
|
return `${date.getFullYear()}-${month}-${date.getDate()}T${date.getHours()}:${date.getMinutes()}`;
|
||||||
}
|
|
||||||
const year = date.getFullYear();
|
|
||||||
const month = stringAndPad(date.getMonth() + 1);
|
|
||||||
const day = stringAndPad(date.getDate());
|
|
||||||
const hours = stringAndPad(date.getHours());
|
|
||||||
const minutes = stringAndPad(date.getMinutes());
|
|
||||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -99,72 +92,37 @@ function getEl(selector) {
|
||||||
return document.getElementsByClassName(selector)
|
return document.getElementsByClassName(selector)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return document.getElementsByTagName(selector)
|
return document.getElementsByName(selector)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Applies different behaviors to elements based on multiple conditional configurations.
|
* @description Does something to elements when something happens.
|
||||||
* Each configuration is an array containing a condition function, an array of target element selectors,
|
* @param {() => boolean} condition The condition that is being tested.
|
||||||
* and two callback functions for handling matched and unmatched conditions.
|
* @param {string[]} targetElements
|
||||||
* @param {...Array} configs Each configuration is an array of the form:
|
* @param {(elementName: HTMLElement) => void} callbackfn1 Called when the condition matches.
|
||||||
* - 0: {function(): boolean} condition - Function that returns true or false based on a condition.
|
* @param {(elementName: HTMLElement) => void} callbackfn2 Called when the condition doesn't match.
|
||||||
* - 1: {string[]} targetElements - Array of CSS selectors for target elements.
|
|
||||||
* - 2: {function(HTMLElement): void} callbackfn1 - Function to execute when condition is true.
|
|
||||||
* - 3: {function(HTMLElement): void} callbackfn2 - Function to execute when condition is false.
|
|
||||||
*/
|
*/
|
||||||
function conditionalElementHandler(...configs) {
|
function conditionalElementHandler(condition, targetElements, callbackfn1, callbackfn2) {
|
||||||
configs.forEach(([condition, targetElements, callbackfn1, callbackfn2]) => {
|
if (condition()) {
|
||||||
if (condition()) {
|
targetElements.forEach((elementName) => {
|
||||||
targetElements.forEach(elementName => {
|
let el = getEl(elementName);
|
||||||
let el = getEl(elementName);
|
if (el === null) {
|
||||||
if (el === null) {
|
console.error("Element ${elementName} doesn't exist.");
|
||||||
console.error(`Element ${elementName} doesn't exist.`);
|
} else {
|
||||||
} else {
|
callbackfn1(el);
|
||||||
callbackfn1(el);
|
}
|
||||||
}
|
});
|
||||||
});
|
} else {
|
||||||
} else {
|
targetElements.forEach((elementName) => {
|
||||||
targetElements.forEach(elementName => {
|
let el = getEl(elementName);
|
||||||
let el = getEl(elementName);
|
if (el === null) {
|
||||||
if (el === null) {
|
console.error("Element ${elementName} doesn't exist.");
|
||||||
console.error(`Element ${elementName} doesn't exist.`);
|
} else {
|
||||||
} else {
|
callbackfn2(el);
|
||||||
callbackfn2(el);
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function disableElementsWhenFalse(targetSelect, targetValue, elementList) {
|
export { toISOUTCString, syncSelectInputUntilChanged, getEl, conditionalElementHandler };
|
||||||
return conditionalElementHandler([
|
|
||||||
() => {
|
|
||||||
return getEl(targetSelect).value != targetValue;
|
|
||||||
},
|
|
||||||
elementList,
|
|
||||||
(el) => {
|
|
||||||
el.disabled = "disabled";
|
|
||||||
},
|
|
||||||
(el) => {
|
|
||||||
el.disabled = "";
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function disableElementsWhenTrue(targetSelect, targetValue, elementList) {
|
|
||||||
return conditionalElementHandler([
|
|
||||||
() => {
|
|
||||||
return getEl(targetSelect).value == targetValue;
|
|
||||||
},
|
|
||||||
elementList,
|
|
||||||
(el) => {
|
|
||||||
el.disabled = "disabled";
|
|
||||||
},
|
|
||||||
(el) => {
|
|
||||||
el.disabled = "";
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { toISOUTCString, syncSelectInputUntilChanged, getEl, conditionalElementHandler, disableElementsWhenFalse, disableElementsWhenTrue, getValueFromProperty };
|
|
||||||
|
|
|
@ -6,19 +6,13 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
<table class="mx-auto">
|
<table class="mx-auto">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.as_table }}
|
|
||||||
<tr>
|
{{ form.as_table }}
|
||||||
<td></td>
|
<tr>
|
||||||
<td>
|
<td></td>
|
||||||
<input type="submit" value="Submit" />
|
<td><input type="submit" value="Submit"/></td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
</form>
|
</form>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
{% block scripts %}
|
|
||||||
{% if script_name %}
|
|
||||||
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock scripts %}
|
|
||||||
|
|
|
@ -6,27 +6,17 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
<table class="mx-auto">
|
<table class="mx-auto">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.as_table }}
|
|
||||||
<tr>
|
{{ form.as_table }}
|
||||||
<td></td>
|
<tr>
|
||||||
<td>
|
<td></td>
|
||||||
<input type="submit" name="submit" value="Submit" />
|
<td><input type="submit" value="Submit"/></td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td>
|
|
||||||
<input type="submit"
|
|
||||||
name="submit_and_redirect"
|
|
||||||
value="Submit & Create Purchase" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
</form>
|
</form>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{% if script_name %}
|
{% load static %}
|
||||||
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
|
<script type="module" src="{% static 'js/add_edition.js' %}"></script>
|
||||||
{% endif %}
|
|
||||||
{% endblock scripts %}
|
{% endblock scripts %}
|
||||||
|
|
|
@ -1,38 +1,34 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}
|
|
||||||
{{ title }}
|
{% block title %}{{ title }}{% endblock title %}
|
||||||
{% endblock title %}
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
<table class="mx-auto">
|
<table class="mx-auto">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% for field in form %}
|
|
||||||
<tr>
|
{% for field in form %}
|
||||||
<th>{{ field.label_tag }}</th>
|
<tr>
|
||||||
{% if field.name == "note" %}
|
<th>{{ field.label_tag }}</th>
|
||||||
<td>{{ field }}</td>
|
{% if field.name == "note" %}
|
||||||
{% else %}
|
<td>{{ field }}</td>
|
||||||
<td>{{ field }}</td>
|
{% else %}
|
||||||
{% endif %}
|
<td>{{ field }}</td>
|
||||||
{% if field.name == "timestamp_start" or field.name == "timestamp_end" %}
|
{% endif %}
|
||||||
<td>
|
{% if field.name == "timestamp_start" or field.name == "timestamp_end" %}
|
||||||
<div class="basic-button-container">
|
<td>
|
||||||
<button class="basic-button" data-target="{{ field.name }}" data-type="now">Set to now</button>
|
<div class="basic-button-container">
|
||||||
<button class="basic-button"
|
<button class="basic-button" data-target="{{field.name}}" data-type="now">Set to now</button>
|
||||||
data-target="{{ field.name }}"
|
<button class="basic-button" data-target="{{field.name}}" data-type="toggle">Toggle text</button>
|
||||||
data-type="toggle">Toggle text</button>
|
</div>
|
||||||
<button class="basic-button" data-target="{{ field.name }}" data-type="copy">Copy</button>
|
</td>
|
||||||
</div>
|
{% endif %}
|
||||||
</td>
|
</tr>
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
</tr>
|
<tr>
|
||||||
{% endfor %}
|
<td></td>
|
||||||
<tr>
|
<td><input type="submit" value="Submit"/></td>
|
||||||
<td></td>
|
</tr>
|
||||||
<td>
|
|
||||||
<input type="submit" value="Submit" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
</form>
|
</form>
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
|
@ -2,29 +2,22 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
{% load static %}
|
{% load static %}
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8"/>
|
||||||
<meta name="description" content="Self-hosted time-tracker." />
|
<meta name="description" content="Self-hosted time-tracker."/>
|
||||||
<meta name="keywords" content="time, tracking, video games, self-hosted" />
|
<meta name="keywords" content="time, tracking, video games, self-hosted"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<title>Timetracker -
|
<title>Timetracker - {% block title %}Untitled{% endblock title %}</title>
|
||||||
{% block title %}
|
|
||||||
Untitled
|
|
||||||
{% endblock title %}
|
|
||||||
</title>
|
|
||||||
<script src="{% static 'js/htmx.min.js' %}"></script>
|
<script src="{% static 'js/htmx.min.js' %}"></script>
|
||||||
<link rel="stylesheet" href="{% static 'base.css' %}" />
|
<link rel="stylesheet" href="{% static 'base.css' %}" />
|
||||||
</head>
|
</head>
|
||||||
<body class="dark" hx-indicator="#indicator" hx-boost="true">
|
|
||||||
<img id="indicator"
|
<body class="dark">
|
||||||
src="{% static 'icons/loading.png' %}"
|
<img id="indicator" src="{% static 'icons/loading.png' %}" class="absolute right-3 top-3 animate-spin htmx-indicator" />
|
||||||
class="absolute right-3 top-3 animate-spin htmx-indicator" />
|
|
||||||
<div class="dark:bg-gray-800 min-h-screen">
|
<div class="dark:bg-gray-800 min-h-screen">
|
||||||
<nav class="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded">
|
<nav class="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded">
|
||||||
<div class="container flex flex-wrap items-center justify-between mx-auto">
|
<div class="container flex flex-wrap items-center justify-between mx-auto">
|
||||||
<a href="{% url 'list_sessions_recent' %}" class="flex items-center">
|
<a href="{% url 'list_sessions_recent' %}" class="flex items-center">
|
||||||
<span class="text-4xl">
|
<span class="text-4xl">⌚</span>
|
||||||
<img src="{% static 'icons/schedule.png' %}" width="48" class="mr-4" />
|
|
||||||
</span>
|
|
||||||
<span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span>
|
<span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="w-full md:block md:w-auto">
|
<div class="w-full md:block md:w-auto">
|
||||||
|
@ -98,4 +91,5 @@
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{% endblock scripts %}
|
{% endblock scripts %}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
{% comment %}
|
<button
|
||||||
title
|
type="button"
|
||||||
text
|
title="{{ title }}"
|
||||||
{% endcomment %}
|
autofocus
|
||||||
<a href="{{ link }}"
|
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"
|
||||||
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">
|
<svg
|
||||||
{% 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">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<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" />
|
fill="none"
|
||||||
</svg>
|
viewBox="0 0 24 24"
|
||||||
{% endcomment %}
|
stroke-width="1.5"
|
||||||
{{ text }}
|
stroke="currentColor"
|
||||||
</a>
|
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"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{{ text }}
|
||||||
|
</button>
|
||||||
|
|
|
@ -1,68 +1,73 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% block title %}
|
|
||||||
{{ title }}
|
{% block title %}{{ title }}{% endblock title %}
|
||||||
{% endblock title %}
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if dataset.count >= 1 %}
|
|
||||||
<div class="mx-auto text-center my-4">
|
{% if dataset.count >= 1 %}
|
||||||
<a id="last-session-start"
|
<div class="mx-auto text-center my-4">
|
||||||
href="{% url 'start_session_same_as_last' last.id %}"
|
<a
|
||||||
hx-get="{% url 'start_session_same_as_last' last.id %}"
|
id="last-session-start"
|
||||||
hx-swap="afterbegin"
|
href="{% url 'start_session' last.id %}"
|
||||||
hx-target=".responsive-table tbody"
|
hx-get="{% url 'start_session' last.id %}"
|
||||||
hx-select=".responsive-table tbody tr:first-child"
|
hx-indicator="#indicator"
|
||||||
onClick="document.querySelector('#last-session-start').classList.add('invisible')"
|
hx-swap="afterbegin"
|
||||||
class="{% if last.timestamp_end == null %}invisible{% endif %}">
|
hx-target=".responsive-table tbody"
|
||||||
{% include 'components/button_start.html' with text=last.purchase title="Start session of last played game" only %}
|
hx-select=".responsive-table tbody tr:first-child"
|
||||||
</a>
|
onClick="document.querySelector('#last-session-start').classList.add('invisible')"
|
||||||
</div>
|
class="{% if last.timestamp_end == null %}invisible{% endif %}"
|
||||||
{% endif %}
|
>
|
||||||
{% if dataset.count != 0 %}
|
{% include 'components/button.html' with text=last.purchase title="Start session of last played game" only %}
|
||||||
<table class="responsive-table">
|
</a>
|
||||||
<thead>
|
</div>
|
||||||
<tr>
|
{% endif %}
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2">Name</th>
|
|
||||||
<th class="hidden sm:table-cell px-2 sm:px-4 md:px-6 md:py-2">Start</th>
|
<table class="responsive-table">
|
||||||
<th class="hidden lg:table-cell px-2 sm:px-4 md:px-6 md:py-2">End</th>
|
<thead>
|
||||||
<th class="px-2 sm:px-4 md:px-6 md:py-2">Duration</th>
|
<tr>
|
||||||
</tr>
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Name</th>
|
||||||
</thead>
|
<th class="hidden sm:table-cell px-2 sm:px-4 md:px-6 md:py-2">Start</th>
|
||||||
<tbody>
|
<th class="hidden lg:table-cell px-2 sm:px-4 md:px-6 md:py-2">End</th>
|
||||||
{% for data in dataset %}
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Duration</th>
|
||||||
<tr>
|
</tr>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char md:max-w-40char">
|
</thead>
|
||||||
<a class="underline decoration-slate-500 sm:decoration-2"
|
<tbody>
|
||||||
href="{% url 'view_game' data.purchase.edition.game.id %}">
|
{% for data in dataset %}
|
||||||
{{ data.purchase.edition }}
|
<tr>
|
||||||
</a>
|
<td
|
||||||
</td>
|
class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char md:max-w-40char"
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden sm:table-cell">
|
>
|
||||||
{{ data.timestamp_start | date:"d/m/Y H:i" }}
|
{{ data.purchase.edition }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden lg:table-cell">
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden sm:table-cell">
|
||||||
{% if data.unfinished %}
|
{{ data.timestamp_start | date:"d/m/Y H:i" }}
|
||||||
<a href="{% url 'update_session' data.id %}"
|
</td>
|
||||||
hx-get="{% url 'update_session' data.id %}"
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden lg:table-cell">
|
||||||
hx-swap="outerHTML"
|
{% if data.unfinished %}
|
||||||
hx-target=".responsive-table tbody tr:first-child"
|
<a
|
||||||
hx-select=".responsive-table tbody tr:first-child"
|
href="{% url 'update_session' data.id %}"
|
||||||
hx-indicator="#indicator"
|
hx-get="{% url 'update_session' data.id %}"
|
||||||
onClick="document.querySelector('#last-session-start').classList.remove('invisible')">
|
hx-swap="outerHTML"
|
||||||
<span class="text-yellow-300">Finish now?</span>
|
hx-target=".responsive-table tbody tr:first-child"
|
||||||
</a>
|
hx-select=".responsive-table tbody tr:first-child"
|
||||||
{% elif data.duration_manual %}
|
hx-indicator="#indicator"
|
||||||
--
|
onClick="document.querySelector('#last-session-start').classList.remove('invisible')"
|
||||||
{% else %}
|
>
|
||||||
{{ data.timestamp_end | date:"d/m/Y H:i" }}
|
<span class="text-yellow-300">Finish now?</span>
|
||||||
{% endif %}
|
</a>
|
||||||
</td>
|
{% elif data.duration_manual %}
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ data.duration_formatted }}</td>
|
--
|
||||||
</tr>
|
{% else %}
|
||||||
{% endfor %}
|
{{ data.timestamp_end | date:"d/m/Y H:i" }}
|
||||||
</tbody>
|
{% endif %}
|
||||||
</table>
|
</td>
|
||||||
{% else %}
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
||||||
<div class="mx-auto text-center text-slate-300 text-xl">No sessions found.</div>
|
{{ data.duration_formatted }}
|
||||||
{% endif %}
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
|
@ -194,17 +194,18 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for purchase in all_purchased_this_year %}
|
{% for purchase in all_purchased_this_year %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
||||||
<a class="underline decoration-slate-500 sm:decoration-2"
|
<a class="underline decoration-slate-500 sm:decoration-2" href="{% url 'edit_purchase' purchase.id %}">
|
||||||
href="{% url 'edit_purchase' purchase.id %}">
|
{{ purchase.edition.name }}
|
||||||
{{ purchase.edition.name }}
|
{% if purchase.type != "game" %}
|
||||||
{% if purchase.type != "game" %}({{ purchase.name }}, {{ purchase.get_type_display }}){% endif %}
|
({{ purchase.name }}, {{ purchase.get_type_display }})
|
||||||
</a>
|
{% endif %}
|
||||||
</td>
|
</a>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.price }}</td>
|
</td>
|
||||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_purchased | date:"d/m/Y" }}</td>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.price }}</td>
|
||||||
</tr>
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_purchased | date:"d/m/Y" }}</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -22,37 +22,42 @@
|
||||||
</h1>
|
</h1>
|
||||||
<ul>
|
<ul>
|
||||||
{% for edition in editions %}
|
{% for edition in editions %}
|
||||||
<li class="sm:pl-2 flex items-center">
|
<li class="sm:pl-2 flex items-center">
|
||||||
{{ edition.name }} ({{ edition.platform }}, {{ edition.year_released }})
|
{{ edition.name }} ({{ edition.platform }}, {{ edition.year_released }})
|
||||||
{% if edition.wikidata %}
|
{% if edition.wikidata %}
|
||||||
<span class="hidden sm:inline">
|
<span class="hidden sm:inline">
|
||||||
<a href="https://www.wikidata.org/wiki/{{ edition.wikidata }}">
|
<a href="https://www.wikidata.org/wiki/{{ edition.wikidata }}">
|
||||||
<img class="inline mx-2 w-6" src="{% static 'icons/wikidata.png' %}" />
|
<img class="inline mx-2 w-6" src="{% static 'icons/wikidata.png' %}"/>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% url 'edit_edition' edition.id as edit_url %}
|
{% url 'edit_edition' edition.id as edit_url %}
|
||||||
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
||||||
</li>
|
</li>
|
||||||
<ul>
|
{% endfor %}
|
||||||
{% for purchase in edition.game_purchases %}
|
</ul>
|
||||||
<li class="sm:pl-6 flex items-center">
|
<h1 class="text-3xl mt-4 mb-1">Purchases <span class="dark:text-slate-500">({{ purchases.count }})</span></h1>
|
||||||
{{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }}
|
<ul>
|
||||||
{% if purchase.price != 0 %}({{ purchase.price }} {{ purchase.price_currency }}){% endif %}
|
{% for purchase in purchases %}
|
||||||
{% url 'edit_purchase' purchase.id as edit_url %}
|
<li class="sm:pl-2 flex items-center">
|
||||||
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
{{ purchase.platform }}
|
||||||
</li>
|
({{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }}, {{ purchase.price }} {{ purchase.price_currency}})
|
||||||
|
{% url 'edit_purchase' purchase.id as edit_url %}
|
||||||
|
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
||||||
|
{% if purchase.related_purchases %}
|
||||||
|
<li>
|
||||||
<ul>
|
<ul>
|
||||||
{% for related_purchase in purchase.nongame_related_purchases %}
|
{% for related_purchase in purchase.related_purchases %}
|
||||||
<li class="sm:pl-12 flex items-center">
|
<li class="sm:pl-6 flex items-center">
|
||||||
{{ related_purchase.name }} ({{ related_purchase.get_type_display }}, {{ purchase.platform }}, {{ related_purchase.date_purchased | date:"Y" }}, {{ related_purchase.price }} {{ related_purchase.price_currency }})
|
{{ related_purchase.name}} ({{ related_purchase.get_type_display }}, {{ related_purchase.date_purchased | date:"Y" }}, {{ related_purchase.price }} {{ related_purchase.price_currency}})
|
||||||
{% url 'edit_purchase' related_purchase.id as edit_url %}
|
{% url 'edit_purchase' related_purchase.id as edit_url %}
|
||||||
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
{% include 'components/edit_button.html' with edit_url=edit_url %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endfor %}
|
</li>
|
||||||
</ul>
|
{% endif %}
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<h1 class="text-3xl mt-4 mb-1 flex gap-2 items-center">
|
<h1 class="text-3xl mt-4 mb-1 flex gap-2 items-center">
|
||||||
|
|
|
@ -93,10 +93,4 @@ urlpatterns = [
|
||||||
{"filter": "ownership_type"},
|
{"filter": "ownership_type"},
|
||||||
name="list_sessions_by_ownership_type",
|
name="list_sessions_by_ownership_type",
|
||||||
),
|
),
|
||||||
path("stats/", views.stats, name="stats_current_year"),
|
|
||||||
path(
|
|
||||||
"stats/<int:year>",
|
|
||||||
views.stats,
|
|
||||||
name="stats_by_year",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,15 +1,8 @@
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Callable
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from common.time import now as now_with_tz
|
||||||
from django.db.models import Count, F, Prefetch, Sum
|
from django.conf import settings
|
||||||
from django.db.models.functions import TruncDate
|
|
||||||
from django.http import (
|
|
||||||
HttpRequest,
|
|
||||||
HttpResponse,
|
|
||||||
HttpResponseBadRequest,
|
|
||||||
HttpResponseRedirect,
|
|
||||||
)
|
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -140,10 +133,23 @@ def edit_game(request, game_id=None):
|
||||||
|
|
||||||
def view_game(request, game_id=None):
|
def view_game(request, game_id=None):
|
||||||
game = Game.objects.get(id=game_id)
|
game = Game.objects.get(id=game_id)
|
||||||
nongame_related_purchases_prefetch = Prefetch(
|
context["title"] = "View Game"
|
||||||
"related_purchases",
|
context["game"] = game
|
||||||
queryset=Purchase.objects.exclude(type=Purchase.GAME),
|
context["editions"] = Edition.objects.filter(game_id=game_id)
|
||||||
to_attr="nongame_related_purchases",
|
game_purchases = Purchase.objects.filter(edition__game_id=game_id).filter(
|
||||||
|
type=Purchase.GAME
|
||||||
|
)
|
||||||
|
for purchase in game_purchases:
|
||||||
|
purchase.related_purchases = Purchase.objects.exclude(
|
||||||
|
type=Purchase.GAME
|
||||||
|
).filter(related_purchase=purchase.id)
|
||||||
|
|
||||||
|
context["purchases"] = game_purchases
|
||||||
|
context["sessions"] = Session.objects.filter(
|
||||||
|
purchase__edition__game_id=game_id
|
||||||
|
).order_by("-timestamp_start")
|
||||||
|
context["total_hours"] = float(
|
||||||
|
format_duration(context["sessions"].total_duration_unformatted(), "%2.1H")
|
||||||
)
|
)
|
||||||
game_purchases_prefetch = Prefetch(
|
game_purchases_prefetch = Prefetch(
|
||||||
"purchase_set",
|
"purchase_set",
|
||||||
|
@ -253,12 +259,6 @@ def start_session_same_as_last(request, last_session_id: int):
|
||||||
return redirect("list_sessions")
|
return redirect("list_sessions")
|
||||||
|
|
||||||
|
|
||||||
# def delete_session(request, session_id=None):
|
|
||||||
# session = Session.objects.get(id=session_id)
|
|
||||||
# session.delete()
|
|
||||||
# return redirect("list_sessions")
|
|
||||||
|
|
||||||
|
|
||||||
def list_sessions(
|
def list_sessions(
|
||||||
request,
|
request,
|
||||||
filter="",
|
filter="",
|
||||||
|
@ -287,12 +287,10 @@ def list_sessions(
|
||||||
dataset = Session.objects.filter(purchase__ownership_type=ownership_type)
|
dataset = Session.objects.filter(purchase__ownership_type=ownership_type)
|
||||||
context["ownership_type"] = dict(Purchase.OWNERSHIP_TYPES)[ownership_type]
|
context["ownership_type"] = dict(Purchase.OWNERSHIP_TYPES)[ownership_type]
|
||||||
elif filter == "recent":
|
elif filter == "recent":
|
||||||
current_year = timezone.now().year
|
|
||||||
first_day_of_year = timezone.make_aware(datetime(current_year, 1, 1))
|
|
||||||
dataset = Session.objects.filter(
|
dataset = Session.objects.filter(
|
||||||
timestamp_start__gte=first_day_of_year
|
timestamp_start__gte=datetime.now() - timedelta(days=30)
|
||||||
).order_by("-timestamp_start")
|
)
|
||||||
context["title"] = "This year"
|
context["title"] = "Last 30 days"
|
||||||
else:
|
else:
|
||||||
# by default, sort from newest to oldest
|
# by default, sort from newest to oldest
|
||||||
dataset = Session.objects.order_by("-timestamp_start")
|
dataset = Session.objects.order_by("-timestamp_start")
|
||||||
|
@ -306,10 +304,8 @@ def list_sessions(
|
||||||
|
|
||||||
context["total_duration"] = dataset.total_duration_formatted()
|
context["total_duration"] = dataset.total_duration_formatted()
|
||||||
context["dataset"] = dataset
|
context["dataset"] = dataset
|
||||||
try:
|
# cannot use dataset[0] here because that might be only partial QuerySet
|
||||||
context["last"] = Session.objects.latest()
|
context["last"] = Session.objects.all().order_by("timestamp_start").last()
|
||||||
except ObjectDoesNotExist:
|
|
||||||
context["last"] = None
|
|
||||||
|
|
||||||
return render(request, "list_sessions.html", context)
|
return render(request, "list_sessions.html", context)
|
||||||
|
|
||||||
|
@ -547,19 +543,3 @@ def add_platform(request):
|
||||||
context["form"] = form
|
context["form"] = form
|
||||||
context["title"] = "Add New Platform"
|
context["title"] = "Add New Platform"
|
||||||
return render(request, "add.html", context)
|
return render(request, "add.html", context)
|
||||||
|
|
||||||
|
|
||||||
def add_device(request):
|
|
||||||
context = {}
|
|
||||||
form = DeviceForm(request.POST or None)
|
|
||||||
if form.is_valid():
|
|
||||||
form.save()
|
|
||||||
return redirect("index")
|
|
||||||
|
|
||||||
context["form"] = form
|
|
||||||
context["title"] = "Add New Device"
|
|
||||||
return render(request, "add.html", context)
|
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
|
||||||
return redirect("list_sessions_recent")
|
|
||||||
|
|
|
@ -98,17 +98,6 @@ editorconfig = ">=0.12.2"
|
||||||
jsbeautifier = "*"
|
jsbeautifier = "*"
|
||||||
six = ">=1.13.0"
|
six = ">=1.13.0"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "distlib"
|
|
||||||
version = "0.3.7"
|
|
||||||
description = "Distribution utilities"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
files = [
|
|
||||||
{file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"},
|
|
||||||
{file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django"
|
name = "django"
|
||||||
version = "4.2.7"
|
version = "4.2.7"
|
||||||
|
@ -248,9 +237,7 @@ files = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"]
|
test = ["pytest (>=6)"]
|
||||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
|
|
||||||
typing = ["typing-extensions (>=4.8)"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gunicorn"
|
name = "gunicorn"
|
||||||
|
@ -361,20 +348,6 @@ files = [
|
||||||
editorconfig = ">=0.12.2"
|
editorconfig = ">=0.12.2"
|
||||||
six = ">=1.13.0"
|
six = ">=1.13.0"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "json5"
|
|
||||||
version = "0.9.14"
|
|
||||||
description = "A Python implementation of the JSON5 data format."
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
files = [
|
|
||||||
{file = "json5-0.9.14-py2.py3-none-any.whl", hash = "sha256:740c7f1b9e584a468dbb2939d8d458db3427f2c93ae2139d05f47e453eae964f"},
|
|
||||||
{file = "json5-0.9.14.tar.gz", hash = "sha256:9ed66c3a6ca3510a976a9ef9b8c0787de24802724ab1860bc0153c7fdd589b02"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
dev = ["hypothesis"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markupsafe"
|
name = "markupsafe"
|
||||||
version = "2.1.3"
|
version = "2.1.3"
|
||||||
|
@ -505,23 +478,22 @@ files = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nodeenv"
|
name = "packaging"
|
||||||
version = "1.8.0"
|
version = "22.0"
|
||||||
description = "Node.js virtual environment builder"
|
description = "Core utilities for Python packages"
|
||||||
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
|
{file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"},
|
||||||
{file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
|
{file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
setuptools = "*"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "pathspec"
|
||||||
version = "23.2"
|
version = "0.10.3"
|
||||||
description = "Core utilities for Python packages"
|
description = "Utility library for gitignore style pattern matching of file paths."
|
||||||
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
|
@ -657,24 +629,6 @@ files = [
|
||||||
dev = ["pre-commit", "tox"]
|
dev = ["pre-commit", "tox"]
|
||||||
testing = ["pytest", "pytest-benchmark"]
|
testing = ["pytest", "pytest-benchmark"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pre-commit"
|
|
||||||
version = "3.5.0"
|
|
||||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"},
|
|
||||||
{file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
cfgv = ">=2.0.0"
|
|
||||||
identify = ">=1.0.0"
|
|
||||||
nodeenv = ">=0.11.1"
|
|
||||||
pyyaml = ">=5.1"
|
|
||||||
virtualenv = ">=20.10.0"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "7.4.3"
|
version = "7.4.3"
|
||||||
|
@ -693,7 +647,7 @@ packaging = "*"
|
||||||
pluggy = ">=0.12,<2.0"
|
pluggy = ">=0.12,<2.0"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
|
@ -871,6 +825,7 @@ testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jar
|
||||||
name = "six"
|
name = "six"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
description = "Python 2 and 3 compatibility utilities"
|
description = "Python 2 and 3 compatibility utilities"
|
||||||
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
files = [
|
files = [
|
||||||
|
|
Loading…
Reference in New Issue