Compare commits
33 Commits
1.0.3
...
787ee8640f
Author | SHA1 | Date | |
---|---|---|---|
787ee8640f | |||
ab41222f3c | |||
29bf3b1946 | |||
3f7ccea2e2 | |||
b5ffb3586b | |||
26d57a238e | |||
2d5ad3182c | |||
49cc3ea0cc | |||
440e1cfb71 | |||
1cbd8c5c55 | |||
bc81a0ee8e | |||
c5653977ff | |||
f151730ab6 | |||
f469a67d94 | |||
104ffc9d03 | |||
a4b13eb247 | |||
2307fac83a | |||
6b52c0d4c4 | |||
ff5d8c215d | |||
cdb3b89b08 | |||
ffa8198540 | |||
0b7da3550c | |||
e1655d6cfa | |||
29c41865d0 | |||
d21b461726 | |||
95489cfb78 | |||
fa4f1c4810 | |||
366c25a1ff | |||
a3042caa20 | |||
7997f9bbb2 | |||
b78c4ba9c5
|
|||
1df889c45d
|
|||
468d05a9e2
|
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.{js,py}]
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
# 4 space indentation
|
||||||
|
[*.py]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[**/*.js]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -5,6 +5,5 @@ __pycache__
|
|||||||
node_modules
|
node_modules
|
||||||
package-lock.json
|
package-lock.json
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
static/admin/
|
/static/
|
||||||
static/django_extensions/
|
|
||||||
dist/
|
dist/
|
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -4,5 +4,8 @@
|
|||||||
],
|
],
|
||||||
"python.testing.unittestEnabled": false,
|
"python.testing.unittestEnabled": false,
|
||||||
"python.testing.pytestEnabled": true,
|
"python.testing.pytestEnabled": true,
|
||||||
"python.analysis.typeCheckingMode": "basic"
|
"python.analysis.typeCheckingMode": "basic",
|
||||||
|
"[python]": {
|
||||||
|
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||||
|
},
|
||||||
}
|
}
|
14
CHANGELOG.md
14
CHANGELOG.md
@ -1,3 +1,17 @@
|
|||||||
|
## Unreleased
|
||||||
|
|
||||||
|
* Improve form appearance
|
||||||
|
* Add helper buttons next to datime fields
|
||||||
|
* Change recent session view to current year instead of last 30 days
|
||||||
|
* 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
|
||||||
|
@ -28,7 +28,7 @@ RUN chmod +x /entrypoint.sh
|
|||||||
USER timetracker
|
USER timetracker
|
||||||
ENV PATH="$PATH:/home/timetracker/.local/bin"
|
ENV PATH="$PATH:/home/timetracker/.local/bin"
|
||||||
RUN pip install --no-cache-dir poetry
|
RUN pip install --no-cache-dir poetry
|
||||||
RUN poetry install --without dev
|
RUN poetry install
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
CMD [ "/entrypoint.sh" ]
|
CMD [ "/entrypoint.sh" ]
|
@ -2,35 +2,84 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "IBM Plex Mono";
|
||||||
|
src: url("fonts/IBMPlexMono-regular.woff2") format("woff2");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "IBM Plex Sans";
|
||||||
|
src: url("fonts/IBMPlexSans-Regular.woff2") format("woff2");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "IBM Plex Serif";
|
||||||
|
src: url("fonts/IBMPlexSerif-Regular.woff2") format("woff2");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
form label {
|
form label {
|
||||||
@apply dark:text-slate-400;
|
@apply dark:text-slate-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.responsive-table {
|
||||||
|
@apply dark:text-white mx-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsive-table tr:nth-child(even) {
|
||||||
|
@apply bg-slate-800
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsive-table tbody tr:nth-child(odd) {
|
||||||
|
@apply bg-slate-900
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsive-table thead th {
|
||||||
|
@apply text-left border-b-2 border-b-slate-500 text-xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsive-table thead th:not(:first-child),
|
||||||
|
.responsive-table td:not(:first-child) {
|
||||||
|
@apply border-l border-l-slate-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.max-w-20char {
|
||||||
|
max-width: 20ch;
|
||||||
|
}
|
||||||
|
.max-w-35char {
|
||||||
|
max-width: 40ch;
|
||||||
|
}
|
||||||
|
.max-w-40char {
|
||||||
|
max-width: 40ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
form input,
|
form input,
|
||||||
select,
|
select,
|
||||||
textarea {
|
textarea {
|
||||||
@apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
|
@apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
#session-table {
|
@media screen and (min-width: 768px) {
|
||||||
display: grid;
|
form input,
|
||||||
grid-template-columns: 3fr 2fr repeat(2, 1fr) 0.5fr 1fr;
|
select,
|
||||||
|
textarea {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.purchase-name > span:nth-child(2) {
|
@media screen and (max-width: 768px) {
|
||||||
@apply ml-4
|
form input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.purchase-name > span:nth-child(2) > a > img {
|
|
||||||
@apply opacity-0 transition-opacity duration-500
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase-name:hover > span:nth-child(2) > a > img {
|
|
||||||
@apply opacity-50
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase-name > span:nth-child(2) > a > img:hover {
|
|
||||||
@apply opacity-100
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#button-container button {
|
#button-container button {
|
||||||
@ -38,9 +87,17 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
@apply text-left;
|
@apply text-right;
|
||||||
}
|
}
|
||||||
|
|
||||||
th label {
|
th label {
|
||||||
@apply mr-4;
|
@apply mr-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.basic-button-container {
|
||||||
|
@apply flex space-x-2 justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basic-button {
|
||||||
|
@apply inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out;
|
||||||
|
}
|
||||||
|
@ -7,5 +7,13 @@ poetry run python manage.py migrate
|
|||||||
echo "Collect static files"
|
echo "Collect static files"
|
||||||
poetry run python manage.py collectstatic --clear --no-input
|
poetry run python manage.py collectstatic --clear --no-input
|
||||||
|
|
||||||
|
_term() {
|
||||||
|
echo "Caught SIGTERM signal!"
|
||||||
|
kill -SIGTERM "$gunicorn_pid"
|
||||||
|
}
|
||||||
|
trap _term SIGTERM
|
||||||
|
|
||||||
echo "Starting app"
|
echo "Starting app"
|
||||||
poetry run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile -
|
poetry run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile - & gunicorn_pid=$!
|
||||||
|
|
||||||
|
wait "$gunicorn_pid"
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from games.models import Game, Platform, Purchase, Session
|
from games.models import Game, Platform, Purchase, Session, Edition, Device
|
||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
admin.site.register(Game)
|
admin.site.register(Game)
|
||||||
admin.site.register(Purchase)
|
admin.site.register(Purchase)
|
||||||
admin.site.register(Platform)
|
admin.site.register(Platform)
|
||||||
admin.site.register(Session)
|
admin.site.register(Session)
|
||||||
|
admin.site.register(Edition)
|
||||||
|
admin.site.register(Device)
|
||||||
|
@ -2,13 +2,28 @@ from django import forms
|
|||||||
|
|
||||||
from games.models import Game, Platform, Purchase, Session, Edition, Device
|
from games.models import Game, Platform, Purchase, Session, Edition, Device
|
||||||
|
|
||||||
|
custom_date_widget = forms.DateInput(attrs={"type": "date"})
|
||||||
|
custom_datetime_widget = forms.DateTimeInput(
|
||||||
|
attrs={"type": "datetime-local"}, format="%Y-%m-%d %H:%M"
|
||||||
|
)
|
||||||
|
autofocus_select_widget = forms.Select(attrs={"autofocus": "autofocus"})
|
||||||
|
autofocus_input_widget = forms.TextInput(attrs={"autofocus": "autofocus"})
|
||||||
|
|
||||||
|
|
||||||
class SessionForm(forms.ModelForm):
|
class SessionForm(forms.ModelForm):
|
||||||
|
# purchase = forms.ModelChoiceField(
|
||||||
|
# queryset=Purchase.objects.filter(date_refunded=None).order_by("edition__name")
|
||||||
|
# )
|
||||||
purchase = forms.ModelChoiceField(
|
purchase = forms.ModelChoiceField(
|
||||||
queryset=Purchase.objects.order_by("edition__name")
|
queryset=Purchase.objects.order_by("edition__name"),
|
||||||
|
widget=autofocus_select_widget,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
widgets = {
|
||||||
|
"timestamp_start": custom_datetime_widget,
|
||||||
|
"timestamp_end": custom_datetime_widget,
|
||||||
|
}
|
||||||
model = Session
|
model = Session
|
||||||
fields = [
|
fields = [
|
||||||
"purchase",
|
"purchase",
|
||||||
@ -26,10 +41,16 @@ class EditionChoiceField(forms.ModelChoiceField):
|
|||||||
|
|
||||||
|
|
||||||
class PurchaseForm(forms.ModelForm):
|
class PurchaseForm(forms.ModelForm):
|
||||||
edition = EditionChoiceField(queryset=Edition.objects.order_by("name"))
|
edition = EditionChoiceField(
|
||||||
|
queryset=Edition.objects.order_by("name"), widget=autofocus_select_widget
|
||||||
|
)
|
||||||
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
|
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
widgets = {
|
||||||
|
"date_purchased": custom_date_widget,
|
||||||
|
"date_refunded": custom_date_widget,
|
||||||
|
}
|
||||||
model = Purchase
|
model = Purchase
|
||||||
fields = [
|
fields = [
|
||||||
"edition",
|
"edition",
|
||||||
@ -43,6 +64,9 @@ class PurchaseForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class EditionForm(forms.ModelForm):
|
class EditionForm(forms.ModelForm):
|
||||||
|
game = forms.ModelChoiceField(
|
||||||
|
queryset=Game.objects.order_by("name"), widget=autofocus_select_widget
|
||||||
|
)
|
||||||
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
|
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -54,15 +78,18 @@ class GameForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Game
|
model = Game
|
||||||
fields = ["name", "wikidata"]
|
fields = ["name", "wikidata"]
|
||||||
|
widgets = {"name": autofocus_input_widget}
|
||||||
|
|
||||||
|
|
||||||
class PlatformForm(forms.ModelForm):
|
class PlatformForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Platform
|
model = Platform
|
||||||
fields = ["name", "group"]
|
fields = ["name", "group"]
|
||||||
|
widgets = {"name": autofocus_input_widget}
|
||||||
|
|
||||||
|
|
||||||
class DeviceForm(forms.ModelForm):
|
class DeviceForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = ["name", "type"]
|
fields = ["name", "type"]
|
||||||
|
widgets = {"name": autofocus_input_widget}
|
||||||
|
File diff suppressed because it is too large
Load Diff
BIN
games/static/fonts/IBMPlexMono-Regular.woff2
Normal file
BIN
games/static/fonts/IBMPlexMono-Regular.woff2
Normal file
Binary file not shown.
BIN
games/static/fonts/IBMPlexSans-Regular.woff2
Normal file
BIN
games/static/fonts/IBMPlexSans-Regular.woff2
Normal file
Binary file not shown.
BIN
games/static/fonts/IBMPlexSerif-Regular.woff2
Normal file
BIN
games/static/fonts/IBMPlexSerif-Regular.woff2
Normal file
Binary file not shown.
BIN
games/static/icons/loading.png
Normal file
BIN
games/static/icons/loading.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 787 B |
29
games/static/js/add_edition.js
Normal file
29
games/static/js/add_edition.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceElement.addEventListener("change", sourceElementHandler);
|
||||||
|
targetElement.addEventListener("focus", targetElementHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
syncSelectInputUntilChanged("#id_game", "#id_name");
|
||||||
|
});
|
19
games/static/js/add_session.js
Normal file
19
games/static/js/add_session.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { toISOUTCString } from "./utils.js";
|
||||||
|
|
||||||
|
for (let button of document.querySelectorAll("[data-target]")) {
|
||||||
|
let target = button.getAttribute("data-target");
|
||||||
|
let type = button.getAttribute("data-type");
|
||||||
|
let targetElement = document.querySelector(`#id_${target}`);
|
||||||
|
button.addEventListener("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (type == "now") {
|
||||||
|
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") {
|
||||||
|
if (targetElement.type == "datetime-local") targetElement.type = "text";
|
||||||
|
else targetElement.type = "datetime-local";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
1
games/static/js/htmx.min.js
vendored
Normal file
1
games/static/js/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
16
games/static/js/utils.js
Normal file
16
games/static/js/utils.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* @description Formats Date to a UTC string accepted by the datetime-local input field.
|
||||||
|
* @param {Date} date
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function toISOUTCString(date) {
|
||||||
|
function stringAndPad(number) {
|
||||||
|
return number.toString().padStart(2, 0);
|
||||||
|
}
|
||||||
|
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}`;
|
||||||
|
}
|
43
games/static/main.js
Normal file
43
games/static/main.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
function elt(type, props, ...children) {
|
||||||
|
let dom = document.createElement(type);
|
||||||
|
if (props) Object.assign(dom, props);
|
||||||
|
for (let child of children) {
|
||||||
|
if (typeof child != "string") dom.appendChild(child);
|
||||||
|
else dom.appendChild(document.createTextNode(child));
|
||||||
|
}
|
||||||
|
return dom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Node} targetNode
|
||||||
|
*/
|
||||||
|
function addToggleButton(targetNode) {
|
||||||
|
let manualToggleButton = elt(
|
||||||
|
"td",
|
||||||
|
{},
|
||||||
|
elt(
|
||||||
|
"div",
|
||||||
|
{ className: "basic-button" },
|
||||||
|
elt(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
onclick: (event) => {
|
||||||
|
let textInputField = elt("input", { type: "text", id: targetNode.id });
|
||||||
|
targetNode.replaceWith(textInputField);
|
||||||
|
event.target.addEventListener("click", (event) => {
|
||||||
|
textInputField.replaceWith(targetNode);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Toggle manual"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
targetNode.parentElement.appendChild(manualToggleButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleableFields = ["#id_game", "#id_edition", "#id_platform"];
|
||||||
|
|
||||||
|
toggleableFields.map((selector) => {
|
||||||
|
addToggleButton(document.querySelector(selector));
|
||||||
|
});
|
2
games/static/robots.txt
Normal file
2
games/static/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
{{ form.as_table }}
|
{{ form.as_table }}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td></td>
|
||||||
<td><input type="submit" value="Submit"/></td>
|
<td><input type="submit" value="Submit"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
22
games/templates/add_edition.html
Normal file
22
games/templates/add_edition.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
<table class="mx-auto">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{{ form.as_table }}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td><input type="submit" value="Submit"/></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{% load static %}
|
||||||
|
<script type="module" src="{% static 'js/add_edition.js' %}"></script>
|
||||||
|
{% endblock scripts %}
|
37
games/templates/add_session.html
Normal file
37
games/templates/add_session.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }}{% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
<table class="mx-auto">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% for field in form %}
|
||||||
|
<tr>
|
||||||
|
<th>{{ field.label_tag }}</th>
|
||||||
|
{% if field.name == "note" %}
|
||||||
|
<td>{{ field }}</td>
|
||||||
|
{% else %}
|
||||||
|
<td>{{ field }}</td>
|
||||||
|
{% endif %}
|
||||||
|
{% if field.name == "timestamp_start" or field.name == "timestamp_end" %}
|
||||||
|
<td>
|
||||||
|
<div class="basic-button-container">
|
||||||
|
<button class="basic-button" data-target="{{field.name}}" data-type="now">Set to now</button>
|
||||||
|
<button class="basic-button" data-target="{{field.name}}" data-type="toggle">Toggle text</button>
|
||||||
|
<button class="basic-button" data-target="{{field.name}}" data-type="copy">Copy</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td><input type="submit" value="Submit"/></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
{% load static %}
|
||||||
|
<script type="module" src="{% static 'js/add_session.js' %}"></script>
|
||||||
|
{% endblock content %}
|
@ -9,11 +9,12 @@
|
|||||||
<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 - {% block title %}Untitled{% endblock title %}</title>
|
<title>Timetracker - {% block title %}Untitled{% endblock title %}</title>
|
||||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"/>
|
<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">
|
<body class="dark">
|
||||||
|
<img id="indicator" src="{% static 'icons/loading.png' %}" 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">
|
||||||
@ -47,6 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% load version %}
|
{% load version %}
|
||||||
<span class="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span>
|
<span class="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span>
|
||||||
|
{% block scripts %}{% endblock scripts %}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
22
games/templates/components/button.html
Normal file
22
games/templates/components/button.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="{{ title }}"
|
||||||
|
autofocus
|
||||||
|
class="truncate max-w-xs sm:max-w-md lg:max-w-lg py-1 px-2 bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="self-center w-6 h-6 inline"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{{ text }}
|
||||||
|
</button>
|
@ -5,121 +5,69 @@
|
|||||||
{% block title %}{{ title }}{% endblock title %}
|
{% block title %}{{ title }}{% endblock title %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="text-center text-xl mb-4 dark:text-slate-400">
|
|
||||||
{% if dataset.count >= 2 %}
|
|
||||||
<img src="data:image/svg+xml;base64,{{ chart|safe }}" class="mx-auto mb-3" />
|
|
||||||
{% endif %}
|
|
||||||
{% if dataset.count >= 1 %}
|
|
||||||
<div class="mb-4">Total playtime: {{ total_duration }} over {{ dataset.count }} sessions.</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if purchase or platform or edition or game or ownership_type %}
|
|
||||||
<span class="block">
|
|
||||||
<a class="text-red-400 inline" href="{% url 'list_sessions' %}">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 inline">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
{% if purchase %}
|
|
||||||
Filtering by purchase "{{ purchase }}"
|
|
||||||
(<a href="{% url 'edit_purchase' purchase.id %}" class="hover:underline dark:text-white">Edit</a>)
|
|
||||||
{% elif platform %}
|
|
||||||
Filtering by purchase "{{ platform }}"
|
|
||||||
(<a href="{% url 'edit_platform' platform.id %}" class="hover:underline dark:text-white">Edit</a>)
|
|
||||||
{% elif game %}
|
|
||||||
Filtering by purchase "{{ game }}"
|
|
||||||
(<a href="{% url 'edit_game' game.id %}" class="hover:underline dark:text-white">Edit</a>)
|
|
||||||
{% elif edition %}
|
|
||||||
Filtering by purchase "{{ edition }}"
|
|
||||||
(<a href="{% url 'edit_edition' edition.id %}" class="hover:underline dark:text-white">Edit</a>)
|
|
||||||
{% elif ownership_type %}
|
|
||||||
Filtering by ownership type "{{ ownership_type }}"
|
|
||||||
{% endif%}
|
|
||||||
</span>
|
|
||||||
{% if purchase %}<a class="dark:text-white hover:underline block" href="{% url 'list_sessions_by_edition' purchase.edition.id %}">See all platforms</a>{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% if dataset.count >= 1 %}
|
|
||||||
<a href="{% url 'start_session' last.id %}">
|
|
||||||
<button type="button" title="Track last tracked" class="mt-10 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 ">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="self-center w-6 h-6 inline">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
|
|
||||||
</svg>
|
|
||||||
{{ last.purchase }}
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div id="session-table" class="gap-4 shadow rounded-xl max-w-screen-2xl mx-auto dark:bg-slate-700 p-2 justify-center">
|
|
||||||
<div class="dark:border-white dark:text-slate-300 text-lg">Purchase</div>
|
|
||||||
<div class="dark:border-white dark:text-slate-300 text-lg">Platform</div>
|
|
||||||
<div class="dark:border-white dark:text-slate-300 text-lg text-center">Start</div>
|
|
||||||
<div class="dark:border-white dark:text-slate-300 text-lg text-center">End</div>
|
|
||||||
<div class="dark:border-white dark:text-slate-300 text-lg">Duration</div>
|
|
||||||
<div class="dark:border-white dark:text-slate-300 text-lg text-right">Manage</div>
|
|
||||||
{% for data in dataset %}
|
|
||||||
<div class="purchase-name">
|
|
||||||
<span class="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">{{ data.purchase.edition }} <span class="dark:text-slate-400">(<a class="hover:underline" href="{% url 'list_sessions_by_ownership_type' data.purchase.ownership_type %}">{{ data.purchase.get_ownership_type_display }}</a>)</span></span>
|
|
||||||
|
|
||||||
<span>
|
{% if dataset.count >= 1 %}
|
||||||
<a href="{% url 'list_sessions_by_game' data.purchase.edition.game.id %}">
|
<div class="mx-auto text-center my-4">
|
||||||
<img src="{% static 'icons/game_white.png' %}" width="32" class="inline" alt="Filter by this game" title="Filter by this game" />
|
<a
|
||||||
|
id="last-session-start"
|
||||||
|
href="{% url 'start_session' last.id %}"
|
||||||
|
hx-get="{% url 'start_session' last.id %}"
|
||||||
|
hx-indicator="#indicator"
|
||||||
|
hx-swap="afterbegin"
|
||||||
|
hx-target=".responsive-table tbody"
|
||||||
|
hx-select=".responsive-table tbody tr:first-child"
|
||||||
|
onClick="document.querySelector('#last-session-start').classList.add('invisible')"
|
||||||
|
class="{% if last.timestamp_end == null %}invisible{% endif %}"
|
||||||
|
>
|
||||||
|
{% include 'components/button.html' with text=last.purchase title="Start session of last played game" only %}
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'list_sessions_by_edition' data.purchase.edition.id %}">
|
|
||||||
<img src="{% static 'icons/edition_white.png' %}" width="32" class="inline" alt="Filter by this edition" title="Filter by this edition" />
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'list_sessions_by_purchase' data.purchase.id %}">
|
|
||||||
<img src="{% static 'icons/purchase_white.png' %}" width="32" class="inline" alt="Filter by this purchase" title="Filter by this purchase" />
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
|
|
||||||
<a class="hover:underline" href="{% url 'list_sessions_by_platform' data.purchase.platform.id %}">
|
|
||||||
{% if data.purchase.platform != data.purchase.edition.platform %}
|
|
||||||
{{data.purchase.edition.platform}} on {{ data.purchase.platform }}
|
|
||||||
{% else %}
|
|
||||||
{{ data.purchase.platform }}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
|
||||||
</div>
|
<table class="responsive-table">
|
||||||
<div class="dark:text-slate-400 text-center">{{ data.timestamp_start | date:"d/m/Y H:i" }}</div>
|
<thead>
|
||||||
<div class="dark:text-slate-400 text-center">
|
<tr>
|
||||||
|
<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>
|
||||||
|
<th class="hidden lg:table-cell px-2 sm:px-4 md:px-6 md:py-2">End</th>
|
||||||
|
<th class="px-2 sm:px-4 md:px-6 md:py-2">Duration</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for data in dataset %}
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char md:max-w-40char"
|
||||||
|
>
|
||||||
|
{{ data.purchase.edition }}
|
||||||
|
</td>
|
||||||
|
<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" }}
|
||||||
|
</td>
|
||||||
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden lg:table-cell">
|
||||||
{% if data.unfinished %}
|
{% if data.unfinished %}
|
||||||
<span class="text-red-400">Not finished yet.</span>
|
<a
|
||||||
|
href="{% url 'update_session' data.id %}"
|
||||||
|
hx-get="{% url 'update_session' data.id %}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-target=".responsive-table tbody tr:first-child"
|
||||||
|
hx-select=".responsive-table tbody tr:first-child"
|
||||||
|
hx-indicator="#indicator"
|
||||||
|
onClick="document.querySelector('#last-session-start').classList.remove('invisible')"
|
||||||
|
>
|
||||||
|
<span class="text-yellow-300">Finish now?</span>
|
||||||
|
</a>
|
||||||
{% elif data.duration_manual %}
|
{% elif data.duration_manual %}
|
||||||
--
|
--
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ data.timestamp_end | date:"d/m/Y H:i" }}
|
{{ data.timestamp_end | date:"d/m/Y H:i" }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</td>
|
||||||
<div class="dark:text-slate-400 flex">{{ data.duration_formatted }}{% if data.duration_manual %} <svg title="Added manually" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5 ml-1 self-center">
|
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
||||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
|
{{ data.duration_formatted }}
|
||||||
</svg>
|
</td>
|
||||||
{% endif %}</div>
|
</tr>
|
||||||
<div id="button-container" class="flex justify-end">
|
|
||||||
{% if data.unfinished %}
|
|
||||||
<a href="{% url 'update_session' data.id %}">
|
|
||||||
<button type="button" title="Set to finished" class="py-1 px-2 flex justify-center items-center 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 w-7 h-4 rounded-lg ">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
|
||||||
<path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
<a href="{% url 'edit_session' data.id %}">
|
|
||||||
<button type="button" title="Edit" class="py-1 px-2 flex justify-center items-center bg-blue-600 hover:bg-blue-700 focus:ring-blue-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 w-7 h-4 rounded-lg ">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z" />
|
|
||||||
<path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'delete_session' data.id %}">
|
|
||||||
<button type="button" edit="Delete" class="py-1 px-2 flex justify-center items-center bg-red-600 hover:bg-red-700 focus:ring-red-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 w-7 h-4 rounded-lg ">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</tbody>
|
||||||
|
</table>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
@ -23,11 +23,11 @@ urlpatterns = [
|
|||||||
views.start_session,
|
views.start_session,
|
||||||
name="start_session",
|
name="start_session",
|
||||||
),
|
),
|
||||||
path(
|
# path(
|
||||||
"delete_session/by-id/<int:session_id>",
|
# "delete_session/by-id/<int:session_id>",
|
||||||
views.delete_session,
|
# views.delete_session,
|
||||||
name="delete_session",
|
# name="delete_session",
|
||||||
),
|
# ),
|
||||||
path("add-purchase/", views.add_purchase, name="add_purchase"),
|
path("add-purchase/", views.add_purchase, name="add_purchase"),
|
||||||
path("add-edition/", views.add_edition, name="add_edition"),
|
path("add-edition/", views.add_edition, name="add_edition"),
|
||||||
path("edit-edition/<int:edition_id>", views.edit_edition, name="edit_edition"),
|
path("edit-edition/<int:edition_id>", views.edit_edition, name="edit_edition"),
|
||||||
|
@ -45,7 +45,7 @@ def add_session(request):
|
|||||||
|
|
||||||
context["title"] = "Add New Session"
|
context["title"] = "Add New Session"
|
||||||
context["form"] = form
|
context["form"] = form
|
||||||
return render(request, "add.html", context)
|
return render(request, "add_session.html", context)
|
||||||
|
|
||||||
|
|
||||||
def update_session(request, session_id=None):
|
def update_session(request, session_id=None):
|
||||||
@ -64,7 +64,7 @@ def edit_session(request, session_id=None):
|
|||||||
return redirect("list_sessions")
|
return redirect("list_sessions")
|
||||||
context["title"] = "Edit Session"
|
context["title"] = "Edit Session"
|
||||||
context["form"] = form
|
context["form"] = form
|
||||||
return render(request, "add.html", context)
|
return render(request, "add_session.html", context)
|
||||||
|
|
||||||
|
|
||||||
def edit_purchase(request, purchase_id=None):
|
def edit_purchase(request, purchase_id=None):
|
||||||
@ -128,10 +128,10 @@ def start_session(request, last_session_id: int):
|
|||||||
return redirect("list_sessions")
|
return redirect("list_sessions")
|
||||||
|
|
||||||
|
|
||||||
def delete_session(request, session_id=None):
|
# def delete_session(request, session_id=None):
|
||||||
session = Session.objects.get(id=session_id)
|
# session = Session.objects.get(id=session_id)
|
||||||
session.delete()
|
# session.delete()
|
||||||
return redirect("list_sessions")
|
# return redirect("list_sessions")
|
||||||
|
|
||||||
|
|
||||||
def list_sessions(
|
def list_sessions(
|
||||||
@ -162,10 +162,12 @@ 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 = datetime.now().year
|
||||||
|
first_day_of_year = datetime(current_year, 1, 1)
|
||||||
dataset = Session.objects.filter(
|
dataset = Session.objects.filter(
|
||||||
timestamp_start__gte=datetime.now() - timedelta(days=30)
|
timestamp_start__gte=first_day_of_year
|
||||||
).order_by("-timestamp_start")
|
).order_by("-timestamp_start")
|
||||||
context["title"] = "Last 30 days"
|
context["title"] = "This year"
|
||||||
else:
|
else:
|
||||||
# by default, sort from newest to oldest
|
# by default, sort from newest to oldest
|
||||||
dataset = Session.objects.all().order_by("-timestamp_start")
|
dataset = Session.objects.all().order_by("-timestamp_start")
|
||||||
@ -179,8 +181,9 @@ def list_sessions(
|
|||||||
context["dataset"] = dataset
|
context["dataset"] = dataset
|
||||||
# cannot use dataset[0] here because that might be only partial QuerySet
|
# cannot use dataset[0] here because that might be only partial QuerySet
|
||||||
context["last"] = Session.objects.all().order_by("timestamp_start").last()
|
context["last"] = Session.objects.all().order_by("timestamp_start").last()
|
||||||
|
# only if 2 or more non-manual sessions exist
|
||||||
|
if dataset.filter(timestamp_end__isnull=False).count() >= 2:
|
||||||
# charts are always oldest->newest
|
# charts are always oldest->newest
|
||||||
if dataset.count() >= 2:
|
|
||||||
context["chart"] = playtime_over_time_chart(dataset.order_by("timestamp_start"))
|
context["chart"] = playtime_over_time_chart(dataset.order_by("timestamp_start"))
|
||||||
|
|
||||||
return render(request, "list_sessions.html", context)
|
return render(request, "list_sessions.html", context)
|
||||||
@ -221,7 +224,7 @@ def add_edition(request):
|
|||||||
|
|
||||||
context["form"] = form
|
context["form"] = form
|
||||||
context["title"] = "Add New Edition"
|
context["title"] = "Add New Edition"
|
||||||
return render(request, "add.html", context)
|
return render(request, "add_edition.html", context)
|
||||||
|
|
||||||
|
|
||||||
def add_platform(request):
|
def add_platform(request):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.6",
|
||||||
"@tailwindcss/typography": "^0.5.8",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
"tailwindcss": "^3.2.4"
|
"tailwindcss": "^3.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
|
const defaultTheme = require('tailwindcss/defaultTheme')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
content: ["./**/*.{html,js}"],
|
content: ["./games/**/*.{html,js}"],
|
||||||
theme: {
|
theme: {
|
||||||
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter', 'sans-serif'],
|
'sans': ['IBM Plex Sans', ...defaultTheme.fontFamily.sans],
|
||||||
|
'mono': ['IBM Plex Mono', ...defaultTheme.fontFamily.mono],
|
||||||
|
'serif': ['IBM Plex Serif', ...defaultTheme.fontFamily.serif],
|
||||||
|
}
|
||||||
},
|
},
|
||||||
extend: {},
|
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
require('@tailwindcss/typography'),
|
require('@tailwindcss/typography'),
|
||||||
|
@ -40,7 +40,7 @@ INSTALLED_APPS = [
|
|||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
]
|
]
|
||||||
|
|
||||||
if DEBUG:
|
# if DEBUG:
|
||||||
INSTALLED_APPS.append("django_extensions")
|
INSTALLED_APPS.append("django_extensions")
|
||||||
INSTALLED_APPS.append("django.contrib.admin")
|
INSTALLED_APPS.append("django.contrib.admin")
|
||||||
|
|
||||||
@ -149,3 +149,5 @@ if _csrf_trusted_origins:
|
|||||||
CSRF_TRUSTED_ORIGINS = _csrf_trusted_origins.split(",")
|
CSRF_TRUSTED_ORIGINS = _csrf_trusted_origins.split(",")
|
||||||
else:
|
else:
|
||||||
CSRF_TRUSTED_ORIGINS = []
|
CSRF_TRUSTED_ORIGINS = []
|
||||||
|
|
||||||
|
USE_L10N = False
|
||||||
|
@ -23,5 +23,6 @@ urlpatterns = [
|
|||||||
path("tracker/", include("games.urls")),
|
path("tracker/", include("games.urls")),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
# if settings.DEBUG:
|
||||||
|
# urlpatterns.append(path("admin/", admin.site.urls))
|
||||||
urlpatterns.append(path("admin/", admin.site.urls))
|
urlpatterns.append(path("admin/", admin.site.urls))
|
||||||
|
Reference in New Issue
Block a user