diff --git a/CHANGELOG.md b/CHANGELOG.md index f4596a7..833d238 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * ignore manual sessions when calculating session average * stats: improve purchase name consistency * session list: use display name instead of sort name +* unify the appearance of game links, and make them expand to full size on hover ## Fixed * Fix title not being displayed on the Recent sessions page diff --git a/common/input.css b/common/input.css index b68ac5f..97f3ccd 100644 --- a/common/input.css +++ b/common/input.css @@ -23,18 +23,18 @@ font-style: normal; } -a:hover { +/* 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 { - @apply dark:text-white mx-auto; + @apply dark:text-white mx-auto table-fixed; } .responsive-table tr:nth-child(even) { @@ -58,8 +58,11 @@ form label { .max-w-20char { max-width: 20ch; } + .max-w-30char { + max-width: 30ch; + } .max-w-35char { - max-width: 40ch; + max-width: 35ch; } .max-w-40char { max-width: 40ch; @@ -145,3 +148,13 @@ th label { margin-bottom: 0.5em; padding-left: 1em; } + +.truncate-container { + @apply inline-block relative transition-all; + a { + @apply inline-block truncate max-w-20char; + &:hover { + @apply absolute max-w-none -top-8 -left-6 min-w-60 px-6 py-3.5 bg-purple-600 rounded-sm outline-dashed outline-purple-400 outline-4; + } + } +} \ No newline at end of file diff --git a/games/static/base.css b/games/static/base.css index 72c1cd3..fbbeedf 100644 --- a/games/static/base.css +++ b/games/static/base.css @@ -1,5 +1,5 @@ /* -! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com +! tailwindcss v3.4.4 | MIT License | https://tailwindcss.com */ /* @@ -783,6 +783,18 @@ select { } } +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + .invisible { visibility: hidden; } @@ -819,6 +831,14 @@ select { top: 0.75rem; } +.-left-6 { + left: -1.5rem; +} + +.-top-\[31\.5px\] { + top: -31.5px; +} + .mx-2 { margin-left: 0.5rem; margin-right: 0.5rem; @@ -860,6 +880,10 @@ select { margin-bottom: 2rem; } +.me-2 { + margin-inline-end: 0.5rem; +} + .ml-1 { margin-left: 0.25rem; } @@ -892,6 +916,10 @@ select { display: flex; } +.inline-flex { + display: inline-flex; +} + .table { display: table; } @@ -900,12 +928,12 @@ select { display: none; } -.h-24 { - height: 6rem; +.h-12 { + height: 3rem; } -.h-3 { - height: 0.75rem; +.h-24 { + height: 6rem; } .h-4 { @@ -924,6 +952,10 @@ select { min-height: 100vh; } +.w-24 { + width: 6rem; +} + .w-5 { width: 1.25rem; } @@ -944,6 +976,10 @@ select { width: 100%; } +.min-w-60 { + min-width: 15rem; +} + .max-w-screen-lg { max-width: 1024px; } @@ -1036,6 +1072,11 @@ select { border-color: rgb(100 116 139 / var(--tw-border-opacity)); } +.bg-blue-700 { + --tw-bg-opacity: 1; + background-color: rgb(29 78 216 / var(--tw-bg-opacity)); +} + .bg-gray-200 { --tw-bg-opacity: 1; background-color: rgb(229 231 235 / var(--tw-bg-opacity)); @@ -1051,6 +1092,14 @@ select { background-color: rgb(124 58 237 / var(--tw-bg-opacity)); } +.p-2 { + padding: 0.5rem; +} + +.p-2\.5 { + padding: 0.625rem; +} + .p-4 { padding: 1rem; } @@ -1075,6 +1124,16 @@ select { padding-bottom: 0.5rem; } +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.py-\[13\.5px\] { + padding-top: 13.5px; + padding-bottom: 13.5px; +} + .pb-16 { padding-bottom: 4rem; } @@ -1099,6 +1158,10 @@ select { text-align: center; } +.align-top { + vertical-align: top; +} + .font-mono { font-family: IBM Plex Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } @@ -1128,6 +1191,11 @@ select { line-height: 1.75rem; } +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + .text-xl { font-size: 1.25rem; line-height: 1.75rem; @@ -1138,6 +1206,10 @@ select { line-height: 1rem; } +.font-medium { + font-weight: 500; +} + .font-semibold { font-weight: 600; } @@ -1244,11 +1316,11 @@ select { font-style: normal; } -a:hover { +/* a:hover { text-decoration-color: #ff4400; color: rgb(254, 185, 160); transition: all 0.2s ease-out; -} +} */ form label:is(.dark *) { --tw-text-opacity: 1; @@ -1258,6 +1330,7 @@ form label:is(.dark *) { .responsive-table { margin-left: auto; margin-right: auto; + table-layout: fixed; } .responsive-table:is(.dark *) { @@ -1448,6 +1521,71 @@ th label { padding-left: 1em; } +.truncate-container { + position: relative; + display: inline-block; + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; + a { + display: inline-block; + } + a { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + a { + max-width: 20ch; + } + a { + &:hover { + position: absolute; + } + &:hover { + top: -2rem; + } + &:hover { + left: -1.5rem; + } + &:hover { + min-width: 15rem; + } + &:hover { + max-width: none; + } + &:hover { + border-radius: 0.125rem; + } + &:hover { + --tw-bg-opacity: 1; + background-color: rgb(147 51 234 / var(--tw-bg-opacity)); + } + &:hover { + padding-left: 1.5rem; + padding-right: 1.5rem; + } + &:hover { + padding-top: 0.875rem; + padding-bottom: 0.875rem; + } + &:hover { + outline-style: dashed; + } + &:hover { + outline-width: 4px; + } + &:hover { + outline-color: #c084fc; + } + } +} + +.hover\:bg-blue-800:hover { + --tw-bg-opacity: 1; + background-color: rgb(30 64 175 / var(--tw-bg-opacity)); +} + .hover\:bg-gray-400:hover { --tw-bg-opacity: 1; background-color: rgb(156 163 175 / var(--tw-bg-opacity)); @@ -1478,6 +1616,17 @@ th label { box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); } +.focus\:ring-4:focus { + --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(4px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.focus\:ring-blue-300:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity)); +} + .focus\:ring-green-500:focus { --tw-ring-opacity: 1; --tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity)); @@ -1504,6 +1653,11 @@ th label { display: block; } +.dark\:bg-blue-600:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(37 99 235 / var(--tw-bg-opacity)); +} + .dark\:bg-gray-800:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(31 41 55 / var(--tw-bg-opacity)); @@ -1534,6 +1688,16 @@ th label { color: rgb(255 255 255 / var(--tw-text-opacity)); } +.dark\:hover\:bg-blue-700:hover:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(29 78 216 / var(--tw-bg-opacity)); +} + +.dark\:focus\:ring-blue-800:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(30 64 175 / var(--tw-ring-opacity)); +} + @media (min-width: 640px) { .sm\:inline { display: inline; @@ -1599,10 +1763,6 @@ th label { padding-top: 0.5rem; padding-bottom: 0.5rem; } - - .md\:max-w-40char { - max-width: 40ch; - } } @media (min-width: 1024px) { @@ -1617,4 +1777,4 @@ th label { .lg\:max-w-lg { max-width: 32rem; } -} +} \ No newline at end of file diff --git a/games/templates/components.yml b/games/templates/components.yml new file mode 100644 index 0000000..8a92329 --- /dev/null +++ b/games/templates/components.yml @@ -0,0 +1,2 @@ +components: + gamelink: "components/game_link.html" \ No newline at end of file diff --git a/games/templates/components/game_link.html b/games/templates/components/game_link.html new file mode 100644 index 0000000..ae3fa4a --- /dev/null +++ b/games/templates/components/game_link.html @@ -0,0 +1,9 @@ + + + {% if children %} + {{ children }} + {% else %} + {{ name }} + {% endif %} + + \ No newline at end of file diff --git a/games/templates/list_sessions.html b/games/templates/list_sessions.html index ad9a0c9..5e9533e 100644 --- a/games/templates/list_sessions.html +++ b/games/templates/list_sessions.html @@ -33,11 +33,8 @@ {% for session in dataset %} {% partialdef session-row inline=True %} - - - {{ session.purchase.edition.name }} - + + {% gamelink game_id=session.purchase.edition.game.id name=session.purchase.edition.name %} {{ session.timestamp_start | date:"d/m/Y H:i" }} diff --git a/games/templates/stats.html b/games/templates/stats.html index a766341..45b48b7 100644 --- a/games/templates/stats.html +++ b/games/templates/stats.html @@ -6,9 +6,11 @@ {% partialdef purchase-name %} {% if purchase.type != 'game' %} - {{ purchase.name }} ({{ purchase.edition.name }} {{ purchase.get_type_display }}) + {% #gamelink game_id=purchase.edition.game.id %} + {{ purchase.name }} ({{ purchase.edition.name }} {{ purchase.get_type_display }}) + {% /gamelink %} {% else %} - {{ purchase.edition.name }} + {% gamelink game_id=purchase.edition.game.id name=purchase.edition.name %} {% endif %} {% endpartialdef %} @@ -60,25 +62,25 @@ Longest session - {{ longest_session_time }} ({{ longest_session_game }}) + {{ longest_session_time }} ({% gamelink game_id=longest_session_game.id name=longest_session_game.name %}) Most sessions - {{ highest_session_count }} ({{ highest_session_count_game }}) + {{ highest_session_count }} ({% gamelink game_id=highest_session_count_game.id name=highest_session_count_game.name %}) Highest session average - {{ highest_session_average }} ({{ highest_session_average_game }}) + {{ highest_session_average }} ({% gamelink game_id=highest_session_average_game.id name=highest_session_average_game.name %}) First play - {{ first_play_name }} ({{ first_play_date }}) + {% gamelink game_id=first_play_game.id name=first_play_game.name %} ({{ first_play_date }}) Last play - {{ last_play_name }} ({{ last_play_date }}) + {% gamelink game_id=last_play_game.id name=last_play_game.name %} ({{ last_play_date }}) @@ -142,8 +144,7 @@ {% for game in top_10_games_by_playtime %} - {{ game.name }} + {% gamelink game_id=game.id name=game.name %} {{ game.formatted_playtime }} @@ -179,10 +180,7 @@ {% for purchase in all_finished_this_year %} - - {% partial purchase-name %} - + {% partial purchase-name %} {{ purchase.date_finished | date:"d/m/Y" }} @@ -201,8 +199,7 @@ {% for purchase in this_year_finished_this_year %} - {{ purchase.edition.name }} + {% partial purchase-name %} {{ purchase.date_finished | date:"d/m/Y" }} @@ -221,8 +218,7 @@ {% for purchase in purchased_this_year_finished_this_year %} - {{ purchase.edition.name }} + {% partial purchase-name %} {{ purchase.date_finished | date:"d/m/Y" }} @@ -243,10 +239,7 @@ {% for purchase in purchased_unfinished %} - - {% partial purchase-name %} - + {% partial purchase-name %} {{ purchase.price }} {{ purchase.date_purchased | date:"d/m/Y" }} @@ -268,10 +261,7 @@ {% for purchase in all_purchased_this_year %} - - {% partial purchase-name %} - + {% partial purchase-name %} {{ purchase.price }} {{ purchase.date_purchased | date:"d/m/Y" }} diff --git a/games/views.py b/games/views.py index 5d72290..89d6afa 100644 --- a/games/views.py +++ b/games/views.py @@ -504,10 +504,10 @@ def stats(request, year: int = 0): last_play_date = "N/A" if this_year_sessions: first_session = this_year_sessions.earliest() - first_play_name = first_session.purchase.edition.name + first_play_game = first_session.purchase.edition.game first_play_date = first_session.timestamp_start.strftime("%x") last_session = this_year_sessions.latest() - last_play_name = last_session.purchase.edition.name + last_play_game = last_session.purchase.edition.game last_play_date = last_session.timestamp_start.strftime("%x") all_purchased_this_year_count = this_year_purchases_with_currency.count() @@ -578,7 +578,7 @@ def stats(request, year: int = 0): else 0 ), "longest_session_game": ( - longest_session.purchase.edition.name if longest_session else "N/A" + longest_session.purchase.edition.game if longest_session else None ), "highest_session_count": ( game_highest_session_count.session_count @@ -586,7 +586,7 @@ def stats(request, year: int = 0): else 0 ), "highest_session_count_game": ( - game_highest_session_count.name if game_highest_session_count else "N/A" + game_highest_session_count if game_highest_session_count else None ), "highest_session_average": ( format_duration( @@ -596,9 +596,9 @@ def stats(request, year: int = 0): else 0 ), "highest_session_average_game": highest_session_average_game, - "first_play_name": first_play_name, + "first_play_game": first_play_game, "first_play_date": first_play_date, - "last_play_name": last_play_name, + "last_play_game": last_play_game, "last_play_date": last_play_date, "title": f"{year} Stats", "month_playtimes": month_playtimes, diff --git a/poetry.lock b/poetry.lock index 1b2de5c..ce8d551 100644 --- a/poetry.lock +++ b/poetry.lock @@ -829,6 +829,23 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "slippers" +version = "0.6.2" +description = "Build reusable components in Django without writing a single line of Python." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "slippers-0.6.2-py3-none-any.whl", hash = "sha256:739e05f85354becbf0a65daab831eea62557d89e7512042209ab629af4378bca"}, + {file = "slippers-0.6.2.tar.gz", hash = "sha256:4cb555b8822ba0d404e5405723f5d723994022c29046008ee917081031bc0cf1"}, +] + +[package.dependencies] +Django = ">=3.2" +PyYAML = ">=5.4.0" +typeguard = ">=2.13.3,<3.0.0" +typing-extensions = ">=4.4.0" + [[package]] name = "sqlparse" version = "0.5.0" @@ -875,6 +892,21 @@ notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] +[[package]] +name = "typeguard" +version = "2.13.3" +description = "Run-time type checker for Python" +optional = false +python-versions = ">=3.5.3" +files = [ + {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"}, + {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, +] + +[package.extras] +doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["mypy", "pytest", "typing-extensions"] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -938,4 +970,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "f82f589e57bb158d66df005669003bd51084d5eac795ca66201d8b991d1a8b61" +content-hash = "ca9188453f62ec4470e2b2f8bc1e4a17ad421272f401328f82d6fede231dc737" diff --git a/pyproject.toml b/pyproject.toml index 79bcc10..0f5f6ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ django-template-partials = "^24.2" markdown = "^3.6" +slippers = "^0.6.2" [tool.isort] profile = "black" diff --git a/timetracker/settings.py b/timetracker/settings.py index e11ab3d..5edd33b 100644 --- a/timetracker/settings.py +++ b/timetracker/settings.py @@ -41,6 +41,7 @@ INSTALLED_APPS = [ "template_partials", "graphene_django", "django_htmx", + "slippers", ] GRAPHENE = {"SCHEMA": "games.schema.schema"} @@ -85,7 +86,10 @@ TEMPLATES = [ "games.views.model_counts", "games.views.stats_dropdown_year_range", ], - "builtins": ["template_partials.templatetags.partials"], + "builtins": [ + "template_partials.templatetags.partials", + "slippers.templatetags.slippers", + ], }, }, ]