Improve light/dark theme toggle

This commit is contained in:
2026-01-16 20:25:50 +01:00
parent eb6b6bccef
commit 1ba204fbdc
14 changed files with 190 additions and 65 deletions
+8
View File
@@ -19,6 +19,8 @@ RUN apt-get update && apt-get upgrade -y \
&& apt-get install --no-install-recommends -y \
bash \
curl \
nodejs \
npm \
&& curl -sSL 'https://install.python-poetry.org' | python - \
&& poetry --version \
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
@@ -33,6 +35,12 @@ RUN chown -R timetracker:timetracker /home/timetracker/app
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
USER timetracker
# Install Node.js dependencies and build Svelte app
RUN npm install
RUN npm run build
RUN --mount=type=cache,target="$POETRY_CACHE_DIR" \
echo "$PROD" \
&& poetry version \
+17 -3
View File
@@ -4,6 +4,7 @@ initialize: npm css migrate sethookdir loadplatforms
HTMLFILES := $(shell find games/templates -type f)
PYTHON_VERSION = 3.12
.PHONY: build frontend-dev frontend-build
npm:
npm install
@@ -24,13 +25,26 @@ init:
poetry install
npm install
# Run Django, Tailwind, and Vite development servers concurrently
dev:
@npx concurrently \
--names "Django,Tailwind" \
--prefix-colors "blue,green" \
--names "Django,Tailwind,Vite" \
--prefix-colors "blue,green,yellow" \
"poetry run python -Wa manage.py runserver" \
"npx tailwindcss -i ./common/input.css -o ./games/static/base.css --watch"
"npx tailwindcss -i ./common/input.css -o ./games/static/base.css --watch" \
"npm run dev"
# Only start the Vite development server
frontend-dev:
npm run dev
# Build frontend assets for production
build: frontend-build collectstatic
# Only build frontend assets
frontend-build:
@echo "Building frontend assets with Vite..."
npm run build
caddy:
caddy run --watch
+2 -2
View File
@@ -53,11 +53,11 @@
}
.responsive-table tr:nth-child(even) {
@apply bg-slate-800
@apply bg-indigo-100 dark:bg-slate-800
}
.responsive-table tbody tr:nth-child(odd) {
@apply bg-slate-900
@apply bg-indigo-200 dark:bg-slate-900
}
.responsive-table thead th {
+6
View File
@@ -0,0 +1,6 @@
<script>
export let mastered;
</script>
{#if mastered}
👑
{/if}
+13
View File
@@ -0,0 +1,13 @@
import CrownIcon from './components/CrownIcon.svelte';
// Expose a function to mount the CrownIcon component globally
// This allows Django templates to easily initialize Svelte components.
window.mountCrownIcon = (selector, props) => {
const target = document.querySelector(selector);
if (target) {
new CrownIcon({
target: target,
props: props,
});
}
};
+20 -5
View File
@@ -2268,6 +2268,11 @@ input:checked + .toggle-bg {
color: rgb(107 114 128 / var(--tw-text-opacity, 1));
}
.text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
}
.text-gray-700 {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity, 1));
@@ -2288,11 +2293,6 @@ input:checked + .toggle-bg {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.text-slate-400 {
--tw-text-opacity: 1;
color: rgb(148 163 184 / var(--tw-text-opacity, 1));
}
.text-slate-500 {
--tw-text-opacity: 1;
color: rgb(100 116 139 / var(--tw-text-opacity, 1));
@@ -2491,11 +2491,21 @@ input:checked + .toggle-bg {
}
.responsive-table tr:nth-child(even) {
--tw-bg-opacity: 1;
background-color: rgb(229 237 255 / var(--tw-bg-opacity, 1));
}
.responsive-table tr:nth-child(even):is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(30 41 59 / var(--tw-bg-opacity, 1));
}
.responsive-table tbody tr:nth-child(odd) {
--tw-bg-opacity: 1;
background-color: rgb(205 219 254 / var(--tw-bg-opacity, 1));
}
.responsive-table tbody tr:nth-child(odd):is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(15 23 42 / var(--tw-bg-opacity, 1));
}
@@ -3081,6 +3091,11 @@ div [type="submit"] {
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
}
.dark\:text-slate-300:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
.dark\:text-slate-400:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(148 163 184 / var(--tw-text-opacity, 1));
@@ -0,0 +1,3 @@
<svg class="dark:text-white w-3" viewBox="5 8 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 9L12 15L18 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 284 B

+1 -1
View File
@@ -2,7 +2,7 @@
x="0px"
y="0px"
viewBox="0 0 48 48"
class="text-black dark:text-white w-4 h-4">
class="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>

Before

Width:  |  Height:  |  Size: 861 B

After

Width:  |  Height:  |  Size: 834 B

+29 -16
View File
@@ -39,21 +39,33 @@
<span class="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span>
</div>
{{ scripts }}
<script>
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
// Change the icons inside the button based on previous settings
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
themeToggleLightIcon.classList.remove('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
<script type="module">
document.addEventListener('DOMContentLoaded', () => {
if (window.mountCrownIcon) {
window.mountCrownIcon('#crown-icon-mount-point', {
mastered: {{ game.mastered|yesno:"true,false" }}
});
}
var themeToggleBtn = document.getElementById('theme-toggle');
// Theme toggle logic
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
const themeToggleBtn = document.getElementById('theme-toggle');
// Ensure all elements are found before proceeding
if (themeToggleDarkIcon && themeToggleLightIcon && themeToggleBtn) {
// Initial state of icons based on current theme
// The FOUC script in <head> already set document.documentElement.classList.add/remove('dark')
// So we just need to set the icon visibility based on that.
if (document.documentElement.classList.contains('dark')) {
themeToggleLightIcon.classList.remove('hidden');
themeToggleDarkIcon.classList.add('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
themeToggleLightIcon.classList.add('hidden');
}
themeToggleBtn.addEventListener('click', function () {
// toggle icons inside button
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
@@ -63,22 +75,23 @@
if (localStorage.getItem('color-theme') === 'light') {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
} else {
} else { // current theme is dark, switch to light
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
}
// if NOT set via local storage previously
} else {
if (document.documentElement.classList.contains('dark')) {
} else { // no theme in local storage, use system preference
if (document.documentElement.classList.contains('dark')) { // currently dark, switch to light
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
} else { // currently light, switch to dark
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
}
});
}
});
</script>
</body>
+13 -3
View File
@@ -26,9 +26,19 @@
</button>
<div class="hidden w-full md:block md:w-auto" id="navbar-dropdown">
<ul class="items-center flex flex-col font-medium p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
<li class="text-white flex flex-col items-center text-xs">
<span class="flex uppercase gap-1">Today<span class="text-gray-400">·</span>Last 7 days</span>
<span class="flex items-center gap-1">{{ today_played }}<span class="text-gray-400">·</span>{{ last_7_played }}</span>
<li class="flex items-center">
<button id="theme-toggle" type="button" class="p-2 text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm">
<svg id="theme-toggle-dark-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M3.32031 11.6835C3.32031 16.6541 7.34975 20.6835 12.3203 20.6835C16.1075 20.6835 19.3483 18.3443 20.6768 15.032C19.6402 15.4486 18.5059 15.6834 17.3203 15.6834C12.3497 15.6834 8.32031 11.654 8.32031 6.68342C8.32031 5.50338 8.55165 4.36259 8.96453 3.32996C5.65605 4.66028 3.32031 7.89912 3.32031 11.6835Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg id="theme-toggle-light-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 3V4M12 20V21M4 12H3M6.31412 6.31412L5.5 5.5M17.6859 6.31412L18.5 5.5M6.31412 17.69L5.5 18.5001M17.6859 17.69L18.5 18.5001M21 12H20M16 12C16 14.2091 14.2091 16 12 16C9.79086 16 8 14.2091 8 12C8 9.79086 9.79086 8 12 8C14.2091 8 16 9.79086 16 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</li>
<li class="dark:text-white flex flex-col items-center text-xs">
<span class="flex uppercase gap-1">Today<span class="dark:text-gray-400">·</span>Last 7 days</span>
<span class="flex items-center gap-1">{{ today_played }}<span class="dark:text-gray-400">·</span>{{ last_7_played }}</span>
</li>
<li>
<a href="#"
@@ -23,16 +23,14 @@
}"
>
<div class="inline-flex rounded-md shadow-xs" role="group" @click.outside="open = false">
<button type="button" @click="open = !open" class="relative px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg 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 align-middle">
<button type="button" @click="open = !open" class="relative px-4 py-2 text-sm font-medium bg-white border border-gray-200 rounded-lg 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:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white align-middle">
<span class="flex flex-row gap-4 justify-between items-center">
{% for status_value, status_label in game_statuses %}
<template x-if="status == '{{ status_value }}'">
<c-gamestatus display="flex" status="{{ status_value }}" class="text-slate-300">{{ status_label }}</c-gamestatus>
<c-gamestatus display="flex" status="{{ status_value }}">{{ status_label }}</c-gamestatus>
</template>
{% endfor %}
<svg class="text-white w-3" viewBox="5 8 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 9L12 15L18 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<c-icon.arrowdown />
</span>
<div class="absolute top-[105%] left-0 w-full whitespace-nowrap z-10 text-sm font-medium bg-gray-800/20 backdrop-blur-lg rounded-md rounded-t-none border border-gray-200 dark:border-gray-700" x-show="open" style="display: none;">
<ul class="[&_li:first-of-type_a]:rounded-none [&_li:last-of-type_a]:rounded-t-none">
+1 -1
View File
@@ -1,6 +1,6 @@
<ul class="list-disc list-inside">
{% for change in statuschanges %}
<li class="text-slate-500">
{% if change.timestamp %}{{ change.timestamp | date:"d/m/Y H:i" }}: Changed{% else %}At some point changed{% endif %} status from <c-gamestatus :status="change.old_status" class="text-white">{{ change.get_old_status_display }}</c-gamestatus> to <c-gamestatus :status="change.new_status" class="text-white">{{ change.get_new_status_display }}</c-gamestatus> (<a href="{% url 'edit_statuschange' change.id %}">Edit</a>, <a href="{% url 'delete_statuschange' change.id %}">Delete</a>)</li>
{% if change.timestamp %}{{ change.timestamp | date:"d/m/Y H:i" }}: Changed{% else %}At some point changed{% endif %} status from <c-gamestatus :status="change.old_status">{{ change.get_old_status_display }}</c-gamestatus> to <c-gamestatus :status="change.new_status">{{ change.get_new_status_display }}</c-gamestatus> (<a href="{% url 'edit_statuschange' change.id %}">Edit</a>, <a href="{% url 'delete_statuschange' change.id %}">Delete</a>)</li>
{% endfor %}
</ul>
+23 -7
View File
@@ -52,16 +52,16 @@
{{ playrange }}
</c-popover>
</div>
<div class="flex flex-col mb-6 text-slate-400 gap-y-4">
<div class="flex flex-col mb-6 text-gray-600 dark:text-slate-400 gap-y-4">
<div class="flex gap-2 items-center">
<span class="uppercase">Original year</span>
<span class="text-slate-300">{{ game.original_year_released }}</span>
<span class="text-black dark:text-slate-300">{{ game.original_year_released }}</span>
</div>
<div class="flex gap-2 items-center"
>
<span class="uppercase">Status</span>
{% include "partials/gamestatus_selector.html" %}
{% if game.mastered %}👑{% endif %}
<div id="crown-icon-mount-point"></div>
</div>
<div class="flex gap-2 items-center"
x-data="{ open: false }"
@@ -74,9 +74,7 @@
</button>
</a>
<button type="button" x-on:click="open = !open" @click.outside="open = false" class="relative px-4 py-2 text-sm font-medium text-gray-900 bg-white border-e border-b border-t border-gray-200 rounded-e-lg 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 align-middle">
<svg class="text-white w-3" viewBox="5 8 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 9L12 15L18 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<c-icon.arrowdown />
<div
class="absolute top-[100%] -left-[1px] w-auto whitespace-nowrap z-10 text-sm font-medium bg-gray-800/20 backdrop-blur-lg rounded-md rounded-tl-none border border-gray-200 dark:border-gray-700"
x-show="open"
@@ -110,7 +108,7 @@
<div class="flex gap-2 items-center">
<span class="uppercase">Platform</span>
<span class="text-slate-300">{{ game.platform }}</span>
<span class="text-black dark:text-slate-300">{{ game.platform }}</span>
</div>
</div>
<div class="inline-flex rounded-md shadow-sm mb-3" role="group">
@@ -163,4 +161,22 @@
return document.getElementById('session-count').textContent.match("[0-9]+");
}
</script>
{% if debug %} {# Assuming 'debug' context variable is passed from Django view #}
<script type="module" src="http://localhost:5173/@vite/client"></script>
<script type="module" src="http://localhost:5173/frontend/main.js"></script>
{% else %}
{# For production, you would use Django's staticfiles to serve the built assets #}
{# <script type="module" src="{% static 'dist/main.js' %}"></script> #}
{# <link rel="stylesheet" href="{% static 'dist/main.css' %}"> #}
{% endif %}
<script type="module">
document.addEventListener('DOMContentLoaded', () => {
if (window.mountCrownIcon) {
window.mountCrownIcon('#crown-icon-mount-point', {
mastered: {{ game.mastered|yesno:"true,false" }}
});
}
});
</script>
</c-layouts.base>
+29
View File
@@ -0,0 +1,29 @@
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import path from 'path';
export default defineConfig({
plugins: [svelte()],
// The root is the project root where vite.config.js and package.json are
root: path.resolve(__dirname),
// Svelte source code is expected in the 'frontend' subdirectory
build: {
outDir: 'static/dist', // Output to a 'static/dist' folder within the project root
emptyOutDir: true, // Clear the output directory before building
manifest: true, // Generate manifest.json for Django to read asset paths
rollupOptions: {
input: {
main: 'frontend/main.js', // Entry point for your Svelte app, relative to the 'root'
},
},
},
server: {
port: 5173, // Default Vite dev server port
strictPort: true,
hmr: { port: 5173 }, // Ensure HMR also uses the correct port
cors: { // Configure CORS for the Vite development server
origin: '*', // Allow requests from any origin during development
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
},
},
});