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 => { + <> +
+ + { session.url } + +
+ + { session.timestamp_start } + +
+
+ + { session.timestamp_end } + +
+
+ + { session.duration_manual } + +
+
+ + { session.duration_calculated } + +
+
+ + { session.note } + +
+
+ + { session.purchase } + +
+
+ + })} +
+ + ) +} \ 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 %}
- {% csrf_token %} - {{ form.as_table }} - - - - + {% csrf_token %} + + {{ form.as_table }} + + + +
- -
{% 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 %}
- {% csrf_token %} - {{ form.as_table }} - - - - - - - - + {% csrf_token %} + + {{ form.as_table }} + + + +
- -
- -
{% 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 %}
- {% csrf_token %} - {% for field in form %} - - - {% if field.name == "note" %} - - {% else %} - - {% endif %} - {% if field.name == "timestamp_start" or field.name == "timestamp_end" %} - - {% endif %} - - {% endfor %} - - - - + {% csrf_token %} + + {% for field in form %} + + + {% if field.name == "note" %} + + {% else %} + + {% endif %} + {% if field.name == "timestamp_start" or field.name == "timestamp_end" %} + + {% endif %} + + {% endfor %} + + + +
{{ field.label_tag }}{{ field }}{{ field }} -
- - - -
-
- -
{{ field.label_tag }}{{ field }}{{ field }} +
+ + +
+
{% 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 %} - - + + +