Improve light/dark theme toggle
This commit is contained in:
@@ -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 \
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<script>
|
||||
export let mastered;
|
||||
</script>
|
||||
{#if mastered}
|
||||
👑
|
||||
{/if}
|
||||
@@ -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
@@ -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 |
@@ -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 |
@@ -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>
|
||||
|
||||
@@ -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,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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user