33 Commits

Author SHA1 Message Date
787ee8640f Try to shutdown container gracefully and faster
All checks were successful
continuous-integration/drone/push Build is passing
2023-10-01 19:57:15 +02:00
ab41222f3c Switch fonts to WOFF2 2023-10-01 19:53:07 +02:00
29bf3b1946 Further improve session list
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-30 19:44:35 +02:00
3f7ccea2e2 fix tailwind config to only scan relevant files 2023-09-30 18:13:50 +02:00
b5ffb3586b Update base.css 2023-09-30 16:25:19 +02:00
26d57a238e Update .vscode 2023-09-30 16:25:05 +02:00
2d5ad3182c Update CSS 2023-09-30 16:24:54 +02:00
49cc3ea0cc Improve session list
All checks were successful
continuous-integration/drone/push Build is passing
Fixes #53
2023-09-30 15:55:38 +02:00
440e1cfb71 Focus important fields on forms
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-30 12:40:02 +02:00
1cbd8c5c55 Update generated CSS
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-20 19:06:04 +02:00
bc81a0ee8e Add hacky way to not reload page when starting/ending session
All checks were successful
continuous-integration/drone/push Build is passing
Partially fixes #52
2023-09-20 18:54:54 +02:00
c5653977ff Add time copy button, improve session editing
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-18 20:21:05 +02:00
f151730ab6 Fix bug when filtering only manual sessions (#51)
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-18 19:37:54 +02:00
f469a67d94 add robots.txt
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-17 17:58:22 +02:00
104ffc9d03 disable deleting sessions in code
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-17 17:17:22 +02:00
a4b13eb247 allow admin in prod 2/2
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-17 14:12:23 +02:00
2307fac83a allow admin in prod
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-17 14:03:46 +02:00
6b52c0d4c4 disable deleting sessions
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-17 13:28:15 +02:00
ff5d8c215d install dev dependecies
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-16 18:24:10 +02:00
cdb3b89b08 do not filter out refunded games when adding session
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-16 18:18:03 +02:00
ffa8198540 allow django admit 2023-09-16 18:17:36 +02:00
0b7da3550c Change recent session view to current year
All checks were successful
continuous-integration/drone/push Build is passing
2023-09-14 18:49:16 +02:00
e1655d6cfa input datetime-local needs day to also be two digits
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-02 21:33:07 +01:00
29c41865d0 Exclude refunded purchases from start session form
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-01 22:07:49 +01:00
d21b461726 Update styles
All checks were successful
continuous-integration/drone/push Build is passing
2023-02-21 23:51:07 +01:00
95489cfb78 Add .editorconfig 2023-02-21 23:50:09 +01:00
fa4f1c4810 Improve forms, add helper buttons on add session form 2023-02-21 23:49:57 +01:00
366c25a1ff Register Edition, Device in the admin UI 2023-02-20 22:01:20 +01:00
a3042caa20 Use date and datetime inputs
All checks were successful
continuous-integration/drone/push Build is passing
Properly implements 4d91a76513
2023-02-20 21:33:15 +01:00
7997f9bbb2 Sort games by name on edition form 2023-02-20 20:17:42 +01:00
b78c4ba9c5 Sync game name and edition name fields for QOL
All checks were successful
continuous-integration/drone/push Build is passing
2023-02-20 18:21:48 +01:00
1df889c45d Fix gitignoring root static folder 2023-02-20 18:20:49 +01:00
468d05a9e2 Sort games alphabetically on edition form 2023-02-20 17:37:14 +01:00
32 changed files with 721 additions and 4529 deletions

17
.editorconfig Normal file
View 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
View File

@ -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/

View File

@ -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"
},
}

View File

@ -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

View File

@ -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" ]

View File

@ -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 {
.purchase-name > span:nth-child(2) > a > img { width: 150px;
@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;
}

View File

@ -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"

View File

@ -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)

View File

@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

View 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");
});

View 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

File diff suppressed because one or more lines are too long

16
games/static/js/utils.js Normal file
View 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
View 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
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@ -9,8 +9,9 @@
{{ 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>
</form> </form>
{% endblock content %} {% endblock content %}

View 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 %}

View 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 %}

View File

@ -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>

View 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>

View File

@ -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 %} {% if dataset.count >= 1 %}
<img src="data:image/svg+xml;base64,{{ chart|safe }}" class="mx-auto mb-3" /> <div class="mx-auto text-center my-4">
{% endif %} <a
{% if dataset.count >= 1 %} id="last-session-start"
<div class="mb-4">Total playtime: {{ total_duration }} over {{ dataset.count }} sessions.</div> href="{% url 'start_session' last.id %}"
{% endif %} hx-get="{% url 'start_session' last.id %}"
{% if purchase or platform or edition or game or ownership_type %} hx-indicator="#indicator"
<span class="block"> hx-swap="afterbegin"
<a class="text-red-400 inline" href="{% url 'list_sessions' %}"> hx-target=".responsive-table tbody"
<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"> hx-select=".responsive-table tbody tr:first-child"
<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" /> onClick="document.querySelector('#last-session-start').classList.add('invisible')"
</svg> class="{% if last.timestamp_end == null %}invisible{% endif %}"
</a> >
{% if purchase %} {% include 'components/button.html' with text=last.purchase title="Start session of last played game" only %}
Filtering by purchase "{{ purchase }}" </a>
(<a href="{% url 'edit_purchase' purchase.id %}" class="hover:underline dark:text-white">Edit</a>) </div>
{% elif platform %} {% endif %}
Filtering by purchase "{{ platform }}"
(<a href="{% url 'edit_platform' platform.id %}" class="hover:underline dark:text-white">Edit</a>) <table class="responsive-table">
{% elif game %} <thead>
Filtering by purchase "{{ game }}" <tr>
(<a href="{% url 'edit_game' game.id %}" class="hover:underline dark:text-white">Edit</a>) <th class="px-2 sm:px-4 md:px-6 md:py-2">Name</th>
{% elif edition %} <th class="hidden sm:table-cell px-2 sm:px-4 md:px-6 md:py-2">Start</th>
Filtering by purchase "{{ edition }}" <th class="hidden lg:table-cell px-2 sm:px-4 md:px-6 md:py-2">End</th>
(<a href="{% url 'edit_edition' edition.id %}" class="hover:underline dark:text-white">Edit</a>) <th class="px-2 sm:px-4 md:px-6 md:py-2">Duration</th>
{% elif ownership_type %} </tr>
Filtering by ownership type "{{ ownership_type }}" </thead>
{% endif%} <tbody>
</span> {% for data in dataset %}
{% if purchase %}<a class="dark:text-white hover:underline block" href="{% url 'list_sessions_by_edition' purchase.edition.id %}">See all platforms</a>{% endif %} <tr>
{% endif %} <td
{% if dataset.count >= 1 %} class="px-2 sm:px-4 md:px-6 md:py-2 purchase-name truncate max-w-20char md:max-w-40char"
<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 "> {{ data.purchase.edition }}
<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"> </td>
<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" /> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden sm:table-cell">
</svg> {{ data.timestamp_start | date:"d/m/Y H:i" }}
{{ last.purchase }} </td>
</button> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden lg:table-cell">
</a> {% if data.unfinished %}
{% endif %} <a
</div> href="{% url 'update_session' data.id %}"
<div id="session-table" class="gap-4 shadow rounded-xl max-w-screen-2xl mx-auto dark:bg-slate-700 p-2 justify-center"> hx-get="{% url 'update_session' data.id %}"
<div class="dark:border-white dark:text-slate-300 text-lg">Purchase</div> hx-swap="outerHTML"
<div class="dark:border-white dark:text-slate-300 text-lg">Platform</div> hx-target=".responsive-table tbody tr:first-child"
<div class="dark:border-white dark:text-slate-300 text-lg text-center">Start</div> hx-select=".responsive-table tbody tr:first-child"
<div class="dark:border-white dark:text-slate-300 text-lg text-center">End</div> hx-indicator="#indicator"
<div class="dark:border-white dark:text-slate-300 text-lg">Duration</div> onClick="document.querySelector('#last-session-start').classList.remove('invisible')"
<div class="dark:border-white dark:text-slate-300 text-lg text-right">Manage</div> >
{% for data in dataset %} <span class="text-yellow-300">Finish now?</span>
<div class="purchase-name"> </a>
<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> {% elif data.duration_manual %}
--
<span> {% else %}
<a href="{% url 'list_sessions_by_game' data.purchase.edition.game.id %}"> {{ data.timestamp_end | date:"d/m/Y H:i" }}
<img src="{% static 'icons/game_white.png' %}" width="32" class="inline" alt="Filter by this game" title="Filter by this game" /> {% endif %}
</a> </td>
<a href="{% url 'list_sessions_by_edition' data.purchase.edition.id %}"> <td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
<img src="{% static 'icons/edition_white.png' %}" width="32" class="inline" alt="Filter by this edition" title="Filter by this edition" /> {{ data.duration_formatted }}
</a> </td>
<a href="{% url 'list_sessions_by_purchase' data.purchase.id %}"> </tr>
<img src="{% static 'icons/purchase_white.png' %}" width="32" class="inline" alt="Filter by this purchase" title="Filter by this purchase" /> {% endfor %}
</a> </tbody>
</span> </table>
</div> {% endblock content %}
<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 %}
</a>
</div>
<div class="dark:text-slate-400 text-center">{{ data.timestamp_start | date:"d/m/Y H:i" }}</div>
<div class="dark:text-slate-400 text-center">
{% if data.unfinished %}
<span class="text-red-400">Not finished yet.</span>
{% elif data.duration_manual %}
--
{% else %}
{{ data.timestamp_end | date:"d/m/Y H:i" }}
{% endif %}
</div>
<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">
<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" />
</svg>
{% endif %}</div>
<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 %}
</div>
{% endblock content %}

View File

@ -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"),

View File

@ -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()
# charts are always oldest->newest # only if 2 or more non-manual sessions exist
if dataset.count() >= 2: if dataset.filter(timestamp_end__isnull=False).count() >= 2:
# charts are always oldest->newest
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):

View File

@ -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"
} }
} }

View File

@ -1,14 +1,19 @@
const defaultTheme = require('tailwindcss/defaultTheme')
module.exports = { module.exports = {
darkMode: 'class', darkMode: 'class',
content: ["./**/*.{html,js}"], content: ["./games/**/*.{html,js}"],
theme: { theme: {
fontFamily: { extend: {
sans: ['Inter', 'sans-serif'], fontFamily: {
'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'),
require('@tailwindcss/forms') require('@tailwindcss/forms')
], ],
} }

View File

@ -40,9 +40,9 @@ 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")
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
@ -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

View File

@ -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))