From 58cfaca1a9861cf9493ede83706f6488c3e9dee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Sun, 8 Sep 2024 21:03:37 +0200 Subject: [PATCH] add table header actions --- common/components.py | 117 +++++++++++++ common/utils.py | 101 +----------- games/static/base.css | 101 +++++++++++- games/templates/cotton/button.html | 5 +- games/templates/cotton/button_group.html | 8 + .../cotton/button_group_button_sm.html | 6 +- games/templates/cotton/button_group_sm.html | 6 - games/templates/cotton/icon/delete.html | 8 + games/templates/cotton/icon/edit.html | 8 + games/templates/cotton/icon/end.html | 8 + games/templates/cotton/icon/finish.html | 8 + games/templates/cotton/icon/play.html | 8 + games/templates/cotton/icon/plus.html | 10 ++ games/templates/cotton/popover.html | 3 +- games/templates/showcase/buttons.html | 155 ++++++++++++++++++ games/templates/view_game.html | 110 ++++++------- games/views/device.py | 8 +- games/views/edition.py | 9 +- games/views/game.py | 75 +++++++-- games/views/platform.py | 8 +- games/views/purchase.py | 21 +-- games/views/session.py | 68 +++++++- poetry.lock | 38 ++--- 23 files changed, 645 insertions(+), 244 deletions(-) create mode 100644 common/components.py create mode 100644 games/templates/cotton/button_group.html delete mode 100644 games/templates/cotton/button_group_sm.html create mode 100644 games/templates/cotton/icon/delete.html create mode 100644 games/templates/cotton/icon/edit.html create mode 100644 games/templates/cotton/icon/end.html create mode 100644 games/templates/cotton/icon/finish.html create mode 100644 games/templates/cotton/icon/play.html create mode 100644 games/templates/cotton/icon/plus.html create mode 100644 games/templates/showcase/buttons.html diff --git a/common/components.py b/common/components.py new file mode 100644 index 0000000..a969397 --- /dev/null +++ b/common/components.py @@ -0,0 +1,117 @@ +from random import choices as random_choices +from string import ascii_lowercase +from typing import Any, Callable + +from django.template.loader import render_to_string +from django.urls import NoReverseMatch, reverse +from django.utils.safestring import mark_safe + +HTMLAttribute = tuple[str, str | int | bool] +HTMLTag = str + + +def Component( + attributes: list[HTMLAttribute] = [], + children: list[HTMLTag] | HTMLTag = [], + template: str = "", + tag_name: str = "", +) -> HTMLTag: + if not tag_name and not template: + raise ValueError("One of template or tag_name is required.") + if isinstance(children, str): + children = [children] + childrenBlob = "\n".join(children) + attributesList = [f'{name} = "{value}"' for name, value in attributes] + attributesBlob = " ".join(attributesList) + tag: str = "" + if tag_name != "": + tag = f"{childrenBlob}" + elif template != "": + tag = render_to_string( + template, + {name: value for name, value in attributes} + | {"slot": mark_safe("\n".join(children))}, + ) + return mark_safe(tag) + + +def randomid(seed: str = "", length: int = 10) -> str: + return seed + "".join(random_choices(ascii_lowercase, k=length)) + + +def Popover( + popover_content: str, + wrapped_content: str = "", + children: list[HTMLTag] = [], + attributes: list[HTMLAttribute] = [], +) -> str: + if not wrapped_content and not children: + raise ValueError("One of wrapped_content or children is required.") + id = randomid() + return Component( + attributes=attributes + + [ + ("id", id), + ("wrapped_content", wrapped_content), + ("popover_content", popover_content), + ], + children=children, + template="cotton/popover.html", + ) + + +def A( + attributes: list[HTMLAttribute] = [], + children: list[HTMLTag] | HTMLTag = [], + url: str | Callable[..., Any] = "", +): + """ + Returns the HTML tag "a". + "url" can either be: + - URL (string) + - path name passed to reverse() (string) + - function + """ + additional_attributes = [] + if url: + if type(url) is str: + try: + url_result = reverse(url) + except NoReverseMatch: + url_result = url + elif callable(url): + url_result = url() + else: + raise TypeError("'url' is neither str nor function.") + additional_attributes = [("href", url_result)] + return Component( + tag_name="a", attributes=attributes + additional_attributes, children=children + ) + + +def Button( + attributes: list[HTMLAttribute] = [], + children: list[HTMLTag] | HTMLTag = [], + size: str = "base", + icon: bool = False, + color: str = "blue", +): + return Component( + template="cotton/button.html", + attributes=attributes + [("size", size), ("icon", icon), ("color", color)], + children=children, + ) + + +def Div( + attributes: list[HTMLAttribute] = [], + children: list[HTMLTag] | HTMLTag = [], +): + return Component(tag_name="div", attributes=attributes, children=children) + + +def Icon( + name: str, + attributes: list[HTMLAttribute] = [], +): + return Component(template=f"cotton/icon/{name}.html", attributes=attributes) diff --git a/common/utils.py b/common/utils.py index 8695ebd..160ab21 100644 --- a/common/utils.py +++ b/common/utils.py @@ -1,98 +1,7 @@ from datetime import date -from random import choices -from string import ascii_lowercase -from typing import Any, Callable, Generator, TypeVar +from typing import Any, Generator, TypeVar -from django.template.loader import render_to_string -from django.urls import NoReverseMatch, reverse -from django.utils.safestring import mark_safe - - -def Popover( - wrapped_content: str, - popover_content: str = "", -) -> str: - id = randomid() - if popover_content == "": - popover_content = wrapped_content - content = f"{wrapped_content}" - result = mark_safe( - str(content) - + render_to_string( - "cotton/popover.html", - { - "id": id, - "slot": popover_content, - }, - ) - ) - return result - - -HTMLAttribute = tuple[str, str] -HTMLTag = str - - -def Component( - attributes: list[HTMLAttribute] = [], - children: list[HTMLTag] | HTMLTag = [], - template: str = "", - tag_name: str = "", -) -> HTMLTag: - if not tag_name and not template: - raise ValueError("One of template or tag_name is required.") - if isinstance(children, str): - children = [children] - childrenBlob = "\n".join(children) - attributesList = [f'{name} = "{value}"' for name, value in attributes] - attributesBlob = " ".join(attributesList) - tag: str = "" - if tag_name != "": - tag = f"{childrenBlob}" - elif template != "": - tag = render_to_string( - template, - {name: value for name, value in attributes} | {"slot": "\n".join(children)}, - ) - return mark_safe(tag) - - -def A( - attributes: list[HTMLAttribute] = [], - children: list[HTMLTag] | HTMLTag = [], - url: str | Callable[..., Any] = "", -): - """ - Returns the HTML tag "a". - "url" can either be: - - URL (string) - - path name passed to reverse() (string) - - function - """ - additional_attributes = [] - if url: - if type(url) is str: - try: - url_result = reverse(url) - except NoReverseMatch: - url_result = url - elif callable(url): - url_result = url() - else: - raise TypeError("'url' is neither str nor function.") - additional_attributes = [("href", url_result)] - return Component( - tag_name="a", attributes=attributes + additional_attributes, children=children - ) - - -def Button( - attributes: list[HTMLAttribute] = [], - children: list[HTMLTag] | HTMLTag = [], -): - return Component( - template="cotton/button.html", attributes=attributes, children=children - ) +from common.components import Popover def safe_division(numerator: int | float, denominator: int | float) -> int | float: @@ -137,17 +46,11 @@ def truncate(input_string: str, length: int = 30, ellipsis: str = "…") -> str: def truncate_with_popover(input_string: str) -> str: if (truncated := truncate(input_string)) != input_string: - print(f"Not the same after: {truncated=}") return Popover(wrapped_content=truncated, popover_content=input_string) else: - print("Strings are the same!") return input_string -def randomid(seed: str = "", length: int = 10) -> str: - return seed + "".join(choices(ascii_lowercase, k=length)) - - T = TypeVar("T", str, int, date) diff --git a/games/static/base.css b/games/static/base.css index 906e1c1..c4ad10b 100644 --- a/games/static/base.css +++ b/games/static/base.css @@ -1346,6 +1346,10 @@ input:checked + .toggle-bg { z-index: 50; } +.m-4 { + margin: 1rem; +} + .mx-2 { margin-left: 0.5rem; margin-right: 0.5rem; @@ -1861,6 +1865,16 @@ input:checked + .toggle-bg { background-color: rgb(5 122 85 / var(--tw-bg-opacity)); } +.bg-green-700 { + --tw-bg-opacity: 1; + background-color: rgb(4 108 78 / var(--tw-bg-opacity)); +} + +.bg-red-700 { + --tw-bg-opacity: 1; + background-color: rgb(200 30 30 / var(--tw-bg-opacity)); +} + .bg-white { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); @@ -1941,6 +1955,11 @@ input:checked + .toggle-bg { padding-bottom: 0.75rem; } +.py-3\.5 { + padding-top: 0.875rem; + padding-bottom: 0.875rem; +} + .py-4 { padding-top: 1rem; padding-bottom: 1rem; @@ -2067,6 +2086,11 @@ input:checked + .toggle-bg { letter-spacing: -0.025em; } +.text-black { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + .text-blue-600 { --tw-text-opacity: 1; color: rgb(28 100 242 / var(--tw-text-opacity)); @@ -2519,24 +2543,24 @@ textarea:disabled:is(.dark *) { 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 { --tw-bg-opacity: 1; background-color: rgb(4 108 78 / var(--tw-bg-opacity)); } +.hover\:bg-green-800:hover { + --tw-bg-opacity: 1; + background-color: rgb(3 84 63 / var(--tw-bg-opacity)); +} + .hover\:bg-red-100:hover { --tw-bg-opacity: 1; background-color: rgb(253 232 232 / var(--tw-bg-opacity)); } -.hover\:bg-red-500:hover { +.hover\:bg-red-800:hover { --tw-bg-opacity: 1; - background-color: rgb(240 82 82 / var(--tw-bg-opacity)); + background-color: rgb(155 28 28 / var(--tw-bg-opacity)); } .hover\:bg-white:hover { @@ -2544,6 +2568,16 @@ textarea:disabled:is(.dark *) { background-color: rgb(255 255 255 / 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-red-500:hover { + --tw-bg-opacity: 1; + background-color: rgb(240 82 82 / var(--tw-bg-opacity)); +} + .hover\:text-blue-600:hover { --tw-text-opacity: 1; color: rgb(28 100 242 / var(--tw-text-opacity)); @@ -2610,16 +2644,31 @@ textarea:disabled:is(.dark *) { --tw-ring-color: rgb(26 86 219 / var(--tw-ring-opacity)); } +.focus\:ring-gray-100:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(243 244 246 / var(--tw-ring-opacity)); +} + .focus\:ring-gray-200:focus { --tw-ring-opacity: 1; --tw-ring-color: rgb(229 231 235 / var(--tw-ring-opacity)); } +.focus\:ring-green-300:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(132 225 188 / var(--tw-ring-opacity)); +} + .focus\:ring-green-500:focus { --tw-ring-opacity: 1; --tw-ring-color: rgb(14 159 110 / var(--tw-ring-opacity)); } +.focus\:ring-red-300:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(248 180 180 / var(--tw-ring-opacity)); +} + .focus\:ring-green-700:focus { --tw-ring-opacity: 1; --tw-ring-color: rgb(4 108 78 / var(--tw-ring-opacity)); @@ -2771,11 +2820,21 @@ textarea:disabled:is(.dark *) { background-color: rgb(17 24 39 / 0.8); } +.dark\:bg-green-600:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(5 122 85 / var(--tw-bg-opacity)); +} + .dark\:bg-purple-800:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(85 33 181 / var(--tw-bg-opacity)); } +.dark\:bg-red-600:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(224 36 36 / var(--tw-bg-opacity)); +} + .dark\:text-blue-500:is(.dark *) { --tw-text-opacity: 1; color: rgb(63 131 248 / var(--tw-text-opacity)); @@ -2861,9 +2920,9 @@ textarea:disabled:is(.dark *) { background-color: rgb(31 41 55 / var(--tw-bg-opacity)); } -.dark\:hover\:bg-green-600:hover:is(.dark *) { +.dark\:hover\:bg-green-700:hover:is(.dark *) { --tw-bg-opacity: 1; - background-color: rgb(5 122 85 / var(--tw-bg-opacity)); + background-color: rgb(4 108 78 / var(--tw-bg-opacity)); } .dark\:hover\:bg-red-700:hover:is(.dark *) { @@ -2871,6 +2930,11 @@ textarea:disabled:is(.dark *) { background-color: rgb(200 30 30 / 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\:text-blue-500:hover:is(.dark *) { --tw-text-opacity: 1; color: rgb(63 131 248 / var(--tw-text-opacity)); @@ -2906,6 +2970,21 @@ textarea:disabled:is(.dark *) { --tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity)); } +.dark\:focus\:ring-gray-700:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(55 65 81 / var(--tw-ring-opacity)); +} + +.dark\:focus\:ring-green-800:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(3 84 63 / var(--tw-ring-opacity)); +} + +.dark\:focus\:ring-red-900:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(119 29 29 / 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)); @@ -3120,3 +3199,7 @@ textarea:disabled:is(.dark *) { .\[\&_a\]\:underline-offset-4 a { text-underline-offset: 4px; } + +.\[\&_h1\]\:mb-2 h1 { + margin-bottom: 0.5rem; +} diff --git a/games/templates/cotton/button.html b/games/templates/cotton/button.html index 3cc1f1f..22131de 100644 --- a/games/templates/cotton/button.html +++ b/games/templates/cotton/button.html @@ -1,5 +1,6 @@ + diff --git a/games/templates/cotton/button_group.html b/games/templates/cotton/button_group.html new file mode 100644 index 0000000..50559bf --- /dev/null +++ b/games/templates/cotton/button_group.html @@ -0,0 +1,8 @@ +
+ {% if slot %}{{ slot }}{% endif %} + {% for button in buttons %} + {% if button.slot %} + + {% endif %} + {% endfor %} +
diff --git a/games/templates/cotton/button_group_button_sm.html b/games/templates/cotton/button_group_button_sm.html index 2ea43a9..7947127 100644 --- a/games/templates/cotton/button_group_button_sm.html +++ b/games/templates/cotton/button_group_button_sm.html @@ -4,17 +4,17 @@ {% if color == "gray" %} {% elif color == "red" %} {% elif color == "green" %} {% endif %} diff --git a/games/templates/cotton/button_group_sm.html b/games/templates/cotton/button_group_sm.html deleted file mode 100644 index 987db77..0000000 --- a/games/templates/cotton/button_group_sm.html +++ /dev/null @@ -1,6 +0,0 @@ - -
- {% for button in buttons %} - - {% endfor %} -
diff --git a/games/templates/cotton/icon/delete.html b/games/templates/cotton/icon/delete.html new file mode 100644 index 0000000..3e5b03f --- /dev/null +++ b/games/templates/cotton/icon/delete.html @@ -0,0 +1,8 @@ + + + + diff --git a/games/templates/cotton/icon/edit.html b/games/templates/cotton/icon/edit.html new file mode 100644 index 0000000..e8eaa25 --- /dev/null +++ b/games/templates/cotton/icon/edit.html @@ -0,0 +1,8 @@ + + + + diff --git a/games/templates/cotton/icon/end.html b/games/templates/cotton/icon/end.html new file mode 100644 index 0000000..971857f --- /dev/null +++ b/games/templates/cotton/icon/end.html @@ -0,0 +1,8 @@ + + + + diff --git a/games/templates/cotton/icon/finish.html b/games/templates/cotton/icon/finish.html new file mode 100644 index 0000000..8a8da77 --- /dev/null +++ b/games/templates/cotton/icon/finish.html @@ -0,0 +1,8 @@ + + + + diff --git a/games/templates/cotton/icon/play.html b/games/templates/cotton/icon/play.html new file mode 100644 index 0000000..7f4bdc9 --- /dev/null +++ b/games/templates/cotton/icon/play.html @@ -0,0 +1,8 @@ + + + + diff --git a/games/templates/cotton/icon/plus.html b/games/templates/cotton/icon/plus.html new file mode 100644 index 0000000..3564e1a --- /dev/null +++ b/games/templates/cotton/icon/plus.html @@ -0,0 +1,10 @@ + + + + diff --git a/games/templates/cotton/popover.html b/games/templates/cotton/popover.html index 38b104c..7806a71 100644 --- a/games/templates/cotton/popover.html +++ b/games/templates/cotton/popover.html @@ -1,7 +1,8 @@ +{{ wrapped_content|default:slot }} diff --git a/games/templates/showcase/buttons.html b/games/templates/showcase/buttons.html new file mode 100644 index 0000000..de1799a --- /dev/null +++ b/games/templates/showcase/buttons.html @@ -0,0 +1,155 @@ +{% extends "base.html" %} +{% load static %} +{% block title %} + {{ title }} +{% endblock title %} +{% block content %} +
+
+

No size

+ + No attributes + + + No attributes, blue + + + No attributes, red + + + No attributes, green + + + No attributes, gray + +
+
+

No size, icons

+ + + + + + + + + + + + + + + +
+
+

Extra Small, icons

+ + Edit + + + + + + + + + + + + + +
+
+

Small, icons

+ + Edit + + + + + + + + + + + + + +
+
+

Base, icons

+ + Edit + + + + + + + + + + + + + +
+
+

Large, icons

+ + Edit + + + + + + + + + + + + + +
+
+

Extra Large, icons

+ + Edit + + + + + + + + + + + + + +
+
+

Group (sm)

+ + + No attributes + + + No attributes, blue + + + No attributes, red + + + No attributes, green + + + No attributes, gray + + +
+
+{% endblock content %} diff --git a/games/templates/view_game.html b/games/templates/view_game.html index c0f647d..05e3242 100644 --- a/games/templates/view_game.html +++ b/games/templates/view_game.html @@ -9,71 +9,57 @@
- {{ game.name }} {{ game.year_released }} - - Original release year + {{ game.name }}  + + {{ game.year_released }}
- - - - - {{ hours_sum }} - - Total hours played - - - - - - - {{ session_count }} - - Number of sessions - - - - - - - {{ session_average_without_manual }} - - Average playtime per session - - - - - - - {{ playrange }} - - Earliest and latest dates played - - + + + + + {{ hours_sum }} + + + + + + {{ session_count }} + + + + + + {{ session_average_without_manual }} + + + + + + {{ playrange }} +
Sessions - +