Add confirmation before deleting game
Django CI/CD / test (push) Successful in 28s
Django CI/CD / build-and-push (push) Successful in 1m1s

This commit is contained in:
2026-05-12 13:37:55 +02:00
parent b8187c32b1
commit a4e697a274
7 changed files with 70 additions and 81 deletions
+8 -75
View File
@@ -1288,75 +1288,6 @@
.box-border { .box-border {
box-sizing: border-box; box-sizing: border-box;
} }
.form-checkbox {
appearance: none;
padding: 0;
print-color-adjust: exact;
display: inline-block;
vertical-align: middle;
background-origin: border-box;
user-select: none;
flex-shrink: 0;
height: 1rem;
width: 1rem;
color: oklch(54.6% 0.245 262.881);
background-color: #fff;
border-color: oklch(55.1% 0.027 264.364);
border-width: 1px;
--tw-shadow: 0 0 #0000;
border-radius: 0px;
&:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 2px;
--tw-ring-offset-color: #fff;
--tw-ring-color: oklch(54.6% 0.245 262.881);
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
&:checked {
border-color: transparent;
background-color: currentColor;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
&:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
@media (forced-colors: active) {
appearance: auto;
}
}
&:checked:hover {
border-color: transparent;
background-color: currentColor;
}
&:checked:focus {
border-color: transparent;
background-color: currentColor;
}
&:indeterminate {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
border-color: transparent;
background-color: currentColor;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
@media (forced-colors: active) {
appearance: auto;
}
}
&:indeterminate:hover {
border-color: transparent;
background-color: currentColor;
}
&:indeterminate:focus {
border-color: transparent;
background-color: currentColor;
}
}
.datatable-pagination-list-item-link { .datatable-pagination-list-item-link {
.datatable-wrapper .datatable-bottom .datatable-pagination & { .datatable-wrapper .datatable-bottom .datatable-pagination & {
display: flex; display: flex;
@@ -2406,9 +2337,6 @@
.text-right { .text-right {
text-align: right; text-align: right;
} }
.align-baseline {
vertical-align: baseline;
}
.align-middle { .align-middle {
vertical-align: middle; vertical-align: middle;
} }
@@ -2599,9 +2527,6 @@
.text-blue-500 { .text-blue-500 {
color: var(--color-blue-500); color: var(--color-blue-500);
} }
.text-blue-600 {
color: var(--color-blue-600);
}
.text-blue-800 { .text-blue-800 {
color: var(--color-blue-800); color: var(--color-blue-800);
} }
@@ -2653,6 +2578,9 @@
.text-red-500 { .text-red-500 {
color: var(--color-red-500); color: var(--color-red-500);
} }
.text-red-600 {
color: var(--color-red-600);
}
.text-red-800 { .text-red-800 {
color: var(--color-red-800); color: var(--color-red-800);
} }
@@ -3615,6 +3543,11 @@
color: var(--color-red-200); color: var(--color-red-200);
} }
} }
.dark\:text-red-400 {
&:is(.dark *) {
color: var(--color-red-400);
}
}
.dark\:text-red-500 { .dark\:text-red-500 {
&:is(.dark *) { &:is(.dark *) {
color: var(--color-red-500); color: var(--color-red-500);
+3 -3
View File
@@ -1,8 +1,8 @@
<c-vars color="blue" size="base" type="button" /> <c-vars color="blue" size="base" type="button" />
<button <button
{% if hx_get %}hx_get="{{ hx_get }}"{% endif %} {% if hx_get %}hx-get="{{ hx_get }}"{% endif %}
{% if hx_target %}hx_target="{{ hx_target }}"{% endif %} {% if hx_target %}hx-target="{{ hx_target }}"{% endif %}
{% if hx_swap %}hx_swap="{{ hx_swap }}"{% endif %} {% if hx_swap %}hx-swap="{{ hx_swap }}"{% endif %}
{% if type %}type="{{ type }}"{% endif %} {% if type %}type="{{ type }}"{% endif %}
{% if title %}title="{{ title }}"{% endif %} {% if title %}title="{{ title }}"{% endif %}
{% if onclick %}onclick="{{ onclick }}"{% endif %} {% if onclick %}onclick="{{ onclick }}"{% endif %}
+3 -2
View File
@@ -46,7 +46,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 pt-8 pb-16">{{ slot }}</div> <div id="main-container" 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>
@@ -106,7 +106,8 @@
} }
}); });
</script> </script>
<div id="global-modal-container"></div> // hx-swap-oob makes sure the modal gets removed upon any HTMX response
<div id="global-modal-container" hx-swap-oob="true"></div>
<div x-data="toastStore()" <div x-data="toastStore()"
role="region" role="region"
@@ -0,0 +1,36 @@
<div id="delete-game-confirmation-modal" class="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="">
<h1 class="text-2xl leading-6 font-medium dark:text-white text-center">Delete Game</h1>
<p class="dark:text-white text-center mt-5">
Are you sure you want to delete <strong>{{ game.name }}</strong>?
</p>
<form class=""
hx-post="{% url 'games:delete_game' game.id %}"
hx-replace-url="true"
hx-target="#main-container"
hx-select="#main-container"
hx-swap="outerHTML"
hw-swap-oob="#global-modal-container"
>
{% csrf_token %}
<p class="dark:text-white text-center mt-3 text-sm text-gray-600 dark:text-gray-400">
This will permanently delete this game and all associated data:
</p>
<ul class="dark:text-white text-center mt-1 text-sm text-gray-600 dark:text-gray-400 list-disc list-inside">
{% if session_count %}<li>{{ session_count }} session(s)</li>{% endif %}
{% if purchase_count %}<li>{{ purchase_count }} purchase(s)</li>{% endif %}
{% if playevent_count %}<li>{{ playevent_count }} play event(s)</li>{% endif %}
{% if not session_count and not purchase_count and not playevent_count %}<li>No associated data</li>{% endif %}
</ul>
<p class="dark:text-white text-center mt-3 text-sm font-medium text-red-600 dark:text-red-400">
This action cannot be undone.
</p>
<div class="items-center mt-5">
<c-button color="red" size="lg" type="submit" class="w-full">Delete</c-button>
<c-button color="gray" size="base" class="mt-0 w-full" onclick="this.closest('#delete-game-confirmation-modal').remove()">Cancel</c-button>
</div>
</form>
</div>
</div>
</div>
+1 -1
View File
@@ -127,7 +127,7 @@
Edit Edit
</button> </button>
</a> </a>
<a href="{% url 'games:delete_game' game.id %}"> <a href="#" hx-get="{% url 'games:delete_game_confirmation' game.id %}" hx-target="#global-modal-container">
<button type="button" <button type="button"
class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-e-lg hover:bg-red-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-red-700 dark:focus:ring-blue-500 dark:focus:text-white hover:cursor-pointer"> class="px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-e-lg hover:bg-red-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-red-700 dark:focus:ring-blue-500 dark:focus:text-white hover:cursor-pointer">
Delete Delete
+1
View File
@@ -23,6 +23,7 @@ urlpatterns = [
path("game/add", game.add_game, name="add_game"), path("game/add", game.add_game, name="add_game"),
path("game/<int:game_id>/edit", game.edit_game, name="edit_game"), path("game/<int:game_id>/edit", game.edit_game, name="edit_game"),
path("game/<int:game_id>/view", game.view_game, name="view_game"), path("game/<int:game_id>/view", game.view_game, name="view_game"),
path("game/<int:game_id>/delete/confirm", game.delete_game_confirmation, name="delete_game_confirmation"),
path("game/<int:game_id>/delete", game.delete_game, name="delete_game"), path("game/<int:game_id>/delete", game.delete_game, name="delete_game"),
path("game/list", game.list_games, name="list_games"), path("game/list", game.list_games, name="list_games"),
path("platform/add", platform.add_platform, name="add_platform"), path("platform/add", platform.add_platform, name="add_platform"),
+18
View File
@@ -165,6 +165,24 @@ def add_game(request: HttpRequest) -> HttpResponse:
return render(request, "add_game.html", context) return render(request, "add_game.html", context)
@login_required
def delete_game_confirmation(request: HttpRequest, game_id: int) -> HttpResponse:
game = get_object_or_404(Game, id=game_id)
session_count = game.sessions.count()
purchase_count = game.purchases.count()
playevent_count = game.playevents.count()
return render(
request,
"partials/delete_game_confirmation.html",
{
"game": game,
"session_count": session_count,
"purchase_count": purchase_count,
"playevent_count": playevent_count,
},
)
@login_required @login_required
def delete_game(request: HttpRequest, game_id: int) -> HttpResponse: def delete_game(request: HttpRequest, game_id: int) -> HttpResponse:
game = get_object_or_404(Game, id=game_id) game = get_object_or_404(Game, id=game_id)