diff --git a/.drone.yml b/.drone.yml
index e7df4da..49c540a 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -30,9 +30,7 @@ steps:
image: plugins/docker
settings:
repo: registry.kucharczyk.xyz/timetracker
- tags:
- - ${DRONE_COMMIT_REF}
- - ${DRONE_COMMIT_BRANCH}
+ auto_tag: true
when:
branch:
exclude:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c98d5ed..0d4fecf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,17 +1,5 @@
## Unreleased
-## Improved
-* game overview: improve how editions and purchases are displayed
-* add purchase: only allow choosing purchases of selected edition
-
-## 1.5.1 / 2023-11-14 21:10+01:00
-
-## Improved
-* Disallow choosing non-game purchase as related purchase
-* Improve display of purchases
-
-## 1.5.0 / 2023-11-14 19:27+01:00
-
## New
* Add stat for finished this year's games
* Add purchase types:
@@ -20,9 +8,6 @@
* Season Pass
* Battle Pass
-## Fixed
-* Order purchases by date on game view
-
## 1.4.0 / 2023-11-09 21:01+01:00
### New
@@ -110,24 +95,22 @@
### Enhancements
* Improve form appearance
-* Focus important fields on forms
-* Use the same form when editing a session as when adding a session
+* Add helper buttons next to datime fields
* Change recent session view to current year instead of last 30 days
-* Add a hacky way not to reload a page when starting or ending a session (https://git.kucharczyk.xyz/lukas/timetracker/issues/52)
-* Improve session list (https://git.kucharczyk.xyz/lukas/timetracker/issues/53)
-
-### Fixes
-
-* Fix session being wrongly considered in progress if it had a certain amount of manual hours (https://git.kucharczyk.xyz/lukas/timetracker/issues/58)
* Fix bug when filtering only manual sessions (https://git.kucharczyk.xyz/lukas/timetracker/issues/51)
-
+* Add copy button on Add session page to copy times between fields
+* Use the same form when editing a session as when adding a session
+* Add a hacky way not to reload a page when starting or ending a session (https://git.kucharczyk.xyz/lukas/timetracker/issues/52)
+* Focus important fields on forms
+* Improve session list (https://git.kucharczyk.xyz/lukas/timetracker/issues/53)
+* Change fonts to IBM Plex
+* Only use local WOFF2 font files
## 1.0.3 / 2023-02-20 17:16+01:00
* Add wikidata ID and year for editions
-* Add icons for game, edition, purchase filters
* Allow filtering by game, edition, purchase from the session list
-* Allow editing filtered entities from session list
+* Add icons for the above
## 1.0.2 / 2023-02-18 21:48+01:00
diff --git a/Makefile b/Makefile
index 8d45097..3e3c57e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,18 +1,12 @@
-all: css migrate
+all: migrate
-initialize: npm css migrate sethookdir loadplatforms
+initialize: npm migrate sethookdir loadplatforms
HTMLFILES := $(shell find games/templates -type f)
npm:
npm install
-css: common/input.css
- npx tailwindcss -i ./common/input.css -o ./games/static/base.css
-
-css-dev: css
- npx tailwindcss -i ./common/input.css -o ./games/static/base.css --watch
-
makemigrations:
poetry run python manage.py makemigrations
diff --git a/common/input.css b/common/input.css
deleted file mode 100644
index e8a7e7d..0000000
--- a/common/input.css
+++ /dev/null
@@ -1,113 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-@font-face {
- font-family: "IBM Plex Mono";
- src: url("fonts/IBMPlexMono-regular.woff2") format("woff2");
- font-weight: 400;
- font-style: normal;
-}
-
-@font-face {
- font-family: "IBM Plex Sans";
- src: url("fonts/IBMPlexSans-Regular.woff2") format("woff2");
- font-weight: 400;
- font-style: normal;
-}
-
-@font-face {
- font-family: "IBM Plex Serif";
- src: url("fonts/IBMPlexSerif-Regular.woff2") format("woff2");
- font-weight: 400;
- font-style: normal;
-}
-
-form label {
- @apply dark:text-slate-400;
-}
-
-.responsive-table {
- @apply dark:text-white mx-auto;
-}
-
-.responsive-table tr:nth-child(even) {
- @apply bg-slate-800
-}
-
-.responsive-table tbody tr:nth-child(odd) {
- @apply bg-slate-900
-}
-
-.responsive-table thead th {
- @apply text-left border-b-2 border-b-slate-500 text-xl;
-}
-
-.responsive-table thead th:not(:first-child),
-.responsive-table td:not(:first-child) {
- @apply border-l border-l-slate-500;
-}
-
-@layer utilities {
- .max-w-20char {
- max-width: 20ch;
- }
- .max-w-35char {
- max-width: 40ch;
- }
- .max-w-40char {
- max-width: 40ch;
- }
-}
-
-form input,
-select,
-textarea {
- @apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
-}
-
-form input:disabled,
-select:disabled,
-textarea:disabled {
- @apply dark:bg-slate-700 dark:text-slate-400;
-}
-
-.errorlist {
- @apply mt-4 mb-1 pl-3 py-2 bg-red-600 text-slate-200 w-[300px];
-}
-
-@media screen and (min-width: 768px) {
- form input,
- select,
- textarea {
- width: 300px;
- }
-}
-
-@media screen and (max-width: 768px) {
- form input,
- select,
- textarea {
- width: 150px;
- }
-}
-
-#button-container button {
- @apply mx-1;
-}
-
-th {
- @apply text-right;
-}
-
-th label {
- @apply mr-4;
-}
-
-.basic-button-container {
- @apply flex space-x-2 justify-center;
-}
-
-.basic-button {
- @apply inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out;
-}
diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs
new file mode 100644
index 0000000..f1eb049
--- /dev/null
+++ b/frontend/.eslintrc.cjs
@@ -0,0 +1,17 @@
+module.exports = {
+ env: {
+ browser: true,
+ es2021: true
+ },
+ extends: ["eslint/recommended", "plugin:react/recommended", "plugin:prettier/recommended"],
+ overrides: [],
+ parserOptions: {
+ ecmaVersion: "latest",
+ sourceType: "module"
+ },
+ plugins: ["react"],
+ rules: {},
+ parserOptions: {
+ ecmaFeatures: { jsx: true }
+ }
+};
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/.prettierrc b/frontend/.prettierrc
new file mode 100644
index 0000000..b518bea
--- /dev/null
+++ b/frontend/.prettierrc
@@ -0,0 +1,9 @@
+{
+ "semi": true,
+ "tabWidth": 2,
+ "printWidth": 100,
+ "singleQuote": true,
+ "trailingComma": "none",
+ "bracketSameLine": false,
+ "singleAttributePerLine": true
+}
\ No newline at end of file
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..b52f64c
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+ Timetracker
+
+
+
+
+
+
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..28a3138
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "autoprefixer": "^10.4.13",
+ "postcss": "^8.4.21",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "tailwindcss": "^3.2.4"
+ },
+ "devDependencies": {
+ "@types/react": "^18.0.26",
+ "@types/react-dom": "^18.0.9",
+ "@vitejs/plugin-react": "^3.0.0",
+ "eslint": "^8.32.0",
+ "eslint-plugin-import": "^2.27.5",
+ "eslint-plugin-jsx-a11y": "^6.7.1",
+ "eslint-plugin-react": "^7.32.1",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "vite": "^4.0.0"
+ }
+}
diff --git a/frontend/postcss.config.cjs b/frontend/postcss.config.cjs
new file mode 100644
index 0000000..33ad091
--- /dev/null
+++ b/frontend/postcss.config.cjs
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/frontend/src/App.css b/frontend/src/App.css
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
new file mode 100644
index 0000000..93f352a
--- /dev/null
+++ b/frontend/src/App.jsx
@@ -0,0 +1,42 @@
+import { useState } from 'react'
+import './App.css'
+
+function App() {
+
+
+ return (
+ <>
+
+
+ {/* {% block content %}No content here.{% endblock content %} */}
+
+ {/* {% load version %} */}
+ {/* {% version %} ({% version_date %}) */}
+ >
+ )
+}
+
+export default App;
diff --git a/frontend/src/components/Nav.jsx b/frontend/src/components/Nav.jsx
new file mode 100644
index 0000000..b81c171
--- /dev/null
+++ b/frontend/src/components/Nav.jsx
@@ -0,0 +1,71 @@
+import { Link } from 'react-router-dom';
+
+function Nav() {
+ return (
+
+ );
+}
+
+export default Nav;
diff --git a/frontend/src/components/SessionList.jsx b/frontend/src/components/SessionList.jsx
new file mode 100644
index 0000000..4ffc0e0
--- /dev/null
+++ b/frontend/src/components/SessionList.jsx
@@ -0,0 +1,162 @@
+export default function SessionList() {
+ const data = [
+ {
+ "url": "http://localhost:8000/api/sessions/25/",
+ "timestamp_start": "2020-01-01T00:00:00+01:00",
+ "timestamp_end": null,
+ "duration_manual": "12:00:00",
+ "duration_calculated": "00:00:00",
+ "note": "",
+ "purchase": "http://localhost:8000/api/purchases/3/"
+ },
+ {
+ "url": "http://localhost:8000/api/sessions/26/",
+ "timestamp_start": "2022-12-31T15:25:00+01:00",
+ "timestamp_end": "2022-12-31T17:25:00+01:00",
+ "duration_manual": "00:00:00",
+ "duration_calculated": "02:00:00",
+ "note": "",
+ "purchase": "http://localhost:8000/api/purchases/2/"
+ },
+ {
+ "url": "http://localhost:8000/api/sessions/27/",
+ "timestamp_start": "2023-01-01T23:00:00+01:00",
+ "timestamp_end": "2023-01-02T00:28:00+01:00",
+ "duration_manual": "00:00:00",
+ "duration_calculated": "01:28:00",
+ "note": "",
+ "purchase": "http://localhost:8000/api/purchases/3/"
+ },
+ {
+ "url": "http://localhost:8000/api/sessions/28/",
+ "timestamp_start": "2023-01-02T22:08:00+01:00",
+ "timestamp_end": "2023-01-03T01:08:00+01:00",
+ "duration_manual": "00:00:00",
+ "duration_calculated": "03:00:00",
+ "note": "",
+ "purchase": "http://localhost:8000/api/purchases/3/"
+ },
+ {
+ "url": "http://localhost:8000/api/sessions/29/",
+ "timestamp_start": "2023-01-03T22:36:00+01:00",
+ "timestamp_end": "2023-01-04T00:12:00+01:00",
+ "duration_manual": "00:00:00",
+ "duration_calculated": "01:36:00",
+ "note": "",
+ "purchase": "http://localhost:8000/api/purchases/3/"
+ },
+ {
+ "url": "http://localhost:8000/api/sessions/30/",
+ "timestamp_start": "2023-01-04T20:35:00+01:00",
+ "timestamp_end": "2023-01-04T22:36:00+01:00",
+ "duration_manual": "00:00:00",
+ "duration_calculated": "02:01:00",
+ "note": "",
+ "purchase": "http://localhost:8000/api/purchases/3/"
+ },
+ {
+ "url": "http://localhost:8000/api/sessions/31/",
+ "timestamp_start": "2023-01-06T18:48:00+01:00",
+ "timestamp_end": "2023-01-06T23:39:00+01:00",
+ "duration_manual": "00:00:00",
+ "duration_calculated": "04:51:00",
+ "note": "",
+ "purchase": "http://localhost:8000/api/purchases/3/"
+ },
+ {
+ "url": "http://localhost:8000/api/sessions/32/",
+ "timestamp_start": "2023-01-07T23:49:00+01:00",
+ "timestamp_end": "2023-01-08T01:43:00+01:00",
+ "duration_manual": "00:00:00",
+ "duration_calculated": "01:54:00",
+ "note": "",
+ "purchase": "http://localhost:8000/api/purchases/3/"
+ },
+ {
+ "url": "http://localhost:8000/api/sessions/33/",
+ "timestamp_start": "2023-01-08T16:21:00+01:00",
+ "timestamp_end": "2023-01-08T18:27:00+01:00",
+ "duration_manual": "00:00:00",
+ "duration_calculated": "02:06:00",
+ "note": "",
+ "purchase": "http://localhost:8000/api/purchases/3/"
+ },
+ {
+ "url": "http://localhost:8000/api/sessions/34/",
+ "timestamp_start": "2023-01-08T19:04:00+01:00",
+ "timestamp_end": "2023-01-08T22:03:00+01:00",
+ "duration_manual": "00:00:00",
+ "duration_calculated": "02:59:00",
+ "note": "",
+ "purchase": "http://localhost:8000/api/purchases/3/"
+ },
+ {
+ "url": "http://localhost:8000/api/sessions/35/",
+ "timestamp_start": "2023-01-09T19:35:48+01:00",
+ "timestamp_end": "2023-01-09T22:13:20.519058+01:00",
+ "duration_manual": "00:00:00",
+ "duration_calculated": "02:37:32.519058",
+ "note": "",
+ "purchase": "http://localhost:8000/api/purchases/3/"
+ },
+ {
+ "url": "http://localhost:8000/api/sessions/36/",
+ "timestamp_start": "2023-01-10T15:50:12+01:00",
+ "timestamp_end": "2023-01-10T17:03:45.424429+01:00",
+ "duration_manual": "00:00:00",
+ "duration_calculated": "01:13:33.424429",
+ "note": "",
+ "purchase": "http://localhost:8000/api/purchases/4/"
+ }
+ ]
+ const header = ["url", "timestamp_start", "timestamp_end", "duration_manual", "duration_calculated", "note", "purchase"]
+ // const header = ["Name", "Platform", "Start", "End", "Duration", "Manage"]
+ return (
+ <>
+
+ {header.map(column => {
+
{column}
+ })}
+ {data.map(session => {
+ <>
+
+ >
+ })}
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/error-page.jsx b/frontend/src/error-page.jsx
new file mode 100644
index 0000000..02b4361
--- /dev/null
+++ b/frontend/src/error-page.jsx
@@ -0,0 +1,16 @@
+import { useRouteError } from "react-router-dom";
+
+export default function ErrorPage() {
+ const error = useRouteError()
+ console.error(error)
+
+ return (
+
+
Oops!
+
Sorry, an unexpected error has occurred.
+
+ {error.statusText || error.message}
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..a37820a
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,22 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+form label {
+ @apply dark:text-slate-400;
+}
+
+form input,
+select,
+textarea {
+ @apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
+}
+
+#session-table {
+ display: grid;
+ grid-template-columns: 3fr 1fr repeat(2, 2fr) 0.5fr 1fr;
+}
+
+#button-container button {
+ @apply mx-1;
+}
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
new file mode 100644
index 0000000..9c281e2
--- /dev/null
+++ b/frontend/src/main.jsx
@@ -0,0 +1,34 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App'
+import './index.css'
+import { createBrowserRouter, RouterProvider } from 'react-router-dom'
+// import { loader as sessionLoader } from './routes/sessions'
+import ErrorPage from "./error-page"
+import SessionList from './components/SessionList'
+// import Session from './routes/sessions'
+
+const router = createBrowserRouter([
+ {
+ path: "/",
+ element: ,
+ errorElement: ,
+ // loader: sessionLoader,
+ children: [
+ {
+ path: "sessions/",
+ element:
+ }
+ ]
+ },
+ // {
+ // path: "sessions",
+ // element:
+ // }
+])
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
diff --git a/frontend/src/services/ApiService.jsx b/frontend/src/services/ApiService.jsx
new file mode 100644
index 0000000..f7485e7
--- /dev/null
+++ b/frontend/src/services/ApiService.jsx
@@ -0,0 +1,17 @@
+export async function api(url) {
+ const response = await fetch(url);
+ if (response.ok) {
+ const jsonValue = await response.json();
+ return Promise.resolve(jsonValue);
+ } else {
+ return Promise.reject('Response was not OK.');
+ }
+ }
+
+export async function getSession(sessionId) {
+ return await api(`/api/sessions/${sessionId}/`);
+ }
+
+ export async function getSessionList() {
+ return await api(`/api/sessions/`);
+ }
\ No newline at end of file
diff --git a/frontend/tailwind.config.cjs b/frontend/tailwind.config.cjs
new file mode 100644
index 0000000..e8ecc87
--- /dev/null
+++ b/frontend/tailwind.config.cjs
@@ -0,0 +1,12 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ darkMode: "class",
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
+ theme: {
+ fontFamily: {
+ sans: ["Inter", "sans-serif"],
+ },
+ extend: {},
+ },
+ plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
+};
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
new file mode 100644
index 0000000..f473338
--- /dev/null
+++ b/frontend/vite.config.js
@@ -0,0 +1,12 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ proxy: {
+ "/api": "http://127.0.0.1:8001",
+ },
+ },
+});
diff --git a/games/forms.py b/games/forms.py
index b67c294..4a23482 100644
--- a/games/forms.py
+++ b/games/forms.py
@@ -9,6 +9,11 @@ custom_datetime_widget = forms.DateTimeInput(
)
autofocus_input_widget = forms.TextInput(attrs={"autofocus": "autofocus"})
+custom_date_widget = forms.DateInput(attrs={"type": "date"})
+custom_datetime_widget = forms.DateTimeInput(
+ attrs={"type": "datetime-local"}, format="%Y-%m-%d %H:%M"
+)
+
class SessionForm(forms.ModelForm):
# purchase = forms.ModelChoiceField(
@@ -82,7 +87,6 @@ class PurchaseForm(forms.ModelForm):
widgets = {
"date_purchased": custom_date_widget,
"date_refunded": custom_date_widget,
- "date_finished": custom_date_widget,
}
model = Purchase
fields = [
@@ -147,7 +151,7 @@ class EditionForm(forms.ModelForm):
class Meta:
model = Edition
- fields = ["game", "name", "sort_name", "platform", "year_released", "wikidata"]
+ fields = ["game", "name", "platform", "year_released", "wikidata"]
class GameForm(forms.ModelForm):
diff --git a/games/migrations/0027_purchase_related_purchase.py b/games/migrations/0027_purchase_related_purchase.py
index 06e7072..a55f044 100644
--- a/games/migrations/0027_purchase_related_purchase.py
+++ b/games/migrations/0027_purchase_related_purchase.py
@@ -1,11 +1,10 @@
# Generated by Django 4.1.5 on 2023-11-14 08:41
-import django.db.models.deletion
from django.db import migrations, models
+import django.db.models.deletion
class Migration(migrations.Migration):
-
dependencies = [
("games", "0026_purchase_type"),
]
diff --git a/games/migrations/0028_purchase_name.py b/games/migrations/0028_purchase_name.py
index e2c390c..7c08cfc 100644
--- a/games/migrations/0028_purchase_name.py
+++ b/games/migrations/0028_purchase_name.py
@@ -1,7 +1,6 @@
# Generated by Django 4.1.5 on 2023-11-14 11:05
from django.db import migrations, models
-
from games.models import Purchase
diff --git a/games/models.py b/games/models.py
index 18d3c67..81e1cd9 100644
--- a/games/models.py
+++ b/games/models.py
@@ -2,6 +2,7 @@ from datetime import timedelta
from django.core.exceptions import ValidationError
from django.db import models
+from django.core.exceptions import ValidationError
from django.db.models import F, Manager, Sum
from django.utils import timezone
@@ -38,13 +39,9 @@ class Edition(models.Model):
game = models.ForeignKey("Game", on_delete=models.CASCADE)
name = models.CharField(max_length=255)
- sort_name = models.CharField(max_length=255, null=True, blank=True, default=None)
- platform = models.ForeignKey(
- "Platform", on_delete=models.CASCADE, null=True, blank=True, default=None
- )
- year_released = models.IntegerField(null=True, blank=True, default=None)
+ platform = models.ForeignKey("Platform", on_delete=models.CASCADE)
+ year_released = models.IntegerField(default=datetime.today().year)
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
- created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.sort_name
@@ -124,25 +121,14 @@ class Purchase(models.Model):
type = models.CharField(max_length=255, choices=TYPES, default=GAME)
name = models.CharField(max_length=255, default="", null=True, blank=True)
related_purchase = models.ForeignKey(
- "Purchase",
- on_delete=models.SET_NULL,
- default=None,
- null=True,
- blank=True,
- related_name="related_purchases",
+ "Purchase", on_delete=models.SET_NULL, default=None, null=True, blank=True
)
- created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
- additional_info = [
- self.get_type_display() if self.type != Purchase.GAME else "",
- f"{self.edition.platform} version on {self.platform}"
- if self.platform != self.edition.platform
- else self.platform,
- self.edition.year_released,
- self.get_ownership_type_display(),
- ]
- return f"{self.edition} ({', '.join(filter(None, map(str, additional_info)))})"
+ platform_info = self.platform
+ if self.platform != self.edition.platform:
+ platform_info = f"{self.edition.platform} version on {self.platform}"
+ return f"{self.edition} ({platform_info}, {self.edition.year_released}, {self.get_ownership_type_display()})"
def is_game(self):
return self.type == self.GAME
diff --git a/games/serializers.py b/games/serializers.py
new file mode 100644
index 0000000..e69de29
diff --git a/games/static/base.css b/games/static/base.css
index 74cff7a..8ed1af9 100644
--- a/games/static/base.css
+++ b/games/static/base.css
@@ -755,10 +755,6 @@ select {
position: absolute;
}
-.relative {
- position: relative;
-}
-
.bottom-2 {
bottom: 0.5rem;
}
@@ -775,55 +771,20 @@ select {
top: 0.75rem;
}
-.mx-2 {
- margin-left: 0.5rem;
- margin-right: 0.5rem;
-}
-
.mx-auto {
margin-left: auto;
margin-right: auto;
}
-.my-2 {
- margin-top: 0.5rem;
- margin-bottom: 0.5rem;
-}
-
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
}
-.my-6 {
- margin-top: 1.5rem;
- margin-bottom: 1.5rem;
-}
-
-.mb-1 {
- margin-bottom: 0.25rem;
-}
-
-.mb-10 {
- margin-bottom: 2.5rem;
-}
-
-.mb-2 {
- margin-bottom: 0.5rem;
-}
-
.mb-4 {
margin-bottom: 1rem;
}
-.ml-1 {
- margin-left: 0.25rem;
-}
-
-.ml-2 {
- margin-left: 0.5rem;
-}
-
.mr-4 {
margin-right: 1rem;
}
@@ -836,10 +797,6 @@ select {
display: block;
}
-.inline-block {
- display: inline-block;
-}
-
.inline {
display: inline;
}
@@ -856,14 +813,6 @@ select {
display: none;
}
-.h-4 {
- height: 1rem;
-}
-
-.h-5 {
- height: 1.25rem;
-}
-
.h-6 {
height: 1.5rem;
}
@@ -872,22 +821,10 @@ select {
min-height: 100vh;
}
-.w-5 {
- width: 1.25rem;
-}
-
.w-6 {
width: 1.5rem;
}
-.w-7 {
- width: 1.75rem;
-}
-
-.w-auto {
- width: auto;
-}
-
.w-full {
width: 100%;
}
@@ -896,10 +833,6 @@ select {
max-width: 1024px;
}
-.max-w-sm {
- max-width: 24rem;
-}
-
.max-w-xs {
max-width: 20rem;
}
@@ -926,18 +859,10 @@ select {
align-items: center;
}
-.justify-center {
- justify-content: center;
-}
-
.justify-between {
justify-content: space-between;
}
-.gap-2 {
- gap: 0.5rem;
-}
-
.self-center {
align-self: center;
}
@@ -960,35 +885,16 @@ select {
border-radius: 0.5rem;
}
-.rounded-sm {
- border-radius: 0.125rem;
-}
-
.border-gray-200 {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
}
-.border-slate-500 {
- --tw-border-opacity: 1;
- border-color: rgb(100 116 139 / var(--tw-border-opacity));
-}
-
-.bg-gray-200 {
- --tw-bg-opacity: 1;
- background-color: rgb(229 231 235 / var(--tw-bg-opacity));
-}
-
.bg-green-600 {
--tw-bg-opacity: 1;
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
}
-.bg-violet-600 {
- --tw-bg-opacity: 1;
- background-color: rgb(124 58 237 / var(--tw-bg-opacity));
-}
-
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
@@ -1003,11 +909,6 @@ select {
padding-right: 0.5rem;
}
-.px-4 {
- padding-left: 1rem;
- padding-right: 1rem;
-}
-
.py-1 {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
@@ -1026,10 +927,6 @@ select {
padding-right: 1rem;
}
-.pt-1 {
- padding-top: 0.25rem;
-}
-
.text-center {
text-align: center;
}
@@ -1038,9 +935,12 @@ select {
font-family: IBM Plex Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
-.text-3xl {
- font-size: 1.875rem;
- line-height: 2.25rem;
+.font-serif {
+ font-family: IBM Plex Serif, ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
+}
+
+.font-sans {
+ font-family: IBM Plex Sans, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
.text-4xl {
@@ -1048,21 +948,11 @@ select {
line-height: 2.5rem;
}
-.text-5xl {
- font-size: 3rem;
- line-height: 1;
-}
-
.text-base {
font-size: 1rem;
line-height: 1.5rem;
}
-.text-lg {
- font-size: 1.125rem;
- line-height: 1.75rem;
-}
-
.text-xl {
font-size: 1.25rem;
line-height: 1.75rem;
@@ -1073,17 +963,55 @@ select {
line-height: 1rem;
}
+.text-2xl {
+ font-size: 1.5rem;
+ line-height: 2rem;
+}
+
+.text-9xl {
+ font-size: 8rem;
+ line-height: 1;
+}
+
+.text-8xl {
+ font-size: 6rem;
+ line-height: 1;
+}
+
+.text-7xl {
+ font-size: 4.5rem;
+ line-height: 1;
+}
+
+.text-6xl {
+ font-size: 3.75rem;
+ line-height: 1;
+}
+
+.text-5xl {
+ font-size: 3rem;
+ line-height: 1;
+}
+
+.text-lg {
+ font-size: 1.125rem;
+ line-height: 1.75rem;
+}
+
.font-semibold {
font-weight: 600;
}
-.italic {
- font-style: italic;
+.font-bold {
+ font-weight: 700;
}
-.text-gray-700 {
- --tw-text-opacity: 1;
- color: rgb(55 65 81 / var(--tw-text-opacity));
+.capitalize {
+ text-transform: capitalize;
+}
+
+.italic {
+ font-style: italic;
}
.text-slate-300 {
@@ -1101,14 +1029,6 @@ select {
color: rgb(253 224 71 / var(--tw-text-opacity));
}
-.underline {
- text-decoration-line: underline;
-}
-
-.decoration-slate-500 {
- text-decoration-color: #64748b;
-}
-
.shadow-md {
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
@@ -1208,10 +1128,12 @@ select {
}
.responsive-table thead th:not(:first-child),
-.responsive-table td:not(:first-child) {
+td:not(:first-child) {
border-left-width: 1px;
--tw-border-opacity: 1;
border-left-color: rgb(100 116 139 / var(--tw-border-opacity));
+ padding-left: 1rem;
+ padding-right: 1rem;
}
:is(.dark form input),:is(.dark
@@ -1343,21 +1265,11 @@ th label {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
-.hover\:bg-gray-400:hover {
- --tw-bg-opacity: 1;
- background-color: rgb(156 163 175 / var(--tw-bg-opacity));
-}
-
.hover\:bg-green-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(21 128 61 / var(--tw-bg-opacity));
}
-.hover\:bg-violet-700:hover {
- --tw-bg-opacity: 1;
- background-color: rgb(109 40 217 / var(--tw-bg-opacity));
-}
-
.hover\:underline:hover {
text-decoration-line: underline;
}
@@ -1378,11 +1290,6 @@ th label {
--tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity));
}
-.focus\:ring-violet-500:focus {
- --tw-ring-opacity: 1;
- --tw-ring-color: rgb(139 92 246 / var(--tw-ring-opacity));
-}
-
.focus\:ring-offset-2:focus {
--tw-ring-offset-width: 2px;
}
@@ -1391,14 +1298,6 @@ th label {
--tw-ring-offset-color: #bfdbfe;
}
-.focus\:ring-offset-violet-200:focus {
- --tw-ring-offset-color: #ddd6fe;
-}
-
-.group:hover .group-hover\:block {
- display: block;
-}
-
:is(.dark .dark\:bg-gray-800) {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
@@ -1409,16 +1308,6 @@ th label {
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
-:is(.dark .dark\:text-slate-400) {
- --tw-text-opacity: 1;
- color: rgb(148 163 184 / var(--tw-text-opacity));
-}
-
-:is(.dark .dark\:text-slate-500) {
- --tw-text-opacity: 1;
- color: rgb(100 116 139 / var(--tw-text-opacity));
-}
-
:is(.dark .dark\:text-slate-600) {
--tw-text-opacity: 1;
color: rgb(71 85 105 / var(--tw-text-opacity));
@@ -1430,10 +1319,6 @@ th label {
}
@media (min-width: 640px) {
- .sm\:inline {
- display: inline;
- }
-
.sm\:table-cell {
display: table-cell;
}
@@ -1442,19 +1327,11 @@ th label {
max-width: 28rem;
}
- .sm\:max-w-xl {
- max-width: 36rem;
- }
-
.sm\:px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
- .sm\:pl-12 {
- padding-left: 3rem;
- }
-
.sm\:pl-2 {
padding-left: 0.5rem;
}
@@ -1509,10 +1386,6 @@ th label {
display: table-cell;
}
- .lg\:max-w-3xl {
- max-width: 48rem;
- }
-
.lg\:max-w-lg {
max-width: 32rem;
}
diff --git a/games/static/js/add_edition.js b/games/static/js/add_edition.js
index dabfca6..cda1368 100644
--- a/games/static/js/add_edition.js
+++ b/games/static/js/add_edition.js
@@ -1,24 +1,29 @@
-import { syncSelectInputUntilChanged } from './utils.js';
+/**
+ * @description Sync select field with input field until user focuses it.
+ * @param {HTMLSelectElement} sourceElementSelector
+ * @param {HTMLInputElement} targetElementSelector
+ */
+function syncSelectInputUntilChanged(
+ sourceElementSelector,
+ targetElementSelector
+) {
+ const sourceElement = document.querySelector(sourceElementSelector);
+ const targetElement = document.querySelector(targetElementSelector);
+ function sourceElementHandler(event) {
+ let selected = event.target.value;
+ let selectedValue = document.querySelector(
+ `#id_game option[value='${selected}']`
+ ).textContent;
+ targetElement.value = selectedValue;
+ }
+ function targetElementHandler(event) {
+ sourceElement.removeEventListener("change", sourceElementHandler);
+ }
-let syncData = [
- {
- "source": "#id_game",
- "source_value": "dataset.name",
- "target": "#id_name",
- "target_value": "value"
- },
- {
- "source": "#id_game",
- "source_value": "textContent",
- "target": "#id_sort_name",
- "target_value": "value"
- },
- {
- "source": "#id_game",
- "source_value": "dataset.year",
- "target": "#id_year_released",
- "target_value": "value"
- },
-]
+ sourceElement.addEventListener("change", sourceElementHandler);
+ targetElement.addEventListener("focus", targetElementHandler);
+}
-syncSelectInputUntilChanged(syncData, "form");
+window.addEventListener("load", () => {
+ syncSelectInputUntilChanged("#id_game", "#id_name");
+});
diff --git a/games/static/js/add_purchase.js b/games/static/js/add_purchase.js
index cfacecc..c427652 100644
--- a/games/static/js/add_purchase.js
+++ b/games/static/js/add_purchase.js
@@ -1,9 +1,4 @@
-import {
- syncSelectInputUntilChanged,
- getEl,
- disableElementsWhenTrue,
- disableElementsWhenFalse,
-} from "./utils.js";
+import { syncSelectInputUntilChanged, getEl, conditionalElementHandler } from "./utils.js";
let syncData = [
{
@@ -16,28 +11,21 @@ let syncData = [
syncSelectInputUntilChanged(syncData, "form");
-function setupElementHandlers() {
- disableElementsWhenTrue("#id_type", "game", [
- "#id_name",
- "#id_related_purchase",
- ]);
- disableElementsWhenFalse("#id_type", "game", ["#id_date_finished"]);
-}
-document.addEventListener("DOMContentLoaded", setupElementHandlers);
-document.addEventListener("htmx:afterSwap", setupElementHandlers);
-getEl("#id_type").onchange = () => {
- setupElementHandlers();
-};
-
-document.body.addEventListener('htmx:beforeRequest', function(event) {
- // Assuming 'Purchase1' is the element that triggers the HTMX request
- if (event.target.id === 'id_edition') {
- var idEditionValue = document.getElementById('id_edition').value;
-
- // Condition to check - replace this with your actual logic
- if (idEditionValue != '') {
- event.preventDefault(); // This cancels the HTMX request
- }
+let myConfig = [
+ () => {
+ return getEl("#id_type").value == "game";
+ },
+ ["#id_name", "#id_related_purchase"],
+ (el) => {
+ el.disabled = "disabled";
+ },
+ (el) => {
+ el.disabled = "";
}
-});
+]
+
+document.DOMContentLoaded = conditionalElementHandler(...myConfig)
+getEl("#id_type").onchange = () => {
+ conditionalElementHandler(...myConfig)
+}
diff --git a/games/static/js/add_session.js b/games/static/js/add_session.js
index e190820..87d86de 100644
--- a/games/static/js/add_session.js
+++ b/games/static/js/add_session.js
@@ -8,9 +8,6 @@ for (let button of document.querySelectorAll("[data-target]")) {
event.preventDefault();
if (type == "now") {
targetElement.value = toISOUTCString(new Date);
- } else if (type == "copy") {
- const oppositeName = targetElement.name == "timestamp_start" ? "timestamp_end" : "timestamp_start";
- document.querySelector(`[name='${oppositeName}']`).value = targetElement.value;
} else if (type == "toggle") {
if (targetElement.type == "datetime-local") targetElement.type = "text";
else targetElement.type = "datetime-local";
diff --git a/games/static/js/utils.js b/games/static/js/utils.js
index 5b7eddc..669735e 100644
--- a/games/static/js/utils.js
+++ b/games/static/js/utils.js
@@ -3,16 +3,9 @@
* @param {Date} date
* @returns {string}
*/
-function toISOUTCString(date) {
- function stringAndPad(number) {
- return number.toString().padStart(2, 0);
- }
- const year = date.getFullYear();
- const month = stringAndPad(date.getMonth() + 1);
- const day = stringAndPad(date.getDate());
- const hours = stringAndPad(date.getHours());
- const minutes = stringAndPad(date.getMinutes());
- return `${year}-${month}-${day}T${hours}:${minutes}`;
+export function toISOUTCString(date) {
+ let month = (date.getMonth() + 1).toString().padStart(2, 0);
+ return `${date.getFullYear()}-${month}-${date.getDate()}T${date.getHours()}:${date.getMinutes()}`;
}
/**
@@ -99,72 +92,37 @@ function getEl(selector) {
return document.getElementsByClassName(selector)
}
else {
- return document.getElementsByTagName(selector)
+ return document.getElementsByName(selector)
}
}
/**
- * @description Applies different behaviors to elements based on multiple conditional configurations.
- * Each configuration is an array containing a condition function, an array of target element selectors,
- * and two callback functions for handling matched and unmatched conditions.
- * @param {...Array} configs Each configuration is an array of the form:
- * - 0: {function(): boolean} condition - Function that returns true or false based on a condition.
- * - 1: {string[]} targetElements - Array of CSS selectors for target elements.
- * - 2: {function(HTMLElement): void} callbackfn1 - Function to execute when condition is true.
- * - 3: {function(HTMLElement): void} callbackfn2 - Function to execute when condition is false.
+ * @description Does something to elements when something happens.
+ * @param {() => boolean} condition The condition that is being tested.
+ * @param {string[]} targetElements
+ * @param {(elementName: HTMLElement) => void} callbackfn1 Called when the condition matches.
+ * @param {(elementName: HTMLElement) => void} callbackfn2 Called when the condition doesn't match.
*/
-function conditionalElementHandler(...configs) {
- configs.forEach(([condition, targetElements, callbackfn1, callbackfn2]) => {
- if (condition()) {
- targetElements.forEach(elementName => {
- let el = getEl(elementName);
- if (el === null) {
- console.error(`Element ${elementName} doesn't exist.`);
- } else {
- callbackfn1(el);
- }
- });
- } else {
- targetElements.forEach(elementName => {
- let el = getEl(elementName);
- if (el === null) {
- console.error(`Element ${elementName} doesn't exist.`);
- } else {
- callbackfn2(el);
- }
- });
- }
- });
+function conditionalElementHandler(condition, targetElements, callbackfn1, callbackfn2) {
+ if (condition()) {
+ targetElements.forEach((elementName) => {
+ let el = getEl(elementName);
+ if (el === null) {
+ console.error("Element ${elementName} doesn't exist.");
+ } else {
+ callbackfn1(el);
+ }
+ });
+ } else {
+ targetElements.forEach((elementName) => {
+ let el = getEl(elementName);
+ if (el === null) {
+ console.error("Element ${elementName} doesn't exist.");
+ } else {
+ callbackfn2(el);
+ }
+ });
+ }
}
-function disableElementsWhenFalse(targetSelect, targetValue, elementList) {
- return conditionalElementHandler([
- () => {
- return getEl(targetSelect).value != targetValue;
- },
- elementList,
- (el) => {
- el.disabled = "disabled";
- },
- (el) => {
- el.disabled = "";
- },
- ]);
-}
-
-function disableElementsWhenTrue(targetSelect, targetValue, elementList) {
- return conditionalElementHandler([
- () => {
- return getEl(targetSelect).value == targetValue;
- },
- elementList,
- (el) => {
- el.disabled = "disabled";
- },
- (el) => {
- el.disabled = "";
- },
- ]);
-}
-
-export { toISOUTCString, syncSelectInputUntilChanged, getEl, conditionalElementHandler, disableElementsWhenFalse, disableElementsWhenTrue, getValueFromProperty };
+export { toISOUTCString, syncSelectInputUntilChanged, getEl, conditionalElementHandler };
diff --git a/games/templates/add.html b/games/templates/add.html
index 3da44b1..a3c9e5d 100644
--- a/games/templates/add.html
+++ b/games/templates/add.html
@@ -6,19 +6,13 @@
{% block content %}
{% endblock content %}
-{% block scripts %}
- {% if script_name %}
-
- {% endif %}
-{% endblock scripts %}
diff --git a/games/templates/add_edition.html b/games/templates/add_edition.html
index 219c595..67abc98 100644
--- a/games/templates/add_edition.html
+++ b/games/templates/add_edition.html
@@ -6,27 +6,17 @@
{% block content %}
{% endblock content %}
{% block scripts %}
- {% if script_name %}
-
- {% endif %}
+{% load static %}
+
{% endblock scripts %}
diff --git a/games/templates/add_session.html b/games/templates/add_session.html
index 26ff0b7..9352e0f 100644
--- a/games/templates/add_session.html
+++ b/games/templates/add_session.html
@@ -1,38 +1,34 @@
{% extends "base.html" %}
-{% block title %}
- {{ title }}
-{% endblock title %}
+
+{% block title %}{{ title }}{% endblock title %}
+
{% block content %}
{% load static %}
diff --git a/games/templates/base.html b/games/templates/base.html
index f9f55a6..865aeef 100644
--- a/games/templates/base.html
+++ b/games/templates/base.html
@@ -2,29 +2,22 @@
{% load static %}
-
-
-
-
- Timetracker -
- {% block title %}
- Untitled
- {% endblock title %}
-
+
+
+
+
+ Timetracker - {% block title %}Untitled{% endblock title %}
-
-
+
+
+