svelte-integration #93

Closed
lukas wants to merge 5 commits from svelte-integration into main
20 changed files with 303 additions and 90 deletions
+7
View File
@@ -1,3 +1,10 @@
## Unreleased
### New
* Pre-fill time played into new playevent, also tracks time since last playevent
* Improve light theme and fix light/dark theme switcher
* Fix purchase form logic
## 1.6.0 / 2025-01-15 23:13+01:00
### New
+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 \
+18 -4
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" \
"poetry run python -Wa manage.py runserver" \
"npx tailwindcss -i ./common/input.css -o ./games/static/base.css --watch"
--names "Django,Tailwind,Vite" \
--prefix-colors "blue,green,yellow" \
"uv run python -Wa manage.py runserver" \
"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));
+3 -14
View File
@@ -25,18 +25,7 @@ function setupElementHandlers() {
document.addEventListener("DOMContentLoaded", setupElementHandlers);
document.addEventListener("htmx:afterSwap", setupElementHandlers);
getEl("#id_type").onchange = () => {
getEl("#id_type").addEventListener("change", () => {
setupElementHandlers();
};
document.body.addEventListener("htmx:beforeRequest", function (event) {
// Assuming 'Purchase1' is the element that triggers the HTMX request
if (event.target.id === "id_games") {
var idEditionValue = document.getElementById("id_games").value;
// Condition to check - replace this with your actual logic
if (idEditionValue != "") {
event.preventDefault(); // This cancels the HTMX request
}
}
});
}
);
+5
View File
@@ -43,6 +43,7 @@ function syncSelectInputUntilChanged(syncData, parentSelector = document) {
const targetElement = document.querySelector(syncItem.target);
if (targetElement && valueToSync !== null) {
console.log(`Changing value of ${syncItem.target} to ${valueToSync}`)
targetElement[syncItem.target_value] = valueToSync;
}
}
@@ -184,13 +185,17 @@ function disableElementsWhenValueNotEqual(
function disableElementsWhenTrue(targetSelect, targetValue, elementList) {
return conditionalElementHandler([
() => {
console.log(`${disableElementsWhenTrue.name}: triggered on ${targetSelect}`)
console.log(`Value of ${targetSelect} is ${targetValue}: ${getEl(targetSelect).value == targetValue}`)
return getEl(targetSelect).value == targetValue;
},
elementList,
(el) => {
console.log(`${disableElementsWhenTrue.name}: disabling ${el.id}`)
el.disabled = "disabled";
},
(el) => {
console.log(`${disableElementsWhenTrue.name}: enabling ${el.id}`)
el.disabled = "";
},
]);
@@ -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

+51 -38
View File
@@ -39,46 +39,59 @@
<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');
}
var themeToggleBtn = document.getElementById('theme-toggle');
themeToggleBtn.addEventListener('click', function () {
// toggle icons inside button
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
// if set via local storage previously
if (localStorage.getItem('color-theme')) {
if (localStorage.getItem('color-theme') === 'light') {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
}
// if NOT set via local storage previously
} else {
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
<script type="module">
document.addEventListener('DOMContentLoaded', () => {
if (window.mountCrownIcon) {
window.mountCrownIcon('#crown-icon-mount-point', {
mastered: {{ game.mastered|yesno:"true,false" }}
});
}
// 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');
// if set via local storage previously
if (localStorage.getItem('color-theme')) {
if (localStorage.getItem('color-theme') === 'light') {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
} 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 { // 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 { // 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>
+75 -4
View File
@@ -1,4 +1,5 @@
import logging
from datetime import datetime, timedelta
from typing import Any, Callable, TypedDict
from django.contrib.auth.decorators import login_required
@@ -11,9 +12,9 @@ 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.time import dateformat, format_duration, local_strftime
from games.forms import PlayEventForm
from games.models import Game, PlayEvent
from games.models import Game, PlayEvent, Session
logger = logging.getLogger("games")
@@ -83,6 +84,39 @@ def create_playevent_tabledata(
}
def _get_formatted_playtime_for_game_sessions_in_range(
game: Game,
start_timestamp: datetime | None = None,
end_timestamp: datetime | None = None,
) -> str:
"""
Calculates and formats the total playtime for a game's sessions
between specified start and end timestamps. If timestamps are not provided,
it uses the earliest and latest session start times for the game.
Returns "0h 00m" if no sessions exist for the game or if the range is invalid.
"""
sessions_queryset = game.sessions.all()
if not sessions_queryset.exists():
return "0h 00m"
actual_start_ts = (
start_timestamp
if start_timestamp is not None
else sessions_queryset.earliest("timestamp_start").timestamp_start
)
actual_end_ts = (
end_timestamp
if end_timestamp is not None
else sessions_queryset.latest("timestamp_start").timestamp_start
)
sessions_in_range = sessions_queryset.filter(
timestamp_start__gte=actual_start_ts, timestamp_start__lte=actual_end_ts
)
return format_duration(sessions_in_range.total_duration_unformatted(), "%Hh %mm")
@login_required
def list_playevents(request: HttpRequest) -> HttpResponse:
page_number = request.GET.get("page", 1)
@@ -115,8 +149,45 @@ def add_playevent(request: HttpRequest, game_id: int = 0) -> HttpResponse:
# coming from add_playevent_for_game url path
game = get_object_or_404(Game, id=game_id)
initial["game"] = game
initial["started"] = game.sessions.earliest().timestamp_start
initial["ended"] = game.sessions.latest().timestamp_start
try:
# First, try to get the latest session. If no sessions, then no playtime.
latest_session = game.sessions.latest("timestamp_start")
latest_session_ts = latest_session.timestamp_start
# Now, determine the start date for the new playevent.
# This will be either the day after the last playevent ended, or the earliest session.
try:
latest_playevent = game.playevents.latest("ended")
# Start date for the new PlayEvent form
new_playevent_form_start_date = latest_playevent.ended + timedelta(
days=1
)
initial["started"] = new_playevent_form_start_date
# Start timestamp for playtime calculation
playtime_calc_start_ts = datetime.combine(
new_playevent_form_start_date, datetime.min.time()
)
except PlayEvent.DoesNotExist:
# No previous playevents, so the new playevent starts from the earliest session.
earliest_session_ts = game.sessions.earliest(
"timestamp_start"
).timestamp_start
initial["started"] = earliest_session_ts.date()
playtime_calc_start_ts = earliest_session_ts
# The end date for the new PlayEvent form and playtime calculation is the latest session's start date.
initial["ended"] = latest_session_ts.date()
playtime_calc_end_ts = latest_session_ts
initial["note"] = _get_formatted_playtime_for_game_sessions_in_range(
game, playtime_calc_start_ts, playtime_calc_end_ts
)
except Session.DoesNotExist:
initial["started"] = None
initial["ended"] = None
initial["note"] = "0h 00m"
form = PlayEventForm(request.POST or None, initial=initial)
if form.is_valid():
form.save()
+10 -5
View File
@@ -140,7 +140,7 @@ def add_purchase(request: HttpRequest, game_id: int = 0) -> HttpResponse:
context["form"] = form
context["title"] = "Add New Purchase"
# context["script_name"] = "add_purchase.js"
context["script_name"] = "add_purchase.js"
return render(request, "add_purchase.html", context)
@@ -156,7 +156,7 @@ def edit_purchase(request: HttpRequest, purchase_id: int) -> HttpResponse:
context["title"] = "Edit Purchase"
context["form"] = form
context["purchase_id"] = str(purchase_id)
# context["script_name"] = "add_purchase.js"
context["script_name"] = "add_purchase.js"
return render(request, "add_purchase.html", context)
@@ -208,7 +208,12 @@ def related_purchase_by_game(request: HttpRequest) -> HttpResponse:
if isinstance(games, int) or isinstance(games, str):
games = [games]
form = PurchaseForm()
form.fields["related_purchase"].queryset = Purchase.objects.filter(
games__in=games, type=Purchase.GAME
).order_by("games__sort_name")
qs = Purchase.objects.filter(games__in=games, type=Purchase.GAME).order_by(
"games__sort_name"
)
form.fields["related_purchase"].queryset = qs
first_option = qs.first()
if first_option:
form.fields["related_purchase"].initial = first_option.id
return render(request, "partials/related_purchase_field.html", {"form": form})
+12 -1
View File
@@ -1,10 +1,21 @@
{
"name": "timetracker-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.13",
"concurrently": "^8.2.2",
"npm-check-updates": "^16.14.20",
"tailwindcss": "^3.4.14"
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"svelte": "^4.2.8",
"tailwindcss": "^3.4.14",
"vite": "^5.0.8"
},
"dependencies": {
"flowbite": "^2.4.1"
+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',
},
},
});