Housekeeping

* Updated flowbite to 4.x
* Start revamping styles
* Remove unused GraphQL code
* Make some templates more robuts
This commit is contained in:
2026-02-17 22:14:16 +01:00
parent 277ecd1b55
commit 996c0107c9
19 changed files with 2191 additions and 481 deletions
+37 -57
View File
@@ -4,7 +4,8 @@
@plugin '@tailwindcss/forms'; @plugin '@tailwindcss/forms';
@plugin 'flowbite/plugin'; @plugin 'flowbite/plugin';
@source '../node_modules/flowbite/**/*.js'; @source "../node_modules/flowbite";
@import "flowbite/src/themes/default";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@@ -25,6 +26,7 @@
--color-background: #1f2937; --color-background: #1f2937;
} }
/* /*
The default border color has changed to `currentcolor` in Tailwind CSS v4, The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still so we've added these compatibility styles to make sure everything still
@@ -103,16 +105,6 @@
font-style: normal; font-style: normal;
} }
/* a:hover {
text-decoration-color: #ff4400;
color: rgb(254, 185, 160);
transition: all 0.2s ease-out;
} */
/* form label {
@apply dark:text-slate-400;
} */
.responsive-table { .responsive-table {
@apply dark:text-white mx-auto table-fixed; @apply dark:text-white mx-auto table-fixed;
} }
@@ -135,38 +127,17 @@
} }
} }
/* form input,
select,
textarea {
@apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
} */
form input:disabled, form input:disabled,
select:disabled, select:disabled,
textarea:disabled { textarea:disabled {
@apply dark:bg-slate-800 dark:text-slate-500 cursor-not-allowed; @apply cursor-not-allowed bg-neutral-secondary-strong text-fg-disabled;
} }
.errorlist { .errorlist {
@apply mt-4 mb-1 pl-3 py-2 bg-red-600 text-slate-200 w-[300px]; @apply mt-4 mb-1 pl-3 py-2 bg-red-600 text-slate-200 w-[300px];
} }
/* @media screen and (min-width: 768px) {
form input,
select,
textarea {
width: 300px;
}
} */
/* @media screen and (max-width: 768px) {
form input,
select,
textarea {
width: 150px;
}
} */
#button-container button { #button-container button {
@apply mx-1; @apply mx-1;
} }
@@ -207,34 +178,43 @@ textarea:disabled {
padding-left: 1em; padding-left: 1em;
} }
/* .truncate-container { #add-form {
@apply inline-block relative; .form-row-button-group {
a { display: flex;
@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; flex-direction: row;
@apply gap-0 p-0;
button {
@apply mr-0;
&:first-child {
@apply rounded-e-none;
}
&:nth-child(2) {
@apply rounded-none;
}
&:last-child {
@apply rounded-s-none;
}
}
} }
} */
label { label {
@apply dark:text-slate-500; @apply mb-2.5 text-sm font-medium text-heading;
} }
input:not([type="checkbox"]) {
[type="text"], [type="password"], [type="datetime-local"], [type="datetime"], [type="date"], [type="number"], select, textarea { @apply mb-3 bg-neutral-secondary-medium border border-default-medium text-heading text-sm rounded-base focus:ring-brand focus:border-brand block w-full px-3 py-2.5 shadow-xs placeholder:text-body;
@apply dark:bg-slate-600 dark:text-slate-300;
} }
input[type="checkbox"] {
[type="submit"] { @apply w-4 h-4 border border-default-medium rounded-xs bg-neutral-secondary-medium focus:ring-2 focus:ring-brand-soft;
@apply dark:text-white font-bold dark:bg-blue-600 px-4 py-2;
} }
select {
form div label { @apply w-full px-3 py-2.5 bg-neutral-secondary-medium border border-default-medium text-heading text-sm rounded-base focus:ring-brand focus:border-brand shadow-xs placeholder:text-body;
@apply dark:text-white;
} }
textarea {
form div { @apply bg-neutral-secondary-medium border border-default-medium text-heading text-sm rounded-base focus:ring-brand focus:border-brand block w-full p-3.5 shadow-xs placeholder:text-body;
@apply flex flex-col; }
:has(> label + input[type="checkbox"]) {
@apply mb-3 mt-6;
display: flex;
flex-direction: row;
justify-content: space-between;
} }
div [type="submit"] {
@apply mt-3;
} }
+6 -5
View File
@@ -1,7 +1,8 @@
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from games.models import Device as DeviceModel from games.models import Device as DeviceModel
from games.models import Edition as EditionModel
# from games.models import Edition as EditionModel
from games.models import Game as GameModel from games.models import Game as GameModel
from games.models import Platform as PlatformModel from games.models import Platform as PlatformModel
from games.models import Purchase as PurchaseModel from games.models import Purchase as PurchaseModel
@@ -14,10 +15,10 @@ class Game(DjangoObjectType):
fields = "__all__" fields = "__all__"
class Edition(DjangoObjectType): # class Edition(DjangoObjectType):
class Meta: # class Meta:
model = EditionModel # model = EditionModel
fields = "__all__" # fields = "__all__"
class Purchase(DjangoObjectType): class Purchase(DjangoObjectType):
+4
View File
@@ -232,6 +232,10 @@ class Purchase(models.Model):
or self.price_currency != purchase_to_compare.price_currency or self.price_currency != purchase_to_compare.price_currency
) )
def refund(self):
self.date_refunded = timezone.now()
self.save()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.type != Purchase.GAME and not self.related_purchase: if self.type != Purchase.GAME and not self.related_purchase:
raise ValidationError( raise ValidationError(
-2
View File
@@ -3,7 +3,6 @@ import graphene
from games.graphql.mutations import GameMutation from games.graphql.mutations import GameMutation
from games.graphql.queries import ( from games.graphql.queries import (
DeviceQuery, DeviceQuery,
EditionQuery,
GameQuery, GameQuery,
PlatformQuery, PlatformQuery,
PurchaseQuery, PurchaseQuery,
@@ -13,7 +12,6 @@ from games.graphql.queries import (
class Query( class Query(
GameQuery, GameQuery,
EditionQuery,
DeviceQuery, DeviceQuery,
PlatformQuery, PlatformQuery,
PurchaseQuery, PurchaseQuery,
+2032 -356
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -1,7 +1,9 @@
<c-layouts.add> <c-layouts.add>
<c-slot name="additional_row"> <c-slot name="additional_row">
<input type="submit" <c-button type="submit" color="gray"
name="submit_and_redirect" name="submit_and_redirect"
value="Submit & Create Purchase" /> >
Submit & Create Purchase
</c-button>
</c-slot> </c-slot>
</c-layouts.add> </c-layouts.add>
+4 -2
View File
@@ -3,9 +3,11 @@
<tr> <tr>
<td></td> <td></td>
<td> <td>
<input type="submit" <c-button type="submit"
color="gray"
name="submit_and_redirect" name="submit_and_redirect"
value="Submit & Create Session" /> value="Submit & Create Session">
</c-button>
</td> </td>
</tr> </tr>
</c-slot> </c-slot>
+25 -23
View File
@@ -1,36 +1,38 @@
<c-layouts.add> <c-layouts.add>
<c-slot name="form_content"> <c-slot name="form_content">
<form method="post" enctype="multipart/form-data"> <div class="max-width-container">
<table class="mx-auto"> <div id="add-form" class="form-container max-w-xl mx-auto">
<form method="post" enctype="multipart/form-data" class="">
{% csrf_token %} {% csrf_token %}
{% for field in form %} {% for field in form %}
<tr> <div>
<th>{{ field.label_tag }}</th> {{ field.label_tag }}
{% if field.name == "note" %} {% if field.name == "note" %}
<td>{{ field }}</td> {{ field }}
{% else %} {% else %}
<td>{{ field }}</td> {{ field }}
{% endif %} {% endif %}
{% if field.name == "timestamp_start" or field.name == "timestamp_end" %} {% if field.name == "timestamp_start" or field.name == "timestamp_end" %}
<td> <span class="form-row-button-group flex-row gap-3 justify-start mt-3" hx-boost="false">
<div class="basic-button-container" hx-boost="false"> <c-button data-target="{{ field.name }}" data-type="now" size="xs">Set to now</c-button>
<button class="basic-button" data-target="{{ field.name }}" data-type="now">Set to now</button> <c-button data-target="{{ field.name }}" data-type="toggle" size="xs">Toggle text</c-button>
<button class="basic-button" <c-button data-target="{{ field.name }}" data-type="copy" size="xs">
data-target="{{ field.name }}" Copy {%if field.name == "timestamp_start" %}start{% else %}end{% endif %} value to {%if field.name == "timestamp_start" %}end{% else %}start{% endif %}
data-type="toggle">Toggle text</button> </c-button>
<button class="basic-button" data-target="{{ field.name }}" data-type="copy">Copy</button> </span>
</div>
</td>
{% endif %} {% endif %}
</tr> </div>
{% endfor %} {% endfor %}
<tr> <div>
<td></td> <c-button type="submit">
<td> Submit
<input type="submit" value="Submit" /> </c-button>
</td> </div>
</tr> <div class="submit-button-container">
</table> {{ additional_row }}
</div>
</form> </form>
</div>
</div>
</c-slot> </c-slot>
</c-layouts.add> </c-layouts.add>
+7 -2
View File
@@ -1,6 +1,11 @@
<c-vars color="blue" size="base" type="button" /> <c-vars color="blue" size="base" type="button" />
<button type="{{ type }}" <button
hx_get="{{ hx_get }}"
hx_target="{{ hx_target }}"
hx_swap="{{ hx_swap }}"
type="{{ type }}"
title="{{ title }}" title="{{ title }}"
class="{{ class }} {% if color == "blue" %} bg-blue-700 dark:bg-blue-600 dark:focus:ring-blue-800 dark:hover:bg-blue-700 focus:ring-blue-300 hover:bg-blue-800 text-white {% elif color == "red" %} bg-red-700 dark:bg-red-600 dark:focus:ring-red-900 dark:hover:bg-red-700 focus:ring-red-300 hover:bg-red-800 text-white {% elif color == "gray" %} bg-white border-gray-200 dark:bg-gray-800 dark:border-gray-600 dark:focus:ring-gray-700 dark:hover:bg-gray-700 dark:hover:text-white dark:text-gray-400 focus:ring-gray-100 hover:bg-gray-100 hover:text-blue-700 text-gray-900 border {% elif color == "green" %} bg-green-700 dark:bg-green-600 dark:focus:ring-green-800 dark:hover:bg-green-700 focus:ring-green-300 hover:bg-green-800 text-white {% endif %} focus:outline-hidden focus:ring-4 font-medium mb-2 me-2 rounded-lg {% if size == "xs" %} px-3 py-2 text-xs {% elif size == "sm" %} px-3 py-2 text-sm {% elif size == "base" %} px-5 py-2.5 text-sm {% elif size == "lg" %} px-5 py-3 text-base {% elif size == "xl" %} px-6 py-3.5 text-base {% endif %} {% if icon %} inline-flex text-center items-center gap-2 {% else %} {% endif %} "> onclick="{{ onclick }}"
class="{% if class %}{{ class }} {%else%}{%endif%}{% if color == "blue" %}text-white bg-brand box-border border border-transparent hover:bg-brand-strong focus:ring-4 focus:ring-brand-medium {% elif color == "red" %} bg-red-700 dark:bg-red-600 dark:focus:ring-red-900 dark:hover:bg-red-700 focus:ring-red-300 hover:bg-red-800 text-white {% elif color == "gray" %} bg-white border-gray-200 dark:bg-gray-800 dark:border-gray-600 dark:focus:ring-gray-700 dark:hover:bg-gray-700 dark:hover:text-white dark:text-gray-400 focus:ring-gray-100 hover:bg-gray-100 hover:text-blue-700 text-gray-900 border {% elif color == "green" %} bg-green-700 dark:bg-green-600 dark:focus:ring-green-800 dark:hover:bg-green-700 focus:ring-green-300 hover:bg-green-800 text-white {% endif %} leading-5 focus:outline-hidden focus:ring-4 font-medium mb-2 me-2 rounded-base {% if size == "xs" %} px-3 py-2 text-xs shadow-xs {% elif size == "sm" %} px-3 py-2 text-sm {% elif size == "base" %} px-5 py-2.5 text-sm {% elif size == "lg" %} px-5 py-3 text-base {% elif size == "xl" %} px-6 py-3.5 text-base {% endif %} {% if icon %} inline-flex text-center items-center gap-2 {% else %} {% endif %} ">
{{ slot }} {{ slot }}
</button> </button>
+1 -1
View File
@@ -2,7 +2,7 @@
{% if slot %}{{ slot }}{% endif %} {% if slot %}{{ slot }}{% endif %}
{% for button in buttons %} {% for button in buttons %}
{% if button.slot %} {% if button.slot %}
<c-button-group-button-sm :href=button.href :slot=button.slot :color=button.color :hover=button.hover :title=button.title /> <c-button-group-button-sm :href=button.href :slot=button.slot :color=button.color :hover=button.hover :title=button.title :hx_get=button.hx_get :hx_target=button.hx_target :hx_swap=button.hx_swap />
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
@@ -1,5 +1,7 @@
<c-vars color="gray" /> <c-vars color="gray" />
<a href="{{ href }}" <a href="{{ href }}"
{% if hx_get %}hx-get="{{ hx_get }}"{% endif %}
{% if hx_target %}hx-target="{{ hx_target }}"{% endif %}
class="[&:first-of-type_button]:rounded-s-lg [&:last-of-type_button]:rounded-e-lg"> class="[&:first-of-type_button]:rounded-s-lg [&:last-of-type_button]:rounded-e-lg">
{% if color == "gray" %} {% if color == "gray" %}
<button type="button" <button type="button"
+6 -2
View File
@@ -4,11 +4,15 @@
{{ form_content }} {{ form_content }}
{% else %} {% else %}
<div class="max-width-container"> <div class="max-width-container">
<div class="form-container max-w-xl mx-auto"> <div id="add-form" class="form-container max-w-xl mx-auto">
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{{ form.as_div }} {{ form.as_div }}
<div><input type="submit" value="Submit" /></div> <div>
<c-button type="submit" class="mt-3">
Submit
</c-button>
</div>
<div class="submit-button-container"> <div class="submit-button-container">
{{ additional_row }} {{ additional_row }}
</div> </div>
+3 -2
View File
@@ -25,7 +25,7 @@
} }
</script> </script>
</head> </head>
<body hx-indicator="#indicator"> <body hx-indicator="#indicator" class="bg-neutral-primary">
<img id="indicator" <img id="indicator"
src="{% static 'icons/loading.png' %}" src="{% static 'icons/loading.png' %}"
class="absolute right-3 top-3 animate-spin htmx-indicator" class="absolute right-3 top-3 animate-spin htmx-indicator"
@@ -34,7 +34,7 @@
alt="loading indicator" /> alt="loading indicator" />
<div class="flex flex-col min-h-screen"> <div class="flex flex-col min-h-screen">
{% include "navbar.html" %} {% include "navbar.html" %}
<div class="flex flex-1 flex-col dark:bg-gray-800 pt-8">{{ slot }}</div> <div class="flex flex-1 flex-col pt-8 pb-16">{{ slot }}</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>
</div> </div>
@@ -94,5 +94,6 @@
} }
}); });
</script> </script>
<div id="global-modal-container"></div>
</body> </body>
</html> </html>
+17
View File
@@ -0,0 +1,17 @@
<c-vars without_buttons="false" submit_text="Submit" close_text="Cancel" />
<div id="modal-container">
<div class="tt-modal fixed inset-0 bg-black/70 dark:bg-gray-600/50 overflow-y-auto h-full w-full flex items-center justify-center">
<div class="relative mx-auto p-5 border-accent border w-full max-w-md shadow-lg/50 rounded-md bg-white dark:bg-gray-900">
<div class="{{ container_class }}">
{{ slot }}
{% if not without_buttons %}
<div class="items-center mt-5">
<c-button color="blue" size="lg" type="submit" class="w-full">{{ submit_text }}</c-button>
<c-button color="gray" size="base" class="mt-0 w-full" onclick="this.closest('.tt-modal').remove()">{{ close_text }}</c-button>
</div>
{% endif %}
</form>
</div>
</div>
</div>
</div>
+14 -1
View File
@@ -1,5 +1,5 @@
<c-vars :name="id" /> <c-vars :name="id" />
<div class="pb-4 bg-white dark:bg-gray-900"> <!-- <div class="pb-4 bg-white dark:bg-gray-900">
<label for="table-search" class="sr-only">Search</label> <label for="table-search" class="sr-only">Search</label>
<div class="relative mt-1"> <div class="relative mt-1">
<div class="absolute inset-y-3 rtl:inset-r-0 start-0 flex items-center ps-3 pointer-events-none"> <div class="absolute inset-y-3 rtl:inset-r-0 start-0 flex items-center ps-3 pointer-events-none">
@@ -9,4 +9,17 @@
</div> </div>
<input type="text" id="{{ id }}" name="{{ name }}" value="{{ search_string }}" class="block pt-2 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg w-80 bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="{% if placeholder %}{{ placeholder }}{% else %}Search{% endif %}"> <input type="text" id="{{ id }}" name="{{ name }}" value="{{ search_string }}" class="block pt-2 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg w-80 bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="{% if placeholder %}{{ placeholder }}{% else %}Search{% endif %}">
</div> </div>
</div> -->
<form class="max-w-md mx-auto">
<label for="search" class="block mb-2.5 text-sm font-medium text-heading sr-only ">Search</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-body" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="m21 21-3.5-3.5M17 10a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z"/></svg>
</div> </div>
<input type="search" id="{{ id }}" name="{{ name }}" value="{{ search_string }}" class="block w-full p-3 ps-9 bg-neutral-secondary-medium border border-default-medium text-heading text-sm rounded-base focus:ring-brand focus:border-brand shadow-xs placeholder:text-body" placeholder="{% if placeholder %}{{ placeholder }}{% else %}Search{% endif %}" required />
<button type="button" class="absolute end-1.5 bottom-1.5 text-white bg-brand hover:bg-brand-strong box-border border border-transparent focus:ring-4 focus:ring-brand-medium shadow-xs font-medium leading-5 rounded text-xs px-3 py-1.5 focus:outline-none">Search</button>
</div>
</form>
+1 -1
View File
@@ -1,5 +1,5 @@
{% load static %} {% load static %}
<nav class="bg-white border-gray-200 dark:bg-gray-900 dark:border-gray-700"> <nav class="bg-neutral-primary-soft border-b border-default">
<div class="max-w-(--breakpoint-xl) flex flex-wrap items-center justify-between mx-auto p-4"> <div class="max-w-(--breakpoint-xl) flex flex-wrap items-center justify-between mx-auto p-4">
<a href="{% url 'index' %}" <a href="{% url 'index' %}"
class="flex items-center space-x-3 rtl:space-x-reverse"> class="flex items-center space-x-3 rtl:space-x-reverse">
+9 -6
View File
@@ -5,13 +5,13 @@ from django.core.paginator import Paginator
from django.http import ( from django.http import (
HttpRequest, HttpRequest,
HttpResponse, HttpResponse,
HttpResponseBadRequest,
HttpResponseRedirect, 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 django.utils import timezone from django.utils import timezone
from django.views.decorators.http import require_POST
from common.components import A, Button, Icon, LinkedPurchase, PurchasePrice from common.components import A, Button, Icon, LinkedPurchase, PurchasePrice
from common.time import dateformat from common.time import dateformat
@@ -202,11 +202,10 @@ def finish_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
def related_purchase_by_game(request: HttpRequest) -> HttpResponse: def related_purchase_by_game(request: HttpRequest) -> HttpResponse:
games: list[str] = []
games = request.GET.getlist("games") games = request.GET.getlist("games")
if not games: context = {}
return HttpResponseBadRequest("Invalid game_id") if games:
if isinstance(games, int) or isinstance(games, str):
games = [games]
form = PurchaseForm() form = PurchaseForm()
qs = Purchase.objects.filter(games__in=games, type=Purchase.GAME).order_by( qs = Purchase.objects.filter(games__in=games, type=Purchase.GAME).order_by(
"games__sort_name" "games__sort_name"
@@ -216,4 +215,8 @@ def related_purchase_by_game(request: HttpRequest) -> HttpResponse:
first_option = qs.first() first_option = qs.first()
if first_option: if first_option:
form.fields["related_purchase"].initial = first_option.id form.fields["related_purchase"].initial = first_option.id
return render(request, "partials/related_purchase_field.html", {"form": form}) context["form"] = form
return render(request, "partials/related_purchase_field.html", context)
else:
# abort swap
return HttpResponse(status=204)
+2 -2
View File
@@ -208,9 +208,9 @@ def add_session(request: HttpRequest, game_id: int = 0) -> HttpResponse:
context["title"] = "Add New Session" context["title"] = "Add New Session"
# TODO: re-add custom buttons #91 # TODO: re-add custom buttons #91
# context["script_name"] = "add_session.js" context["script_name"] = "add_session.js"
context["form"] = form context["form"] = form
return render(request, "add.html", context) return render(request, "add_session.html", context)
@login_required @login_required
+1 -1
View File
@@ -8,6 +8,6 @@
}, },
"dependencies": { "dependencies": {
"@tailwindcss/cli": "^4.1.18", "@tailwindcss/cli": "^4.1.18",
"flowbite": "^2.4.1" "flowbite": "^4.0.1"
} }
} }