Major redesign #73

Merged
lukas merged 45 commits from manage-purchases into main 2024-09-02 15:47:44 +00:00
14 changed files with 330 additions and 18 deletions
Showing only changes of commit 3de40ccad3 - Show all commits

View File

@ -6,6 +6,7 @@
* Add stats for dropped purchases, monthly playtimes * Add stats for dropped purchases, monthly playtimes
* Allow deleting purchases * Allow deleting purchases
* Add all-time stats * Add all-time stats
* Manage purchases
## Improved ## Improved
* mark refunded purchases red on game overview * mark refunded purchases red on game overview

View File

@ -120,14 +120,6 @@ textarea:disabled {
@apply mx-1; @apply mx-1;
} }
th {
@apply text-right;
}
th label {
@apply mr-4;
}
.basic-button-container { .basic-button-container {
@apply flex space-x-2 justify-center; @apply flex space-x-2 justify-center;
} }
@ -170,4 +162,4 @@ th label {
@apply inline-block truncate max-w-20char transition-all group-hover:absolute group-hover:max-w-none group-hover:-top-8 group-hover:-left-6 group-hover:min-w-60 group-hover:px-6 group-hover:py-3.5 group-hover:bg-purple-600 group-hover:rounded-sm group-hover:outline-dashed group-hover:outline-purple-400 group-hover:outline-4; @apply inline-block truncate max-w-20char transition-all group-hover:absolute group-hover:max-w-none group-hover:-top-8 group-hover:-left-6 group-hover:min-w-60 group-hover:px-6 group-hover:py-3.5 group-hover:bg-purple-600 group-hover:rounded-sm group-hover:outline-dashed group-hover:outline-purple-400 group-hover:outline-4;
} }
} */ } */

83
games/purchaseviews.py Normal file
View File

@ -0,0 +1,83 @@
from typing import Any
from django.contrib.auth.decorators import login_required
from django.db.models.manager import BaseManager
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from django.template.loader import render_to_string
from django.urls import reverse
from games.models import Purchase
from games.views import dateformat
@login_required
def list_purchases(request: HttpRequest) -> HttpResponse:
context: dict[Any, Any] = {}
purchases: BaseManager[Purchase] = Purchase.objects.all()[0:10]
context = {
"title": "Manage purchases",
"data": {
"columns": [
"Name",
"Platform",
"Price",
"Currency",
"Infinite",
"Purchased",
"Refunded",
"Finished",
"Dropped",
"Created",
"Actions",
],
"rows": [
[
purchase.edition.name,
purchase.platform,
purchase.price,
purchase.price_currency,
purchase.infinite,
purchase.date_purchased.strftime(dateformat),
(
purchase.date_refunded.strftime(dateformat)
if purchase.date_refunded
else "-"
),
(
purchase.date_finished.strftime(dateformat)
if purchase.date_finished
else "-"
),
(
purchase.date_dropped.strftime(dateformat)
if purchase.date_dropped
else "-"
),
purchase.created_at.strftime(dateformat),
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
],
},
}
return render(request, "list_purchases.html", context)

View File

@ -1659,6 +1659,10 @@ input:checked + .toggle-bg {
overflow: hidden; overflow: hidden;
} }
.overflow-x-auto {
overflow-x: auto;
}
.truncate { .truncate {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -1717,6 +1721,10 @@ input:checked + .toggle-bg {
border-width: 0px; border-width: 0px;
} }
.border-b {
border-bottom-width: 1px;
}
.border-blue-600 { .border-blue-600 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(28 100 242 / var(--tw-border-opacity)); border-color: rgb(28 100 242 / var(--tw-border-opacity));
@ -1762,6 +1770,11 @@ input:checked + .toggle-bg {
background-color: rgb(229 231 235 / var(--tw-bg-opacity)); background-color: rgb(229 231 235 / var(--tw-bg-opacity));
} }
.bg-gray-50 {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
}
.bg-gray-800 { .bg-gray-800 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity)); background-color: rgb(31 41 55 / var(--tw-bg-opacity));
@ -1822,6 +1835,11 @@ input:checked + .toggle-bg {
padding-right: 1.25rem; padding-right: 1.25rem;
} }
.px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.py-1 { .py-1 {
padding-top: 0.25rem; padding-top: 0.25rem;
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
@ -1842,6 +1860,11 @@ input:checked + .toggle-bg {
padding-bottom: 0.75rem; padding-bottom: 0.75rem;
} }
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.pb-16 { .pb-16 {
padding-bottom: 4rem; padding-bottom: 4rem;
} }
@ -1866,6 +1889,10 @@ input:checked + .toggle-bg {
padding-top: 2rem; padding-top: 2rem;
} }
.text-left {
text-align: left;
}
.text-center { .text-center {
text-align: center; text-align: center;
} }
@ -1943,6 +1970,10 @@ input:checked + .toggle-bg {
font-weight: 600; font-weight: 600;
} }
.uppercase {
text-transform: uppercase;
}
.leading-6 { .leading-6 {
line-height: 1.5rem; line-height: 1.5rem;
} }
@ -2256,13 +2287,13 @@ textarea:disabled:is(.dark *) {
margin-right: 0.25rem; margin-right: 0.25rem;
} }
th { /* th {
text-align: right; @apply text-right;
} } */
th label { /* th label {
margin-right: 1rem; @apply mr-4;
} } */
.basic-button-container { .basic-button-container {
display: flex; display: flex;
@ -2366,11 +2397,26 @@ th label {
} }
} */ } */
.odd\:bg-white:nth-child(odd) {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.even\:bg-gray-50:nth-child(even) {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
}
.hover\:border-gray-300:hover { .hover\:border-gray-300:hover {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity)); border-color: rgb(209 213 219 / var(--tw-border-opacity));
} }
.hover\:border-green-600:hover {
--tw-border-opacity: 1;
border-color: rgb(5 122 85 / var(--tw-border-opacity));
}
.hover\:bg-blue-800:hover { .hover\:bg-blue-800:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(30 66 159 / var(--tw-bg-opacity)); background-color: rgb(30 66 159 / var(--tw-bg-opacity));
@ -2386,6 +2432,16 @@ th label {
background-color: rgb(156 163 175 / var(--tw-bg-opacity)); background-color: rgb(156 163 175 / var(--tw-bg-opacity));
} }
.hover\:bg-gray-50:hover {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
}
.hover\:bg-green-500:hover {
--tw-bg-opacity: 1;
background-color: rgb(14 159 110 / var(--tw-bg-opacity));
}
.hover\:bg-green-700:hover { .hover\:bg-green-700:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(4 108 78 / var(--tw-bg-opacity)); background-color: rgb(4 108 78 / var(--tw-bg-opacity));
@ -2396,6 +2452,11 @@ th label {
background-color: rgb(253 232 232 / var(--tw-bg-opacity)); background-color: rgb(253 232 232 / var(--tw-bg-opacity));
} }
.hover\:bg-red-500:hover {
--tw-bg-opacity: 1;
background-color: rgb(240 82 82 / var(--tw-bg-opacity));
}
.hover\:bg-violet-700:hover { .hover\:bg-violet-700:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(109 40 217 / var(--tw-bg-opacity)); background-color: rgb(109 40 217 / var(--tw-bg-opacity));
@ -2426,6 +2487,11 @@ th label {
color: rgb(17 24 39 / var(--tw-text-opacity)); color: rgb(17 24 39 / var(--tw-text-opacity));
} }
.hover\:text-white:hover {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.hover\:underline:hover { .hover\:underline:hover {
text-decoration-line: underline; text-decoration-line: underline;
} }
@ -2476,6 +2542,11 @@ th label {
--tw-ring-color: rgb(14 159 110 / var(--tw-ring-opacity)); --tw-ring-color: rgb(14 159 110 / var(--tw-ring-opacity));
} }
.focus\:ring-green-700:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(4 108 78 / var(--tw-ring-opacity));
}
.focus\:ring-violet-500:focus { .focus\:ring-violet-500:focus {
--tw-ring-opacity: 1; --tw-ring-opacity: 1;
--tw-ring-color: rgb(139 92 246 / var(--tw-ring-opacity)); --tw-ring-color: rgb(139 92 246 / var(--tw-ring-opacity));
@ -2654,6 +2725,26 @@ th label {
color: rgb(255 255 255 / var(--tw-text-opacity)); color: rgb(255 255 255 / var(--tw-text-opacity));
} }
.odd\:dark\:bg-gray-900:is(.dark *):nth-child(odd) {
--tw-bg-opacity: 1;
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
.even\:dark\:bg-gray-800:is(.dark *):nth-child(even) {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
}
.dark\:hover\:border-green-700:hover:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(4 108 78 / var(--tw-border-opacity));
}
.dark\:hover\:border-red-700:hover:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(200 30 30 / var(--tw-border-opacity));
}
.dark\:hover\:bg-blue-700:hover:is(.dark *) { .dark\:hover\:bg-blue-700:hover:is(.dark *) {
--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));
@ -2674,6 +2765,11 @@ th label {
background-color: rgb(31 41 55 / var(--tw-bg-opacity)); background-color: rgb(31 41 55 / var(--tw-bg-opacity));
} }
.dark\:hover\:bg-green-600:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(5 122 85 / var(--tw-bg-opacity));
}
.dark\:hover\:bg-red-700:hover:is(.dark *) { .dark\:hover\:bg-red-700:hover:is(.dark *) {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(200 30 30 / var(--tw-bg-opacity)); background-color: rgb(200 30 30 / var(--tw-bg-opacity));
@ -2704,6 +2800,11 @@ th label {
--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-green-500:focus:is(.dark *) {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(14 159 110 / var(--tw-ring-opacity));
}
@media (min-width: 640px) { @media (min-width: 640px) {
.sm\:inline { .sm\:inline {
display: inline; display: inline;
@ -2721,6 +2822,10 @@ th label {
max-width: 36rem; max-width: 36rem;
} }
.sm\:rounded-lg {
border-radius: 0.5rem;
}
.sm\:px-4 { .sm\:px-4 {
padding-left: 1rem; padding-left: 1rem;
padding-right: 1rem; padding-right: 1rem;
@ -2792,4 +2897,46 @@ th label {
.rtl\:space-x-reverse:where([dir="rtl"], [dir="rtl"] *) > :not([hidden]) ~ :not([hidden]) { .rtl\:space-x-reverse:where([dir="rtl"], [dir="rtl"] *) > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 1; --tw-space-x-reverse: 1;
} }
.rtl\:text-right:where([dir="rtl"], [dir="rtl"] *) {
text-align: right;
}
.\[\&\:first-of-type_button\]\:rounded-s-lg:first-of-type button {
border-start-start-radius: 0.5rem;
border-end-start-radius: 0.5rem;
}
.\[\&\:last-of-type_button\]\:rounded-e-lg:last-of-type button {
border-start-end-radius: 0.5rem;
border-end-end-radius: 0.5rem;
}
.\[\&_td\:first-of-type\]\:whitespace-nowrap td:first-of-type {
white-space: nowrap;
}
.\[\&_td\:first-of-type\]\:px-6 td:first-of-type {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.\[\&_td\:first-of-type\]\:py-4 td:first-of-type {
padding-top: 1rem;
padding-bottom: 1rem;
}
.\[\&_td\:first-of-type\]\:font-medium td:first-of-type {
font-weight: 500;
}
.\[\&_td\:first-of-type\]\:text-gray-900 td:first-of-type {
--tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity));
}
.\[\&_td\:first-of-type\]\:dark\:text-white:is(.dark *) td:first-of-type {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}

View File

@ -1,3 +1,9 @@
components: components:
gamelink: "components/game_link.html" gamelink: "components/game_link.html"
popover: "components/popover.html" popover: "components/popover.html"
table: "components/table.html"
table_row: "components/table_row.html"
table_td: "components/table_td.html"
simple_table: "components/simple_table.html"
button_group_sm: "components/button_group_sm.html"
button_group_button_sm: "components/button_group_button_sm.html"

View File

@ -0,0 +1,20 @@
{% var color=color|default:"gray" %}
<a href="{{ href }}"
class="[&:first-of-type_button]:rounded-s-lg [&:last-of-type_button]:rounded-e-lg">
{% if color == "gray" %}
<button type="button"
class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white">
{{ text }}
</button>
{% elif color == "red" %}
<button type="button"
class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 hover:bg-red-500 hover:text-white focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:border-red-700 dark:hover:bg-red-700 dark:focus:ring-blue-500 dark:focus:text-white">
{{ text }}
</button>
{% elif color == "green" %}
<button type="button"
class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 hover:bg-green-500 hover:border-green-600 hover:text-white focus:z-10 focus:ring-2 focus:ring-green-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:border-green-700 dark:hover:bg-green-600 dark:focus:ring-green-500 dark:focus:text-white">
{{ text }}
</button>
{% endif %}
</a>

View File

@ -0,0 +1,5 @@
<div class="inline-flex rounded-md shadow-sm" role="group">
{% for button in buttons %}
{% button_group_button_sm href=button.href text=button.text color=button.color %}
{% endfor %}
</div>

View File

@ -0,0 +1,14 @@
<div class="relative overflow-x-auto">
<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">
<tr>
{% for column in columns %}<th scope="col" class="px-6 py-3">{{ column }}</th>{% endfor %}
</tr>
</thead>
<tbody>
{% for row in rows %}
{% table_row data=row %}
{% endfor %}
</tbody>
</table>
</div>

View File

@ -0,0 +1,12 @@
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
<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">
<tr>
{% for column in columns %}<th scope="col" class="px-6 py-3">{{ column }}</th>{% endfor %}
</tr>
</thead>
<tbody>
{{ children }}
</tbody>
</table>
</div>

View File

@ -0,0 +1,15 @@
{% fragment as default_content %}
{% for td in data %}
{% if forloop.first %}
<th scope="row"
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">{{ td }}</th>
{% else %}
{% #table_td %}
{{ td }}
{% /table_td %}
{% endif %}
{% endfor %}
{% endfragment %}
<tr class="[&_td:first-of-type]:px-6 [&_td:first-of-type]:py-4 [&_td:first-of-type]:font-medium [&_td:first-of-type]:text-gray-900 [&_td:first-of-type]:whitespace-nowrap [&_td:first-of-type]:dark:text-white odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 border-b">
{{ children|default:default_content }}
</tr>

View File

@ -0,0 +1 @@
<td class="px-6 py-4">{{ children }}</td>

View File

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% load static %}
{% block title %}
{{ title }}
{% endblock title %}
{% block content %}
{% simple_table columns=data.columns rows=data.rows %}
{% endblock content %}

View File

@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from games import views from games import purchaseviews, views
urlpatterns = [ urlpatterns = [
path("", views.index, name="index"), path("", views.index, name="index"),
@ -25,6 +25,11 @@ urlpatterns = [
views.delete_purchase, views.delete_purchase,
name="delete_purchase", name="delete_purchase",
), ),
path(
"purchase/list",
purchaseviews.list_purchases,
name="list_purchases",
),
path( path(
"purchase/related-purchase-by-edition", "purchase/related-purchase-by-edition",
views.related_purchase_by_edition, views.related_purchase_by_edition,

View File

@ -27,6 +27,9 @@ from .forms import (
) )
from .models import Edition, Game, Platform, Purchase, Session from .models import Edition, Game, Platform, Purchase, Session
dateformat: str = "%d/%m/%Y"
datetimeformat: str = "%d/%m/%Y %H:%M"
def model_counts(request): def model_counts(request):
return { return {