Compare commits

..

201 Commits

Author SHA1 Message Date
Lukáš Kucharczyk 8acc4f9c5b
make table work better on small screens
Django CI/CD / test (push) Successful in 56s Details
Django CI/CD / build-and-push (push) Successful in 2m12s Details
2024-11-13 21:28:44 +01:00
Lukáš Kucharczyk 6b7a96dc06
make PopoverTruncated customizable 2024-11-13 21:28:17 +01:00
Lukáš Kucharczyk 5c5fd5f26a
truncate: strip trailing whitespace 2024-11-13 21:07:26 +01:00
Lukáš Kucharczyk 7181b6472c
fix mistakenly hardcoded value in truncate() 2024-11-13 21:06:52 +01:00
Lukáš Kucharczyk af06d07ee3
pre-commit hook: disable isort 2024-11-13 21:06:38 +01:00
Lukáš Kucharczyk 315e22a8ac
Add yaml to dependencies
Django CI/CD / test (push) Successful in 1m28s Details
Django CI/CD / build-and-push (push) Successful in 2m33s Details
2024-11-11 18:14:48 +01:00
Lukáš Kucharczyk 19676f8441 Implement converting prices (#79)
Django CI/CD / test (push) Successful in 1m17s Details
Django CI/CD / build-and-push (push) Successful in 2m10s Details
Reviewed-on: #79
2024-11-11 16:36:57 +00:00
Lukáš Kucharczyk f61cde180f Pass search_string to search_field.html
Django CI/CD / test (push) Successful in 1m10s Details
Django CI/CD / build-and-push (push) Successful in 2m1s Details
2024-11-10 00:05:33 +01:00
Lukáš Kucharczyk a53818257c Fix being unable to override c-vars from render_from_string 2024-11-10 00:05:11 +01:00
Lukáš Kucharczyk 2d3ea714c4 Extend session search 2024-11-09 23:52:09 +01:00
Lukáš Kucharczyk 832bb48983 Device: safe long type names directly in database 2024-11-09 23:51:28 +01:00
Lukáš Kucharczyk c6b1badf39 add session search
Django CI/CD / test (push) Successful in 1m10s Details
Django CI/CD / build-and-push (push) Successful in 2m16s Details
2024-11-09 21:34:01 +00:00
Lukáš Kucharczyk a3ed93c154 handle non-existent icons
Django CI/CD / test (push) Successful in 1m15s Details
Django CI/CD / build-and-push (push) Has been cancelled Details
2024-11-09 21:30:13 +00:00
Lukáš Kucharczyk cf503a7b7d improve devcontainer
Django CI/CD / test (push) Successful in 1m7s Details
Django CI/CD / build-and-push (push) Successful in 2m26s Details
2024-11-09 11:56:20 +01:00
Lukáš Kucharczyk d81df6452a add dev container
Django CI/CD / test (push) Successful in 1m15s Details
Django CI/CD / build-and-push (push) Successful in 2m26s Details
2024-11-09 11:22:21 +01:00
Lukáš Kucharczyk d9290373b0
also sort purchases by created_at
Django CI/CD / test (push) Successful in 59s Details
Django CI/CD / build-and-push (push) Successful in 2m24s Details
2024-10-18 09:50:10 +02:00
Lukáš Kucharczyk f8d621e710
fix stat dropdown
Django CI/CD / test (push) Successful in 1m16s Details
Django CI/CD / build-and-push (push) Successful in 2m7s Details
2024-10-16 18:31:12 +02:00
Lukáš Kucharczyk 9992d9c9bd
set edition platform to unspecified if none 2024-10-16 18:06:40 +02:00
Lukáš Kucharczyk 2ae81bb00f
update django-cotton to 1.2.1 2024-10-16 17:49:55 +02:00
Lukáš Kucharczyk 993abb4710
editorconfig: do not add newline to HTML 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk 23502eab85
do not throw error when no stats to calculate 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk c517d735c7
use unified dateformat more 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk 19056f846e
view_game: display timezone-aware time for end timestamp 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk 0759ad0804
make purchase price a float 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk 228fc2bf5f
avoid exception on game overview when sessions are 0 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk a5a7041920
rename icon 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk fbd829f70e
order platforms by name 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk 4873f25248
remove css cruft 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk 3578f1707f
add more icons 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk b74ccb6eaa
Remove extraneous statement 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk b0b1bb2d42
add icon field to platform, use everywhere 2024-10-16 17:45:23 +02:00
Lukáš Kucharczyk c40764a02f
fix bug in Component
A leftover from when there was only the A component function,
this bug was not found earlier because we used
templates instead of tags most of the time.
2024-09-14 10:40:03 +02:00
Lukáš Kucharczyk 649351efde
implement platform icons
Django CI/CD / test (push) Successful in 1m1s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-09-14 06:42:34 +02:00
Lukáš Kucharczyk 698c8966c0
add purchase date to game view
Django CI/CD / test (push) Successful in 1m9s Details
Django CI/CD / build-and-push (push) Successful in 2m8s Details
2024-09-11 11:40:17 +02:00
Lukáš Kucharczyk 7f6584ecf7
finish purchase from list 2024-09-11 11:39:54 +02:00
Lukáš Kucharczyk 540f5ee42c
align last column to the right 2024-09-11 11:39:48 +02:00
Lukáš Kucharczyk 1c73268258
redirect to purchase list after modifying purchase 2024-09-10 14:50:49 +02:00
Lukáš Kucharczyk 3063a3d143
refund purchase from list 2024-09-10 14:50:02 +02:00
Lukáš Kucharczyk b589199ca6
drop purchase from list 2024-09-10 14:46:50 +02:00
Lukáš Kucharczyk 2fc661dade
re-add button titles 2024-09-10 14:46:10 +02:00
Lukáš Kucharczyk 1f535a6e84
formatting 2024-09-10 14:26:06 +02:00
Lukáš Kucharczyk a9c1135639
improve layout
Django CI/CD / test (push) Successful in 1m7s Details
Django CI/CD / build-and-push (push) Successful in 2m12s Details
2024-09-09 11:25:29 +02:00
Lukáš Kucharczyk 58cfaca1a9
add table header actions
Django CI/CD / test (push) Successful in 1m3s Details
Django CI/CD / build-and-push (push) Successful in 2m23s Details
2024-09-08 21:03:37 +02:00
Lukáš Kucharczyk c1b3493c80
Merge calculated and manual duration
Django CI/CD / test (push) Successful in 1m2s Details
Django CI/CD / build-and-push (push) Successful in 1m55s Details
2024-09-07 23:35:59 +02:00
Lukáš Kucharczyk a1df8720f5
Fix missing variable reference
Django CI/CD / test (push) Successful in 1m11s Details
Django CI/CD / build-and-push (push) Successful in 2m3s Details
2024-09-07 23:20:17 +02:00
Lukáš Kucharczyk 5a852bc2b9
tailwind: define accent and background colors
Django CI/CD / test (push) Successful in 55s Details
Django CI/CD / build-and-push (push) Successful in 2m6s Details
2024-09-04 21:59:29 +02:00
Lukáš Kucharczyk 8ab9bfeeeb
update deps 2024-09-04 21:59:06 +02:00
Lukáš Kucharczyk 5eee7176d4
add streak-releted basic functionality 2024-09-04 21:58:56 +02:00
Lukáš Kucharczyk 98c9c1faee
move time-related functionality out of views.general
Django CI/CD / test (push) Successful in 58s Details
Django CI/CD / build-and-push (push) Successful in 1m52s Details
2024-09-04 21:55:22 +02:00
Lukáš Kucharczyk 645ffa0dad
update styles
Django CI/CD / test (push) Successful in 1m3s Details
Django CI/CD / build-and-push (push) Successful in 2m4s Details
2024-09-03 22:39:25 +02:00
Lukáš Kucharczyk 4358708262
add links to add a new X to: game, edition, purchase, session, device, platform
Django CI/CD / test (push) Successful in 55s Details
Django CI/CD / build-and-push (push) Successful in 1m57s Details
2024-09-03 15:48:58 +02:00
Lukáš Kucharczyk c738245783
Properly display non-game type names
Django CI/CD / test (push) Successful in 1m8s Details
Django CI/CD / build-and-push (push) Successful in 1m55s Details
2024-09-02 23:52:28 +02:00
Lukáš Kucharczyk 57184ceea0
add one more breakpoint to better utilize smaller screens
Django CI/CD / test (push) Successful in 53s Details
Django CI/CD / build-and-push (push) Successful in 1m56s Details
2024-09-02 23:44:18 +02:00
Lukáš Kucharczyk c2b9409562
update styles
Django CI/CD / test (push) Successful in 53s Details
Django CI/CD / build-and-push (push) Successful in 1m52s Details
2024-09-02 20:14:52 +02:00
Lukáš Kucharczyk e067e65bce
linkify game, edition, purchase, session references
Django CI/CD / test (push) Successful in 1m0s Details
Django CI/CD / build-and-push (push) Has been cancelled Details
also add link styles for links in a table row
2024-09-02 20:04:21 +02:00
Lukáš Kucharczyk b8258e2937
replace slippers with django-cotton
Django CI/CD / test (push) Successful in 59s Details
Django CI/CD / build-and-push (push) Successful in 2m4s Details
main reason: slippers cannot pass request via context inside its
components, making it annoying to use template takes that take request.
more reasons: not actively worked on, no named slots, having to define
components in components.yaml + new components do not get registered
without restarting server
2024-09-02 17:43:41 +02:00
Lukáš Kucharczyk 9af4c79947
improve game view
Django CI/CD / test (push) Successful in 56s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-19 21:58:43 +02:00
Lukáš Kucharczyk d8b8182b91
fix table top rounding 2024-08-13 08:36:40 +02:00
Lukáš Kucharczyk 2fd44c1f53
separate views out 2/2
Django CI/CD / test (push) Successful in 57s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-12 21:52:26 +02:00
Lukáš Kucharczyk c3f99d124c
update base.css 2024-08-12 21:42:56 +02:00
Lukáš Kucharczyk 51f5b9fceb
update ruff path 2024-08-12 21:42:47 +02:00
Lukáš Kucharczyk 973f4416de
separate views out 1/2 2024-08-12 21:42:34 +02:00
Lukáš Kucharczyk a84209eb81
sort by timestamp
Django CI/CD / test (push) Successful in 51s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-11 21:39:14 +02:00
Lukáš Kucharczyk 498cd69328
improve display of game names, durations 2024-08-11 20:29:47 +02:00
Lukáš Kucharczyk b28c42d945
delete platform
Django CI/CD / test (push) Successful in 51s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-11 20:21:44 +02:00
Lukáš Kucharczyk 3099f02145
list editions 2024-08-11 20:21:27 +02:00
Lukáš Kucharczyk 74b9d0421c
list platforms, fix editing platform 2024-08-11 18:34:50 +02:00
Lukáš Kucharczyk c61adad180
list games 2024-08-11 18:21:11 +02:00
Lukáš Kucharczyk 298ecb4092
formatting 2024-08-11 17:58:35 +02:00
Lukáš Kucharczyk 020e12e20b
remove session recent filter 2024-08-11 17:58:08 +02:00
Lukáš Kucharczyk 6ef56bfed5
list, edit, and delete devices 2024-08-11 17:53:36 +02:00
Lukáš Kucharczyk fda4913c97
add ruff to shell.nix
Django CI/CD / test (push) Successful in 56s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-11 17:24:50 +02:00
Lukáš Kucharczyk e85b32e22f
update styles 2024-08-11 17:24:33 +02:00
Lukáš Kucharczyk 2d6d6d24a4
formatting 2024-08-11 17:24:26 +02:00
Lukáš Kucharczyk 00993a85db
remove black 2024-08-11 17:24:19 +02:00
Lukáš Kucharczyk 4f7e708255
vscode: replace black with ruff 2024-08-11 17:23:59 +02:00
Lukáš Kucharczyk 238e4839e0
formatting 2024-08-11 17:23:28 +02:00
Lukáš Kucharczyk b0ad806a93
fix version_date 2024-08-11 17:23:18 +02:00
Lukáš Kucharczyk 453b4fd922
add manage -> sessions 2024-08-11 17:22:58 +02:00
Lukáš Kucharczyk bb0d24809e
make sure titles are truncated 2024-08-11 17:13:31 +02:00
Lukáš Kucharczyk 3abd4c4af9
reuse existing variable 2024-08-09 13:59:14 +02:00
Lukáš Kucharczyk 2e5e77b4e5
replace navbar
Django CI/CD / test (push) Successful in 1m12s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-09 13:14:18 +02:00
Lukáš Kucharczyk e79cf5de7a
fix non-working views 2024-08-09 13:12:47 +02:00
Lukáš Kucharczyk c15eaca205
only overflow table, not paginator, improve styling
Django CI/CD / test (push) Successful in 1m5s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-09 12:42:54 +02:00
Lukáš Kucharczyk 496c99ccf1
formatting 2024-08-09 12:23:49 +02:00
Lukáš Kucharczyk 992622e8d1
make it possible to not use paginator when limit = 0 2024-08-09 12:23:40 +02:00
Lukáš Kucharczyk cabe36c822
add dark/light mode toggle 2024-08-09 12:22:26 +02:00
Lukáš Kucharczyk d84b67c460
improve pagination 2024-08-09 11:47:10 +02:00
Lukáš Kucharczyk 1c28950b53
add pagination
Django CI/CD / test (push) Successful in 1m1s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-08 22:54:15 +02:00
Lukáš Kucharczyk b54bcdd9e9
remove cruft 2024-08-08 21:20:17 +02:00
Lukáš Kucharczyk 9ec6c958c8
remove unnecessary styles 2024-08-08 21:20:08 +02:00
Lukáš Kucharczyk 25deac6ea9
add more types 2024-08-08 21:19:43 +02:00
Lukáš Kucharczyk a5ac10b20d
use model variables for foreign keys where possible 2024-08-08 20:22:25 +02:00
Lukáš Kucharczyk 3de40ccad3
create purchase list without paging 2024-08-08 20:17:43 +02:00
Lukáš Kucharczyk 6a5dc9b62c
even more formatting 2024-08-08 15:08:50 +02:00
Lukáš Kucharczyk b6014a72e0
.gitignore: add .direnv 2024-08-08 14:49:09 +02:00
Lukáš Kucharczyk 245b47b8b3
improve shell.nix
do not let poetry manage venvs
no need to override python3
2024-08-08 14:48:58 +02:00
Lukáš Kucharczyk e33f23c18f
add .envrc 2024-08-08 14:48:20 +02:00
Lukáš Kucharczyk 33012bc328
vscode: add extensions and settings 2024-08-08 14:48:10 +02:00
Lukáš Kucharczyk 447bd4820c
reformat with djlint --reformat 2024-08-08 14:47:51 +02:00
Lukáš Kucharczyk 72e89dae77
remove cruft
Django CI/CD / test (push) Successful in 1m7s Details
Django CI/CD / build-and-push (push) Successful in 2m0s Details
2024-08-08 09:47:06 +02:00
Lukáš Kucharczyk 1cd0a8c0fb
add shell.nix 2024-08-08 09:27:51 +02:00
Lukáš Kucharczyk a9a430f856
change vscode settings 2024-08-08 09:27:36 +02:00
Lukáš Kucharczyk 0ee4c50a24
update dependencies
Django CI/CD / test (push) Successful in 1m5s Details
Django CI/CD / build-and-push (push) Successful in 2m1s Details
2024-08-08 09:17:09 +02:00
Lukáš Kucharczyk 714f0d97a9
Reformat
Django CI/CD / test (push) Successful in 1m0s Details
Django CI/CD / build-and-push (push) Successful in 2m10s Details
2024-08-04 22:40:43 +02:00
Lukáš Kucharczyk d622ddfbf3
Add all-time stats 2024-08-04 22:40:37 +02:00
Lukáš Kucharczyk 86fd40cc4a
Do not save non-durations as manual
Django CI/CD / test (push) Successful in 1m56s Details
Django CI/CD / build-and-push (push) Successful in 2m23s Details
2024-07-23 09:51:15 +02:00
Lukáš Kucharczyk e174850262
Update deps
Django CI/CD / test (push) Successful in 1m3s Details
Django CI/CD / build-and-push (push) Successful in 1m56s Details
2024-07-11 13:28:09 +02:00
Lukáš Kucharczyk 6328d835ee
Fix formatting 2024-07-09 23:04:14 +02:00
Lukáš Kucharczyk 34d42e2af5
Fix list session links
Django CI/CD / test (push) Successful in 1m2s Details
Django CI/CD / build-and-push (push) Successful in 2m1s Details
2024-07-09 23:03:52 +02:00
Lukáš Kucharczyk e19caf47bf
Make game overview more appealing
Django CI/CD / build-and-push (push) Blocked by required conditions Details
Django CI/CD / test (push) Has been cancelled Details
2024-07-09 23:03:03 +02:00
Lukáš Kucharczyk 72998ffc02
Fix incorrect font name 2024-07-09 20:38:03 +02:00
Lukáš Kucharczyk ba44814474
Improve game links
Django CI/CD / test (push) Successful in 1m6s Details
Django CI/CD / build-and-push (push) Successful in 1m56s Details
2024-07-09 19:40:47 +02:00
Lukáš Kucharczyk 86f8fde8fa
Avoid errors when displaying game overview with zero sessions
Django CI/CD / test (push) Successful in 1m4s Details
Django CI/CD / build-and-push (push) Successful in 2m10s Details
2024-07-09 07:32:49 +02:00
Lukáš Kucharczyk 811fec4b11
Ignore manual sessions when calculating session average
Django CI/CD / test (push) Successful in 1m1s Details
Django CI/CD / build-and-push (push) Successful in 1m58s Details
2024-07-02 17:27:44 +02:00
Lukáš Kucharczyk fe6cf2758c
make dev does not ignore warnings
Django CI/CD / test (push) Successful in 1m9s Details
Django CI/CD / build-and-push (push) Successful in 1m55s Details
2024-06-26 18:35:05 +02:00
Lukáš Kucharczyk 1e1372ca56
Update Python deps 2024-06-26 18:34:38 +02:00
Lukáš Kucharczyk d91c0bc255
Update npm deps
Django CI/CD / test (push) Successful in 1m1s Details
Django CI/CD / build-and-push (push) Successful in 1m57s Details
2024-06-26 17:39:53 +02:00
Lukáš Kucharczyk a14f5d3ae5
Add npm-check-updates 2024-06-26 17:39:39 +02:00
Lukáš Kucharczyk 4ac13053d5
Use new Poetry section for main deps 2024-06-26 17:31:43 +02:00
Lukáš Kucharczyk e9311225e7
Make setting up and developing easier 2024-06-26 17:18:58 +02:00
Lukáš Kucharczyk 44c70a5ee7
Formatting
Django CI/CD / test (push) Successful in 1m21s Details
Django CI/CD / build-and-push (push) Successful in 1m57s Details
2024-06-03 18:19:11 +02:00
Lukáš Kucharczyk cd804f2c77
Sort url paths 2024-06-03 18:18:58 +02:00
Lukáš Kucharczyk 15997bd5af
Re-enable delete session delete view 2024-06-03 18:07:10 +02:00
Lukáš Kucharczyk 880ea93424
Unify url path names 2024-06-03 18:05:34 +02:00
Lukáš Kucharczyk dc1a9d5c4f
Make sure attribute chains are evaluated safely 2024-05-30 14:26:38 +02:00
Lukáš Kucharczyk 51c25659a9
djhtml formatting
Django CI/CD / test (push) Successful in 1m5s Details
Django CI/CD / build-and-push (push) Successful in 1m57s Details
2024-04-30 12:04:16 +02:00
Lukáš Kucharczyk 973dda59d2
Improve game overview header 2024-04-30 12:03:52 +02:00
Lukáš Kucharczyk 64edca9ffa
Use display name in session list
Django CI/CD / test (push) Successful in 52s Details
Django CI/CD / build-and-push (push) Successful in 1m52s Details
2024-04-29 19:21:05 +02:00
Lukáš Kucharczyk 86e25b84ab
Allow deleting purchases
Django CI/CD / test (push) Successful in 53s Details
Django CI/CD / build-and-push (push) Successful in 1m47s Details
2024-04-29 16:35:54 +02:00
Lukáš Kucharczyk edc1d062bc
Update gunicorn to version 22.0.0
Django CI/CD / test (push) Successful in 1m46s Details
Django CI/CD / build-and-push (push) Successful in 2m28s Details
2024-04-17 12:28:10 +02:00
Lukáš Kucharczyk 12a517c9fa
Update sqlparse to version 0.5
Django CI/CD / test (push) Successful in 1m2s Details
Django CI/CD / build-and-push (push) Successful in 1m58s Details
2024-04-16 11:44:24 +02:00
Lukáš Kucharczyk c1882f66e3
Improve purchase name consistency on stats page
Django CI/CD / test (push) Successful in 1m0s Details
Django CI/CD / build-and-push (push) Successful in 2m0s Details
2024-04-15 13:55:17 +02:00
Lukáš Kucharczyk 1e87e67eb1
Reformat HTML with djhtml
Django CI/CD / test (push) Successful in 1m4s Details
Django CI/CD / build-and-push (push) Successful in 1m36s Details
2024-04-04 11:27:33 +02:00
Lukáš Kucharczyk 84552e088b
Update more dependencies 2024-04-04 11:27:14 +02:00
Lukáš Kucharczyk 79dc8ae25c
Update black
Django CI/CD / test (push) Failing after 44s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-04-04 11:09:09 +02:00
Lukáš Kucharczyk cee06e4f64
Update dependencies
Django CI/CD / test (push) Successful in 47s Details
Django CI/CD / build-and-push (push) Successful in 1m47s Details
2024-04-04 10:46:59 +02:00
Lukáš Kucharczyk d9b5f0eab2
stats: add monthly playtimes
Django CI/CD / test (push) Successful in 58s Details
Django CI/CD / build-and-push (push) Successful in 1m54s Details
2024-04-02 08:18:58 +02:00
Lukáš Kucharczyk ff28600710
Fix timestamp minutes on game page
Django CI/CD / test (push) Successful in 56s Details
Django CI/CD / build-and-push (push) Successful in 2m2s Details
Fixed #72
2024-03-27 14:38:00 +01:00
Lukáš Kucharczyk 7517bf5f37
Add stats for dropped purchases
Django CI/CD / test (push) Successful in 1m2s Details
Django CI/CD / build-and-push (push) Successful in 1m57s Details
2024-03-10 22:48:46 +01:00
Lukáš Kucharczyk 780a04d13f
Do not edit sort_name invisibly
Django CI/CD / test (push) Successful in 1m0s Details
Django CI/CD / build-and-push (push) Successful in 2m0s Details
Fixes #64
2024-03-04 16:50:37 +01:00
Lukáš Kucharczyk fd04e9fa77
Sort prefetch instead of the result
Django CI/CD / test (push) Successful in 57s Details
Django CI/CD / build-and-push (push) Successful in 1m52s Details
order_by on the final queryset results in duplicating editions, 1 for each purchase
to fix it we sort the thing we actually want to sort - non-game purchases - in a prefetch earlier in the code
2024-02-18 12:31:03 +01:00
Lukáš Kucharczyk 18902aedac
Reformat
Django CI/CD / test (push) Successful in 56s Details
Django CI/CD / build-and-push (push) Successful in 1m53s Details
2024-02-18 09:03:35 +01:00
Lukáš Kucharczyk f9e37e9b1e
Sort purchases also by date purchased
Django CI/CD / test (push) Successful in 1m4s Details
Django CI/CD / build-and-push (push) Has been cancelled Details
2024-02-18 09:02:08 +01:00
Lukáš Kucharczyk c747cd1fd8
Reformat
Django CI/CD / test (push) Successful in 55s Details
Django CI/CD / build-and-push (push) Successful in 1m33s Details
2024-02-10 09:50:53 +01:00
Lukáš Kucharczyk 6a5457191a
Add logout button 2024-02-10 09:48:09 +01:00
Lukáš Kucharczyk 76f6d0c377
Fix CSS bug 2024-02-10 09:03:16 +01:00
Lukáš Kucharczyk ae93703c08
Remove login_required from clone_session_by_id
Django CI/CD / test (push) Successful in 53s Details
Django CI/CD / build-and-push (push) Successful in 1m35s Details
2024-02-09 22:27:28 +01:00
Lukáš Kucharczyk c55176090c
Temporarily disable tests
Django CI/CD / test (push) Successful in 52s Details
Django CI/CD / build-and-push (push) Successful in 1m41s Details
2024-02-09 22:08:49 +01:00
Lukáš Kucharczyk 081b8a92de
Require login by default
Django CI/CD / test (push) Failing after 1m1s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-02-09 22:03:24 +01:00
Lukáš Kucharczyk d02a60675f
Render notes as Markdown
Django CI/CD / test (push) Failing after 1m5s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-02-09 21:37:39 +01:00
Lukáš Kucharczyk 4670568acb
Add .DS_Store to .gitignore
Django CI/CD / test (push) Successful in 1m4s Details
Django CI/CD / build-and-push (push) Successful in 1m34s Details
2024-01-15 22:09:29 +01:00
Lukáš Kucharczyk 4b75a1dea9
Increase session count on game overview when starting a new session
Django CI/CD / test (push) Successful in 50s Details
Django CI/CD / build-and-push (push) Successful in 1m32s Details
2024-01-15 21:41:25 +01:00
Lukáš Kucharczyk e2b7ff2e15
Remove cruft
Django CI/CD / test (push) Successful in 53s Details
Django CI/CD / build-and-push (push) Successful in 1m30s Details
2024-01-15 19:17:27 +01:00
Lukáš Kucharczyk b94aa49fc3
Fix title not being displayed on the Recent sessions page 2024-01-15 19:17:24 +01:00
Lukáš Kucharczyk 73a92e5636
Mark refunded purchases red
Django CI/CD / test (push) Successful in 1m3s Details
Django CI/CD / build-and-push (push) Successful in 1m36s Details
2024-01-15 11:19:18 +01:00
Lukáš Kucharczyk 42b28665e1
Version 1.5.2
Django CI/CD / test (push) Successful in 1m15s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-01-14 21:28:38 +01:00
Lukáš Kucharczyk 6ba187f8e4
Make it possible to end session from game overview 2024-01-14 21:27:18 +01:00
Lukáš Kucharczyk a765fd8d00
More game overview optimizations
Django CI/CD / test (push) Successful in 1m15s Details
Django CI/CD / build-and-push (push) Successful in 1m37s Details
2024-01-14 17:04:06 +01:00
Lukáš Kucharczyk 854e3cc54a
Do not copy notes when cloning session
Django CI/CD / test (push) Successful in 1m13s Details
Django CI/CD / build-and-push (push) Successful in 1m45s Details
2024-01-14 13:05:45 +01:00
Lukáš Kucharczyk 2d8eb32e90
Remove cruft
Django CI/CD / test (push) Successful in 1m3s Details
Django CI/CD / build-and-push (push) Successful in 1m29s Details
2024-01-10 17:13:59 +01:00
Lukáš Kucharczyk 1f1ed79ee5
Optimize session listing 2024-01-10 16:57:01 +01:00
Lukáš Kucharczyk 01fd7bad69
Remove cruft 2024-01-10 15:55:08 +01:00
Lukáš Kucharczyk 44f49e5974
Session list: speed up starting new sessions
Django CI/CD / test (push) Successful in 1m1s Details
Django CI/CD / build-and-push (push) Successful in 1m33s Details
2024-01-10 15:54:09 +01:00
Lukáš Kucharczyk 0cf3411f63
Make ending session from session list faster
Django CI/CD / test (push) Successful in 1m16s Details
Django CI/CD / build-and-push (push) Successful in 1m40s Details
2024-01-10 15:12:45 +01:00
Lukáš Kucharczyk aa669710e1
Change update_session to template partial
Django CI/CD / test (push) Failing after 1m3s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-01-10 14:10:13 +01:00
Lukáš Kucharczyk 242833f886
Make it possible to drop purchases, or consider them infinite
Django CI/CD / build-and-push (push) Blocked by required conditions Details
Django CI/CD / test (push) Has been cancelled Details
2024-01-03 22:35:39 +01:00
Lukáš Kucharczyk 0cdfd3c298
Stats: optimize
Django CI/CD / test (push) Successful in 56s Details
Django CI/CD / build-and-push (push) Successful in 1m33s Details
2024-01-03 21:35:47 +01:00
Lukáš Kucharczyk a98b4839dd
Fix wrong unfinished purchases calculation
Django CI/CD / test (push) Successful in 1m10s Details
Django CI/CD / build-and-push (push) Successful in 1m42s Details
2024-01-02 20:03:59 +01:00
Lukáš Kucharczyk 1999f13cf2
stats: add first and last play
Django CI/CD / test (push) Successful in 1m12s Details
Django CI/CD / build-and-push (push) Successful in 1m36s Details
2024-01-01 18:42:14 +01:00
Lukáš Kucharczyk 8466f67c86
Fix errors caused by empty values
Django CI/CD / test (push) Successful in 1m10s Details
Django CI/CD / build-and-push (push) Successful in 1m41s Details
2024-01-01 18:21:50 +01:00
Lukáš Kucharczyk d9fbb4b896
Add title to stats page
Django CI/CD / build-and-push (push) Blocked by required conditions Details
Django CI/CD / test (push) Has been cancelled Details
2023-12-15 10:58:15 +01:00
Lukáš Kucharczyk 4ff3692606
Remove duplicate block
Django CI/CD / test (push) Successful in 1m34s Details
Django CI/CD / build-and-push (push) Successful in 1m29s Details
2023-11-30 18:05:52 +01:00
Lukáš Kucharczyk 8289c48896
Fix CI error
Django CI/CD / test (push) Successful in 1m14s Details
Django CI/CD / build-and-push (push) Successful in 1m24s Details
2023-11-30 17:44:31 +01:00
Lukáš Kucharczyk d1b9202337
Update poetry.lock
Django CI/CD / test (push) Failing after 1m15s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2023-11-30 17:35:44 +01:00
Lukáš Kucharczyk fde93cb875
Organize better 2023-11-30 17:35:44 +01:00
Lukáš Kucharczyk d1c3ac6079
Revert "Move GraphQL to separata app"
This reverts commit 6ac4209492.
2023-11-30 17:35:44 +01:00
Lukáš Kucharczyk d921c2d8a6
Revert "Add UpdateGameMutation"
This reverts commit e9e61403a9.
2023-11-30 17:35:44 +01:00
Lukáš Kucharczyk 52513e1ed8
Add UpdateGameMutation 2023-11-30 17:35:44 +01:00
Lukáš Kucharczyk cb380814a7
Move GraphQL to separata app 2023-11-30 17:35:44 +01:00
Lukáš Kucharczyk 5ef8c07f30
Initial working API 2023-11-30 17:35:44 +01:00
Lukáš Kucharczyk 9573c3b8ff
Better formatting
Django CI/CD / test (push) Successful in 1m11s Details
Django CI/CD / build-and-push (push) Successful in 1m22s Details
2023-11-29 22:26:43 +01:00
Lukáš Kucharczyk c4354a1380
Update poetry.lock
Django CI/CD / test (push) Successful in 1m3s Details
Django CI/CD / build-and-push (push) Successful in 1m14s Details
2023-11-29 22:22:23 +01:00
Lukáš Kucharczyk a245b6ff0f
Fix longest session formatting
Django CI/CD / test (push) Successful in 1m1s Details
Django CI/CD / build-and-push (push) Successful in 1m24s Details
Put space between hours and minutes
2023-11-29 09:08:10 +01:00
Lukáš Kucharczyk 6329d380b7
Editions are unique if name, platform OR year is different
Django CI/CD / test (push) Successful in 1m25s Details
Django CI/CD / build-and-push (push) Successful in 1m20s Details
2023-11-28 14:44:11 +01:00
Lukáš Kucharczyk 76fbc39fed
Disable hx-boost
Django CI/CD / test (push) Successful in 1m1s Details
Django CI/CD / build-and-push (push) Successful in 1m17s Details
2023-11-28 14:29:56 +01:00
Lukáš Kucharczyk 4b6734c173
Add width, height, alt to images 2023-11-28 14:29:11 +01:00
Lukáš Kucharczyk b505b5b430
Stats: add highest session average
Django CI/CD / test (push) Successful in 1m2s Details
Django CI/CD / build-and-push (push) Successful in 1m23s Details
2023-11-21 21:57:17 +01:00
Lukáš Kucharczyk 87553ebdc5
Add djlint pre-commit hook
Django CI/CD / test (push) Successful in 1m13s Details
Django CI/CD / build-and-push (push) Successful in 1m15s Details
2023-11-21 18:19:25 +01:00
Lukáš Kucharczyk ba4fc0cac5
Do not trigger hx-boost for non-submit buttons
Django CI/CD / test (push) Successful in 1m7s Details
Django CI/CD / build-and-push (push) Successful in 1m20s Details
2023-11-21 18:12:58 +01:00
Lukáš Kucharczyk 8cb0276215
Use better way to find out if model record exists 2023-11-21 18:03:01 +01:00
Lukáš Kucharczyk f9a51ee83d
Remove experimental layout 2023-11-21 18:03:01 +01:00
Lukáš Kucharczyk c9deba7d65
Add stats for most sessions, longest session 2023-11-21 17:02:44 +01:00
Lukáš Kucharczyk c55fbe86b5
Support HTMX with Django Debug Toolbar 2023-11-21 16:59:21 +01:00
Lukáš Kucharczyk 0e93993498
Add django-debug-toolbar 2023-11-21 14:42:37 +01:00
Lukáš Kucharczyk 9fccdfbff0
Make links colorful 2023-11-20 23:07:11 +01:00
Lukáš Kucharczyk d78139a5b3
Display finished DLCs in stats better
Django CI/CD / test (push) Successful in 1m6s Details
Django CI/CD / build-and-push (push) Successful in 1m26s Details
2023-11-20 21:56:16 +01:00
Lukáš Kucharczyk 7dc43fbf77
Fix wrong export name 2023-11-20 21:54:51 +01:00
Lukáš Kucharczyk 5442926457
Allow DLC to have date_finished set
Django CI/CD / test (push) Successful in 1m6s Details
Django CI/CD / build-and-push (push) Successful in 1m35s Details
2023-11-20 21:42:23 +01:00
Lukáš Kucharczyk db4c635260
Remote JavaScript files 2023-11-20 21:25:21 +01:00
Lukáš Kucharczyk 4a1d08d4df
Fix CI, re-add test step
Django CI/CD / test (push) Successful in 1m0s Details
Django CI/CD / build-and-push (push) Successful in 1m45s Details
2023-11-18 11:10:33 +01:00
140 changed files with 7580 additions and 2057 deletions

View File

@ -0,0 +1,25 @@
{
"name": "Django Time Tracker",
"dockerFile": "../devcontainer.Dockerfile",
"customizations": {
"vscode": {
"settings": {
"python.pythonPath": "/usr/local/bin/python",
"python.defaultInterpreterPath": "/usr/local/bin/python",
"terminal.integrated.defaultProfile.linux": "bash"
},
"extensions": [
"ms-python.python",
"ms-python.debugpy",
"ms-python.vscode-pylance",
"ms-azuretools.vscode-docker",
"batisteo.vscode-django",
"charliermarsh.ruff",
"bradlc.vscode-tailwindcss",
"EditorConfig.EditorConfig"
]
}
},
"forwardPorts": [8000],
"postCreateCommand": "poetry install && poetry run python manage.py migrate && npm install && make dev",
}

View File

@ -15,3 +15,6 @@ indent_size = 4
[**/*.js] [**/*.js]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
[*.html]
insert_final_newline = false

1
.envrc Normal file
View File

@ -0,0 +1 @@
use nix

View File

@ -1,25 +0,0 @@
name: Django CI/CD
on:
push:
branches: [ main ]
paths-ignore: [ 'README.md' ]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
registry.kucharczyk.xyz/timetracker:latest
registry.kucharczyk.xyz/timetracker:${{ env.VERSION_NUMBER }}
env:
VERSION_NUMBER: 1.5.1

36
.github/workflows/build-docker.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Django CI/CD
on:
push:
paths-ignore: [ 'README.md' ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: 3.12
- run: |
python -m pip install poetry
poetry install
poetry env info
poetry run python manage.py migrate
# PROD=1 poetry run pytest
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
registry.kucharczyk.xyz/timetracker:latest
registry.kucharczyk.xyz/timetracker:${{ env.VERSION_NUMBER }}
env:
VERSION_NUMBER: 1.5.1

5
.gitignore vendored
View File

@ -1,9 +1,12 @@
__pycache__ __pycache__
.mypy_cache .mypy_cache
.pytest_cache .pytest_cache
.venv .venv/
node_modules node_modules
package-lock.json package-lock.json
db.sqlite3 db.sqlite3
/static/ /static/
dist/ dist/
.DS_Store
.python-version
.direnv

View File

@ -1,10 +1,20 @@
repos: repos:
- repo: https://github.com/psf/black # disable due to incomaptible formatting between
rev: 22.12.0 # black and ruff
# TODO: replace with ruff when it works on NixOS
# - repo: https://github.com/psf/black
# rev: 24.8.0
# hooks:
# - id: black
# - repo: https://github.com/pycqa/isort
# rev: 5.13.2
# hooks:
# - id: isort
# name: isort (python)
- repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.34.0
hooks: hooks:
- id: black - id: djlint-reformat-django
- repo: https://github.com/pycqa/isort args: ["--ignore", "H011"]
rev: 5.12.0 - id: djlint-django
hooks: args: ["--ignore", "H011"]
- id: isort
name: isort (python)

11
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"recommendations": [
"charliermarsh.ruff",
"ms-python.python",
"ms-python.vscode-pylance",
"ms-python.debugpy",
"batisteo.vscode-django",
"bradlc.vscode-tailwindcss",
"EditorConfig.EditorConfig"
]
}

26
.vscode/settings.json vendored
View File

@ -4,8 +4,30 @@
], ],
"python.testing.unittestEnabled": false, "python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true, "python.testing.pytestEnabled": true,
"python.analysis.typeCheckingMode": "basic", "python.analysis.typeCheckingMode": "strict",
"[python]": { "[python]": {
"editor.defaultFormatter": "ms-python.black-formatter" "editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
}, },
},
"ruff.path": ["/nix/store/jaibb3v0rrnlw5ib54qqq3452yhp1xcb-ruff-0.5.7/bin/ruff"],
"tailwind-fold.supportedLanguages": [
"html",
"typescriptreact",
"javascriptreact",
"typescript",
"javascript",
"vue-html",
"vue",
"php",
"markdown",
"coffeescript",
"svelte",
"astro",
"erb",
"django-html"
]
} }

View File

@ -1,8 +1,39 @@
## Unreleased ## Unreleased
## New
* Render notes as Markdown
* Require login by default
* Add stats for dropped purchases, monthly playtimes
* Allow deleting purchases
* Add all-time stats
* Manage purchases
* Automatically convert purchase prices
## Improved ## Improved
* game overview: improve how editions and purchases are displayed * mark refunded purchases red on game overview
* increase session count on game overview when starting a new session
* game overview:
* sort purchases also by date purchased (on top of date released)
* improve header format, make it more appealing
* ignore manual sessions when calculating session average
* stats: improve purchase name consistency
* session list: use display name instead of sort name
* unify the appearance of game links, and make them expand to full size on hover
## Fixed
* Fix title not being displayed on the Recent sessions page
* Avoid errors when displaying game overview with zero sessions
## 1.5.2 / 2024-01-14 21:27+01:00
## Improved
* game overview:
* improve how editions and purchases are displayed
* make it possible to end session from overview
* add purchase: only allow choosing purchases of selected edition * add purchase: only allow choosing purchases of selected edition
* session list:
* starting and ending sessions is much faster/doest not reload the page
* listing sessions is much faster
## 1.5.1 / 2023-11-14 21:10+01:00 ## 1.5.1 / 2023-11-14 21:10+01:00

View File

@ -1,6 +1,6 @@
FROM python:3.12.0-slim-bullseye FROM python:3.12.0-slim-bullseye
ENV VERSION_NUMBER=1.5.1 \ ENV VERSION_NUMBER=1.5.2 \
PROD=1 \ PROD=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
PYTHONFAULTHANDLER=1 \ PYTHONFAULTHANDLER=1 \

View File

@ -3,6 +3,7 @@ all: css migrate
initialize: npm css migrate sethookdir loadplatforms initialize: npm css migrate sethookdir loadplatforms
HTMLFILES := $(shell find games/templates -type f) HTMLFILES := $(shell find games/templates -type f)
PYTHON_VERSION = 3.12
npm: npm:
npm install npm install
@ -10,17 +11,26 @@ npm:
css: common/input.css css: common/input.css
npx tailwindcss -i ./common/input.css -o ./games/static/base.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: makemigrations:
poetry run python manage.py makemigrations poetry run python manage.py makemigrations
migrate: makemigrations migrate: makemigrations
poetry run python manage.py migrate poetry run python manage.py migrate
dev: migrate init:
poetry run python manage.py runserver pyenv install -s $(PYTHON_VERSION)
pyenv local $(PYTHON_VERSION)
pip install poetry
poetry install
npm install
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"
caddy: caddy:
caddy run --watch caddy run --watch

View File

@ -1,3 +1,15 @@
# Timetracker # Timetracker
A simple game catalogue and play session tracker. A simple game catalogue and play session tracker.
# Development
The project uses `pyenv` to manage installed Python versions.
If you have `pyenv` installed, you can simply run:
```
make init
```
This will make sure the correct Python version is installed, and it will install all dependencies using `poetry`.
Afterwards, you can start the development server using `make dev`.

195
common/components.py Normal file
View File

@ -0,0 +1,195 @@
from random import choices as random_choices
from string import ascii_lowercase
from typing import Any, Callable
from django.template import TemplateDoesNotExist
from django.template.loader import render_to_string
from django.urls import NoReverseMatch, reverse
from django.utils.safestring import SafeText, mark_safe
from common.utils import truncate
HTMLAttribute = tuple[str, str | int | bool]
HTMLTag = str
def Component(
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
template: str = "",
tag_name: str = "",
) -> HTMLTag:
if not tag_name and not template:
raise ValueError("One of template or tag_name is required.")
if isinstance(children, str):
children = [children]
childrenBlob = "\n".join(children)
if len(attributes) == 0:
attributesBlob = ""
else:
attributesList = [f'{name}="{value}"' for name, value in attributes]
# make attribute list into a string
# and insert space between tag and attribute list
attributesBlob = f" {" ".join(attributesList)}"
tag: str = ""
if tag_name != "":
tag = f"<{tag_name}{attributesBlob}>{childrenBlob}</{tag_name}>"
elif template != "":
tag = render_to_string(
template,
{name: value for name, value in attributes}
| {"slot": mark_safe("\n".join(children))},
)
return mark_safe(tag)
def randomid(seed: str = "", length: int = 10) -> str:
return seed + "".join(random_choices(ascii_lowercase, k=length))
def Popover(
popover_content: str,
wrapped_content: str = "",
children: list[HTMLTag] = [],
attributes: list[HTMLAttribute] = [],
) -> str:
if not wrapped_content and not children:
raise ValueError("One of wrapped_content or children is required.")
id = randomid()
return Component(
attributes=attributes
+ [
("id", id),
("wrapped_content", wrapped_content),
("popover_content", popover_content),
],
children=children,
template="cotton/popover.html",
)
def PopoverTruncated(input_string: str, length: int = 30, ellipsis: str = "") -> str:
if (truncated := truncate(input_string, length, ellipsis)) != input_string:
return Popover(wrapped_content=truncated, popover_content=input_string)
else:
return input_string
def A(
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
url: str | Callable[..., Any] = "",
):
"""
Returns the HTML tag "a".
"url" can either be:
- URL (string)
- path name passed to reverse() (string)
- function
"""
additional_attributes = []
if url:
if type(url) is str:
try:
url_result = reverse(url)
except NoReverseMatch:
url_result = url
elif callable(url):
url_result = url()
else:
raise TypeError("'url' is neither str nor function.")
additional_attributes = [("href", url_result)]
return Component(
tag_name="a", attributes=attributes + additional_attributes, children=children
)
def Button(
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
size: str = "base",
icon: bool = False,
color: str = "blue",
):
return Component(
template="cotton/button.html",
attributes=attributes + [("size", size), ("icon", icon), ("color", color)],
children=children,
)
def Div(
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
):
return Component(tag_name="div", attributes=attributes, children=children)
def Input(
type: str = "text",
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
):
return Component(
tag_name="input", attributes=attributes + [("type", type)], children=children
)
def Form(
action="",
method="get",
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
):
return Component(
tag_name="form",
attributes=attributes + [("action", action), ("method", method)],
children=children,
)
def Icon(
name: str,
attributes: list[HTMLAttribute] = [],
):
try:
result = Component(template=f"cotton/icon/{name}.html", attributes=attributes)
except TemplateDoesNotExist:
result = Icon(name="unspecified", attributes=attributes)
return result
def LinkedNameWithPlatformIcon(name: str, game_id: int, platform: str) -> SafeText:
link = reverse("view_game", args=[int(game_id)])
a_content = Div(
[("class", "inline-flex gap-2 items-center")],
[
Icon(
platform.icon,
[("title", platform.name)],
),
PopoverTruncated(name),
],
)
return mark_safe(
A(
url=link,
children=[a_content],
),
)
def NameWithPlatformIcon(name: str, platform: str) -> SafeText:
content = Div(
[("class", "inline-flex gap-2 items-center")],
[
Icon(
platform.icon,
[("title", platform.name)],
),
PopoverTruncated(name),
],
)
return mark_safe(content)

View File

@ -4,7 +4,7 @@
@font-face { @font-face {
font-family: "IBM Plex Mono"; font-family: "IBM Plex Mono";
src: url("fonts/IBMPlexMono-regular.woff2") format("woff2"); src: url("fonts/IBMPlexMono-Regular.woff2") format("woff2");
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
} }
@ -23,12 +23,33 @@
font-style: normal; font-style: normal;
} }
@font-face {
font-family: "IBM Plex Serif";
src: url("fonts/IBMPlexSerif-Bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: "IBM Plex Sans Condensed";
src: url("fonts/IBMPlexSansCondensed-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
}
/* a:hover {
text-decoration-color: #ff4400;
color: rgb(254, 185, 160);
transition: all 0.2s ease-out;
} */
form label { form label {
@apply dark:text-slate-400; @apply dark:text-slate-400;
} }
.responsive-table { .responsive-table {
@apply dark:text-white mx-auto; @apply dark:text-white mx-auto table-fixed;
} }
.responsive-table tr:nth-child(even) { .responsive-table tr:nth-child(even) {
@ -49,11 +70,20 @@ form label {
} }
@layer utilities { @layer utilities {
.min-w-20char {
min-width: 20ch;
}
.max-w-20char { .max-w-20char {
max-width: 20ch; max-width: 20ch;
} }
.min-w-30char {
min-width: 30ch;
}
.max-w-30char {
max-width: 30ch;
}
.max-w-35char { .max-w-35char {
max-width: 40ch; max-width: 35ch;
} }
.max-w-40char { .max-w-40char {
max-width: 40ch; max-width: 40ch;
@ -96,14 +126,6 @@ textarea:disabled {
@apply mx-1; @apply mx-1;
} }
th {
@apply text-right;
}
th label {
@apply mr-4;
}
.basic-button-container { .basic-button-container {
@apply flex space-x-2 justify-center; @apply flex space-x-2 justify-center;
} }
@ -111,3 +133,39 @@ th label {
.basic-button { .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; @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;
} }
.markdown-content ul {
list-style-type: disc;
list-style-position: inside;
padding-left: 1em;
}
.markdown-content ol {
list-style-type: decimal;
list-style-position: inside;
padding-left: 1em;
}
.markdown-content ul,
.markdown-content ol {
list-style-position: outside;
padding-left: 1em;
}
.markdown-content ul ul,
.markdown-content ul ol,
.markdown-content ol ul,
.markdown-content ol ol {
list-style-type: circle;
margin-top: 0.5em;
margin-bottom: 0.5em;
padding-left: 1em;
}
/* .truncate-container {
@apply inline-block relative;
a {
@apply inline-block truncate max-w-20char transition-all group-hover:absolute group-hover:max-w-none group-hover:-top-8 group-hover:-left-6 group-hover:min-w-60 group-hover:px-6 group-hover:py-3.5 group-hover:bg-purple-600 group-hover:rounded-sm group-hover:outline-dashed group-hover:outline-purple-400 group-hover:outline-4;
}
} */

View File

@ -1,5 +1,15 @@
import re import re
from datetime import timedelta from datetime import date, datetime, timedelta
from django.utils import timezone
from common.utils import generate_split_ranges
dateformat: str = "%d/%m/%Y"
datetimeformat: str = "%d/%m/%Y %H:%M"
timeformat: str = "%H:%M"
durationformat: str = "%2.1H hours"
durationformat_manual: str = "%H hours"
def _safe_timedelta(duration: timedelta | int | None): def _safe_timedelta(duration: timedelta | int | None):
@ -12,7 +22,7 @@ def _safe_timedelta(duration: timedelta | int | None):
def format_duration( def format_duration(
duration: timedelta | int | None, format_string: str = "%H hours" duration: timedelta | int | float | None, format_string: str = "%H hours"
) -> str: ) -> str:
""" """
Format timedelta into the specified format_string. Format timedelta into the specified format_string.
@ -70,3 +80,90 @@ def format_duration(
rf"%\d*\.?\d*{pattern}", replacement, formatted_string rf"%\d*\.?\d*{pattern}", replacement, formatted_string
) )
return formatted_string return formatted_string
def local_strftime(datetime: datetime, format: str = datetimeformat) -> str:
return timezone.localtime(datetime).strftime(format)
def daterange(start: date, end: date, end_inclusive: bool = False) -> list[date]:
time_between: timedelta = end - start
if (days_between := time_between.days) < 1:
raise ValueError("start and end have to be at least 1 day apart.")
if end_inclusive:
print(f"{end_inclusive=}")
print(f"{days_between=}")
days_between += 1
print(f"{days_between=}")
return [start + timedelta(x) for x in range(days_between)]
def streak(datelist: list[date]) -> dict[str, int | tuple[date, date]]:
if len(datelist) == 1:
return {"days": 1, "dates": (datelist[0], datelist[0])}
else:
print(f"Processing {len(datelist)} dates.")
missing = sorted(
set(
datelist[0] + timedelta(x)
for x in range((datelist[-1] - datelist[0]).days)
)
- set(datelist)
)
print(f"{len(missing)} days missing.")
datelist_with_missing = sorted(datelist + missing)
ranges = list(generate_split_ranges(datelist_with_missing, missing))
print(f"{len(ranges)} ranges calculated.")
longest_consecutive_days = timedelta(0)
longest_range: tuple[date, date] = (date(1970, 1, 1), date(1970, 1, 1))
for start, end in ranges:
if (current_streak := end - start) > longest_consecutive_days:
longest_consecutive_days = current_streak
longest_range = (start, end)
return {"days": longest_consecutive_days.days + 1, "dates": longest_range}
def streak_bruteforce(datelist: list[date]) -> dict[str, int | tuple[date, date]]:
if (datelist_length := len(datelist)) == 0:
raise ValueError("Number of dates in the list is 0.")
datelist.sort()
current_streak = 1
current_start = datelist[0]
current_end = datelist[0]
current_date = datelist[0]
highest_streak = 1
highest_streak_daterange = (current_start, current_end)
def update_highest_streak():
nonlocal highest_streak, highest_streak_daterange
if current_streak > highest_streak:
highest_streak = current_streak
highest_streak_daterange = (current_start, current_end)
def reset_streak():
nonlocal current_start, current_end, current_streak
current_start = current_end = current_date
current_streak = 1
def increment_streak():
nonlocal current_end, current_streak
current_end = current_date
current_streak += 1
for i, datelist_item in enumerate(datelist, start=1):
current_date = datelist_item
if current_date == current_start or current_date == current_end:
continue
if current_date - timedelta(1) != current_end and i != datelist_length:
update_highest_streak()
reset_streak()
elif current_date - timedelta(1) == current_end and i == datelist_length:
increment_streak()
update_highest_streak()
else:
increment_streak()
return {"days": highest_streak, "dates": highest_streak_daterange}
def available_stats_year_range():
return range(datetime.now().year, 1999, -1)

View File

@ -1,3 +1,7 @@
from datetime import date
from typing import Any, Generator, TypeVar
def safe_division(numerator: int | float, denominator: int | float) -> int | float: def safe_division(numerator: int | float, denominator: int | float) -> int | float:
""" """
Divides without triggering division by zero exception. Divides without triggering division by zero exception.
@ -7,3 +11,56 @@ def safe_division(numerator: int | float, denominator: int | float) -> int | flo
return numerator / denominator return numerator / denominator
except ZeroDivisionError: except ZeroDivisionError:
return 0 return 0
def safe_getattr(obj: object, attr_chain: str, default: Any | None = None) -> object:
"""
Safely get the nested attribute from an object.
Parameters:
obj (object): The object from which to retrieve the attribute.
attr_chain (str): The chain of attributes, separated by dots.
default: The default value to return if any attribute in the chain does not exist.
Returns:
The value of the nested attribute if it exists, otherwise the default value.
"""
attrs = attr_chain.split(".")
for attr in attrs:
try:
obj = getattr(obj, attr)
except AttributeError:
return default
return obj
def truncate(input_string: str, length: int = 30, ellipsis: str = "") -> str:
return (
(f"{input_string[:length-len(ellipsis)].rstrip()}{ellipsis}")
if len(input_string) > length
else input_string
)
T = TypeVar("T", str, int, date)
def generate_split_ranges(
value_list: list[T], split_points: list[T]
) -> Generator[tuple[T, T], None, None]:
for x in range(0, len(split_points) + 1):
if x == 0:
start = 0
elif x >= len(split_points):
start = value_list.index(split_points[x - 1]) + 1
else:
start = value_list.index(split_points[x - 1]) + 1
try:
end = value_list.index(split_points[x])
except IndexError:
end = len(value_list)
yield (value_list[start], value_list[end - 1])
def format_float_or_int(number: int | float):
return int(number) if float(number).is_integer() else f"{number:03.2f}"

24
devcontainer.Dockerfile Normal file
View File

@ -0,0 +1,24 @@
FROM python:3.13-slim
# Set up environment
ENV PYTHONUNBUFFERED=1
WORKDIR /workspace
# Install Poetry
RUN apt-get update && apt-get install -y \
curl \
make \
npm \
&& rm -rf /var/lib/apt/lists/*
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH="/root/.local/bin:$PATH"
# Copy pyproject.toml and poetry.lock for dependency installation
COPY pyproject.toml poetry.lock* ./
RUN poetry install --no-root
# Copy the rest of the application code
COPY . .
# Set up Django development server
EXPOSE 8000

View File

@ -10,10 +10,14 @@ poetry run python manage.py collectstatic --clear --no-input
_term() { _term() {
echo "Caught SIGTERM signal!" echo "Caught SIGTERM signal!"
kill -SIGTERM "$gunicorn_pid" kill -SIGTERM "$gunicorn_pid"
kill -SIGTERM "$django_q_pid"
} }
trap _term SIGTERM trap _term SIGTERM
echo "Starting Django-Q cluster"
poetry run python manage.py qcluster & django_q_pid=$!
echo "Starting app" echo "Starting app"
poetry run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile - & gunicorn_pid=$! poetry run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile - & gunicorn_pid=$!
wait "$gunicorn_pid" wait "$gunicorn_pid" "$django_q_pid"

View File

@ -1,6 +1,14 @@
from django.contrib import admin from django.contrib import admin
from games.models import Device, Edition, Game, Platform, Purchase, Session from games.models import (
Device,
Edition,
ExchangeRate,
Game,
Platform,
Purchase,
Session,
)
# Register your models here. # Register your models here.
admin.site.register(Game) admin.site.register(Game)
@ -9,3 +17,4 @@ admin.site.register(Platform)
admin.site.register(Session) admin.site.register(Session)
admin.site.register(Edition) admin.site.register(Edition)
admin.site.register(Device) admin.site.register(Device)
admin.site.register(ExchangeRate)

View File

@ -1,6 +1,33 @@
from datetime import timedelta
from django.apps import AppConfig from django.apps import AppConfig
from django.core.management import call_command
from django.db.models.signals import post_migrate
from django.utils.timezone import now
class GamesConfig(AppConfig): class GamesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"
name = "games" name = "games"
def ready(self):
post_migrate.connect(schedule_tasks, sender=self)
def schedule_tasks(sender, **kwargs):
from django_q.models import Schedule
from django_q.tasks import schedule
if not Schedule.objects.filter(name="Update converted prices").exists():
schedule(
"games.tasks.convert_prices",
name="Update converted prices",
schedule_type=Schedule.MINUTES,
next_run=now() + timedelta(seconds=30),
)
from games.models import ExchangeRate
if not ExchangeRate.objects.exists():
print("ExchangeRate table is empty. Loading fixture...")
call_command("loaddata", "exchangerates.yaml")

View File

@ -0,0 +1,112 @@
- model: games.exchangerate
pk: 1
fields:
currency_from: USD
currency_to: CZK
year: 2024
rate: 23.4
- model: games.exchangerate
pk: 2
fields:
currency_from: CNY
currency_to: CZK
year: 2024
rate: 3.267
- model: games.exchangerate
pk: 3
fields:
currency_from: USD
currency_to: CZK
year: 2019
rate: 22.466
- model: games.exchangerate
pk: 4
fields:
currency_from: USD
currency_to: CZK
year: 2023
rate: 22.63
- model: games.exchangerate
pk: 5
fields:
currency_from: USD
currency_to: CZK
year: 2017
rate: 25.819
- model: games.exchangerate
pk: 6
fields:
currency_from: USD
currency_to: CZK
year: 2013
rate: 19.023
- model: games.exchangerate
pk: 7
fields:
currency_from: CNY
currency_to: CZK
year: 2019
rate: 3.295
- model: games.exchangerate
pk: 8
fields:
currency_from: CNY
currency_to: CZK
year: 2016
rate: 3.795
- model: games.exchangerate
pk: 9
fields:
currency_from: CNY
currency_to: CZK
year: 2015
rate: 3.707
- model: games.exchangerate
pk: 10
fields:
currency_from: CNY
currency_to: CZK
year: 2020
rate: 3.26
- model: games.exchangerate
pk: 11
fields:
currency_from: EUR
currency_to: CZK
year: 2012
rate: 25.51
- model: games.exchangerate
pk: 12
fields:
currency_from: EUR
currency_to: CZK
year: 2010
rate: 26.465
- model: games.exchangerate
pk: 13
fields:
currency_from: EUR
currency_to: CZK
year: 2014
rate: 27.52
- model: games.exchangerate
pk: 14
fields:
currency_from: EUR
currency_to: CZK
year: 2024
rate: 25.21
- model: games.exchangerate
pk: 15
fields:
currency_from: EUR
currency_to: CZK
year: 2022
rate: 24.325
- model: games.exchangerate
pk: 16
fields:
currency_from: CNY
currency_to: CZK
year: 2018
rate: 3.268

View File

@ -1,6 +1,7 @@
from django import forms from django import forms
from django.urls import reverse from django.urls import reverse
from common.utils import safe_getattr
from games.models import Device, Edition, Game, Platform, Purchase, Session from games.models import Device, Edition, Game, Platform, Purchase, Session
custom_date_widget = forms.DateInput(attrs={"type": "date"}) custom_date_widget = forms.DateInput(attrs={"type": "date"})
@ -45,8 +46,8 @@ class EditionChoiceField(forms.ModelChoiceField):
class IncludePlatformSelect(forms.Select): class IncludePlatformSelect(forms.Select):
def create_option(self, name, value, *args, **kwargs): def create_option(self, name, value, *args, **kwargs):
option = super().create_option(name, value, *args, **kwargs) option = super().create_option(name, value, *args, **kwargs)
if value: if platform_id := safe_getattr(value, "instance.platform.id"):
option["attrs"]["data-platform"] = value.instance.platform.id option["attrs"]["data-platform"] = platform_id
return option return option
@ -83,6 +84,7 @@ class PurchaseForm(forms.ModelForm):
"date_purchased": custom_date_widget, "date_purchased": custom_date_widget,
"date_refunded": custom_date_widget, "date_refunded": custom_date_widget,
"date_finished": custom_date_widget, "date_finished": custom_date_widget,
"date_dropped": custom_date_widget,
} }
model = Purchase model = Purchase
fields = [ fields = [
@ -91,6 +93,8 @@ class PurchaseForm(forms.ModelForm):
"date_purchased", "date_purchased",
"date_refunded", "date_refunded",
"date_finished", "date_finished",
"date_dropped",
"infinite",
"price", "price",
"price_currency", "price_currency",
"ownership_type", "ownership_type",
@ -160,7 +164,11 @@ class GameForm(forms.ModelForm):
class PlatformForm(forms.ModelForm): class PlatformForm(forms.ModelForm):
class Meta: class Meta:
model = Platform model = Platform
fields = ["name", "group"] fields = [
"name",
"icon",
"group",
]
widgets = {"name": autofocus_input_widget} widgets = {"name": autofocus_input_widget}

View File

@ -0,0 +1 @@
from .game import Mutation as GameMutation

View File

@ -0,0 +1,29 @@
import graphene
from games.graphql.types import Game
from games.models import Game as GameModel
class UpdateGameMutation(graphene.Mutation):
class Arguments:
id = graphene.ID(required=True)
name = graphene.String()
year_released = graphene.Int()
wikidata = graphene.String()
game = graphene.Field(Game)
def mutate(self, info, id, name=None, year_released=None, wikidata=None):
game_instance = GameModel.objects.get(pk=id)
if name is not None:
game_instance.name = name
if year_released is not None:
game_instance.year_released = year_released
if wikidata is not None:
game_instance.wikidata = wikidata
game_instance.save()
return UpdateGameMutation(game=game_instance)
class Mutation(graphene.ObjectType):
update_game = UpdateGameMutation.Field()

View File

@ -0,0 +1,6 @@
from .device import Query as DeviceQuery
from .edition import Query as EditionQuery
from .game import Query as GameQuery
from .platform import Query as PlatformQuery
from .purchase import Query as PurchaseQuery
from .session import Query as SessionQuery

View File

@ -0,0 +1,11 @@
import graphene
from games.graphql.types import Device
from games.models import Device as DeviceModel
class Query(graphene.ObjectType):
devices = graphene.List(Device)
def resolve_devices(self, info, **kwargs):
return DeviceModel.objects.all()

View File

@ -0,0 +1,11 @@
import graphene
from games.graphql.types import Edition
from games.models import Game as EditionModel
class Query(graphene.ObjectType):
editions = graphene.List(Edition)
def resolve_editions(self, info, **kwargs):
return EditionModel.objects.all()

View File

@ -0,0 +1,18 @@
import graphene
from games.graphql.types import Game
from games.models import Game as GameModel
class Query(graphene.ObjectType):
games = graphene.List(Game)
game_by_name = graphene.Field(Game, name=graphene.String(required=True))
def resolve_games(self, info, **kwargs):
return GameModel.objects.all()
def resolve_game_by_name(self, info, name):
try:
return GameModel.objects.get(name=name)
except GameModel.DoesNotExist:
return None

View File

@ -0,0 +1,11 @@
import graphene
from games.graphql.types import Platform
from games.models import Platform as PlatformModel
class Query(graphene.ObjectType):
platforms = graphene.List(Platform)
def resolve_platforms(self, info, **kwargs):
return PlatformModel.objects.all()

View File

@ -0,0 +1,11 @@
import graphene
from games.graphql.types import Purchase
from games.models import Purchase as PurchaseModel
class Query(graphene.ObjectType):
purchases = graphene.List(Purchase)
def resolve_purchases(self, info, **kwargs):
return PurchaseModel.objects.all()

View File

@ -0,0 +1,11 @@
import graphene
from games.graphql.types import Session
from games.models import Session as SessionModel
class Query(graphene.ObjectType):
sessions = graphene.List(Session)
def resolve_sessions(self, info, **kwargs):
return SessionModel.objects.all()

44
games/graphql/types.py Normal file
View File

@ -0,0 +1,44 @@
from graphene_django import DjangoObjectType
from games.models import Device as DeviceModel
from games.models import Edition as EditionModel
from games.models import Game as GameModel
from games.models import Platform as PlatformModel
from games.models import Purchase as PurchaseModel
from games.models import Session as SessionModel
class Game(DjangoObjectType):
class Meta:
model = GameModel
fields = "__all__"
class Edition(DjangoObjectType):
class Meta:
model = EditionModel
fields = "__all__"
class Purchase(DjangoObjectType):
class Meta:
model = PurchaseModel
fields = "__all__"
class Session(DjangoObjectType):
class Meta:
model = SessionModel
fields = "__all__"
class Platform(DjangoObjectType):
class Meta:
model = PlatformModel
fields = "__all__"
class Device(DjangoObjectType):
class Meta:
model = DeviceModel
fields = "__all__"

View File

@ -0,0 +1,24 @@
from datetime import timedelta
from django.core.management.base import BaseCommand
from django.utils.timezone import now
from django_q.models import Schedule
from django_q.tasks import schedule
class Command(BaseCommand):
help = "Manually schedule the next update_converted_prices task"
def handle(self, *args, **kwargs):
if not Schedule.objects.filter(name="Update converted prices").exists():
schedule(
"games.tasks.convert_prices",
name="Update converted prices",
schedule_type=Schedule.MINUTES,
next_run=now() + timedelta(seconds=30),
)
self.stdout.write(
self.style.SUCCESS("Scheduled the update_converted_prices task.")
)
else:
self.stdout.write(self.style.WARNING("Task is already scheduled."))

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.7 on 2023-11-28 13:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("games", "0032_alter_session_options_session_modified_at_and_more"),
]
operations = [
migrations.AlterUniqueTogether(
name="edition",
unique_together={("name", "platform", "year_released")},
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.7 on 2024-01-03 21:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0033_alter_edition_unique_together"),
]
operations = [
migrations.AddField(
model_name="purchase",
name="date_dropped",
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name="purchase",
name="infinite",
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 5.1 on 2024-08-11 15:50
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0034_purchase_date_dropped_purchase_infinite"),
]
operations = [
migrations.AlterField(
model_name="session",
name="device",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="games.device",
),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.1 on 2024-08-11 16:48
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0035_alter_session_device'),
]
operations = [
migrations.AlterField(
model_name='edition',
name='platform',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='games.platform'),
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 5.1.1 on 2024-09-14 07:05
from django.db import migrations, models
from django.utils.text import slugify
def update_empty_icons(apps, schema_editor):
Platform = apps.get_model("games", "Platform")
for platform in Platform.objects.filter(icon=""):
platform.icon = slugify(platform.name)
platform.save()
class Migration(migrations.Migration):
dependencies = [
("games", "0036_alter_edition_platform"),
]
operations = [
migrations.AddField(
model_name="platform",
name="icon",
field=models.SlugField(blank=True),
),
migrations.RunPython(update_empty_icons),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.1 on 2024-10-04 09:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0037_platform_icon'),
]
operations = [
migrations.AlterField(
model_name='purchase',
name='price',
field=models.FloatField(default=0),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2024-11-09 22:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0038_alter_purchase_price'),
]
operations = [
migrations.AlterField(
model_name='device',
name='type',
field=models.CharField(choices=[('PC', 'PC'), ('Console', 'Console'), ('Handheld', 'Handheld'), ('Mobile', 'Mobile'), ('Single-board computer', 'Single-board computer'), ('Unknown', 'Unknown')], default='Unknown', max_length=255),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 5.1.2 on 2024-11-09 22:39
from django.db import migrations
def update_device_types(apps, schema_editor):
Device = apps.get_model("games", "Device")
# Mapping of short names to long names
type_map = {
"pc": "PC",
"co": "Console",
"ha": "Handheld",
"mo": "Mobile",
"sbc": "Single-board computer",
"un": "Unknown",
}
# Loop through all devices and update the type field
for device in Device.objects.all():
if device.type in type_map:
device.type = type_map[device.type]
device.save()
class Migration(migrations.Migration):
dependencies = [
("games", "0039_alter_device_type"),
]
operations = [
migrations.RunPython(update_device_types),
]

View File

@ -0,0 +1,36 @@
# Generated by Django 5.1.3 on 2024-11-10 15:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0040_migrate_device_types'),
]
operations = [
migrations.AddField(
model_name='purchase',
name='converted_currency',
field=models.CharField(max_length=3, null=True),
),
migrations.AddField(
model_name='purchase',
name='converted_price',
field=models.FloatField(null=True),
),
migrations.CreateModel(
name='ExchangeRate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('currency_from', models.CharField(max_length=255)),
('currency_to', models.CharField(max_length=255)),
('year', models.PositiveIntegerField()),
('rate', models.FloatField()),
],
options={
'unique_together': {('currency_from', 'currency_to', 'year')},
},
),
]

View File

@ -2,7 +2,8 @@ from datetime import timedelta
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import F, Manager, Sum from django.db.models import F, Sum
from django.template.defaultfilters import slugify
from django.utils import timezone from django.utils import timezone
from common.time import format_duration from common.time import format_duration
@ -15,32 +16,43 @@ class Game(models.Model):
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None) wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
session_average: float | int | timedelta | None
session_count: int | None
def __str__(self):
return self.name
class Platform(models.Model):
name = models.CharField(max_length=255)
group = models.CharField(max_length=255, null=True, blank=True, default=None)
icon = models.SlugField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
return self.name return self.name
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
def get_sort_name(name): if not self.icon:
articles = ["a", "an", "the"] self.icon = slugify(self.name)
name_parts = name.split()
first_word = name_parts[0].lower()
if first_word in articles:
return f"{' '.join(name_parts[1:])}, {name_parts[0]}"
else:
return name
self.sort_name = get_sort_name(self.name)
super().save(*args, **kwargs) super().save(*args, **kwargs)
def get_sentinel_platform():
return Platform.objects.get_or_create(
name="Unspecified", icon="unspecified", group="Unspecified"
)[0]
class Edition(models.Model): class Edition(models.Model):
class Meta: class Meta:
unique_together = [["name", "platform"]] unique_together = [["name", "platform", "year_released"]]
game = models.ForeignKey("Game", on_delete=models.CASCADE) game = models.ForeignKey(Game, on_delete=models.CASCADE)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
sort_name = models.CharField(max_length=255, null=True, blank=True, default=None) sort_name = models.CharField(max_length=255, null=True, blank=True, default=None)
platform = models.ForeignKey( platform = models.ForeignKey(
"Platform", on_delete=models.CASCADE, null=True, blank=True, default=None Platform, on_delete=models.SET_DEFAULT, null=True, blank=True, default=None
) )
year_released = models.IntegerField(null=True, blank=True, default=None) year_released = models.IntegerField(null=True, blank=True, default=None)
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None) wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
@ -50,16 +62,8 @@ class Edition(models.Model):
return self.sort_name return self.sort_name
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
def get_sort_name(name): if self.platform is None:
articles = ["a", "an", "the"] self.platform = get_sentinel_platform()
name_parts = name.split()
first_word = name_parts[0].lower()
if first_word in articles:
return f"{' '.join(name_parts[1:])}, {name_parts[0]}"
else:
return name
self.sort_name = get_sort_name(self.name)
super().save(*args, **kwargs) super().save(*args, **kwargs)
@ -109,22 +113,26 @@ class Purchase(models.Model):
objects = PurchaseQueryset().as_manager() objects = PurchaseQueryset().as_manager()
edition = models.ForeignKey("Edition", on_delete=models.CASCADE) edition = models.ForeignKey(Edition, on_delete=models.CASCADE)
platform = models.ForeignKey( platform = models.ForeignKey(
"Platform", on_delete=models.CASCADE, default=None, null=True, blank=True Platform, on_delete=models.CASCADE, default=None, null=True, blank=True
) )
date_purchased = models.DateField() date_purchased = models.DateField()
date_refunded = models.DateField(blank=True, null=True) date_refunded = models.DateField(blank=True, null=True)
date_finished = models.DateField(blank=True, null=True) date_finished = models.DateField(blank=True, null=True)
price = models.IntegerField(default=0) date_dropped = models.DateField(blank=True, null=True)
infinite = models.BooleanField(default=False)
price = models.FloatField(default=0)
price_currency = models.CharField(max_length=3, default="USD") price_currency = models.CharField(max_length=3, default="USD")
converted_price = models.FloatField(null=True)
converted_currency = models.CharField(max_length=3, null=True)
ownership_type = models.CharField( ownership_type = models.CharField(
max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL
) )
type = models.CharField(max_length=255, choices=TYPES, default=GAME) type = models.CharField(max_length=255, choices=TYPES, default=GAME)
name = models.CharField(max_length=255, default="", null=True, blank=True) name = models.CharField(max_length=255, default="", null=True, blank=True)
related_purchase = models.ForeignKey( related_purchase = models.ForeignKey(
"Purchase", "self",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
default=None, default=None,
null=True, null=True,
@ -136,9 +144,11 @@ class Purchase(models.Model):
def __str__(self): def __str__(self):
additional_info = [ additional_info = [
self.get_type_display() if self.type != Purchase.GAME else "", self.get_type_display() if self.type != Purchase.GAME else "",
(
f"{self.edition.platform} version on {self.platform}" f"{self.edition.platform} version on {self.platform}"
if self.platform != self.edition.platform if self.platform != self.edition.platform
else self.platform, else self.platform
),
self.edition.year_released, self.edition.year_released,
self.get_ownership_type_display(), self.get_ownership_type_display(),
] ]
@ -154,18 +164,19 @@ class Purchase(models.Model):
raise ValidationError( raise ValidationError(
f"{self.get_type_display()} must have a related purchase." f"{self.get_type_display()} must have a related purchase."
) )
if self.pk is not None:
# Retrieve the existing instance from the database
existing_purchase = Purchase.objects.get(pk=self.pk)
# If price has changed, reset converted fields
if (
existing_purchase.price != self.price
or existing_purchase.price_currency != self.price_currency
):
self.converted_price = None
self.converted_currency = None
super().save(*args, **kwargs) super().save(*args, **kwargs)
class Platform(models.Model):
name = models.CharField(max_length=255)
group = models.CharField(max_length=255, null=True, blank=True, default=None)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class SessionQuerySet(models.QuerySet): class SessionQuerySet(models.QuerySet):
def total_duration_formatted(self): def total_duration_formatted(self):
return format_duration(self.total_duration_unformatted()) return format_duration(self.total_duration_unformatted())
@ -176,19 +187,32 @@ class SessionQuerySet(models.QuerySet):
) )
return result["duration"] return result["duration"]
def calculated_duration_formatted(self):
return format_duration(self.calculated_duration_unformatted())
def calculated_duration_unformatted(self):
result = self.aggregate(duration=Sum(F("duration_calculated")))
return result["duration"]
def without_manual(self):
return self.exclude(duration_calculated__iexact=0)
def only_manual(self):
return self.filter(duration_calculated__iexact=0)
class Session(models.Model): class Session(models.Model):
class Meta: class Meta:
get_latest_by = "timestamp_start" get_latest_by = "timestamp_start"
purchase = models.ForeignKey("Purchase", on_delete=models.CASCADE) purchase = models.ForeignKey(Purchase, on_delete=models.CASCADE)
timestamp_start = models.DateTimeField() timestamp_start = models.DateTimeField()
timestamp_end = models.DateTimeField(blank=True, null=True) timestamp_end = models.DateTimeField(blank=True, null=True)
duration_manual = models.DurationField(blank=True, null=True, default=timedelta(0)) duration_manual = models.DurationField(blank=True, null=True, default=timedelta(0))
duration_calculated = models.DurationField(blank=True, null=True) duration_calculated = models.DurationField(blank=True, null=True)
device = models.ForeignKey( device = models.ForeignKey(
"Device", "Device",
on_delete=models.CASCADE, on_delete=models.SET_DEFAULT,
null=True, null=True,
blank=True, blank=True,
default=None, default=None,
@ -212,7 +236,7 @@ class Session(models.Model):
def duration_seconds(self) -> timedelta: def duration_seconds(self) -> timedelta:
manual = timedelta(0) manual = timedelta(0)
calculated = timedelta(0) calculated = timedelta(0)
if self.is_manual(): if self.is_manual() and isinstance(self.duration_manual, timedelta):
manual = self.duration_manual manual = self.duration_manual
if self.timestamp_end != None and self.timestamp_start != None: if self.timestamp_end != None and self.timestamp_start != None:
calculated = self.timestamp_end - self.timestamp_start calculated = self.timestamp_end - self.timestamp_start
@ -229,12 +253,15 @@ class Session(models.Model):
def duration_sum(self) -> str: def duration_sum(self) -> str:
return Session.objects.all().total_duration_formatted() return Session.objects.all().total_duration_formatted()
def save(self, *args, **kwargs): def save(self, *args, **kwargs) -> None:
if self.timestamp_start != None and self.timestamp_end != None: if self.timestamp_start != None and self.timestamp_end != None:
self.duration_calculated = self.timestamp_end - self.timestamp_start self.duration_calculated = self.timestamp_end - self.timestamp_start
else: else:
self.duration_calculated = timedelta(0) self.duration_calculated = timedelta(0)
if not isinstance(self.duration_manual, timedelta):
self.duration_manual = timedelta(0)
if not self.device: if not self.device:
default_device, _ = Device.objects.get_or_create( default_device, _ = Device.objects.get_or_create(
type=Device.UNKNOWN, defaults={"name": "Unknown"} type=Device.UNKNOWN, defaults={"name": "Unknown"}
@ -244,12 +271,12 @@ class Session(models.Model):
class Device(models.Model): class Device(models.Model):
PC = "pc" PC = "PC"
CONSOLE = "co" CONSOLE = "Console"
HANDHELD = "ha" HANDHELD = "Handheld"
MOBILE = "mo" MOBILE = "Mobile"
SBC = "sbc" SBC = "Single-board computer"
UNKNOWN = "un" UNKNOWN = "Unknown"
DEVICE_TYPES = [ DEVICE_TYPES = [
(PC, "PC"), (PC, "PC"),
(CONSOLE, "Console"), (CONSOLE, "Console"),
@ -259,8 +286,21 @@ class Device(models.Model):
(UNKNOWN, "Unknown"), (UNKNOWN, "Unknown"),
] ]
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
type = models.CharField(max_length=3, choices=DEVICE_TYPES, default=UNKNOWN) type = models.CharField(max_length=255, choices=DEVICE_TYPES, default=UNKNOWN)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
return f"{self.name} ({self.get_type_display()})" return f"{self.name} ({self.type})"
class ExchangeRate(models.Model):
currency_from = models.CharField(max_length=255)
currency_to = models.CharField(max_length=255)
year = models.PositiveIntegerField()
rate = models.FloatField()
class Meta:
unique_together = ("currency_from", "currency_to", "year")
def __str__(self):
return f"{self.currency_from}/{self.currency_to} - {self.rate} ({self.year})"

30
games/schema.py Normal file
View File

@ -0,0 +1,30 @@
import graphene
from games.graphql.mutations import GameMutation
from games.graphql.queries import (
DeviceQuery,
EditionQuery,
GameQuery,
PlatformQuery,
PurchaseQuery,
SessionQuery,
)
class Query(
GameQuery,
EditionQuery,
DeviceQuery,
PlatformQuery,
PurchaseQuery,
SessionQuery,
graphene.ObjectType,
):
pass
class Mutation(GameMutation, graphene.ObjectType):
pass
schema = graphene.Schema(query=Query, mutation=Mutation)

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@ -1,24 +1,24 @@
import { syncSelectInputUntilChanged } from './utils.js'; import { syncSelectInputUntilChanged } from "./utils.js";
let syncData = [ let syncData = [
{ {
"source": "#id_game", source: "#id_game",
"source_value": "dataset.name", source_value: "dataset.name",
"target": "#id_name", target: "#id_name",
"target_value": "value" target_value: "value",
}, },
{ {
"source": "#id_game", source: "#id_game",
"source_value": "textContent", source_value: "textContent",
"target": "#id_sort_name", target: "#id_sort_name",
"target_value": "value" target_value: "value",
}, },
{ {
"source": "#id_game", source: "#id_game",
"source_value": "dataset.year", source_value: "dataset.year",
"target": "#id_year_released", target: "#id_year_released",
"target_value": "value" target_value: "value",
}, },
] ];
syncSelectInputUntilChanged(syncData, "form"); syncSelectInputUntilChanged(syncData, "form");

View File

@ -1,12 +1,12 @@
import { syncSelectInputUntilChanged } from './utils.js' import { syncSelectInputUntilChanged } from "./utils.js";
let syncData = [ let syncData = [
{ {
"source": "#id_name", source: "#id_name",
"source_value": "value", source_value: "value",
"target": "#id_sort_name", target: "#id_sort_name",
"target_value": "value" target_value: "value",
} },
] ];
syncSelectInputUntilChanged(syncData, "form") syncSelectInputUntilChanged(syncData, "form");

View File

@ -2,7 +2,7 @@ import {
syncSelectInputUntilChanged, syncSelectInputUntilChanged,
getEl, getEl,
disableElementsWhenTrue, disableElementsWhenTrue,
disableElementsWhenFalse, disableElementsWhenValueNotEqual,
} from "./utils.js"; } from "./utils.js";
let syncData = [ let syncData = [
@ -21,7 +21,11 @@ function setupElementHandlers() {
"#id_name", "#id_name",
"#id_related_purchase", "#id_related_purchase",
]); ]);
disableElementsWhenFalse("#id_type", "game", ["#id_date_finished"]); disableElementsWhenValueNotEqual(
"#id_type",
["game", "dlc"],
["#id_date_finished"]
);
} }
document.addEventListener("DOMContentLoaded", setupElementHandlers); document.addEventListener("DOMContentLoaded", setupElementHandlers);
@ -30,13 +34,13 @@ getEl("#id_type").onchange = () => {
setupElementHandlers(); setupElementHandlers();
}; };
document.body.addEventListener('htmx:beforeRequest', function(event) { document.body.addEventListener("htmx:beforeRequest", function (event) {
// Assuming 'Purchase1' is the element that triggers the HTMX request // Assuming 'Purchase1' is the element that triggers the HTMX request
if (event.target.id === 'id_edition') { if (event.target.id === "id_edition") {
var idEditionValue = document.getElementById('id_edition').value; var idEditionValue = document.getElementById("id_edition").value;
// Condition to check - replace this with your actual logic // Condition to check - replace this with your actual logic
if (idEditionValue != '') { if (idEditionValue != "") {
event.preventDefault(); // This cancels the HTMX request event.preventDefault(); // This cancels the HTMX request
} }
} }

View File

@ -7,10 +7,14 @@ for (let button of document.querySelectorAll("[data-target]")) {
button.addEventListener("click", (event) => { button.addEventListener("click", (event) => {
event.preventDefault(); event.preventDefault();
if (type == "now") { if (type == "now") {
targetElement.value = toISOUTCString(new Date); targetElement.value = toISOUTCString(new Date());
} else if (type == "copy") { } else if (type == "copy") {
const oppositeName = targetElement.name == "timestamp_start" ? "timestamp_end" : "timestamp_start"; const oppositeName =
document.querySelector(`[name='${oppositeName}']`).value = targetElement.value; targetElement.name == "timestamp_start"
? "timestamp_end"
: "timestamp_start";
document.querySelector(`[name='${oppositeName}']`).value =
targetElement.value;
} else if (type == "toggle") { } else if (type == "toggle") {
if (targetElement.type == "datetime-local") targetElement.type = "text"; if (targetElement.type == "datetime-local") targetElement.type = "text";
else targetElement.type = "datetime-local"; else targetElement.type = "datetime-local";

View File

@ -75,7 +75,10 @@ function syncSelectInputUntilChanged(syncData, parentSelector = document) {
* @param {string} property - The property to retrieve the value from. * @param {string} property - The property to retrieve the value from.
*/ */
function getValueFromProperty(sourceElement, property) { function getValueFromProperty(sourceElement, property) {
let source = (sourceElement instanceof HTMLSelectElement) ? sourceElement.selectedOptions[0] : sourceElement let source =
sourceElement instanceof HTMLSelectElement
? sourceElement.selectedOptions[0]
: sourceElement;
if (property.startsWith("dataset.")) { if (property.startsWith("dataset.")) {
let datasetKey = property.slice(8); // Remove 'dataset.' part let datasetKey = property.slice(8); // Remove 'dataset.' part
return source.dataset[datasetKey]; return source.dataset[datasetKey];
@ -93,13 +96,11 @@ function getValueFromProperty(sourceElement, property) {
*/ */
function getEl(selector) { function getEl(selector) {
if (selector.startsWith("#")) { if (selector.startsWith("#")) {
return document.getElementById(selector.slice(1)) return document.getElementById(selector.slice(1));
} } else if (selector.startsWith(".")) {
else if (selector.startsWith(".")) { return document.getElementsByClassName(selector);
return document.getElementsByClassName(selector) } else {
} return document.getElementsByTagName(selector);
else {
return document.getElementsByTagName(selector)
} }
} }
@ -116,7 +117,7 @@ function getEl(selector) {
function conditionalElementHandler(...configs) { function conditionalElementHandler(...configs) {
configs.forEach(([condition, targetElements, callbackfn1, callbackfn2]) => { configs.forEach(([condition, targetElements, callbackfn1, callbackfn2]) => {
if (condition()) { if (condition()) {
targetElements.forEach(elementName => { targetElements.forEach((elementName) => {
let el = getEl(elementName); let el = getEl(elementName);
if (el === null) { if (el === null) {
console.error(`Element ${elementName} doesn't exist.`); console.error(`Element ${elementName} doesn't exist.`);
@ -125,7 +126,7 @@ function conditionalElementHandler(...configs) {
} }
}); });
} else { } else {
targetElements.forEach(elementName => { targetElements.forEach((elementName) => {
let el = getEl(elementName); let el = getEl(elementName);
if (el === null) { if (el === null) {
console.error(`Element ${elementName} doesn't exist.`); console.error(`Element ${elementName} doesn't exist.`);
@ -137,16 +138,44 @@ function conditionalElementHandler(...configs) {
}); });
} }
function disableElementsWhenFalse(targetSelect, targetValue, elementList) { function disableElementsWhenValueNotEqual(
targetSelect,
targetValue,
elementList
) {
return conditionalElementHandler([ return conditionalElementHandler([
() => { () => {
return getEl(targetSelect).value != targetValue; let target = getEl(targetSelect);
console.debug(
`${disableElementsWhenTrue.name}: triggered on ${target.id}`
);
console.debug(`
${disableElementsWhenTrue.name}: matching against value(s): ${targetValue}`);
if (targetValue instanceof Array) {
if (targetValue.every((value) => target.value != value)) {
console.debug(
`${disableElementsWhenTrue.name}: none of the values is equal to ${target.value}, returning true.`
);
return true;
}
} else {
console.debug(
`${disableElementsWhenTrue.name}: none of the values is equal to ${target.value}, returning true.`
);
return target.value != targetValue;
}
}, },
elementList, elementList,
(el) => { (el) => {
console.debug(
`${disableElementsWhenTrue.name}: evaluated true, disabling ${el.id}.`
);
el.disabled = "disabled"; el.disabled = "disabled";
}, },
(el) => { (el) => {
console.debug(
`${disableElementsWhenTrue.name}: evaluated false, NOT disabling ${el.id}.`
);
el.disabled = ""; el.disabled = "";
}, },
]); ]);
@ -167,4 +196,12 @@ function disableElementsWhenTrue(targetSelect, targetValue, elementList) {
]); ]);
} }
export { toISOUTCString, syncSelectInputUntilChanged, getEl, conditionalElementHandler, disableElementsWhenFalse, disableElementsWhenTrue, getValueFromProperty }; export {
toISOUTCString,
syncSelectInputUntilChanged,
getEl,
conditionalElementHandler,
disableElementsWhenValueNotEqual,
disableElementsWhenTrue,
getValueFromProperty,
};

57
games/tasks.py Normal file
View File

@ -0,0 +1,57 @@
import requests
from games.models import ExchangeRate, Purchase
# fixme: save preferred currency in user model
currency_to = "CZK"
currency_to = currency_to.upper()
def save_converted_info(purchase, converted_price, converted_currency):
print(
f"Changing converted price of {purchase} to {converted_price} {converted_currency} "
)
purchase.converted_price = converted_price
purchase.converted_currency = converted_currency
purchase.save()
def convert_prices():
purchases = Purchase.objects.filter(
converted_price__isnull=True, converted_currency__isnull=True
)
for purchase in purchases:
if purchase.price_currency.upper() == currency_to or purchase.price == 0:
save_converted_info(purchase, purchase.price, currency_to)
continue
year = purchase.date_purchased.year
currency_from = purchase.price_currency.upper()
exchange_rate = ExchangeRate.objects.filter(
currency_from=currency_from, currency_to=currency_to, year=year
).first()
if not exchange_rate:
try:
response = requests.get(
f"https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@{year}-01-01/v1/currencies/{currency_from}.json"
)
response.raise_for_status()
data = response.json()
rate = data[currency_from].get(currency_to)
if rate:
exchange_rate = ExchangeRate.objects.create(
currency_from=currency_from,
currency_to=currency_to,
year=year,
rate=rate,
)
except requests.RequestException as e:
print(
f"Failed to fetch exchange rate for {currency_from}->{currency_to} in {year}: {e}"
)
if exchange_rate:
save_converted_info(
purchase, purchase.price * exchange_rate.rate, currency_to
)

View File

@ -1,24 +1,2 @@
{% extends "base.html" %} <c-layouts.add>
{% load static %} </c-layouts.add>
{% block title %}
{{ title }}
{% endblock title %}
{% block content %}
<form method="post" enctype="multipart/form-data">
<table class="mx-auto">
{% csrf_token %}
{{ form.as_table }}
<tr>
<td></td>
<td>
<input type="submit" value="Submit" />
</td>
</tr>
</table>
</form>
{% endblock content %}
{% block scripts %}
{% if script_name %}
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
{% endif %}
{% endblock scripts %}

View File

@ -1,19 +1,5 @@
{% extends "base.html" %} <c-layouts.add>
{% load static %} <c-slot name="additional_row">
{% block title %}
{{ title }}
{% endblock title %}
{% block content %}
<form method="post" enctype="multipart/form-data">
<table class="mx-auto">
{% csrf_token %}
{{ form.as_table }}
<tr>
<td></td>
<td>
<input type="submit" name="submit" value="Submit" />
</td>
</tr>
<tr> <tr>
<td></td> <td></td>
<td> <td>
@ -22,11 +8,5 @@
value="Submit & Create Purchase" /> value="Submit & Create Purchase" />
</td> </td>
</tr> </tr>
</table> </c-slot>
</form> </c-layouts.add>
{% endblock content %}
{% block scripts %}
{% if script_name %}
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
{% endif %}
{% endblock scripts %}

View File

@ -1,19 +1,5 @@
{% extends "base.html" %} <c-layouts.add>
{% load static %} <c-slot name="additional_row">
{% block title %}
{{ title }}
{% endblock title %}
{% block content %}
<form method="post" enctype="multipart/form-data">
<table class="mx-auto">
{% csrf_token %}
{{ form.as_table }}
<tr>
<td></td>
<td>
<input type="submit" name="submit" value="Submit" />
</td>
</tr>
<tr> <tr>
<td></td> <td></td>
<td> <td>
@ -22,11 +8,5 @@
value="Submit & Create Edition" /> value="Submit & Create Edition" />
</td> </td>
</tr> </tr>
</table> </c-slot>
</form> </c-layouts.add>
{% endblock content %}
{% block scripts %}
{% if script_name %}
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
{% endif %}
{% endblock scripts %}

View File

@ -1,19 +1,5 @@
{% extends "base.html" %} <c-layouts.add>
{% load static %} <c-slot name="additional_row">
{% block title %}
{{ title }}
{% endblock title %}
{% block content %}
<form method="post" enctype="multipart/form-data">
<table class="mx-auto">
{% csrf_token %}
{{ form.as_table }}
<tr>
<td></td>
<td>
<input type="submit" name="submit" value="Submit" />
</td>
</tr>
<tr> <tr>
<td></td> <td></td>
<td> <td>
@ -22,11 +8,5 @@
value="Submit & Create Session" /> value="Submit & Create Session" />
</td> </td>
</tr> </tr>
</table> </c-slot>
</form> </c-layouts.add>
{% endblock content %}
{% block scripts %}
{% if script_name %}
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
{% endif %}
{% endblock scripts %}

View File

@ -1,8 +1,5 @@
{% extends "base.html" %} <c-layouts.add>
{% block title %} <c-slot name="form_content">
{{ title }}
{% endblock title %}
{% block content %}
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<table class="mx-auto"> <table class="mx-auto">
{% csrf_token %} {% csrf_token %}
@ -16,7 +13,7 @@
{% endif %} {% endif %}
{% if field.name == "timestamp_start" or field.name == "timestamp_end" %} {% if field.name == "timestamp_start" or field.name == "timestamp_end" %}
<td> <td>
<div class="basic-button-container"> <div class="basic-button-container" hx-boost="false">
<button class="basic-button" data-target="{{ field.name }}" data-type="now">Set to now</button> <button class="basic-button" data-target="{{ field.name }}" data-type="now">Set to now</button>
<button class="basic-button" <button class="basic-button"
data-target="{{ field.name }}" data-target="{{ field.name }}"
@ -35,6 +32,5 @@
</tr> </tr>
</table> </table>
</form> </form>
{% load static %} </c-slot>
<script type="module" src="{% static 'js/add_session.js' %}"></script> </c-layouts.add>
{% endblock content %}

View File

@ -1,101 +0,0 @@
<!DOCTYPE html>
<html lang="en">
{% load static %}
<head>
<meta charset="utf-8" />
<meta name="description" content="Self-hosted time-tracker." />
<meta name="keywords" content="time, tracking, video games, self-hosted" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Timetracker -
{% block title %}
Untitled
{% endblock title %}
</title>
<script src="{% static 'js/htmx.min.js' %}"></script>
<link rel="stylesheet" href="{% static 'base.css' %}" />
</head>
<body class="dark" hx-indicator="#indicator" hx-boost="true">
<img id="indicator"
src="{% static 'icons/loading.png' %}"
class="absolute right-3 top-3 animate-spin htmx-indicator" />
<div class="dark:bg-gray-800 min-h-screen">
<nav class="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded">
<div class="container flex flex-wrap items-center justify-between mx-auto">
<a href="{% url 'list_sessions_recent' %}" class="flex items-center">
<span class="text-4xl">
<img src="{% static 'icons/schedule.png' %}" width="48" class="mr-4" />
</span>
<span class="self-center text-xl font-semibold whitespace-nowrap text-white">Timetracker</span>
</a>
<div class="w-full md:block md:w-auto">
<ul class="flex flex-col md:flex-row p-4 mt-4 dark:text-white">
<li class="relative group">
<a class="block py-2 pl-3 pr-4 hover:underline"
href="{% url 'add_game' %}">New</a>
<ul class="absolute hidden text-gray-700 pt-1 group-hover:block w-auto whitespace-nowrap">
{% if purchase_available %}
<li>
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
href="{% url 'add_device' %}">Device</a>
</li>
{% endif %}
<li>
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
href="{% url 'add_game' %}">Game</a>
</li>
{% if game_available and platform_available %}
<li>
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
href="{% url 'add_edition' %}">Edition</a>
</li>
{% endif %}
<li>
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
href="{% url 'add_platform' %}">Platform</a>
</li>
{% if edition_available %}
<li>
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
href="{% url 'add_purchase' %}">Purchase</a>
</li>
{% endif %}
{% if purchase_available %}
<li>
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
href="{% url 'add_session' %}">Session</a>
</li>
{% endif %}
</ul>
</li>
{% if session_count > 0 %}
<li class="relative group">
<a class="block py-2 pl-3 pr-4 hover:underline"
href="{% url 'stats_current_year' %}">Stats</a>
<ul class="absolute hidden text-gray-700 pt-1 group-hover:block">
{% for year in stats_dropdown_year_range %}
<li>
<a class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap"
href="{% url 'stats_by_year' year %}">{{ year }}</a>
</li>
{% endfor %}
</ul>
</li>
<li>
<a class="block py-2 pl-3 pr-4 hover:underline"
href="{% url 'list_sessions' %}">All Sessions</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
{% block content %}
No content here.
{% endblock content %}
</div>
{% load version %}
<span class="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span>
{% block scripts %}
{% endblock scripts %}
</body>
</html>

View File

@ -1,13 +0,0 @@
<a href="{{ edit_url }}">
<button type="button"
title="Edit"
class="ml-1 py-1 px-2 flex justify-center items-center bg-violet-600 hover:bg-violet-700 focus:ring-violet-500 focus:ring-offset-violet-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 w-7 h-4 rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5">
<path d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z" />
<path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z" />
</svg>
</button>
</a>

View File

@ -0,0 +1,6 @@
<c-vars color="blue" size="base" />
<button type="button"
title="{{ title }}"
class=" {% if color == "blue" %} bg-blue-700 dark:bg-blue-600 dark:focus:ring-blue-800 dark:hover:bg-blue-700 focus:ring-blue-300 hover:bg-blue-800 text-white {% elif color == "red" %} bg-red-700 dark:bg-red-600 dark:focus:ring-red-900 dark:hover:bg-red-700 focus:ring-red-300 hover:bg-red-800 text-white {% elif color == "gray" %} bg-white border-gray-200 dark:bg-gray-800 dark:border-gray-600 dark:focus:ring-gray-700 dark:hover:bg-gray-700 dark:hover:text-white dark:text-gray-400 focus:ring-gray-100 hover:bg-gray-100 hover:text-blue-700 text-gray-900 border {% elif color == "green" %} bg-green-700 dark:bg-green-600 dark:focus:ring-green-800 dark:hover:bg-green-700 focus:ring-green-300 hover:bg-green-800 text-white {% endif %} focus:outline-none focus:ring-4 font-medium mb-2 me-2 rounded-lg {% if size == "xs" %} px-3 py-2 text-xs {% elif size == "sm" %} px-3 py-2 text-sm {% elif size == "base" %} px-5 py-2.5 text-sm {% elif size == "lg" %} px-5 py-3 text-base {% elif size == "xl" %} px-6 py-3.5 text-base {% endif %} {% if icon %} inline-flex text-center items-center gap-2 {% else %} {% endif %} ">
{{ slot }}
</button>

View File

@ -0,0 +1,8 @@
<div class="inline-flex rounded-md shadow-sm" role="group">
{% if slot %}{{ slot }}{% endif %}
{% for button in buttons %}
{% if button.slot %}
<c-button-group-button-sm :href=button.href :slot=button.slot :color=button.color :hover=button.hover :title=button.title />
{% endif %}
{% endfor %}
</div>

View File

@ -0,0 +1,23 @@
<c-vars color="gray" />
<a href="{{ href }}"
class="[&:first-of-type_button]:rounded-s-lg [&:last-of-type_button]:rounded-e-lg">
{% if color == "gray" %}
<button type="button"
title="{{ title }}"
class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 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">
{{ slot }}
</button>
{% elif color == "red" %}
<button type="button"
title="{{ title }}"
class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 hover:bg-red-500 hover:text-white 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:border-red-700 dark:hover:bg-red-700 dark:focus:ring-blue-500 dark:focus:text-white">
{{ slot }}
</button>
{% elif color == "green" %}
<button type="button"
title="{{ title }}"
class="px-2 py-1 text-xs font-medium text-gray-900 bg-white border border-gray-200 hover:bg-green-500 hover:border-green-600 hover:text-white focus:z-10 focus:ring-2 focus:ring-green-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:border-green-700 dark:hover:bg-green-600 dark:focus:ring-green-500 dark:focus:text-white">
{{ slot }}
</button>
{% endif %}
</a>

View File

@ -0,0 +1,10 @@
<span class="truncate-container">
<a class="underline decoration-slate-500 sm:decoration-2"
href="{% url 'view_game' game_id %}">
{% if slot %}
{{ slot }}
{% else %}
{{ name }}
{% endif %}
</a>
</span>

View File

@ -0,0 +1,8 @@
<h1 class="{% if badge %}flex items-center {% endif %}mb-4 text-3xl font-extrabold leading-none tracking-tight text-gray-900 dark:text-white">
{{ slot }}
{% if badge %}
<span class="bg-blue-100 text-blue-800 text-2xl font-semibold me-2 px-2.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800 ms-2">
{{ badge }}
</span>
{% endif %}
</h1>

View File

@ -0,0 +1,5 @@
<c-svg title="Battle.net">
<c-slot name="path">
M 43.113281 22.152344 C 43.113281 22.152344 47.058594 22.351563 47.058594 20.03125 C 47.058594 16.996094 41.804688 14.261719 41.804688 14.261719 C 41.804688 14.261719 42.628906 12.515625 43.140625 11.539063 C 43.65625 10.5625 45.101563 6.753906 45.230469 5.886719 C 45.394531 4.792969 45.144531 4.449219 45.144531 4.449219 C 44.789063 6.792969 40.972656 13.539063 40.671875 13.769531 C 36.949219 12.023438 31.835938 11.539063 31.835938 11.539063 C 31.835938 11.539063 26.832031 1 22.125 1 C 17.457031 1 17.480469 10.023438 17.480469 10.023438 C 17.480469 10.023438 16.160156 7.464844 14.507813 7.464844 C 12.085938 7.464844 11.292969 11.128906 11.292969 15.097656 C 6.511719 15.097656 2.492188 16.164063 2.132813 16.265625 C 1.773438 16.371094 0.644531 17.191406 1.15625 17.089844 C 2.203125 16.753906 7.113281 15.992188 11.410156 16.367188 C 11.648438 20.140625 13.851563 25.054688 13.851563 25.054688 C 13.851563 25.054688 9.128906 31.894531 9.128906 36.78125 C 9.128906 38.066406 9.6875 40.417969 13.078125 40.417969 C 15.917969 40.417969 19.105469 38.710938 19.707031 38.363281 C 19.183594 39.113281 18.796875 40.535156 18.796875 41.191406 C 18.796875 41.726563 19.113281 43.246094 21.304688 43.246094 C 24.117188 43.246094 27.257813 41.089844 27.257813 41.089844 C 27.257813 41.089844 30.222656 46.019531 32.761719 48.28125 C 33.445313 48.890625 34.097656 49 34.097656 49 C 34.097656 49 31.578125 46.574219 28.257813 40.324219 C 31.34375 38.417969 34.554688 33.921875 34.554688 33.921875 C 34.554688 33.921875 34.933594 33.933594 37.863281 33.933594 C 42.453125 33.933594 48.972656 32.96875 48.972656 29.320313 C 48.972656 25.554688 43.113281 22.152344 43.113281 22.152344 Z M 43.625 19.886719 C 43.625 21.21875 42.359375 21.199219 42.359375 21.199219 L 41.394531 21.265625 C 41.394531 21.265625 39.566406 20.304688 38.460938 19.855469 C 38.460938 19.855469 40.175781 17.207031 40.578125 16.46875 C 40.882813 16.644531 43.625 18.363281 43.625 19.886719 Z M 24.421875 6.308594 C 26.578125 6.308594 29.65625 11.402344 29.65625 11.402344 C 29.65625 11.402344 24.851563 10.972656 20.898438 13.296875 C 21.003906 9.628906 22.238281 6.308594 24.421875 6.308594 Z M 15.871094 10.4375 C 16.558594 10.4375 17.230469 11.269531 17.507813 11.976563 C 17.507813 12.445313 17.75 15.171875 17.75 15.171875 L 13.789063 15.023438 C 13.789063 11.449219 15.1875 10.4375 15.871094 10.4375 Z M 15.464844 35.246094 C 13.300781 35.246094 12.851563 34.039063 12.851563 32.953125 C 12.851563 30.496094 14.8125 27.058594 14.8125 27.058594 C 14.8125 27.058594 17.011719 31.683594 20.851563 33.636719 C 18.945313 34.753906 17.375 35.246094 15.464844 35.246094 Z M 22.492188 40.089844 C 20.972656 40.089844 20.789063 39.105469 20.789063 38.878906 C 20.789063 38.171875 21.339844 37.335938 21.339844 37.335938 C 21.339844 37.335938 23.890625 35.613281 24.054688 35.429688 L 25.9375 38.945313 C 25.9375 38.945313 24.007813 40.089844 22.492188 40.089844 Z M 27.226563 38.171875 C 26.300781 36.554688 25.621094 34.867188 25.621094 34.867188 C 25.621094 34.867188 29.414063 35.113281 31.453125 33.007813 C 30.183594 33.578125 28.15625 34.300781 25.800781 34.082031 C 30.726563 29.742188 33.601563 26.597656 36.03125 23.34375 C 35.824219 23.09375 34.710938 22.316406 34.4375 22.1875 C 32.972656 23.953125 27.265625 30.054688 21.984375 33.074219 C 15.292969 29.425781 13.890625 18.691406 13.746094 16.460938 L 17.402344 16.8125 C 17.402344 16.8125 16.027344 19.246094 16.027344 21.039063 C 16.027344 22.828125 16.242188 22.925781 16.242188 22.925781 C 16.242188 22.925781 16.195313 19.800781 18.125 17.390625 C 19.59375 25.210938 21.125 29.21875 22.320313 31.605469 C 22.925781 31.355469 24.058594 30.851563 24.058594 30.851563 C 24.058594 30.851563 20.683594 21.121094 20.871094 14.535156 C 22.402344 13.71875 24.667969 12.875 27.226563 12.875 C 33.957031 12.875 39.367188 15.773438 39.367188 15.773438 L 37.25 18.730469 C 37.25 18.730469 35.363281 15.3125 32.699219 14.703125 C 34.105469 15.753906 35.679688 17.136719 36.496094 19.128906 C 30.917969 16.949219 24.1875 15.796875 22.027344 15.542969 C 21.839844 16.339844 21.863281 17.480469 21.863281 17.480469 C 21.863281 17.480469 30.890625 19.144531 37.460938 22.90625 C 37.414063 31.125 28.460938 37.4375 27.226563 38.171875 Z M 35.777344 32.027344 C 35.777344 32.027344 38.578125 28.347656 38.535156 23.476563 C 38.535156 23.476563 43.0625 26.28125 43.0625 29.015625 C 43.0625 32.074219 35.777344 32.027344 35.777344 32.027344 Z
</c-slot>
</c-svg>

View File

@ -0,0 +1,5 @@
<c-svg viewBox="0 0 20 20">
<c-slot name="path">
M2.069,11 L5,11 L5,9 L2.069,9 C2.252,7.542 2.828,6.208 3.688,5.102 L5.757,7.172 L7.171,5.757 L5.102,3.688 C6.208,2.828 8,2.252 9,2.069 L9,5 L11,5 L11,2.069 C12,2.252 13.791,2.828 14.897,3.688 L12.828,5.757 L14.242,7.172 L16.311,5.102 C17.171,6.208 17.747,7.542 17.93,9 L15,9 L15,11 L17.93,11 C17.747,12.458 17.171,13.792 16.311,14.898 L14.242,12.828 L12.828,14.243 L14.897,16.312 C13.791,17.172 12,17.748 11,17.931 L11,15 L9,15 L9,17.931 C8,17.748 6.208,17.172 5.102,16.312 L7.171,14.243 L5.757,12.828 L3.688,14.898 C2.828,13.792 2.252,12.458 2.069,11 M10,0 C4.477,0 0,4.477 0,10 C0,15.523 4.477,20 10,20 C15.522,20 20,15.523 20,10 C20,4.477 15.522,0 10,0
</c-slot>
</c-svg>

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 48 48"
class="text-black dark:text-white w-4 h-4">
<path fill="currentColor" d="M 43.470703 8.9863281 A 1.50015 1.50015 0 0 0 42.439453 9.4394531 L 16.5 35.378906 L 5.5605469 24.439453 A 1.50015 1.50015 0 1 0 3.4394531 26.560547 L 15.439453 38.560547 A 1.50015 1.50015 0 0 0 17.560547 38.560547 L 44.560547 11.560547 A 1.50015 1.50015 0 0 0 43.470703 8.9863281 z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 477 B

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 48 48"
class="text-black dark:text-white w-4 h-4">
<path fill="currentColor" d="M 24 4 C 20.491685 4 17.570396 6.6214322 17.080078 10 L 10.238281 10 A 1.50015 1.50015 0 0 0 9.9804688 9.9785156 A 1.50015 1.50015 0 0 0 9.7578125 10 L 6.5 10 A 1.50015 1.50015 0 1 0 6.5 13 L 8.6386719 13 L 11.15625 39.029297 C 11.427329 41.835926 13.811782 44 16.630859 44 L 31.367188 44 C 34.186411 44 36.570826 41.836168 36.841797 39.029297 L 39.361328 13 L 41.5 13 A 1.50015 1.50015 0 1 0 41.5 10 L 38.244141 10 A 1.50015 1.50015 0 0 0 37.763672 10 L 30.919922 10 C 30.429604 6.6214322 27.508315 4 24 4 z M 24 7 C 25.879156 7 27.420767 8.2681608 27.861328 10 L 20.138672 10 C 20.579233 8.2681608 22.120844 7 24 7 z M 11.650391 13 L 36.347656 13 L 33.855469 38.740234 C 33.730439 40.035363 32.667963 41 31.367188 41 L 16.630859 41 C 15.331937 41 14.267499 40.033606 14.142578 38.740234 L 11.650391 13 z M 20.476562 17.978516 A 1.50015 1.50015 0 0 0 19 19.5 L 19 34.5 A 1.50015 1.50015 0 1 0 22 34.5 L 22 19.5 A 1.50015 1.50015 0 0 0 20.476562 17.978516 z M 27.476562 17.978516 A 1.50015 1.50015 0 0 0 26 19.5 L 26 34.5 A 1.50015 1.50015 0 1 0 29 34.5 L 29 19.5 A 1.50015 1.50015 0 0 0 27.476562 17.978516 z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,9 @@
<c-svg viewbox="0 0 50 50">
<g transform="scale(0.09765625)">
<title>EA/Origin</title>
<g>
<path fill="currentColor" d="M299.125,126.274H126.628L97.876,183.93h172.499L299.125,126.274z" />
<path fill="currentColor" d="M342.248,126.274L224.462,328.066h-105.8l32.862-57.653h61.347l28.758-57.658H69.125l-28.746,57.658H85.31L26.001,385.727h232.784l83.463-153.654l18.169,38.342h-18.169l-28.75,57.654h75.67l28.75,57.658h68.081L342.248,126.274z" />
</g>
</g>
</c-svg>

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 48 48"
class="text-black dark:text-white w-4 h-4">
<path fill="currentColor" d="M 40.5 6 C 40.11625 6 39.732453 6.1464531 39.439453 6.4394531 L 21.462891 24.417969 L 20 28 L 23.582031 26.537109 L 41.560547 8.5605469 C 42.145547 7.9745469 42.145547 7.0254531 41.560547 6.4394531 C 41.267547 6.1464531 40.88375 6 40.5 6 z M 12.5 7 C 9.4802259 7 7 9.4802259 7 12.5 L 7 35.5 C 7 38.519774 9.4802259 41 12.5 41 L 35.5 41 C 38.519774 41 41 38.519774 41 35.5 L 41 18.5 A 1.50015 1.50015 0 1 0 38 18.5 L 38 35.5 C 38 36.898226 36.898226 38 35.5 38 L 12.5 38 C 11.101774 38 10 36.898226 10 35.5 L 10 12.5 C 10 11.101774 11.101774 10 12.5 10 L 29.5 10 A 1.50015 1.50015 0 1 0 29.5 7 L 12.5 7 z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 798 B

View File

@ -0,0 +1,6 @@
<c-vars title="Epic Games Store" />
<c-svg :title=title viewbox="0 0 50 50">
<c-slot name="path">
M 10 3 C 6.69 3 4 5.69 4 9 L 4 41.240234 L 25 47.539062 L 46 41.240234 L 46 9 C 46 5.69 43.31 3 40 3 L 10 3 z M 11 8 L 15 8 L 15 11 L 11 11 L 11 18 L 14 18 L 14 21 L 11 21 L 11 28 L 15 28 L 15 31 L 11 31 C 9.34 31 8 29.66 8 28 L 8 11 C 8 9.34 9.34 8 11 8 z M 17 8 L 23 8 C 24.66 8 26 9.34 26 11 L 26 18 C 26 19.66 24.66 21 23 21 L 20 21 L 20 31 L 17 31 L 17 8 z M 28 8 L 31 8 L 31 31 L 28 31 L 28 8 z M 36 8 L 39 8 C 40.66 8 42 9.34 42 11 L 42 15 L 39 15 L 39 11 L 36 11 L 36 28 L 39 28 L 39 24 L 42 24 L 42 28 C 42 29.66 40.66 31 39 31 L 36 31 C 34.34 31 33 29.66 33 28 L 33 11 C 33 9.34 34.34 8 36 8 z M 20 11 L 20 18 L 23 18 L 23 11 L 20 11 z M 9 34 L 13 34 C 13.55 34 14 34.45 14 35 L 14 36 L 13 36 L 13 35.25 C 13 35.11 12.89 35 12.75 35 L 9.25 35 C 9.11 35 9 35.11 9 35.25 L 9 38.75 C 9 38.89 9.11 39 9.25 39 L 12.75 39 C 12.89 39 13 38.89 13 38.75 L 13 38 L 12 38 L 12 37 L 14 37 L 14 39 C 14 39.55 13.55 40 13 40 L 9 40 C 8.45 40 8 39.55 8 39 L 8 35 C 8 34.45 8.45 34 9 34 z M 18 34 L 19 34 L 22 40 L 21 40 L 20.5 39 L 16.5 39 L 16 40 L 15 40 L 18 34 z M 23 34 L 24 34 L 26 38 L 28 34 L 29 34 L 29 40 L 28 40 L 28 36 L 26.5 39 L 25.5 39 L 24 36 L 24 40 L 23 40 L 23 34 z M 30 34 L 35 34 L 35 35 L 31 35 L 31 36.5 L 33 36.5 L 33 37.5 L 31 37.5 L 31 39 L 35 39 L 35 40 L 30 40 L 30 34 z M 37 34 L 41 34 C 41.55 34 42 34.45 42 35 L 42 35.5 L 41 35.5 L 41 35.25 C 41 35.11 40.89 35 40.75 35 L 37.25 35 C 37.11 35 37 35.11 37 35.25 L 37 36.25 C 37 36.39 37.11 36.5 37.25 36.5 L 41 36.5 C 41.55 36.5 42 36.95 42 37.5 L 42 39 C 42 39.55 41.55 40 41 40 L 37 40 C 36.45 40 36 39.55 36 39 L 36 38.5 L 37 38.5 L 37 38.75 C 37 38.89 37.11 39 37.25 39 L 40.75 39 C 40.89 39 41 38.89 41 38.75 L 41 37.75 C 41 37.61 40.89 37.5 40.75 37.5 L 37 37.5 C 36.45 37.5 36 37.05 36 36.5 L 36 35 C 36 34.45 36.45 34 37 34 z M 18.5 35 L 17 38 L 20 38 L 18.5 35 z
</c-slot>
</c-svg>

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 48 48"
class="text-black dark:text-white w-4 h-4">
<path fill="currentColor" d="M 24 5.0507812 C 22.945045 5.0507812 21.890232 5.4877258 21.160156 6.3613281 L 7.0566406 23.257812 C 5.2226244 25.45627 6.8812774 29 9.7441406 29 L 38.255859 29 C 41.119312 29 42.778426 25.453888 40.943359 23.255859 L 26.839844 6.3613281 C 26.109768 5.4877258 25.054955 5.0507812 24 5.0507812 z M 24 8.015625 C 24.194045 8.015625 24.387185 8.105759 24.537109 8.2851562 L 38.638672 25.179688 C 38.977605 25.585659 38.784407 26 38.255859 26 L 9.7441406 26 C 9.2150038 26 9.0213443 25.58723 9.3613281 25.179688 L 23.462891 8.2851562 C 23.612815 8.1057586 23.805955 8.015625 24 8.015625 z M 10.5 33 C 8.0324991 33 6 35.032499 6 37.5 L 6 38.5 C 6 40.967501 8.0324991 43 10.5 43 L 37.5 43 C 39.967501 43 42 40.967501 42 38.5 L 42 37.5 C 42 35.032499 39.967501 33 37.5 33 L 10.5 33 z M 10.5 36 L 37.5 36 C 38.346499 36 39 36.653501 39 37.5 L 39 38.5 C 39 39.346499 38.346499 40 37.5 40 L 10.5 40 C 9.6535009 40 9 39.346499 9 38.5 L 9 37.5 C 9 36.653501 9.6535009 36 10.5 36 z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 48 48"
class="text-black dark:text-white w-4 h-4">
<path fill="currentColor" d="M 35.5 6 C 33.585045 6 32 7.5850452 32 9.5 L 32 19.365234 L 11.339844 6.6074219 C 9.0734225 5.2081236 6 6.9228749 6 9.5859375 L 6 38.414062 C 6 41.077126 9.0734225 42.791876 11.339844 41.392578 L 32 28.634766 L 32 38.5 C 32 40.414955 33.585045 42 35.5 42 L 38.5 42 C 40.414955 42 42 40.414955 42 38.5 L 42 9.5 C 42 7.5850452 40.414955 6 38.5 6 L 35.5 6 z M 35.5 9 L 38.5 9 C 38.795045 9 39 9.2049548 39 9.5 L 39 38.5 C 39 38.795045 38.795045 39 38.5 39 L 35.5 39 C 35.204955 39 35 38.795045 35 38.5 L 35 9.5 C 35 9.2049548 35.204955 9 35.5 9 z M 9.4765625 9.0566406 C 9.5668015 9.0647233 9.6637771 9.0984812 9.7636719 9.1601562 L 32 22.892578 L 32 25.107422 L 9.7636719 38.839844 C 9.364093 39.086546 9 38.883001 9 38.414062 L 9 9.5859375 C 9 9.3514688 9.091623 9.1841053 9.2324219 9.1054688 C 9.3028213 9.0661502 9.3863235 9.048558 9.4765625 9.0566406 z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 48 48"
class="text-black dark:text-white w-4 h-4">
<path fill="currentColor" d="M 7.4765625 4.9785156 A 1.50015 1.50015 0 0 0 6 6.5 L 6 31.5 L 6 43.5 A 1.50015 1.50015 0 1 0 9 43.5 L 9 33 L 40.5 33 C 41.329 33 42 32.328 42 31.5 L 42 6.5 C 42 5.672 41.329 5 40.5 5 L 7.7460938 5 A 1.50015 1.50015 0 0 0 7.4765625 4.9785156 z M 16 8 L 24 8 L 24 15 L 32 15 L 32 8 L 39 8 L 39 15 L 32 15 L 32 23 L 39 23 L 39 30 L 32 30 L 32 23 L 24 23 L 24 30 L 16 30 L 16 23 L 9 23 L 9 15 L 16 15 L 16 8 z M 16 15 L 16 23 L 24 23 L 24 15 L 16 15 z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 643 B

View File

@ -0,0 +1,5 @@
<c-svg title="GOG.com" viewbox="0 0 50 50">
<c-slot name="path">
M 5.75 6 C 3.703125 6 2 7.703125 2 9.75 L 2 16.25 C 2 18.296875 3.703125 20 5.75 20 L 12 20 L 12 22 L 4 22 C 3.277344 21.988281 2.609375 22.367188 2.246094 22.992188 C 1.878906 23.613281 1.878906 24.386719 2.246094 25.007813 C 2.609375 25.632813 3.277344 26.011719 4 26 L 12.25 26 C 14.296875 26 16 24.296875 16 22.25 L 16 15.75 C 16.003906 15.6875 16.003906 15.625 16 15.5625 L 16 9.75 C 16 7.703125 14.296875 6 12.25 6 Z M 21.75 6 C 19.703125 6 18 7.703125 18 9.75 L 18 16.25 C 18 18.296875 19.703125 20 21.75 20 L 28.25 20 C 30.296875 20 32 18.296875 32 16.25 L 32 9.75 C 32 7.703125 30.296875 6 28.25 6 Z M 37.75 6 C 35.703125 6 34 7.703125 34 9.75 L 34 16.25 C 34 18.296875 35.703125 20 37.75 20 L 44 20 L 44 22 L 36 22 C 35.277344 21.988281 34.609375 22.367188 34.246094 22.992188 C 33.878906 23.613281 33.878906 24.386719 34.246094 25.007813 C 34.609375 25.632813 35.277344 26.011719 36 26 L 44.25 26 C 46.296875 26 48 24.296875 48 22.25 L 48 15.75 C 48.003906 15.6875 48.003906 15.625 48 15.5625 L 48 9.75 C 48 7.703125 46.296875 6 44.25 6 Z M 6 10 L 12 10 L 12 15.59375 C 11.996094 15.644531 11.996094 15.699219 12 15.75 L 12 16 L 6 16 Z M 22 10 L 28 10 L 28 16 L 22 16 Z M 38 10 L 44 10 L 44 15.59375 C 43.996094 15.644531 43.996094 15.699219 44 15.75 L 44 16 L 38 16 Z M 5.75 30 C 3.703125 30 2 31.703125 2 33.75 L 2 40.25 C 2 42.296875 3.703125 44 5.75 44 L 12 44 C 12.722656 44.011719 13.390625 43.632813 13.753906 43.007813 C 14.121094 42.386719 14.121094 41.613281 13.753906 40.992188 C 13.390625 40.367188 12.722656 39.988281 12 40 L 6 40 L 6 34 L 12 34 C 12.722656 34.011719 13.390625 33.632813 13.753906 33.007813 C 14.121094 32.386719 14.121094 31.613281 13.753906 30.992188 C 13.390625 30.367188 12.722656 29.988281 12 30 Z M 19.75 30 C 17.703125 30 16 31.703125 16 33.75 L 16 40.25 C 16 42.296875 17.703125 44 19.75 44 L 26.25 44 C 28.296875 44 29.996094 42.296875 30 40.25 L 30 33.75 C 30 31.703125 28.296875 30 26.25 30 Z M 38.65625 30 C 38.65625 30 37.933594 30 37.15625 30.03125 C 36.769531 30.046875 36.355469 30.066406 36 30.09375 C 35.824219 30.105469 35.667969 30.136719 35.5 30.15625 C 35.332031 30.175781 35.242188 30.152344 34.8125 30.3125 C 33.738281 30.714844 32.972656 31.429688 32.4375 32.4375 C 32.5 32.320313 32.355469 32.496094 32.21875 32.90625 C 32.082031 33.316406 32.078125 33.566406 32.0625 33.875 C 32.03125 34.496094 32.011719 35.507813 32 37.8125 C 31.992188 39.167969 32 40.203125 32 40.90625 C 32 41.257813 31.996094 41.515625 32 41.71875 C 32 41.820313 31.996094 41.914063 32 42 C 32 42.042969 31.992188 42.085938 32 42.15625 C 32.007813 42.226563 31.914063 42.171875 32.125 42.71875 C 32.453125 43.707031 33.484375 44.277344 34.492188 44.035156 C 35.503906 43.789063 36.160156 42.808594 36 41.78125 C 36 41.777344 36 41.722656 36 41.71875 C 36 41.703125 36 41.707031 36 41.6875 C 35.996094 41.527344 36 41.25 36 40.90625 C 36 40.21875 35.992188 39.199219 36 37.84375 C 36.011719 35.640625 36.011719 34.671875 36.03125 34.25 C 36.101563 34.183594 36.167969 34.117188 36.1875 34.09375 C 36.230469 34.089844 36.257813 34.097656 36.3125 34.09375 C 36.585938 34.074219 36.949219 34.046875 37.3125 34.03125 C 37.667969 34.015625 37.75 34.003906 38 34 L 38 42 C 37.988281 42.722656 38.367188 43.390625 38.992188 43.753906 C 39.613281 44.121094 40.386719 44.121094 41.007813 43.753906 C 41.632813 43.390625 42.011719 42.722656 42 42 L 42 34 C 42.800781 34 43.28125 34 44 34 L 44 42 C 43.988281 42.722656 44.367188 43.390625 44.992188 43.753906 C 45.613281 44.121094 46.386719 44.121094 47.007813 43.753906 C 47.632813 43.390625 48.011719 42.722656 48 42 L 48 30 L 46 30 C 46 30 39.59375 29.996094 38.6875 30 Z M 20 34 L 26 34 L 26 40 L 20 40 Z
</c-slot>
</c-svg>

View File

@ -0,0 +1,6 @@
<c-svg title="Itch.io" viewBox="0 0 245.371 220.736" preserveAspectRatio="xMidYMid meet">
<c-slot name="path">
M31.99 1.365C21.287 7.72.2 31.945 0 38.298v10.516C0 62.144 12.46 73.86 23.773 73.86c13.584 0 24.902-11.258 24.903-24.62 0 13.362 10.93 24.62 24.515 24.62 13.586 0 24.165-11.258 24.165-24.62 0 13.362 11.622 24.62 25.207 24.62h.246c13.586 0 25.208-11.258 25.208-24.62 0 13.362 10.58 24.62 24.164 24.62 13.585 0 24.515-11.258 24.515-24.62 0 13.362 11.32 24.62 24.903 24.62 11.313 0 23.773-11.714 23.773-25.046V38.298c-.2-6.354-21.287-30.58-31.988-36.933C180.118.197 157.056-.005 122.685 0c-34.37.003-81.228.54-90.697 1.365zm65.194 66.217a28.025 28.025 0 0 1-4.78 6.155c-5.128 5.014-12.157 8.122-19.906 8.122a28.482 28.482 0 0 1-19.948-8.126c-1.858-1.82-3.27-3.766-4.563-6.032l-.006.004c-1.292 2.27-3.092 4.215-4.954 6.037a28.5 28.5 0 0 1-19.948 8.12c-.934 0-1.906-.258-2.692-.528-1.092 11.372-1.553 22.24-1.716 30.164l-.002.045c-.02 4.024-.04 7.333-.06 11.93.21 23.86-2.363 77.334 10.52 90.473 19.964 4.655 56.7 6.775 93.555 6.788h.006c36.854-.013 73.59-2.133 93.554-6.788 12.883-13.14 10.31-66.614 10.52-90.474-.022-4.596-.04-7.905-.06-11.93l-.003-.045c-.162-7.926-.623-18.793-1.715-30.165-.786.27-1.757.528-2.692.528a28.5 28.5 0 0 1-19.948-8.12c-1.862-1.822-3.662-3.766-4.955-6.037l-.006-.004c-1.294 2.266-2.705 4.213-4.563 6.032a28.48 28.48 0 0 1-19.947 8.125c-7.748 0-14.778-3.11-19.906-8.123a28.025 28.025 0 0 1-4.78-6.155 27.99 27.99 0 0 1-4.736 6.155 28.49 28.49 0 0 1-19.95 8.124c-.27 0-.54-.012-.81-.02h-.007c-.27.008-.54.02-.813.02a28.49 28.49 0 0 1-19.95-8.123 27.992 27.992 0 0 1-4.736-6.155zm-20.486 26.49l-.002.01h.015c8.113.017 15.32 0 24.25 9.746 7.028-.737 14.372-1.105 21.722-1.094h.006c7.35-.01 14.694.357 21.723 1.094 8.93-9.747 16.137-9.73 24.25-9.746h.014l-.002-.01c3.833 0 19.166 0 29.85 30.007L210 165.244c8.504 30.624-2.723 31.373-16.727 31.4-20.768-.773-32.267-15.855-32.267-30.935-11.496 1.884-24.907 2.826-38.318 2.827h-.006c-13.412 0-26.823-.943-38.318-2.827 0 15.08-11.5 30.162-32.267 30.935-14.004-.027-25.23-.775-16.726-31.4L46.85 124.08C57.534 94.073 72.867 94.073 76.7 94.073zm45.985 23.582v.006c-.02.02-21.863 20.08-25.79 27.215l14.304-.573v12.474c0 .584 5.74.346 11.486.08h.006c5.744.266 11.485.504 11.485-.08v-12.474l14.304.573c-3.928-7.135-25.79-27.215-25.79-27.215v-.006l-.003.002z
</c-slot>
</c-svg>
{% comment %} <svg xmlns="http://www.w3.org/2000/svg" height="235.452" width="261.728" viewBox="0 0 245.371 220.736"><path d="" color="#000" /></svg> {% endcomment %}

View File

@ -0,0 +1,5 @@
<c-svg title="Microsoft Store" viewbox="0 0 30 30">
<c-slot name="path">
M 6 4 C 4.895 4 4 4.895 4 6 L 4 12 C 4 13.105 4.895 14 6 14 L 12 14 C 13.105 14 14 13.105 14 12 L 14 6 C 14 4.895 13.105 4 12 4 L 6 4 z M 18 4 C 16.895 4 16 4.895 16 6 L 16 12 C 16 13.105 16.895 14 18 14 L 24 14 C 25.105 14 26 13.105 26 12 L 26 6 C 26 4.895 25.105 4 24 4 L 18 4 z M 6 16 C 4.895 16 4 16.895 4 18 L 4 24 C 4 25.105 4.895 26 6 26 L 12 26 C 13.105 26 14 25.105 14 24 L 14 18 C 14 16.895 13.105 16 12 16 L 6 16 z M 18 16 C 16.895 16 16 16.895 16 18 L 16 24 C 16 25.105 16.895 26 18 26 L 24 26 C 25.105 26 26 25.105 26 24 L 26 18 C 26 16.895 25.105 16 24 16 L 18 16 z
</c-slot>
</c-svg>

View File

@ -0,0 +1 @@
<c-icon.nintendo />

View File

@ -0,0 +1,5 @@
<c-svg title="Nintendo Switch" viewbox="0 0 32 32">
<c-slot name="path">
M18.901 32h4.901c4.5 0 8.198-3.698 8.198-8.198v-15.604c0-4.5-3.698-8.198-8.198-8.198h-5c-0.099 0-0.203 0.099-0.203 0.198v31.604c0 0.099 0.099 0.198 0.302 0.198zM25 14.401c1.802 0 3.198 1.5 3.198 3.198 0 1.802-1.5 3.198-3.198 3.198-1.802 0-3.198-1.396-3.198-3.198-0.104-1.797 1.396-3.198 3.198-3.198zM15.198 0h-7c-4.5 0-8.198 3.698-8.198 8.198v15.604c0 4.5 3.698 8.198 8.198 8.198h7c0.099 0 0.203-0.099 0.203-0.198v-31.604c0-0.099-0.099-0.198-0.203-0.198zM12.901 29.401h-4.703c-3.099 0-5.599-2.5-5.599-5.599v-15.604c0-3.099 2.5-5.599 5.599-5.599h4.604zM5 9.599c0 1.698 1.302 3 3 3s3-1.302 3-3c0-1.698-1.302-3-3-3s-3 1.302-3 3z
</c-slot>
</c-svg>

View File

@ -0,0 +1,6 @@
<c-vars title="Nintendo" />
<c-svg viewBox="0 0 24 24">
<c-slot name="path">
M0 .6h7.1l9.85 15.9V.6H24v22.8h-7.04L7.06 7.5v15.9H0V.6
</c-slot>
</c-svg>

View File

@ -0,0 +1,5 @@
<c-svg viewbox="0 0 512 512">
<title>Physical Media</title>
<path fill="currentColor" d="M277.333,256c0-11.755-9.557-21.333-21.333-21.333s-21.333,9.579-21.333,21.333c0,11.755,9.557,21.333,21.333,21.333 S277.333,267.755,277.333,256z" />
<path fill="currentColor" d="M256,0C114.837,0,0,114.837,0,256s114.837,256,256,256s256-114.837,256-256S397.163,0,256,0z M128,256 c0,11.776-9.536,21.333-21.333,21.333c-11.797,0-21.333-9.557-21.333-21.333c0-94.101,76.565-170.667,170.667-170.667 c11.797,0,21.333,9.557,21.333,21.333S267.797,128,256,128C185.408,128,128,185.408,128,256z M192,256c0-35.285,28.715-64,64-64 s64,28.715,64,64s-28.715,64-64,64S192,291.285,192,256z M256,426.667c-11.797,0-21.333-9.557-21.333-21.333S244.203,384,256,384 c70.592,0,128-57.408,128-128c0-11.776,9.536-21.333,21.333-21.333s21.333,9.557,21.333,21.333 C426.667,350.101,350.101,426.667,256,426.667z" />
</c-svg>

View File

@ -0,0 +1,5 @@
<c-svg viewbox="0 0 512 512">
<title>Physical Media</title>
<path fill="currentColor" d="M277.333,256c0-11.755-9.557-21.333-21.333-21.333s-21.333,9.579-21.333,21.333c0,11.755,9.557,21.333,21.333,21.333 S277.333,267.755,277.333,256z" />
<path fill="currentColor" d="M256,0C114.837,0,0,114.837,0,256s114.837,256,256,256s256-114.837,256-256S397.163,0,256,0z M128,256 c0,11.776-9.536,21.333-21.333,21.333c-11.797,0-21.333-9.557-21.333-21.333c0-94.101,76.565-170.667,170.667-170.667 c11.797,0,21.333,9.557,21.333,21.333S267.797,128,256,128C185.408,128,128,185.408,128,256z M192,256c0-35.285,28.715-64,64-64 s64,28.715,64,64s-28.715,64-64,64S192,291.285,192,256z M256,426.667c-11.797,0-21.333-9.557-21.333-21.333S244.203,384,256,384 c70.592,0,128-57.408,128-128c0-11.776,9.536-21.333,21.333-21.333s21.333,9.557,21.333,21.333 C426.667,350.101,350.101,426.667,256,426.667z" />
</c-svg>

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 48 48"
class="text-black dark:text-white 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>

After

Width:  |  Height:  |  Size: 861 B

View File

@ -0,0 +1,6 @@
<c-vars title="Playstation 1" />
<c-svg viewBox="0 0 50 50">
<c-slot name="path">
M 19.3125 4 C 19.011719 4 18.707031 3.988281 18.40625 4.1875 C 18.105469 4.386719 18 4.699219 18 5 L 18 41.59375 C 18 41.992188 18.289063 42.394531 18.6875 42.59375 L 26.6875 45 L 27 45 C 27.199219 45 27.394531 44.914063 27.59375 44.8125 C 27.894531 44.613281 28 44.300781 28 44 L 28 13.40625 C 28.601563 13.707031 29 14.300781 29 15 L 29 26.09375 C 29 26.394531 29.199219 26.804688 29.5 26.90625 C 29.699219 27.007813 31.199219 27.90625 34 27.90625 C 36.699219 27.90625 40 26.414063 40 19.3125 C 40 13.613281 36.8125 9.292969 31.3125 7.59375 Z M 17 26.40625 L 5.90625 30.40625 L 4.3125 31 C 1.613281 32.101563 0 33.886719 0 35.6875 C 0 39.488281 2.699219 41.6875 7.5 41.6875 C 10.101563 41.6875 13.300781 41.113281 17 39.8125 L 17 36 C 16.101563 36.300781 15.113281 36.699219 14.3125 37 C 12.710938 37.601563 11.5 37.8125 10.5 37.8125 C 9 37.8125 8.300781 37.300781 8 37 C 7.601563 36.699219 7.398438 36.3125 7.5 35.8125 C 7.601563 34.8125 8.800781 33.894531 11 33.09375 C 11.5 32.894531 14.898438 31.699219 17 31 Z M 36.5 28.90625 C 34.101563 29.007813 31.601563 29.394531 29 30.09375 L 29 34.6875 C 30.101563 34.289063 31.585938 33.800781 33.6875 33 C 38.488281 31.300781 40.492188 31.488281 41.09375 31.6875 C 42.292969 31.789063 42.800781 32.5 43 33 C 43.5 34.5 41.613281 35.1875 38.8125 36.1875 C 37.511719 36.6875 31.898438 38.6875 29 39.6875 L 29 44.3125 L 44.5 38.8125 L 45.6875 38.3125 C 47.6875 37.613281 50.199219 36.300781 50 34 C 49.898438 31.800781 47.210938 30.695313 45.3125 30.09375 C 42.511719 29.195313 39.5 28.804688 36.5 28.90625 Z
</c-slot>
</c-svg>

View File

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="50"
height="50"
viewBox="0 0 48 48"
class="w-4 h-4">
<path fill="currentColor" d="M 23.976562 4.9785156 A 1.50015 1.50015 0 0 0 22.5 6.5 L 22.5 22.5 L 6.5 22.5 A 1.50015 1.50015 0 1 0 6.5 25.5 L 22.5 25.5 L 22.5 41.5 A 1.50015 1.50015 0 1 0 25.5 41.5 L 25.5 25.5 L 41.5 25.5 A 1.50015 1.50015 0 1 0 41.5 22.5 L 25.5 22.5 L 25.5 6.5 A 1.50015 1.50015 0 0 0 23.976562 4.9785156 z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1 @@
<c-icon.playstation />

View File

@ -0,0 +1,5 @@
<c-svg title="Playstation 3" viewbox="0 0 50 50">
<c-slot name="path">
M 1 19 A 1.0001 1.0001 0 1 0 1 21 L 12.5 21 C 13.340812 21 14 21.659188 14 22.5 C 14 23.340812 13.340812 24 12.5 24 L 3 24 C 1.3550302 24 0 25.35503 0 27 L 0 30 A 1.0001 1.0001 0 1 0 2 30 L 2 27 C 2 26.43497 2.4349698 26 3 26 L 12.5 26 C 14.28508 26 15.719786 24.619005 15.921875 22.884766 A 1.0001 1.0001 0 0 0 16 22.5 C 16 20.578812 14.421188 19 12.5 19 L 1 19 z M 26 19 C 24.35503 19 23 20.35503 23 22 L 23 28 C 23 28.56503 22.56503 29 22 29 L 16 29 A 1.0001 1.0001 0 1 0 16 31 L 22 31 C 23.64497 31 25 29.64497 25 28 L 25 22 C 25 21.43497 25.43497 21 26 21 L 32 21 A 1.0001 1.0001 0 1 0 32 19 L 26 19 z M 36 19 A 1.0001 1.0001 0 1 0 36 21 L 46.5 21 C 47.340812 21 48 21.659188 48 22.5 C 48 23.340812 47.340812 24 46.5 24 L 36 24 A 1.0001 1.0001 0 1 0 36 26 L 46.5 26 C 47.340812 26 48 26.659188 48 27.5 C 48 28.340812 47.340812 29 46.5 29 L 36 29 A 1.0001 1.0001 0 1 0 36 31 L 46.5 31 C 48.421188 31 50 29.421188 50 27.5 C 50 26.523075 49.58945 25.637295 48.935547 25 C 49.58945 24.362705 50 23.476925 50 22.5 C 50 20.578812 48.421188 19 46.5 19 L 36 19 z
</c-slot>
</c-svg>

View File

@ -0,0 +1,6 @@
<c-vars title="Playstation 4" />
<c-svg :title=title viewbox="0 0 50 50">
<c-slot name="path">
M 1 19 A 1.0001 1.0001 0 1 0 1 21 L 12.5 21 C 13.340812 21 14 21.659188 14 22.5 C 14 23.340812 13.340812 24 12.5 24 L 3 24 C 1.3550302 24 0 25.35503 0 27 L 0 30 A 1.0001 1.0001 0 1 0 2 30 L 2 27 C 2 26.43497 2.4349698 26 3 26 L 12.5 26 C 14.28508 26 15.719786 24.619005 15.921875 22.884766 A 1.0001 1.0001 0 0 0 16 22.5 C 16 20.578812 14.421188 19 12.5 19 L 1 19 z M 26 19 C 24.35503 19 23 20.35503 23 22 L 23 28 C 23 28.56503 22.56503 29 22 29 L 16 29 A 1.0001 1.0001 0 1 0 16 31 L 22 31 C 23.64497 31 25 29.64497 25 28 L 25 22 C 25 21.43497 25.43497 21 26 21 L 32 21 A 1.0001 1.0001 0 1 0 32 19 L 26 19 z M 46.970703 19 A 1.0001 1.0001 0 0 0 46.503906 19.130859 L 32.503906 27.130859 A 1.0001 1.0001 0 0 0 33 29 L 46 29 L 46 30 A 1.0001 1.0001 0 1 0 48 30 L 48 29 L 49 29 A 1.0001 1.0001 0 1 0 49 27 L 48 27 L 48 20 A 1.0001 1.0001 0 0 0 46.970703 19 z M 46 21.724609 L 46 27 L 36.767578 27 L 46 21.724609 z
</c-slot>
</c-svg>

View File

@ -0,0 +1,5 @@
<c-svg title="Playstation 5" viewbox="0 0 50 50">
<c-slot name="path">
M25.185 19.606c-1.612 0-2.919 1.307-2.919 2.919v4.981c0 .911-.739 1.65-1.65 1.65h-5.619v1.237h6.683c1.612 0 2.919-1.307 2.919-2.919v-4.981c0-.911.739-1.65 1.65-1.65l5.724 0v-1.237H25.185zM0 19.606v1.237h11.738c.936 0 1.694.758 1.694 1.694 0 .936-.758 1.694-1.694 1.694H2.919C1.307 24.231 0 25.538 0 27.15v3.244h2.333v-3.276c0-.911.739-1.65 1.65-1.65h8.851c1.619 0 2.931-1.312 2.931-2.931 0-1.619-1.312-2.931-2.931-2.931H0zM34.221 19.606v4.028c0 1.012.821 1.833 1.833 1.833h9.768c1.019 0 1.845.826 1.845 1.845 0 1.019-.826 1.845-1.845 1.845H34.221v1.237h12.697c1.702 0 3.082-1.38 3.082-3.082 0-1.702-1.38-3.082-3.082-3.082h-9.628c-.407 0-.737-.33-.737-.737v-2.651h13.023v-1.237H34.221z
</c-slot>
</c-svg>

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 48 48"
class="text-black dark:text-white w-4 h-4">
<path fill="currentColor" d="M 26 3 C 20.494917 3 16 7.494921 16 13 C 16 18.505079 20.494917 23 26 23 C 31.505083 23 36 18.505079 36 13 C 36 7.494921 31.505083 3 26 3 z M 26 6 C 29.883764 6 33 9.1162385 33 13 C 33 16.883762 29.883764 20 26 20 C 22.116236 20 19 16.883762 19 13 C 19 9.1162385 22.116236 6 26 6 z M 24.75 9 C 24.273 9 23.862531 9.3366875 23.769531 9.8046875 L 23.269531 12.304688 C 23.210531 12.598688 23.286562 12.903766 23.476562 13.134766 C 23.666562 13.366766 23.95 13.5 24.25 13.5 L 26.25 13.5 C 26.664 13.5 27 13.836 27 14.25 C 27 14.765 26.481 15 26 15 C 25.115 15 24.583922 14.685156 24.544922 14.660156 C 24.085922 14.363156 23.472969 14.489313 23.167969 14.945312 C 22.861969 15.405313 22.986313 16.026031 23.445312 16.332031 C 23.548313 16.400031 24.491 17 26 17 C 27.71 17 29 15.818 29 14.25 C 29 12.733 27.767 11.5 26.25 11.5 L 25.470703 11.5 L 25.570312 11 L 27.5 11 C 28.052 11 28.5 10.552 28.5 10 C 28.5 9.448 28.052 9 27.5 9 L 24.75 9 z M 41.613281 22.019531 C 40.493082 22.029231 39.429184 22.473279 38.484375 23.175781 C 37.470126 23.929243 34.418425 26.208042 31.777344 28.179688 C 31.204622 26.351508 29.505924 25 27.5 25 L 23.107422 25 C 20.296203 25 18.985532 24.772226 17.859375 24.533203 C 16.733218 24.29418 15.646783 24 13.826172 24 C 9.9413941 24 7.0123317 26.492986 5.09375 28.791016 C 3.1751683 31.089045 2.1347656 33.384766 2.1347656 33.384766 A 1.5002787 1.5002787 0 1 0 4.8652344 34.628906 C 4.8652344 34.628906 5.7643161 32.669814 7.3964844 30.714844 C 9.0286527 28.759873 11.25995 27 13.826172 27 C 15.347561 27 16.006735 27.20582 17.236328 27.466797 C 18.465921 27.727774 20.123641 28 23.107422 28 L 27.5 28 C 28.346499 28 29 28.653501 29 29.5 C 29 29.969499 28.794195 30.374296 28.470703 30.646484 C 28.470416 30.646699 28.429688 30.677734 28.429688 30.677734 A 1.5001988 1.5001988 0 0 0 28.345703 30.748047 A 1.5001988 1.5001988 0 0 0 28.34375 30.75 C 28.105295 30.908613 27.816466 31 27.5 31 L 20.5 31 A 1.50015 1.50015 0 1 0 20.5 34 L 27.5 34 C 28.440637 34 29.315307 33.701451 30.041016 33.199219 C 30.042186 33.198409 30.043753 33.198076 30.044922 33.197266 A 1.5001988 1.5001988 0 0 0 30.224609 33.082031 C 30.224609 33.082031 38.775959 26.696426 40.273438 25.583984 A 1.50015 1.50015 0 0 0 40.273438 25.582031 C 40.837627 25.162534 41.309824 25.022381 41.640625 25.019531 C 41.971426 25.016631 42.218287 25.096901 42.560547 25.439453 C 43.150922 26.029324 43.147391 26.935102 42.572266 27.533203 C 37.217133 32.036197 33.848465 35.036886 31.623047 36.794922 C 29.369881 38.574924 28.424996 39 27.5 39 C 23.847885 39 19.80067 38 15.5 38 C 13 38 11.242781 39.343609 10.300781 40.599609 C 9.3587815 41.855609 9.0449219 43.136719 9.0449219 43.136719 A 1.50015 1.50015 0 1 0 11.955078 43.863281 C 11.955078 43.863281 12.141219 43.144391 12.699219 42.400391 C 13.257219 41.656391 14 41 15.5 41 C 19.30733 41 23.336115 42 27.5 42 C 29.402004 42 31.084837 41.044435 33.482422 39.150391 C 35.849764 37.280238 39.175413 34.30991 44.498047 29.833984 A 1.50015 1.50015 0 0 0 44.681641 29.681641 C 44.688541 29.674741 44.690436 29.665143 44.697266 29.658203 L 44.701172 29.662109 L 44.753906 29.607422 A 1.50015 1.50015 0 0 0 45.083984 29.074219 C 46.330485 27.321792 46.248887 24.884269 44.681641 23.318359 C 43.8529 22.488911 42.73348 22.009881 41.613281 22.019531 z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,5 @@
<c-svg title="Steam" viewbox="0 0 50 50">
<c-slot name="path">
M 25 3 C 13.59 3 4.209375 11.680781 3.109375 22.800781 L 14.300781 28.529297 C 15.430781 27.579297 16.9 27 18.5 27 L 18.550781 27 C 18.940781 26.4 19.389375 25.649141 19.859375 24.869141 C 20.839375 23.259141 21.939531 21.439062 23.019531 20.039062 C 23.259531 15.569063 26.97 12 31.5 12 C 36.19 12 40 15.81 40 20.5 C 40 25.03 36.430937 28.740469 31.960938 28.980469 C 30.560938 30.060469 28.750859 31.160859 27.130859 32.130859 C 26.350859 32.610859 25.6 33.059219 25 33.449219 L 25 33.5 C 25 37.09 22.09 40 18.5 40 C 14.91 40 12 37.09 12 33.5 C 12 33.33 12.009531 33.17 12.019531 33 L 3.2792969 28.519531 C 4.9692969 38.999531 14.05 47 25 47 C 37.15 47 47 37.15 47 25 C 47 12.85 37.15 3 25 3 z M 31.5 14 C 27.92 14 25 16.92 25 20.5 C 25 24.08 27.92 27 31.5 27 C 35.08 27 38 24.08 38 20.5 C 38 16.92 35.08 14 31.5 14 z M 31.5 16 C 33.99 16 36 18.01 36 20.5 C 36 22.99 33.99 25 31.5 25 C 29.01 25 27 22.99 27 20.5 C 27 18.01 29.01 16 31.5 16 z M 18.5 29 C 17.71 29 16.960313 29.200312 16.320312 29.570312 L 19.640625 31.269531 C 20.870625 31.899531 21.350469 33.410625 20.730469 34.640625 C 20.280469 35.500625 19.41 36 18.5 36 C 18.11 36 17.729375 35.910469 17.359375 35.730469 L 14.029297 34.019531 C 14.289297 36.259531 16.19 38 18.5 38 C 20.99 38 23 35.99 23 33.5 C 23 31.01 20.99 29 18.5 29 z
</c-slot>
</c-svg>

View File

@ -0,0 +1,5 @@
<c-svg title="Ubisoft" viewbox="0 0 50 50">
<c-slot name="path">
M 19.5 0 C 17.570313 0 16 1.570313 16 3.5 C 16 5.429688 17.570313 7 19.5 7 C 21.429688 7 23 5.429688 23 3.5 C 23 1.570313 21.429688 0 19.5 0 Z M 7.59375 2 C 3.261719 2 0 5.550781 0 10.25 C 0 14.527344 4.402344 16.5 7.375 16.5 C 10.441406 16.5 15 14.429688 15 8.65625 C 15 3.398438 10.152344 2 7.59375 2 Z M 31.3125 5.03125 C 18.890625 5.03125 8.8125 16.082031 8.8125 29.65625 C 8.8125 41.664063 23.785156 49 31.9375 49 C 40.46875 49 50 38.972656 50 25.53125 C 50 12.53125 35.816406 5.03125 31.3125 5.03125 Z M 18 19 L 23 19 L 23 30.5 C 23 31.328125 23.671875 32 24.5 32 L 33.5 32 C 34.328125 32 35 31.328125 35 30.5 L 35 19 L 40 19 L 40 37 L 24.5 37 C 20.910156 37 18 34.089844 18 30.5 Z
</c-slot>
</c-svg>

View File

@ -0,0 +1,8 @@
<c-svg viewbox="0 0 50 50">
<g transform="scale(0.390625)">
<title>Unspecified platform</title>
<path fill="currentColor" d="M64.9,28.9c-5.2-0.2-10.1,1.6-13.9,5.2c-3.8,3.6-5.8,8.4-5.8,13.6h7.7c0-3.1,1.2-5.9,3.5-8.1c2.2-2.1,5.1-3.2,8.2-3.1 c5.7,0.3,10.3,4.9,10.6,10.6c0.3,6-5.7,11.5-8.3,13.6l-7.7,6.8v7.7h7.7V71l4.9-4.3c4.3-3.5,11.5-10.8,11.1-19.9 C82.4,37.2,74.5,29.3,64.9,28.9z" />
<rect fill="currentColor" height="8" width="7.7" x="59.2" y="83.3" />
<path fill="currentColor" d="M1,127h126V1H1V127z M9,9h110v110H9V9z" />
</g>
</c-svg>

View File

@ -0,0 +1,5 @@
<c-svg title="Xbox/GamePass" viewbox="0 0 30 30">
<c-slot name="path">
M 15 3 C 13.051 3 10.635984 3.6181719 8.8339844 4.7011719 C 8.7039844 4.7771719 8.3470625 5.0221406 8.1640625 5.2441406 L 8.1640625 5.2460938 C 9.8830625 3.3500938 14.893 7.3485937 15 7.4335938 L 15 7.4355469 C 15.107 7.3505469 20.116938 3.3520937 21.835938 5.2460938 C 21.651937 5.0240937 21.295063 4.780125 21.164062 4.703125 C 19.363063 3.622125 17.254953 3 15.001953 3 L 15 3 z M 7.4414062 6.1035156 C 7.0363594 6.1687656 6.5830625 6.4272031 6.1953125 6.8457031 C 4.2123125 8.9867031 3 11.850047 3 14.998047 C 3 18.106507 4.1826933 20.935533 6.1210938 23.066406 C 5.4850937 19.988406 6.0637812 17.819047 7.8007812 14.998047 C 9.5407813 12.174047 12.599609 8.9980469 12.599609 8.9980469 C 10.075609 6.6150469 8.2821719 6.1885156 7.8261719 6.1035156 C 7.7061719 6.0810156 7.5764219 6.0817656 7.4414062 6.1035156 z M 6.1210938 23.066406 C 6.1210938 23.066406 6.1210938 23.068359 6.1210938 23.068359 L 6.1210938 23.070312 C 8.3160938 25.482313 11.494953 27 15.001953 27 C 18.518953 27 21.684859 25.485219 23.880859 23.074219 L 23.880859 23.072266 C 25.818859 20.940266 27 18.109 27 15 C 27 11.852 25.788687 8.9896563 23.804688 6.8476562 C 23.287688 6.2896563 22.653828 6.0154687 22.173828 6.1054688 C 21.718828 6.1914688 19.924391 6.618 17.400391 9 C 17.400391 9 20.459219 12.176 22.199219 15 C 23.935219 17.822 24.514906 19.990359 23.878906 23.068359 C 23.872906 23.033359 23.629672 21.45375 22.013672 19.21875 C 20.750672 17.47575 17 13.300391 15 11.400391 C 13 13.300391 9.2493281 17.471797 7.9863281 19.216797 C 6.3703281 21.449797 6.1270937 23.030406 6.1210938 23.066406 z
</c-slot>
</c-svg>

View File

@ -0,0 +1,5 @@
<c-svg viewbox="0 0 80 80" preserveAspectRatio="xMidYMid meet" stroke-width="3.6">
<title>Yuzu (Switch emulator)</title>
<path fill="currentColor" d="m30,2a28,30 0 1,0 0,60z" />
<path fill="currentColor" d="m42,78a28,30 0 1,0 0-60z" />
</c-svg>

View File

@ -0,0 +1,25 @@
<c-layouts.base>
{% load static %}
{% if form_content %}
{{ form_content }}
{% else %}
<form method="post" enctype="multipart/form-data">
<table class="mx-auto">
{% csrf_token %}
{{ form.as_table }}
<tr>
<td></td>
<td>
<input type="submit" value="Submit" />
</td>
</tr>
{{ additional_row }}
</table>
</form>
{% endif %}
<c-slot name="scripts">
{% if script_name %}
<script type="module" src="{% static 'js/'|add:script_name %}"></script>
{% endif %}
</c-slot>
</c-layouts.base>

Some files were not shown because too many files have changed in this diff Show More