add table header actions
This commit is contained in:
parent
c1b3493c80
commit
58cfaca1a9
|
@ -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"<a {attributesBlob}>{childrenBlob}</a>"
|
||||
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)
|
101
common/utils.py
101
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"<span data-popover-target={id}>{wrapped_content}</span>"
|
||||
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"<a {attributesBlob}>{childrenBlob}</a>"
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<c-vars color="blue" size="base" />
|
||||
<button type="button"
|
||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 mt-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||
{{ text }}
|
||||
title="{{ title }}"
|
||||
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-none 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 %} ">
|
||||
{{ slot }}
|
||||
</button>
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<div class="inline-flex rounded-md shadow-sm" role="group">
|
||||
{% if slot %}{{ slot }}{% endif %}
|
||||
{% for button in buttons %}
|
||||
{% if button.slot %}
|
||||
<c-button-group-button-sm :href=button.href :slot=button.slot :color=button.color :hover=button.hover :title=button.title />
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
|
@ -4,17 +4,17 @@
|
|||
{% 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 }}
|
||||
{{ slot }}
|
||||
</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 }}
|
||||
{{ slot }}
|
||||
</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 }}
|
||||
{{ slot }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</a>
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
<c-vars color="gray" />
|
||||
<div class="inline-flex rounded-md shadow-sm" role="group">
|
||||
{% for button in buttons %}
|
||||
<c-button-group-button-sm :href=button.href :text=button.text :color=button.color />
|
||||
{% endfor %}
|
||||
</div>
|
|
@ -0,0 +1,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 48 48"
|
||||
class="text-black dark:text-white w-4 h-4">
|
||||
<path fill="currentColor" d="M 24 4 C 20.491685 4 17.570396 6.6214322 17.080078 10 L 10.238281 10 A 1.50015 1.50015 0 0 0 9.9804688 9.9785156 A 1.50015 1.50015 0 0 0 9.7578125 10 L 6.5 10 A 1.50015 1.50015 0 1 0 6.5 13 L 8.6386719 13 L 11.15625 39.029297 C 11.427329 41.835926 13.811782 44 16.630859 44 L 31.367188 44 C 34.186411 44 36.570826 41.836168 36.841797 39.029297 L 39.361328 13 L 41.5 13 A 1.50015 1.50015 0 1 0 41.5 10 L 38.244141 10 A 1.50015 1.50015 0 0 0 37.763672 10 L 30.919922 10 C 30.429604 6.6214322 27.508315 4 24 4 z M 24 7 C 25.879156 7 27.420767 8.2681608 27.861328 10 L 20.138672 10 C 20.579233 8.2681608 22.120844 7 24 7 z M 11.650391 13 L 36.347656 13 L 33.855469 38.740234 C 33.730439 40.035363 32.667963 41 31.367188 41 L 16.630859 41 C 15.331937 41 14.267499 40.033606 14.142578 38.740234 L 11.650391 13 z M 20.476562 17.978516 A 1.50015 1.50015 0 0 0 19 19.5 L 19 34.5 A 1.50015 1.50015 0 1 0 22 34.5 L 22 19.5 A 1.50015 1.50015 0 0 0 20.476562 17.978516 z M 27.476562 17.978516 A 1.50015 1.50015 0 0 0 26 19.5 L 26 34.5 A 1.50015 1.50015 0 1 0 29 34.5 L 29 19.5 A 1.50015 1.50015 0 0 0 27.476562 17.978516 z">
|
||||
</path>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 48 48"
|
||||
class="text-black dark:text-white w-4 h-4">
|
||||
<path fill="currentColor" d="M 40.5 6 C 40.11625 6 39.732453 6.1464531 39.439453 6.4394531 L 21.462891 24.417969 L 20 28 L 23.582031 26.537109 L 41.560547 8.5605469 C 42.145547 7.9745469 42.145547 7.0254531 41.560547 6.4394531 C 41.267547 6.1464531 40.88375 6 40.5 6 z M 12.5 7 C 9.4802259 7 7 9.4802259 7 12.5 L 7 35.5 C 7 38.519774 9.4802259 41 12.5 41 L 35.5 41 C 38.519774 41 41 38.519774 41 35.5 L 41 18.5 A 1.50015 1.50015 0 1 0 38 18.5 L 38 35.5 C 38 36.898226 36.898226 38 35.5 38 L 12.5 38 C 11.101774 38 10 36.898226 10 35.5 L 10 12.5 C 10 11.101774 11.101774 10 12.5 10 L 29.5 10 A 1.50015 1.50015 0 1 0 29.5 7 L 12.5 7 z">
|
||||
</path>
|
||||
</svg>
|
After Width: | Height: | Size: 798 B |
|
@ -0,0 +1,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 48 48"
|
||||
class="text-black dark:text-white w-4 h-4">
|
||||
<path fill="currentColor" d="M 35.5 6 C 33.585045 6 32 7.5850452 32 9.5 L 32 19.365234 L 11.339844 6.6074219 C 9.0734225 5.2081236 6 6.9228749 6 9.5859375 L 6 38.414062 C 6 41.077126 9.0734225 42.791876 11.339844 41.392578 L 32 28.634766 L 32 38.5 C 32 40.414955 33.585045 42 35.5 42 L 38.5 42 C 40.414955 42 42 40.414955 42 38.5 L 42 9.5 C 42 7.5850452 40.414955 6 38.5 6 L 35.5 6 z M 35.5 9 L 38.5 9 C 38.795045 9 39 9.2049548 39 9.5 L 39 38.5 C 39 38.795045 38.795045 39 38.5 39 L 35.5 39 C 35.204955 39 35 38.795045 35 38.5 L 35 9.5 C 35 9.2049548 35.204955 9 35.5 9 z M 9.4765625 9.0566406 C 9.5668015 9.0647233 9.6637771 9.0984812 9.7636719 9.1601562 L 32 22.892578 L 32 25.107422 L 9.7636719 38.839844 C 9.364093 39.086546 9 38.883001 9 38.414062 L 9 9.5859375 C 9 9.3514688 9.091623 9.1841053 9.2324219 9.1054688 C 9.3028213 9.0661502 9.3863235 9.048558 9.4765625 9.0566406 z">
|
||||
</path>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -0,0 +1,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 48 48"
|
||||
class="text-black dark:text-white w-4 h-4">
|
||||
<path fill="currentColor" d="M 7.4765625 4.9785156 A 1.50015 1.50015 0 0 0 6 6.5 L 6 31.5 L 6 43.5 A 1.50015 1.50015 0 1 0 9 43.5 L 9 33 L 40.5 33 C 41.329 33 42 32.328 42 31.5 L 42 6.5 C 42 5.672 41.329 5 40.5 5 L 7.7460938 5 A 1.50015 1.50015 0 0 0 7.4765625 4.9785156 z M 16 8 L 24 8 L 24 15 L 32 15 L 32 8 L 39 8 L 39 15 L 32 15 L 32 23 L 39 23 L 39 30 L 32 30 L 32 23 L 24 23 L 24 30 L 16 30 L 16 23 L 9 23 L 9 15 L 16 15 L 16 8 z M 16 15 L 16 23 L 24 23 L 24 15 L 16 15 z">
|
||||
</path>
|
||||
</svg>
|
After Width: | Height: | Size: 643 B |
|
@ -0,0 +1,8 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 48 48"
|
||||
class="text-black dark:text-white w-4 h-4">
|
||||
<path fill="currentColor" d="M 11.396484 4.1113281 C 9.1042001 4.2020187 7 6.0721788 7 8.5917969 L 7 39.408203 C 7 42.767694 10.742758 44.971891 13.681641 43.34375 L 41.490234 27.935547 C 44.513674 26.260259 44.513674 21.739741 41.490234 20.064453 L 13.681641 4.65625 C 12.94692 4.2492148 12.160579 4.0810979 11.396484 4.1113281 z M 11.431641 7.0664062 C 11.690234 7.0652962 11.961284 7.1323321 12.226562 7.2792969 L 40.037109 22.6875 C 41.13567 23.296212 41.13567 24.703788 40.037109 25.3125 L 12.226562 40.720703 C 11.165446 41.308562 10 40.620712 10 39.408203 L 10 8.5917969 C 10 7.9855423 10.290709 7.5116121 10.714844 7.2617188 C 10.926911 7.136772 11.173048 7.0675163 11.431641 7.0664062 z">
|
||||
</path>
|
||||
</svg>
|
After Width: | Height: | Size: 861 B |
|
@ -0,0 +1,10 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="50"
|
||||
height="50"
|
||||
viewBox="0 0 48 48"
|
||||
class="w-4 h-4">
|
||||
<path fill="currentColor" d="M 23.976562 4.9785156 A 1.50015 1.50015 0 0 0 22.5 6.5 L 22.5 22.5 L 6.5 22.5 A 1.50015 1.50015 0 1 0 6.5 25.5 L 22.5 25.5 L 22.5 41.5 A 1.50015 1.50015 0 1 0 25.5 41.5 L 25.5 25.5 L 41.5 25.5 A 1.50015 1.50015 0 1 0 41.5 22.5 L 25.5 22.5 L 25.5 6.5 A 1.50015 1.50015 0 0 0 23.976562 4.9785156 z">
|
||||
</path>
|
||||
</svg>
|
After Width: | Height: | Size: 496 B |
|
@ -1,7 +1,8 @@
|
|||
<span data-popover-target={{ id }} class="{{ class }}">{{ wrapped_content|default:slot }}</span>
|
||||
<div data-popover
|
||||
id="{{ id }}"
|
||||
role="tooltip"
|
||||
class="absolute z-10 invisible inline-block text-sm text-white transition-opacity duration-300 bg-white border border-purple-200 rounded-lg shadow-sm opacity-0 dark:text-white dark:border-purple-600 dark:bg-purple-800">
|
||||
<div class="px-3 py-2">{{ slot }}</div>
|
||||
<div class="px-3 py-2">{{ popover_content }}</div>
|
||||
<div data-popper-arrow></div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% block title %}
|
||||
{{ title }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
<div class="flex self-center m-4 flex-col gap-4 [&_h1]:mb-2">
|
||||
<div>
|
||||
<h1 class="text-white text-lg">No size</h1>
|
||||
<c-button>
|
||||
No attributes
|
||||
</c-button>
|
||||
<c-button color="blue">
|
||||
No attributes, blue
|
||||
</c-button>
|
||||
<c-button color="red">
|
||||
No attributes, red
|
||||
</c-button>
|
||||
<c-button color="green">
|
||||
No attributes, green
|
||||
</c-button>
|
||||
<c-button color="gray">
|
||||
No attributes, gray
|
||||
</c-button>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-white text-lg">No size, icons</h1>
|
||||
<c-button>
|
||||
<c-icon.edit />
|
||||
</c-button>
|
||||
<c-button>
|
||||
<c-icon.finish />
|
||||
</c-button>
|
||||
<c-button>
|
||||
<c-icon.end />
|
||||
</c-button>
|
||||
<c-button>
|
||||
<c-icon.delete />
|
||||
</c-button>
|
||||
<c-button>
|
||||
<c-icon.play />
|
||||
</c-button>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-white text-lg">Extra Small, icons</h1>
|
||||
<c-button icon="true" size="xs">
|
||||
<c-icon.edit /> Edit
|
||||
</c-button>
|
||||
<c-button icon="true" size="xs">
|
||||
<c-icon.finish />
|
||||
</c-button>
|
||||
<c-button icon="true" size="xs">
|
||||
<c-icon.end />
|
||||
</c-button>
|
||||
<c-button icon="true" size="xs">
|
||||
<c-icon.delete />
|
||||
</c-button>
|
||||
<c-button icon="true" size="xs">
|
||||
<c-icon.play />
|
||||
</c-button>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-white text-lg">Small, icons</h1>
|
||||
<c-button icon="true" size="sm">
|
||||
<c-icon.edit /> Edit
|
||||
</c-button>
|
||||
<c-button icon="true" size="sm">
|
||||
<c-icon.finish />
|
||||
</c-button>
|
||||
<c-button icon="true" size="sm">
|
||||
<c-icon.end />
|
||||
</c-button>
|
||||
<c-button icon="true" size="sm">
|
||||
<c-icon.delete />
|
||||
</c-button>
|
||||
<c-button icon="true" size="sm">
|
||||
<c-icon.play />
|
||||
</c-button>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-white text-lg">Base, icons</h1>
|
||||
<c-button icon="true" size="base">
|
||||
<c-icon.edit /> Edit
|
||||
</c-button>
|
||||
<c-button icon="true" size="base">
|
||||
<c-icon.finish />
|
||||
</c-button>
|
||||
<c-button icon="true" size="base">
|
||||
<c-icon.end />
|
||||
</c-button>
|
||||
<c-button icon="true" size="base">
|
||||
<c-icon.delete />
|
||||
</c-button>
|
||||
<c-button icon="true" size="base">
|
||||
<c-icon.play />
|
||||
</c-button>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-white text-lg">Large, icons</h1>
|
||||
<c-button icon="true" size="lg">
|
||||
<c-icon.edit /> Edit
|
||||
</c-button>
|
||||
<c-button icon="true" size="lg">
|
||||
<c-icon.finish />
|
||||
</c-button>
|
||||
<c-button icon="true" size="lg">
|
||||
<c-icon.end />
|
||||
</c-button>
|
||||
<c-button icon="true" size="lg">
|
||||
<c-icon.delete />
|
||||
</c-button>
|
||||
<c-button icon="true" size="lg">
|
||||
<c-icon.play />
|
||||
</c-button>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-white text-lg">Extra Large, icons</h1>
|
||||
<c-button icon="true" size="xl">
|
||||
<c-icon.edit /> Edit
|
||||
</c-button>
|
||||
<c-button icon="true" size="xl">
|
||||
<c-icon.finish />
|
||||
</c-button>
|
||||
<c-button icon="true" size="xl">
|
||||
<c-icon.end />
|
||||
</c-button>
|
||||
<c-button icon="true" size="xl">
|
||||
<c-icon.delete />
|
||||
</c-button>
|
||||
<c-button icon="true" size="xl">
|
||||
<c-icon.play />
|
||||
</c-button>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-white text-lg">Group (sm)</h1>
|
||||
<c-button-group>
|
||||
<c-button-group-button-sm>
|
||||
No attributes
|
||||
</c-button-group-button-sm>
|
||||
<c-button-group-button-sm color="blue">
|
||||
No attributes, blue
|
||||
</c-button-group-button-sm>
|
||||
<c-button-group-button-sm color="red">
|
||||
No attributes, red
|
||||
</c-button-group-button-sm>
|
||||
<c-button-group-button-sm color="green">
|
||||
No attributes, green
|
||||
</c-button-group-button-sm>
|
||||
<c-button-group-button-sm color="gray">
|
||||
No attributes, gray
|
||||
</c-button-group-button-sm>
|
||||
</c-button-group>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
|
@ -9,71 +9,57 @@
|
|||
<div id="game-info" class="mb-10">
|
||||
<div class="flex gap-5 mb-3">
|
||||
<span class="text-wrap max-w-80 text-4xl">
|
||||
<span class="font-bold font-serif">{{ game.name }}</span> <span data-popover-target="popover-year" class="text-slate-500 text-2xl">{{ game.year_released }}</span>
|
||||
<c-popover id="popover-year">
|
||||
Original release year
|
||||
<span class="font-bold font-serif">{{ game.name }}</span>
|
||||
<c-popover id="popover-year" popover_content="Original release year" class="text-slate-500 text-2xl">
|
||||
{{ game.year_released }}
|
||||
</c-popover>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-4 dark:text-slate-400 mb-3">
|
||||
<span data-popover-target="popover-hours" class="flex gap-2 items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
{{ hours_sum }}
|
||||
<c-popover id="popover-hours">
|
||||
Total hours played
|
||||
</c-popover>
|
||||
</span>
|
||||
<span data-popover-target="popover-sessions"
|
||||
class="flex gap-2 items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5-3.9 19.5m-2.1-19.5-3.9 19.5" />
|
||||
</svg>
|
||||
{{ session_count }}
|
||||
<c-popover id="popover-sessions">
|
||||
Number of sessions
|
||||
</c-popover>
|
||||
</span>
|
||||
<span data-popover-target="popover-average" class="flex gap-2 items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 14.25v2.25m3-4.5v4.5m3-6.75v6.75m3-9v9M6 20.25h12A2.25 2.25 0 0 0 20.25 18V6A2.25 2.25 0 0 0 18 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25Z" />
|
||||
</svg>
|
||||
{{ session_average_without_manual }}
|
||||
<c-popover id="popover-average">
|
||||
Average playtime per session
|
||||
</c-popover>
|
||||
</span>
|
||||
<span data-popover-target="popover-playrange"
|
||||
class="flex gap-2 items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12v-.008ZM12 15h.008v.008H12V15Zm0 2.25h.008v.008H12v-.008ZM9.75 15h.008v.008H9.75V15Zm0 2.25h.008v.008H9.75v-.008ZM7.5 15h.008v.008H7.5V15Zm0 2.25h.008v.008H7.5v-.008Zm6.75-4.5h.008v.008h-.008v-.008Zm0 2.25h.008v.008h-.008V15Zm0 2.25h.008v.008h-.008v-.008Zm2.25-4.5h.008v.008H16.5v-.008Zm0 2.25h.008v.008H16.5V15Z" />
|
||||
</svg>
|
||||
{{ playrange }}
|
||||
<c-popover id="popover-playrange">
|
||||
Earliest and latest dates played
|
||||
</c-popover>
|
||||
</span>
|
||||
<c-popover id="popover-hours" popover_content="Total hours played" class="flex gap-2 items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
{{ hours_sum }}
|
||||
</c-popover>
|
||||
<c-popover id="popover-sessions" popover_content="Number of sessions" class="flex gap-2 items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5-3.9 19.5m-2.1-19.5-3.9 19.5" />
|
||||
</svg>
|
||||
{{ session_count }}
|
||||
</c-popover>
|
||||
<c-popover id="popover-average" popover_content="Average playtime per session" class="flex gap-2 items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 14.25v2.25m3-4.5v4.5m3-6.75v6.75m3-9v9M6 20.25h12A2.25 2.25 0 0 0 20.25 18V6A2.25 2.25 0 0 0 18 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25Z" />
|
||||
</svg>
|
||||
{{ session_average_without_manual }}
|
||||
</c-popover>
|
||||
<c-popover id="popover-playrange" popover_content="Earliest and latest dates played" class="flex gap-2 items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12v-.008ZM12 15h.008v.008H12V15Zm0 2.25h.008v.008H12v-.008ZM9.75 15h.008v.008H9.75V15Zm0 2.25h.008v.008H9.75v-.008ZM7.5 15h.008v.008H7.5V15Zm0 2.25h.008v.008H7.5v-.008Zm6.75-4.5h.008v.008h-.008v-.008Zm0 2.25h.008v.008h-.008V15Zm0 2.25h.008v.008h-.008v-.008Zm2.25-4.5h.008v.008H16.5v-.008Zm0 2.25h.008v.008H16.5V15Z" />
|
||||
</svg>
|
||||
{{ playrange }}
|
||||
</c-popover>
|
||||
</div>
|
||||
<div class="inline-flex rounded-md shadow-sm mb-3" role="group">
|
||||
<a href="{% url 'edit_game' game.id %}">
|
||||
|
@ -100,7 +86,7 @@
|
|||
</div>
|
||||
<div class="mb-6">
|
||||
<c-h1 :badge=session_count>Sessions</c-h1>
|
||||
<c-simple-table :rows=session_data.rows :columns=session_data.columns :page_obj=session_page_obj :elided_page_range=session_elided_page_range />
|
||||
<c-simple-table :rows=session_data.rows :columns=session_data.columns :header_action=session_data.header_action :page_obj=session_page_obj :elided_page_range=session_elided_page_range />
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
|
|
@ -7,8 +7,8 @@ from django.shortcuts import get_object_or_404, redirect, render
|
|||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
|
||||
from common.components import A, Button, Icon
|
||||
from common.time import dateformat, local_strftime
|
||||
from common.utils import A, Button
|
||||
from games.forms import DeviceForm
|
||||
from games.models import Device
|
||||
|
||||
|
@ -49,17 +49,17 @@ def list_devices(request: HttpRequest) -> HttpResponse:
|
|||
device.get_type_display(),
|
||||
local_strftime(device.created_at, dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group_sm.html",
|
||||
"cotton/button_group.html",
|
||||
{
|
||||
"buttons": [
|
||||
{
|
||||
"href": reverse("edit_device", args=[device.pk]),
|
||||
"text": "Edit",
|
||||
"slot": Icon("edit"),
|
||||
"color": "gray",
|
||||
},
|
||||
{
|
||||
"href": reverse("delete_device", args=[device.pk]),
|
||||
"text": "Delete",
|
||||
"slot": Icon("delete"),
|
||||
"color": "red",
|
||||
},
|
||||
]
|
||||
|
|
|
@ -7,8 +7,9 @@ from django.shortcuts import get_object_or_404, redirect, render
|
|||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
|
||||
from common.components import A, Button, Icon
|
||||
from common.time import dateformat, local_strftime
|
||||
from common.utils import A, Button, truncate_with_popover
|
||||
from common.utils import truncate_with_popover
|
||||
from games.forms import EditionForm
|
||||
from games.models import Edition, Game
|
||||
|
||||
|
@ -77,19 +78,19 @@ def list_editions(request: HttpRequest) -> HttpResponse:
|
|||
edition.wikidata,
|
||||
local_strftime(edition.created_at, dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group_sm.html",
|
||||
"cotton/button_group.html",
|
||||
{
|
||||
"buttons": [
|
||||
{
|
||||
"href": reverse("edit_edition", args=[edition.pk]),
|
||||
"text": "Edit",
|
||||
"slot": Icon("edit"),
|
||||
"color": "gray",
|
||||
},
|
||||
{
|
||||
"href": reverse(
|
||||
"delete_edition", args=[edition.pk]
|
||||
),
|
||||
"text": "Delete",
|
||||
"slot": Icon("delete"),
|
||||
"color": "red",
|
||||
},
|
||||
]
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.shortcuts import get_object_or_404, redirect, render
|
|||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
|
||||
from common.components import A, Button, Div, Icon, Popover
|
||||
from common.time import (
|
||||
dateformat,
|
||||
durationformat,
|
||||
|
@ -16,7 +17,7 @@ from common.time import (
|
|||
local_strftime,
|
||||
timeformat,
|
||||
)
|
||||
from common.utils import A, Button, safe_division, truncate_with_popover
|
||||
from common.utils import safe_division, truncate, truncate_with_popover
|
||||
from games.forms import GameForm
|
||||
from games.models import Edition, Game, Purchase, Session
|
||||
from games.views.general import use_custom_redirect
|
||||
|
@ -77,17 +78,17 @@ def list_games(request: HttpRequest) -> HttpResponse:
|
|||
game.wikidata,
|
||||
local_strftime(game.created_at, dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group_sm.html",
|
||||
"cotton/button_group.html",
|
||||
{
|
||||
"buttons": [
|
||||
{
|
||||
"href": reverse("edit_game", args=[game.pk]),
|
||||
"text": "Edit",
|
||||
"slot": Icon("edit"),
|
||||
"color": "gray",
|
||||
},
|
||||
{
|
||||
"href": reverse("delete_game", args=[game.pk]),
|
||||
"text": "Delete",
|
||||
"slot": Icon("delete"),
|
||||
"color": "red",
|
||||
},
|
||||
]
|
||||
|
@ -206,17 +207,17 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||
edition.platform,
|
||||
edition.year_released,
|
||||
render_to_string(
|
||||
"cotton/button_group_sm.html",
|
||||
"cotton/button_group.html",
|
||||
{
|
||||
"buttons": [
|
||||
{
|
||||
"href": reverse("edit_edition", args=[edition.pk]),
|
||||
"text": "Edit",
|
||||
"slot": Icon("edit"),
|
||||
"color": "gray",
|
||||
},
|
||||
{
|
||||
"href": reverse("delete_edition", args=[edition.pk]),
|
||||
"text": "Delete",
|
||||
"slot": Icon("delete"),
|
||||
"color": "red",
|
||||
},
|
||||
]
|
||||
|
@ -235,17 +236,17 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||
purchase.get_type_display(),
|
||||
f"{purchase.price} {purchase.price_currency}",
|
||||
render_to_string(
|
||||
"cotton/button_group_sm.html",
|
||||
"cotton/button_group.html",
|
||||
{
|
||||
"buttons": [
|
||||
{
|
||||
"href": reverse("edit_purchase", args=[purchase.pk]),
|
||||
"text": "Edit",
|
||||
"slot": Icon("edit"),
|
||||
"color": "gray",
|
||||
},
|
||||
{
|
||||
"href": reverse("delete_purchase", args=[purchase.pk]),
|
||||
"text": "Delete",
|
||||
"slot": Icon("delete"),
|
||||
"color": "red",
|
||||
},
|
||||
]
|
||||
|
@ -259,6 +260,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||
sessions_all = Session.objects.filter(purchase__edition__game=game).order_by(
|
||||
"-timestamp_start"
|
||||
)
|
||||
last_session = sessions_all.latest()
|
||||
session_count = sessions_all.count()
|
||||
session_paginator = Paginator(sessions_all, 5)
|
||||
page_number = request.GET.get("page", 1)
|
||||
|
@ -266,6 +268,38 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||
sessions = session_page_obj.object_list
|
||||
|
||||
session_data: dict[str, Any] = {
|
||||
"header_action": Div(
|
||||
children=[
|
||||
A(
|
||||
url="add_session",
|
||||
children=Button(
|
||||
icon=True,
|
||||
size="xs",
|
||||
children=[Icon("play"), "LOG"],
|
||||
),
|
||||
),
|
||||
A(
|
||||
url=reverse(
|
||||
"list_sessions_start_session_from_session",
|
||||
args=[last_session.pk],
|
||||
),
|
||||
children=Popover(
|
||||
popover_content=last_session.purchase.edition.name,
|
||||
children=[
|
||||
Button(
|
||||
icon=True,
|
||||
color="gray",
|
||||
size="xs",
|
||||
children=[
|
||||
Icon("play"),
|
||||
truncate(f"{last_session.purchase.edition.name}"),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
"columns": ["Date", "Duration", "Actions"],
|
||||
"rows": [
|
||||
[
|
||||
|
@ -276,17 +310,32 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||
else f"{format_duration(session.duration_manual, durationformat_manual)}*"
|
||||
),
|
||||
render_to_string(
|
||||
"cotton/button_group_sm.html",
|
||||
"cotton/button_group.html",
|
||||
{
|
||||
"buttons": [
|
||||
{
|
||||
"href": reverse(
|
||||
"list_sessions_end_session", args=[session.pk]
|
||||
),
|
||||
"slot": Icon("end"),
|
||||
"title": "Finish session now",
|
||||
"color": "green",
|
||||
"hover": "green",
|
||||
}
|
||||
if session.timestamp_end is None
|
||||
# this only works without leaving an empty
|
||||
# a element and wrong rounding of button edges
|
||||
# because we check if button.href is not None
|
||||
# in the button group component
|
||||
else {},
|
||||
{
|
||||
"href": reverse("edit_session", args=[session.pk]),
|
||||
"text": "Edit",
|
||||
"slot": Icon("edit"),
|
||||
"color": "gray",
|
||||
},
|
||||
{
|
||||
"href": reverse("delete_session", args=[session.pk]),
|
||||
"text": "Delete",
|
||||
"slot": Icon("delete"),
|
||||
"color": "red",
|
||||
},
|
||||
]
|
||||
|
|
|
@ -7,8 +7,8 @@ from django.shortcuts import get_object_or_404, redirect, render
|
|||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
|
||||
from common.components import A, Button, Icon
|
||||
from common.time import dateformat, local_strftime
|
||||
from common.utils import A, Button
|
||||
from games.forms import PlatformForm
|
||||
from games.models import Platform
|
||||
from games.views.general import use_custom_redirect
|
||||
|
@ -50,21 +50,21 @@ def list_platforms(request: HttpRequest) -> HttpResponse:
|
|||
platform.group,
|
||||
local_strftime(platform.created_at, dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group_sm.html",
|
||||
"cotton/button_group.html",
|
||||
{
|
||||
"buttons": [
|
||||
{
|
||||
"href": reverse(
|
||||
"edit_platform", args=[platform.pk]
|
||||
),
|
||||
"text": "Edit",
|
||||
"slot": Icon("edit"),
|
||||
"color": "gray",
|
||||
},
|
||||
{
|
||||
"href": reverse(
|
||||
"delete_platform", args=[platform.pk]
|
||||
),
|
||||
"text": "Delete",
|
||||
"slot": Icon("delete"),
|
||||
"color": "red",
|
||||
},
|
||||
]
|
||||
|
|
|
@ -13,8 +13,9 @@ from django.template.loader import render_to_string
|
|||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from common.time import dateformat, local_strftime
|
||||
from common.utils import A, Button, truncate_with_popover
|
||||
from common.components import A, Button, Icon
|
||||
from common.time import dateformat
|
||||
from common.utils import truncate_with_popover
|
||||
from games.forms import PurchaseForm
|
||||
from games.models import Edition, Purchase
|
||||
from games.views.general import use_custom_redirect
|
||||
|
@ -81,39 +82,39 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
|
|||
purchase.price,
|
||||
purchase.price_currency,
|
||||
purchase.infinite,
|
||||
local_strftime(purchase.date_purchased, dateformat),
|
||||
purchase.date_purchased.strftime(dateformat),
|
||||
(
|
||||
local_strftime(purchase.date_refunded, dateformat)
|
||||
purchase.date_refunded.strftime(dateformat)
|
||||
if purchase.date_refunded
|
||||
else "-"
|
||||
),
|
||||
(
|
||||
local_strftime(purchase.date_finished, dateformat)
|
||||
purchase.date_finished.strftime(dateformat)
|
||||
if purchase.date_finished
|
||||
else "-"
|
||||
),
|
||||
(
|
||||
local_strftime(purchase.date_dropped, dateformat)
|
||||
purchase.date_dropped.strftime(dateformat)
|
||||
if purchase.date_dropped
|
||||
else "-"
|
||||
),
|
||||
local_strftime(purchase.created_at, dateformat),
|
||||
purchase.created_at.strftime(dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group_sm.html",
|
||||
"cotton/button_group.html",
|
||||
{
|
||||
"buttons": [
|
||||
{
|
||||
"href": reverse(
|
||||
"edit_purchase", args=[purchase.pk]
|
||||
),
|
||||
"text": "Edit",
|
||||
"slot": Icon("edit"),
|
||||
"color": "gray",
|
||||
},
|
||||
{
|
||||
"href": reverse(
|
||||
"delete_purchase", args=[purchase.pk]
|
||||
),
|
||||
"text": "Delete",
|
||||
"slot": Icon("delete"),
|
||||
"color": "red",
|
||||
},
|
||||
]
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.template.loader import render_to_string
|
|||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from common.components import A, Button, Div, Icon, Popover
|
||||
from common.time import (
|
||||
dateformat,
|
||||
durationformat,
|
||||
|
@ -16,7 +17,7 @@ from common.time import (
|
|||
local_strftime,
|
||||
timeformat,
|
||||
)
|
||||
from common.utils import A, Button, truncate_with_popover
|
||||
from common.utils import truncate, truncate_with_popover
|
||||
from games.forms import SessionForm
|
||||
from games.models import Purchase, Session
|
||||
from games.views.general import use_custom_redirect
|
||||
|
@ -28,6 +29,7 @@ def list_sessions(request: HttpRequest) -> HttpResponse:
|
|||
page_number = request.GET.get("page", 1)
|
||||
limit = request.GET.get("limit", 10)
|
||||
sessions = Session.objects.order_by("-timestamp_start")
|
||||
last_session = sessions.latest()
|
||||
page_obj = None
|
||||
if int(limit) != 0:
|
||||
paginator = Paginator(sessions, limit)
|
||||
|
@ -45,7 +47,40 @@ def list_sessions(request: HttpRequest) -> HttpResponse:
|
|||
else None
|
||||
),
|
||||
"data": {
|
||||
"header_action": A([], Button([], "Add session"), url="add_session"),
|
||||
"header_action": Div(
|
||||
children=[
|
||||
A(
|
||||
url="add_session",
|
||||
children=Button(
|
||||
icon=True,
|
||||
size="xs",
|
||||
children=[Icon("play"), "LOG"],
|
||||
),
|
||||
),
|
||||
A(
|
||||
url=reverse(
|
||||
"list_sessions_start_session_from_session",
|
||||
args=[last_session.pk],
|
||||
),
|
||||
children=Popover(
|
||||
popover_content=last_session.purchase.edition.name,
|
||||
children=[
|
||||
Button(
|
||||
icon=True,
|
||||
color="gray",
|
||||
size="xs",
|
||||
children=[
|
||||
Icon("play"),
|
||||
truncate(
|
||||
f"{last_session.purchase.edition.name}"
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
"columns": [
|
||||
"Name",
|
||||
"Date",
|
||||
|
@ -72,20 +107,39 @@ def list_sessions(request: HttpRequest) -> HttpResponse:
|
|||
session.device,
|
||||
session.created_at.strftime(dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group_sm.html",
|
||||
"cotton/button_group.html",
|
||||
{
|
||||
"buttons": [
|
||||
{
|
||||
"href": reverse(
|
||||
"list_sessions_end_session", args=[session.pk]
|
||||
),
|
||||
"slot": Icon("end"),
|
||||
"title": "Finish session now",
|
||||
"color": "green",
|
||||
"hover": "green",
|
||||
}
|
||||
if session.timestamp_end is None
|
||||
# this only works without leaving an empty
|
||||
# a element and wrong rounding of button edges
|
||||
# because we check if button.href is not None
|
||||
# in the button group component
|
||||
else {},
|
||||
{
|
||||
"href": reverse("edit_session", args=[session.pk]),
|
||||
"text": "Edit",
|
||||
"color": "gray",
|
||||
"slot": Icon("edit"),
|
||||
"title": "Edit",
|
||||
# "color": "gray",
|
||||
"hover": "green",
|
||||
},
|
||||
{
|
||||
"href": reverse(
|
||||
"delete_session", args=[session.pk]
|
||||
),
|
||||
"text": "Delete",
|
||||
"slot": Icon("delete"),
|
||||
"title": "Delete",
|
||||
"color": "red",
|
||||
"hover": "red",
|
||||
},
|
||||
]
|
||||
},
|
||||
|
@ -155,7 +209,6 @@ def clone_session_by_id(session_id: int) -> Session:
|
|||
|
||||
|
||||
@login_required
|
||||
@use_custom_redirect
|
||||
def new_session_from_existing_session(
|
||||
request: HttpRequest, session_id: int, template: str = ""
|
||||
) -> HttpResponse:
|
||||
|
@ -170,7 +223,6 @@ def new_session_from_existing_session(
|
|||
|
||||
|
||||
@login_required
|
||||
@use_custom_redirect
|
||||
def end_session(
|
||||
request: HttpRequest, session_id: int, template: str = ""
|
||||
) -> HttpResponse:
|
||||
|
|
|
@ -133,13 +133,13 @@ bcrypt = ["bcrypt"]
|
|||
|
||||
[[package]]
|
||||
name = "django-cotton"
|
||||
version = "0.9.35"
|
||||
version = "0.9.37"
|
||||
description = "Bringing component based design to Django templates."
|
||||
optional = false
|
||||
python-versions = "<4,>=3.8"
|
||||
files = [
|
||||
{file = "django_cotton-0.9.35-py3-none-any.whl", hash = "sha256:1cbfbbfe58218c7d213607bd1be31b72aa10b318aa26cc11a0e0193c6adc0071"},
|
||||
{file = "django_cotton-0.9.35.tar.gz", hash = "sha256:ad8f9e9fd259b2acb9796ee201f5e8088b86382ba13691f43ce94bbb5e4089b5"},
|
||||
{file = "django_cotton-0.9.37-py3-none-any.whl", hash = "sha256:b917102bee417eb749df25eae75a985db0749fb8ac9711ab57fb5f78de03d8e8"},
|
||||
{file = "django_cotton-0.9.37.tar.gz", hash = "sha256:0ccfc76d6edfc8dfaf0bb5d960c6695fff78cbcc1a3c52e263a0484b8dbbb9a5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -256,19 +256,19 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.15.4"
|
||||
version = "3.16.0"
|
||||
description = "A platform independent file lock."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"},
|
||||
{file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"},
|
||||
{file = "filelock-3.16.0-py3-none-any.whl", hash = "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609"},
|
||||
{file = "filelock-3.16.0.tar.gz", hash = "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
|
||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"]
|
||||
typing = ["typing-extensions (>=4.8)"]
|
||||
docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
|
||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.1.1)", "pytest (>=8.3.2)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.3)"]
|
||||
typing = ["typing-extensions (>=4.12.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "graphene"
|
||||
|
@ -316,13 +316,13 @@ test = ["coveralls", "django-filter (>=22.1)", "djangorestframework (>=3.6.3)",
|
|||
|
||||
[[package]]
|
||||
name = "graphql-core"
|
||||
version = "3.2.3"
|
||||
version = "3.2.4"
|
||||
description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL."
|
||||
optional = false
|
||||
python-versions = ">=3.6,<4"
|
||||
python-versions = "<4,>=3.6"
|
||||
files = [
|
||||
{file = "graphql-core-3.2.3.tar.gz", hash = "sha256:06d2aad0ac723e35b1cb47885d3e5c45e956a53bc1b209a9fc5369007fe46676"},
|
||||
{file = "graphql_core-3.2.3-py3-none-any.whl", hash = "sha256:5766780452bd5ec8ba133f8bf287dc92713e3868ddd83aee4faab9fc3e303dc3"},
|
||||
{file = "graphql-core-3.2.4.tar.gz", hash = "sha256:acbe2e800980d0e39b4685dd058c2f4042660b89ebca38af83020fd872ff1264"},
|
||||
{file = "graphql_core-3.2.4-py3-none-any.whl", hash = "sha256:1604f2042edc5f3114f49cac9d77e25863be51b23a54a61a23245cf32f6476f0"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -564,13 +564,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.2.2"
|
||||
version = "4.3.1"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
|
||||
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
|
||||
{file = "platformdirs-4.3.1-py3-none-any.whl", hash = "sha256:facaa5a3c57aa1e053e3da7b49e0cc31fe0113ca42a4659d5c2e98e545624afe"},
|
||||
{file = "platformdirs-4.3.1.tar.gz", hash = "sha256:63b79589009fa8159973601dd4563143396b35c5f93a58b36f9049ff046949b1"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
@ -907,13 +907,13 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
|
|||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.26.3"
|
||||
version = "20.26.4"
|
||||
description = "Virtual Python Environment builder"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"},
|
||||
{file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"},
|
||||
{file = "virtualenv-20.26.4-py3-none-any.whl", hash = "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55"},
|
||||
{file = "virtualenv-20.26.4.tar.gz", hash = "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
Loading…
Reference in New Issue