Compare commits

..

2 Commits

Author SHA1 Message Date
Lukáš Kucharczyk 9af4c79947
improve game view
Django CI/CD / test (push) Successful in 56s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-19 21:58:43 +02:00
Lukáš Kucharczyk d8b8182b91
fix table top rounding 2024-08-13 08:36:40 +02:00
6 changed files with 238 additions and 116 deletions

View File

@ -1390,6 +1390,10 @@ input:checked + .toggle-bg {
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.me-2 {
margin-inline-end: 0.5rem;
}
.ml-1 { .ml-1 {
margin-left: 0.25rem; margin-left: 0.25rem;
} }
@ -1402,6 +1406,10 @@ input:checked + .toggle-bg {
margin-inline-start: 0px; margin-inline-start: 0px;
} }
.ms-2 {
margin-inline-start: 0.5rem;
}
.ms-2\.5 { .ms-2\.5 {
margin-inline-start: 0.625rem; margin-inline-start: 0.625rem;
} }
@ -1414,6 +1422,14 @@ input:checked + .toggle-bg {
margin-top: 1rem; margin-top: 1rem;
} }
.mb-5 {
margin-bottom: 1.25rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.block { .block {
display: block; display: block;
} }
@ -1826,6 +1842,11 @@ input:checked + .toggle-bg {
border-color: rgb(220 215 254 / var(--tw-border-opacity)); border-color: rgb(220 215 254 / var(--tw-border-opacity));
} }
.bg-blue-100 {
--tw-bg-opacity: 1;
background-color: rgb(225 239 254 / var(--tw-bg-opacity));
}
.bg-blue-700 { .bg-blue-700 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(26 86 219 / var(--tw-bg-opacity)); background-color: rgb(26 86 219 / var(--tw-bg-opacity));
@ -1900,6 +1921,11 @@ input:checked + .toggle-bg {
padding-right: 0.5rem; padding-right: 0.5rem;
} }
.px-2\.5 {
padding-left: 0.625rem;
padding-right: 0.625rem;
}
.px-3 { .px-3 {
padding-left: 0.75rem; padding-left: 0.75rem;
padding-right: 0.75rem; padding-right: 0.75rem;
@ -1920,6 +1946,11 @@ input:checked + .toggle-bg {
padding-right: 1.5rem; padding-right: 1.5rem;
} }
.py-0\.5 {
padding-top: 0.125rem;
padding-bottom: 0.125rem;
}
.py-1 { .py-1 {
padding-top: 0.25rem; padding-top: 0.25rem;
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
@ -1961,6 +1992,10 @@ input:checked + .toggle-bg {
text-align: center; text-align: center;
} }
.text-right {
text-align: right;
}
.align-top { .align-top {
vertical-align: top; vertical-align: top;
} }
@ -2026,6 +2061,10 @@ input:checked + .toggle-bg {
font-weight: 700; font-weight: 700;
} }
.font-extrabold {
font-weight: 800;
}
.font-medium { .font-medium {
font-weight: 500; font-weight: 500;
} }
@ -2050,15 +2089,28 @@ input:checked + .toggle-bg {
line-height: 2.25rem; line-height: 2.25rem;
} }
.leading-none {
line-height: 1;
}
.leading-tight { .leading-tight {
line-height: 1.25; line-height: 1.25;
} }
.tracking-tight {
letter-spacing: -0.025em;
}
.text-blue-600 { .text-blue-600 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(28 100 242 / var(--tw-text-opacity)); color: rgb(28 100 242 / var(--tw-text-opacity));
} }
.text-blue-800 {
--tw-text-opacity: 1;
color: rgb(30 66 159 / var(--tw-text-opacity));
}
.text-gray-300 { .text-gray-300 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity)); color: rgb(209 213 219 / var(--tw-text-opacity));
@ -2718,6 +2770,11 @@ textarea:disabled:is(.dark *) {
border-color: transparent; border-color: transparent;
} }
.dark\:bg-blue-200:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(195 221 253 / var(--tw-bg-opacity));
}
.dark\:bg-blue-600:is(.dark *) { .dark\:bg-blue-600:is(.dark *) {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(28 100 242 / var(--tw-bg-opacity)); background-color: rgb(28 100 242 / var(--tw-bg-opacity));
@ -2761,6 +2818,11 @@ textarea:disabled:is(.dark *) {
color: rgb(63 131 248 / var(--tw-text-opacity)); color: rgb(63 131 248 / var(--tw-text-opacity));
} }
.dark\:text-blue-800:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(30 66 159 / var(--tw-text-opacity));
}
.dark\:text-gray-200:is(.dark *) { .dark\:text-gray-200:is(.dark *) {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(229 231 235 / var(--tw-text-opacity)); color: rgb(229 231 235 / var(--tw-text-opacity));
@ -2876,6 +2938,11 @@ textarea:disabled:is(.dark *) {
--tw-ring-color: rgb(63 131 248 / var(--tw-ring-opacity)); --tw-ring-color: rgb(63 131 248 / var(--tw-ring-opacity));
} }
.dark\:focus\:ring-blue-800:focus:is(.dark *) {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(30 66 159 / var(--tw-ring-opacity));
}
.dark\:focus\:ring-gray-600:focus:is(.dark *) { .dark\:focus\:ring-gray-600:focus:is(.dark *) {
--tw-ring-opacity: 1; --tw-ring-opacity: 1;
--tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity)); --tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity));
@ -2887,10 +2954,6 @@ textarea:disabled:is(.dark *) {
} }
@media (min-width: 640px) { @media (min-width: 640px) {
.sm\:inline {
display: inline;
}
.sm\:table-cell { .sm\:table-cell {
display: table-cell; display: table-cell;
} }
@ -2916,10 +2979,6 @@ textarea:disabled:is(.dark *) {
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;
} }
@ -2928,10 +2987,6 @@ textarea:disabled:is(.dark *) {
padding-left: 1rem; padding-left: 1rem;
} }
.sm\:pl-6 {
padding-left: 1.5rem;
}
.sm\:decoration-2 { .sm\:decoration-2 {
text-decoration-thickness: 2px; text-decoration-thickness: 2px;
} }
@ -3003,6 +3058,11 @@ textarea:disabled:is(.dark *) {
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
.md\:text-5xl {
font-size: 3rem;
line-height: 1;
}
.md\:text-blue-700 { .md\:text-blue-700 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(26 86 219 / var(--tw-text-opacity)); color: rgb(26 86 219 / var(--tw-text-opacity));
@ -3053,6 +3113,11 @@ textarea:disabled:is(.dark *) {
.lg\:max-w-lg { .lg\:max-w-lg {
max-width: 32rem; max-width: 32rem;
} }
.lg\:text-6xl {
font-size: 3.75rem;
line-height: 1;
}
} }
@media (min-width: 1536px) { @media (min-width: 1536px) {
@ -3070,6 +3135,10 @@ textarea:disabled:is(.dark *) {
--tw-space-x-reverse: 1; --tw-space-x-reverse: 1;
} }
.rtl\:text-left:where([dir="rtl"], [dir="rtl"] *) {
text-align: left;
}
.rtl\:text-right:where([dir="rtl"], [dir="rtl"] *) { .rtl\:text-right:where([dir="rtl"], [dir="rtl"] *) {
text-align: right; text-align: right;
} }

View File

@ -7,3 +7,4 @@ components:
simple_table: "components/simple_table.html" simple_table: "components/simple_table.html"
button_group_sm: "components/button_group_sm.html" button_group_sm: "components/button_group_sm.html"
button_group_button_sm: "components/button_group_button_sm.html" button_group_button_sm: "components/button_group_button_sm.html"
h1: "components/h1.html"

View File

@ -0,0 +1,8 @@
<h1 class="{% if badge %}flex items-center {% endif %}mb-4 text-3xl font-extrabold leading-none tracking-tight text-gray-900 dark:text-white">
{{ children }}
{% if badge %}
<span class="bg-blue-100 text-blue-800 text-2xl font-semibold me-2 px-2.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800 ms-2">
{{ badge }}
</span>
{% endif %}
</h1>

View File

@ -1,5 +1,5 @@
<div class="shadow-md sm:rounded-lg" hx-boost="false"> <div class="shadow-md sm:rounded-lg" hx-boost="false">
<div class="relative overflow-x-auto"> <div class="relative overflow-x-auto sm:rounded-lg">
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400"> <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400"> <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr> <tr>
@ -13,7 +13,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
{% if page_obj %} {% if page_obj and elided_page_range %}
<nav class="flex items-center flex-column md:flex-row justify-between px-6 py-4" <nav class="flex items-center flex-column md:flex-row justify-between px-6 py-4"
aria-label="Table navigation"> aria-label="Table navigation">
<span class="text-sm font-normal text-gray-500 dark:text-gray-400 mb-4 md:mb-0 block w-full md:inline md:w-auto">Showing <span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.start_index }}</span><span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.end_index }}</span> of <span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.paginator.count }}</span></span> <span class="text-sm font-normal text-gray-500 dark:text-gray-400 mb-4 md:mb-0 block w-full md:inline md:w-auto">Showing <span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.start_index }}</span><span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.end_index }}</span> of <span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.paginator.count }}</span></span>

View File

@ -90,102 +90,20 @@
</a> </a>
</div> </div>
</div> </div>
<h1 class="text-3xl mt-4 mb-1 font-condensed"> {% #h1 badge=edition_count %}Editions{% /h1 %}
Editions <span class="dark:text-slate-500">({{ edition_count }})</span> and Purchases <span class="dark:text-slate-500">({{ purchase_count }})</span> <div class="mb-6">{% simple_table rows=edition_data.rows columns=edition_data.columns %}</div>
</h1> <div class="mb-6">
<ul> {% #h1 badge=purchase_count %}Purchases{% /h1 %}
{% for edition in editions %} {% simple_table rows=purchase_data.rows columns=purchase_data.columns %}
<li class="sm:pl-2 flex items-center"> </div>
{{ edition.name }} ({{ edition.platform }}, {{ edition.year_released }}) <div class="mb-6">
{% if edition.wikidata %} {% #h1 badge=session_count %}Sessions{% /h1 %}
<span class="hidden sm:inline"> {% simple_table rows=session_data.rows columns=session_data.columns page_obj=session_page_obj elided_page_range=session_elided_page_range %}
<a href="https://www.wikidata.org/wiki/{{ edition.wikidata }}"> </div>
<img class="inline mx-2 w-6" </div>
width="48" <script>
height="48"
alt="Wikidata Icon"
src="{% static 'icons/wikidata.png' %}" />
</a>
</span>
{% endif %}
{% url 'edit_edition' edition.id as edit_url %}
{% include "components/edit_button.html" with edit_url=edit_url %}
</li>
<ul>
{% for purchase in edition.game_purchases %}
<li class="sm:pl-6 flex items-center {% if purchase.date_refunded %}text-red-600{% endif %}">
{{ purchase.get_ownership_type_display }}, {{ purchase.date_purchased | date:"Y" }}
{% if purchase.price != 0 %}({{ purchase.price }} {{ purchase.price_currency }}){% endif %}
{% url 'edit_purchase' purchase.id as edit_url %}
{% include "components/edit_button.html" with edit_url=edit_url %}
</li>
<ul>
{% for related_purchase in purchase.nongame_related_purchases %}
<li class="sm:pl-12 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 }})
{% url 'edit_purchase' related_purchase.id as edit_url %}
{% include "components/edit_button.html" with edit_url=edit_url %}
</li>
{% endfor %}
</ul>
{% endfor %}
</ul>
{% endfor %}
</ul>
<h1 class="text-3xl mt-4 mb-1 flex gap-2 items-center font-condensed">
Sessions
<span class="dark:text-slate-500" id="session-count">({{ session_count }})</span>
{% if latest_session_id %}
{% url 'view_game_start_session_from_session' latest_session_id as add_session_link %}
<a 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"
title="Start new session"
href="{{ add_session_link }}"
hx-get="{{ add_session_link }}"
hx-vals="js:{session_count:getSessionCount()}"
hx-target="#session-list"
hx-swap="afterbegin">New</a>
{% endif %}
and Notes <span class="dark:text-slate-500">({{ sessions_with_notes_count }})</span>
</h1>
<ul id="session-list">
{% for session in sessions %}
{% partialdef session-info inline=True %}
<li class="sm:pl-2 mt-4 mb-2 dark:text-slate-400 flex items-center space-x-1">
{{ session.timestamp_start | date:"d/m/Y H:i" }}
{% if session.timestamp_end %}-{{ session.timestamp_end | date:"H:i" }}{% endif %}
({{ session.device.get_type_display | default:"Unknown" }}, {{ session.duration_formatted }})
{% url 'edit_session' session.id as edit_url %}
{% include "components/edit_button.html" with edit_url=edit_url %}
{% if not session.timestamp_end %}
{% url 'view_game_end_session' session.id as end_session_url %}
<a class="flex bg-green-600 rounded-full px-2 w-7 h-4 text-white justify-center items-center"
href="{{ end_session_url }}"
hx-get="{{ end_session_url }}"
hx-target="closest li"
hx-swap="outerHTML"
hx-vals="js:{session_count:getSessionCount()}"
hx-indicator="#indicator">
<svg xmlns="http://www.w3.org/2000/svg"
fill="#ffffff"
class="h-3"
x="0px"
y="0px"
viewBox="0 0 24 24">
<path d="M 12 2 C 6.486 2 2 6.486 2 12 C 2 17.514 6.486 22 12 22 C 17.514 22 22 17.514 22 12 C 22 10.874 21.803984 9.7942031 21.458984 8.7832031 L 19.839844 10.402344 C 19.944844 10.918344 20 11.453 20 12 C 20 16.411 16.411 20 12 20 C 7.589 20 4 16.411 4 12 C 4 7.589 7.589 4 12 4 C 13.633 4 15.151922 4.4938906 16.419922 5.3378906 L 17.851562 3.90625 C 16.203562 2.71225 14.185 2 12 2 z M 21.292969 3.2929688 L 11 13.585938 L 7.7070312 10.292969 L 6.2929688 11.707031 L 11 16.414062 L 22.707031 4.7070312 L 21.292969 3.2929688 z">
</path>
</svg>
</a>
{% endif %}
</li>
<li class="sm:pl-4 markdown-content">{{ session.note|markdown }}</li>
<div class="hidden" hx-swap-oob="innerHTML:#session-count">({{ session_count }})</div>
{% endpartialdef %}
{% endfor %}
</ul>
</div>
<script>
function getSessionCount() { function getSessionCount() {
return document.getElementById('session-count').textContent.match("[0-9]+"); return document.getElementById('session-count').textContent.match("[0-9]+");
} }
</script> </script>
{% endblock content %} {% endblock content %}

View File

@ -2,16 +2,24 @@ from typing import Any
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Prefetch
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
from common.time import format_duration from common.time import format_duration
from common.utils import safe_division, safe_getattr, truncate_with_popover from common.utils import safe_division, truncate_with_popover
from games.forms import GameForm from games.forms import GameForm
from games.models import Game, Purchase, Session from games.models import Edition, Game, Purchase, Session
from games.views.general import dateformat, use_custom_redirect from games.views.general import (
dateformat,
datetimeformat,
durationformat,
durationformat_manual,
timeformat,
use_custom_redirect,
)
@login_required @login_required
@ -143,6 +151,8 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
.order_by("year_released") .order_by("year_released")
) )
purchases = Purchase.objects.filter(edition__game=game).order_by("date_purchased")
sessions = Session.objects.prefetch_related("device").filter( sessions = Session.objects.prefetch_related("device").filter(
purchase__edition__game=game purchase__edition__game=game
) )
@ -169,7 +179,114 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
total_hours_without_manual = float( total_hours_without_manual = float(
format_duration(sessions.calculated_duration_unformatted(), "%2.1H") format_duration(sessions.calculated_duration_unformatted(), "%2.1H")
) )
context = {
edition_data: dict[str, Any] = {
"columns": [
"Name",
"Platform",
"Year Released",
"Actions",
],
"rows": [
[
edition.name,
edition.platform,
edition.year_released,
render_to_string(
"components/button_group_sm.html",
{
"buttons": [
{
"href": reverse("edit_edition", args=[edition.pk]),
"text": "Edit",
},
{
"href": reverse("delete_edition", args=[edition.pk]),
"text": "Delete",
"color": "red",
},
]
},
),
]
for edition in editions
],
}
purchase_data: dict[str, Any] = {
"columns": ["Name", "Type", "Price", "Actions"],
"rows": [
[
purchase.name if purchase.name else purchase.edition.name,
purchase.get_type_display(),
f"{purchase.price} {purchase.price_currency}",
render_to_string(
"components/button_group_sm.html",
{
"buttons": [
{
"href": reverse("edit_purchase", args=[purchase.pk]),
"text": "Edit",
},
{
"href": reverse("delete_purchase", args=[purchase.pk]),
"text": "Delete",
"color": "red",
},
]
},
),
]
for purchase in purchases
],
}
sessions_all = Session.objects.filter(purchase__edition__game=game).order_by(
"-timestamp_start"
)
session_count = sessions_all.count()
session_paginator = Paginator(sessions_all, 5)
page_number = request.GET.get("page", 1)
session_page_obj = session_paginator.get_page(page_number)
sessions = session_page_obj.object_list
session_data: dict[str, Any] = {
"columns": ["Date", "Duration", "Duration (manual)", "Actions"],
"rows": [
[
f"{session.timestamp_start.strftime(datetimeformat)}{f"{session.timestamp_end.strftime(timeformat)}" if session.timestamp_end else ""}",
(
format_duration(session.duration_calculated, durationformat)
if session.duration_calculated
else "-"
),
(
format_duration(session.duration_manual, durationformat_manual)
if session.duration_manual
else "-"
),
render_to_string(
"components/button_group_sm.html",
{
"buttons": [
{
"href": reverse("edit_session", args=[session.pk]),
"text": "Edit",
},
{
"href": reverse("delete_session", args=[session.pk]),
"text": "Delete",
"color": "red",
},
]
},
),
]
for session in sessions
],
}
context: dict[str, Any] = {
"edition_count": editions.count(), "edition_count": editions.count(),
"editions": editions, "editions": editions,
"game": game, "game": game,
@ -182,11 +299,20 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
1, 1,
), ),
"session_count": session_count, "session_count": session_count,
"sessions_with_notes_count": sessions.exclude(note="").count(), "sessions": sessions,
"sessions": sessions.order_by("-timestamp_start"),
"title": f"Game Overview - {game.name}", "title": f"Game Overview - {game.name}",
"hours_sum": total_hours, "hours_sum": total_hours,
"latest_session_id": safe_getattr(latest_session, "pk"), "edition_data": edition_data,
"purchase_data": purchase_data,
"session_data": session_data,
"session_page_obj": session_page_obj,
"session_elided_page_range": (
session_page_obj.paginator.get_elided_page_range(
page_number, on_each_side=1, on_ends=1
)
if session_page_obj and session_count > 5
else None
),
} }
request.session["return_path"] = request.path request.session["return_path"] = request.path