Compare commits
29 Commits
345315db38
...
1.0.3
Author | SHA1 | Date | |
---|---|---|---|
2640a49734
|
|||
65c175afb2
|
|||
0814071a26
|
|||
5f845f866e
|
|||
c3d4697470
|
|||
77293f03e9 | |||
1fa364e2ec | |||
4a6f4a2f9a | |||
9590988b6a | |||
938c82a395 | |||
33939f631c | |||
ac8cd6534a | |||
51d8e953c0 | |||
2eec677f41 | |||
f2eb14d3ef | |||
c337d2200f | |||
8a8b05b0bd | |||
9446065271 | |||
755093845d | |||
d4ab0596da | |||
8dcbe2f0ad | |||
25bc74eff1 | |||
8a7d083fb2 | |||
8296ebcf31
|
|||
04a4f2e0be | |||
4070b4e46e | |||
4892218c83 | |||
6b00a950ce | |||
feee9d6dac |
@ -25,7 +25,9 @@ steps:
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: registry.kucharczyk.xyz/timetracker
|
||||
auto_tag: true
|
||||
tags:
|
||||
- ${DRONE_COMMIT_REF}
|
||||
- ${DRONE_COMMIT_BRANCH}
|
||||
when:
|
||||
branch:
|
||||
exclude:
|
||||
|
3
.gitignore
vendored
@ -5,5 +5,6 @@ __pycache__
|
||||
node_modules
|
||||
package-lock.json
|
||||
db.sqlite3
|
||||
static
|
||||
static/admin/
|
||||
static/django_extensions/
|
||||
dist/
|
20
CHANGELOG.md
@ -1,6 +1,22 @@
|
||||
## Unrelease
|
||||
## 1.0.3 / 2023-02-20 17:16+01:00
|
||||
|
||||
* Add React
|
||||
* Add wikidata ID and year for editions
|
||||
* Add icons for game, edition, purchase filters
|
||||
* Allow filtering by game, edition, purchase from the session list
|
||||
* Allow editing filtered entities from session list
|
||||
|
||||
## 1.0.2 / 2023-02-18 21:48+01:00
|
||||
|
||||
* Add support for device info (https://git.kucharczyk.xyz/lukas/timetracker/issues/49)
|
||||
* Add support for purchase ownership information (https://git.kucharczyk.xyz/lukas/timetracker/issues/48)
|
||||
* Add support for purchase prices
|
||||
* Add support for game editions (https://git.kucharczyk.xyz/lukas/timetracker/issues/28)
|
||||
|
||||
## 1.0.1 / 2023-01-30 22:17+01:00
|
||||
|
||||
* Make it possible to edit sessions (https://git.kucharczyk.xyz/lukas/timetracker/issues/46)
|
||||
* Show markers on smaller graphs to make it clearer which dates the session belong to
|
||||
* Show only last 30 days on the homepage (https://git.kucharczyk.xyz/lukas/timetracker/issues/47)
|
||||
|
||||
## 1.0.0 / 2023-01-20 19:54+01:00
|
||||
|
||||
|
@ -6,7 +6,7 @@ RUN npm install && \
|
||||
|
||||
FROM python:3.10.9-slim-bullseye
|
||||
|
||||
ENV VERSION_NUMBER 1.0.0
|
||||
ENV VERSION_NUMBER 1.0.3
|
||||
ENV PROD 1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
|
10
Makefile
@ -1,12 +1,18 @@
|
||||
all: migrate
|
||||
all: css migrate
|
||||
|
||||
initialize: npm migrate sethookdir loadplatforms
|
||||
initialize: npm css migrate sethookdir loadplatforms
|
||||
|
||||
HTMLFILES := $(shell find games/templates -type f)
|
||||
|
||||
npm:
|
||||
npm install
|
||||
|
||||
css: common/input.css
|
||||
npx tailwindcss -i ./common/input.css -o ./games/static/base.css
|
||||
|
||||
css-dev: css
|
||||
npx tailwindcss -i ./common/input.css -o ./games/static/base.css --watch
|
||||
|
||||
makemigrations:
|
||||
poetry run python manage.py makemigrations
|
||||
|
||||
|
46
common/input.css
Normal file
@ -0,0 +1,46 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
form label {
|
||||
@apply dark:text-slate-400;
|
||||
}
|
||||
|
||||
form input,
|
||||
select,
|
||||
textarea {
|
||||
@apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
|
||||
}
|
||||
|
||||
#session-table {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 2fr repeat(2, 1fr) 0.5fr 1fr;
|
||||
}
|
||||
|
||||
.purchase-name > span:nth-child(2) {
|
||||
@apply ml-4
|
||||
}
|
||||
|
||||
.purchase-name > span:nth-child(2) > a > img {
|
||||
@apply opacity-0 transition-opacity duration-500
|
||||
}
|
||||
|
||||
.purchase-name:hover > span:nth-child(2) > a > img {
|
||||
@apply opacity-50
|
||||
}
|
||||
|
||||
.purchase-name > span:nth-child(2) > a > img:hover {
|
||||
@apply opacity-100
|
||||
}
|
||||
|
||||
#button-container button {
|
||||
@apply mx-1;
|
||||
}
|
||||
|
||||
th {
|
||||
@apply text-left;
|
||||
}
|
||||
|
||||
th label {
|
||||
@apply mr-4;
|
||||
}
|
@ -64,7 +64,7 @@ def get_chart(data, title="", xlabel="", ylabel=""):
|
||||
plt.switch_backend("SVG")
|
||||
fig, ax = plt.subplots()
|
||||
fig.set_size_inches(10, 4)
|
||||
ax.plot(x, y)
|
||||
lines = ax.plot(x, y, "-o")
|
||||
first = x[0]
|
||||
last = x[-1]
|
||||
difference = last - first
|
||||
@ -76,7 +76,11 @@ def get_chart(data, title="", xlabel="", ylabel=""):
|
||||
elif difference.days < 720:
|
||||
ax.xaxis.set_major_locator(mdates.MonthLocator())
|
||||
ax.xaxis.set_minor_locator(mdates.WeekdayLocator())
|
||||
for line in lines:
|
||||
line.set_marker("")
|
||||
else:
|
||||
for line in lines:
|
||||
line.set_marker("")
|
||||
ax.xaxis.set_major_locator(mdates.YearLocator())
|
||||
ax.xaxis.set_minor_locator(mdates.MonthLocator())
|
||||
|
||||
|
@ -1,17 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true
|
||||
},
|
||||
extends: ["eslint/recommended", "plugin:react/recommended", "plugin:prettier/recommended"],
|
||||
overrides: [],
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module"
|
||||
},
|
||||
plugins: ["react"],
|
||||
rules: {},
|
||||
parserOptions: {
|
||||
ecmaFeatures: { jsx: true }
|
||||
}
|
||||
};
|
24
frontend/.gitignore
vendored
@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"bracketSameLine": false,
|
||||
"singleAttributePerLine": true
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<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.0" />
|
||||
<!-- TODO: replace with own icon -->
|
||||
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"/>
|
||||
<title>Timetracker</title>
|
||||
</head>
|
||||
<body class="dark">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"autoprefixer": "^10.4.13",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"postcss": "^8.4.21",
|
||||
"prettier": "^2.8.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.7.0",
|
||||
"tailwindcss": "^3.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@vitejs/plugin-react": "^3.0.0",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.32.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"vite": "^4.0.0"
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import "./App.css";
|
||||
import Nav from "./components/Nav";
|
||||
import { Outlet, useLoaderData } from "react-router-dom";
|
||||
|
||||
function App() {
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark:bg-gray-800 min-h-screen">
|
||||
<Nav />
|
||||
<Outlet />
|
||||
</div>
|
||||
{/* {% load version %} */}
|
||||
{/* <span className="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
@ -1,71 +0,0 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
function Nav() {
|
||||
return (
|
||||
<nav className="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded">
|
||||
<div className="container flex flex-wrap items-center justify-between mx-auto">
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center"
|
||||
>
|
||||
<span className="text-4xl">⌚</span>
|
||||
<span className="self-center text-xl font-semibold whitespace-nowrap text-white">
|
||||
Timetracker
|
||||
</span>
|
||||
</Link>
|
||||
<div className="w-full md:block md:w-auto">
|
||||
<ul className="flex flex-col md:flex-row p-4 mt-4 dark:text-white">
|
||||
<li>
|
||||
<a
|
||||
className="block py-2 pl-3 pr-4 hover:underline"
|
||||
href="{% url 'add_game' %}"
|
||||
>
|
||||
New Game
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="block py-2 pl-3 pr-4 hover:underline"
|
||||
href="{% url 'add_platform' %}"
|
||||
>
|
||||
New Platform
|
||||
</a>
|
||||
</li>
|
||||
{/* {% if game_available and platform_available %} */}
|
||||
<li>
|
||||
<a
|
||||
className="block py-2 pl-3 pr-4 hover:underline"
|
||||
href="{% url 'add_purchase' %}"
|
||||
>
|
||||
New Purchase
|
||||
</a>
|
||||
</li>
|
||||
{/* {% endif %} */}
|
||||
{/* {% if purchase_available %} */}
|
||||
<li>
|
||||
<a
|
||||
className="block py-2 pl-3 pr-4 hover:underline"
|
||||
href="{% url 'add_session' %}"
|
||||
>
|
||||
New Session
|
||||
</a>
|
||||
</li>
|
||||
{/* {% endif %} */}
|
||||
{/* {% if session_count > 0 %} */}
|
||||
<li>
|
||||
<Link
|
||||
className="block py-2 pl-3 pr-4 hover:underline"
|
||||
to="/sessions"
|
||||
>
|
||||
All Sessions
|
||||
</Link>
|
||||
</li>
|
||||
{/* {% endif %} */}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export default Nav;
|
@ -1,162 +0,0 @@
|
||||
export default function SessionList() {
|
||||
const data = [
|
||||
{
|
||||
"url": "http://localhost:8000/api/sessions/25/",
|
||||
"timestamp_start": "2020-01-01T00:00:00+01:00",
|
||||
"timestamp_end": null,
|
||||
"duration_manual": "12:00:00",
|
||||
"duration_calculated": "00:00:00",
|
||||
"note": "",
|
||||
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/api/sessions/26/",
|
||||
"timestamp_start": "2022-12-31T15:25:00+01:00",
|
||||
"timestamp_end": "2022-12-31T17:25:00+01:00",
|
||||
"duration_manual": "00:00:00",
|
||||
"duration_calculated": "02:00:00",
|
||||
"note": "",
|
||||
"purchase": "http://localhost:8000/api/purchases/2/"
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/api/sessions/27/",
|
||||
"timestamp_start": "2023-01-01T23:00:00+01:00",
|
||||
"timestamp_end": "2023-01-02T00:28:00+01:00",
|
||||
"duration_manual": "00:00:00",
|
||||
"duration_calculated": "01:28:00",
|
||||
"note": "",
|
||||
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/api/sessions/28/",
|
||||
"timestamp_start": "2023-01-02T22:08:00+01:00",
|
||||
"timestamp_end": "2023-01-03T01:08:00+01:00",
|
||||
"duration_manual": "00:00:00",
|
||||
"duration_calculated": "03:00:00",
|
||||
"note": "",
|
||||
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/api/sessions/29/",
|
||||
"timestamp_start": "2023-01-03T22:36:00+01:00",
|
||||
"timestamp_end": "2023-01-04T00:12:00+01:00",
|
||||
"duration_manual": "00:00:00",
|
||||
"duration_calculated": "01:36:00",
|
||||
"note": "",
|
||||
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/api/sessions/30/",
|
||||
"timestamp_start": "2023-01-04T20:35:00+01:00",
|
||||
"timestamp_end": "2023-01-04T22:36:00+01:00",
|
||||
"duration_manual": "00:00:00",
|
||||
"duration_calculated": "02:01:00",
|
||||
"note": "",
|
||||
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/api/sessions/31/",
|
||||
"timestamp_start": "2023-01-06T18:48:00+01:00",
|
||||
"timestamp_end": "2023-01-06T23:39:00+01:00",
|
||||
"duration_manual": "00:00:00",
|
||||
"duration_calculated": "04:51:00",
|
||||
"note": "",
|
||||
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/api/sessions/32/",
|
||||
"timestamp_start": "2023-01-07T23:49:00+01:00",
|
||||
"timestamp_end": "2023-01-08T01:43:00+01:00",
|
||||
"duration_manual": "00:00:00",
|
||||
"duration_calculated": "01:54:00",
|
||||
"note": "",
|
||||
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/api/sessions/33/",
|
||||
"timestamp_start": "2023-01-08T16:21:00+01:00",
|
||||
"timestamp_end": "2023-01-08T18:27:00+01:00",
|
||||
"duration_manual": "00:00:00",
|
||||
"duration_calculated": "02:06:00",
|
||||
"note": "",
|
||||
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/api/sessions/34/",
|
||||
"timestamp_start": "2023-01-08T19:04:00+01:00",
|
||||
"timestamp_end": "2023-01-08T22:03:00+01:00",
|
||||
"duration_manual": "00:00:00",
|
||||
"duration_calculated": "02:59:00",
|
||||
"note": "",
|
||||
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/api/sessions/35/",
|
||||
"timestamp_start": "2023-01-09T19:35:48+01:00",
|
||||
"timestamp_end": "2023-01-09T22:13:20.519058+01:00",
|
||||
"duration_manual": "00:00:00",
|
||||
"duration_calculated": "02:37:32.519058",
|
||||
"note": "",
|
||||
"purchase": "http://localhost:8000/api/purchases/3/"
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:8000/api/sessions/36/",
|
||||
"timestamp_start": "2023-01-10T15:50:12+01:00",
|
||||
"timestamp_end": "2023-01-10T17:03:45.424429+01:00",
|
||||
"duration_manual": "00:00:00",
|
||||
"duration_calculated": "01:13:33.424429",
|
||||
"note": "",
|
||||
"purchase": "http://localhost:8000/api/purchases/4/"
|
||||
}
|
||||
]
|
||||
const header = ["url", "timestamp_start", "timestamp_end", "duration_manual", "duration_calculated", "note", "purchase"]
|
||||
// const header = ["Name", "Platform", "Start", "End", "Duration", "Manage"]
|
||||
return (
|
||||
<>
|
||||
<div id="session-table" className="gap-4 shadow rounded-xl max-w-screen-lg mx-auto dark:bg-slate-700 p-2 justify-center">
|
||||
{header.map(column => {
|
||||
<div className="dark:border-white dark:text-slate-300 text-lg">{column}</div>
|
||||
})}
|
||||
{data.map(session => {
|
||||
<>
|
||||
<div className="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<a className="hover:underline" href="">
|
||||
{ session.url }
|
||||
</a>
|
||||
<div className="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<a className="hover:underline" href="">
|
||||
{ session.timestamp_start }
|
||||
</a>
|
||||
</div>
|
||||
<div className="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<a className="hover:underline" href="">
|
||||
{ session.timestamp_end }
|
||||
</a>
|
||||
</div>
|
||||
<div className="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<a className="hover:underline" href="">
|
||||
{ session.duration_manual }
|
||||
</a>
|
||||
</div>
|
||||
<div className="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<a className="hover:underline" href="">
|
||||
{ session.duration_calculated }
|
||||
</a>
|
||||
</div>
|
||||
<div className="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<a className="hover:underline" href="">
|
||||
{ session.note }
|
||||
</a>
|
||||
</div>
|
||||
<div className="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<a className="hover:underline" href="">
|
||||
{ session.purchase }
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { useRouteError } from "react-router-dom";
|
||||
|
||||
export default function ErrorPage() {
|
||||
const error = useRouteError()
|
||||
console.error(error)
|
||||
|
||||
return (
|
||||
<div className="container text-center">
|
||||
<h1 className="text-3xl">Oops!</h1>
|
||||
<p>Sorry, an unexpected error has occurred.</p>
|
||||
<p>
|
||||
<i>{error.statusText || error.message}</i>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
form label {
|
||||
@apply dark:text-slate-400;
|
||||
}
|
||||
|
||||
form input,
|
||||
select,
|
||||
textarea {
|
||||
@apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
|
||||
}
|
||||
|
||||
#session-table {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr repeat(2, 2fr) 0.5fr 1fr;
|
||||
}
|
||||
|
||||
#button-container button {
|
||||
@apply mx-1;
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
||||
// import { loader as sessionLoader } from './routes/sessions'
|
||||
import ErrorPage from "./error-page"
|
||||
import SessionList from './components/SessionList'
|
||||
// import Session from './routes/sessions'
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <App />,
|
||||
errorElement: <ErrorPage />,
|
||||
// loader: sessionLoader,
|
||||
children: [
|
||||
{
|
||||
path: "sessions/",
|
||||
element: <SessionList />
|
||||
}
|
||||
]
|
||||
},
|
||||
// {
|
||||
// path: "sessions",
|
||||
// element: <SessionList />
|
||||
// }
|
||||
])
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</React.StrictMode>,
|
||||
)
|
@ -1,17 +0,0 @@
|
||||
export async function api(url) {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const jsonValue = await response.json();
|
||||
return Promise.resolve(jsonValue);
|
||||
} else {
|
||||
return Promise.reject('Response was not OK.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSession(sessionId) {
|
||||
return await api(`/api/sessions/${sessionId}/`);
|
||||
}
|
||||
|
||||
export async function getSessionList() {
|
||||
return await api(`/api/sessions/`);
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: "class",
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: ["Inter", "sans-serif"],
|
||||
},
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
|
||||
};
|
@ -1,15 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
@ -1,10 +1,12 @@
|
||||
from django import forms
|
||||
|
||||
from games.models import Game, Platform, Purchase, Session
|
||||
from games.models import Game, Platform, Purchase, Session, Edition, Device
|
||||
|
||||
|
||||
class SessionForm(forms.ModelForm):
|
||||
purchase = forms.ModelChoiceField(queryset=Purchase.objects.order_by("game__name"))
|
||||
purchase = forms.ModelChoiceField(
|
||||
queryset=Purchase.objects.order_by("edition__name")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Session
|
||||
@ -13,17 +15,39 @@ class SessionForm(forms.ModelForm):
|
||||
"timestamp_start",
|
||||
"timestamp_end",
|
||||
"duration_manual",
|
||||
"device",
|
||||
"note",
|
||||
]
|
||||
|
||||
|
||||
class EditionChoiceField(forms.ModelChoiceField):
|
||||
def label_from_instance(self, obj) -> str:
|
||||
return f"{obj.name} ({obj.platform}, {obj.year_released})"
|
||||
|
||||
|
||||
class PurchaseForm(forms.ModelForm):
|
||||
game = forms.ModelChoiceField(queryset=Game.objects.order_by("name"))
|
||||
edition = EditionChoiceField(queryset=Edition.objects.order_by("name"))
|
||||
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
|
||||
|
||||
class Meta:
|
||||
model = Purchase
|
||||
fields = ["game", "platform", "date_purchased", "date_refunded"]
|
||||
fields = [
|
||||
"edition",
|
||||
"platform",
|
||||
"date_purchased",
|
||||
"date_refunded",
|
||||
"price",
|
||||
"price_currency",
|
||||
"ownership_type",
|
||||
]
|
||||
|
||||
|
||||
class EditionForm(forms.ModelForm):
|
||||
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
|
||||
|
||||
class Meta:
|
||||
model = Edition
|
||||
fields = ["game", "name", "platform", "year_released", "wikidata"]
|
||||
|
||||
|
||||
class GameForm(forms.ModelForm):
|
||||
@ -36,3 +60,9 @@ class PlatformForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ["name", "group"]
|
||||
|
||||
|
||||
class DeviceForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ["name", "type"]
|
||||
|
41
games/migrations/0008_edition.py
Normal file
@ -0,0 +1,41 @@
|
||||
# Generated by Django 4.1.5 on 2023-02-18 16:29
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0007_alter_purchase_game_alter_purchase_platform_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Edition",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
(
|
||||
"game",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="games.game"
|
||||
),
|
||||
),
|
||||
(
|
||||
"platform",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="games.platform"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
34
games/migrations/0009_create_editions.py
Normal file
@ -0,0 +1,34 @@
|
||||
# Generated by Django 4.1.5 on 2023-02-18 18:51
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def create_edition_of_game(apps, schema_editor):
|
||||
Game = apps.get_model("games", "Game")
|
||||
Edition = apps.get_model("games", "Edition")
|
||||
Platform = apps.get_model("games", "Platform")
|
||||
first_platform = Platform.objects.first()
|
||||
all_games = Game.objects.all()
|
||||
all_editions = Edition.objects.all()
|
||||
for game in all_games:
|
||||
existing_edition = None
|
||||
try:
|
||||
existing_edition = all_editions.objects.get(game=game.id)
|
||||
except:
|
||||
pass
|
||||
if existing_edition == None:
|
||||
edition = Edition()
|
||||
edition.id = game.id
|
||||
edition.game = game
|
||||
edition.name = game.name
|
||||
edition.platform = first_platform
|
||||
edition.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0008_edition"),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(create_edition_of_game)]
|
21
games/migrations/0010_alter_purchase_game.py
Normal file
@ -0,0 +1,21 @@
|
||||
# Generated by Django 4.1.5 on 2023-02-18 19:06
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0009_create_editions"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="purchase",
|
||||
name="game",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="games.edition"
|
||||
),
|
||||
),
|
||||
]
|
18
games/migrations/0011_rename_game_purchase_edition.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.5 on 2023-02-18 19:18
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0010_alter_purchase_game"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="purchase",
|
||||
old_name="game",
|
||||
new_name="edition",
|
||||
),
|
||||
]
|
@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.1.5 on 2023-02-18 19:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0011_rename_game_purchase_edition"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="purchase",
|
||||
name="price",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="purchase",
|
||||
name="price_currency",
|
||||
field=models.CharField(default="USD", max_length=3),
|
||||
),
|
||||
]
|
31
games/migrations/0013_purchase_ownership_type.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Generated by Django 4.1.5 on 2023-02-18 19:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0012_purchase_price_purchase_price_currency"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="purchase",
|
||||
name="ownership_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("ph", "Physical"),
|
||||
("di", "Digital"),
|
||||
("du", "Digital Upgrade"),
|
||||
("re", "Rented"),
|
||||
("bo", "Borrowed"),
|
||||
("tr", "Trial"),
|
||||
("de", "Demo"),
|
||||
("pi", "Pirated"),
|
||||
],
|
||||
default="di",
|
||||
max_length=2,
|
||||
),
|
||||
),
|
||||
]
|
52
games/migrations/0014_device_session_device.py
Normal file
@ -0,0 +1,52 @@
|
||||
# Generated by Django 4.1.5 on 2023-02-18 19:59
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0013_purchase_ownership_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Device",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
(
|
||||
"type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("pc", "PC"),
|
||||
("co", "Console"),
|
||||
("ha", "Handheld"),
|
||||
("mo", "Mobile"),
|
||||
("sbc", "Single-board computer"),
|
||||
],
|
||||
default="pc",
|
||||
max_length=3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="session",
|
||||
name="device",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="games.device",
|
||||
),
|
||||
),
|
||||
]
|
@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.1.5 on 2023-02-20 14:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("games", "0014_device_session_device"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="edition",
|
||||
name="wikidata",
|
||||
field=models.CharField(blank=True, default=None, max_length=50, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="edition",
|
||||
name="year_released",
|
||||
field=models.IntegerField(default=2023),
|
||||
),
|
||||
]
|
@ -16,14 +16,52 @@ class Game(models.Model):
|
||||
return self.name
|
||||
|
||||
|
||||
class Purchase(models.Model):
|
||||
class Edition(models.Model):
|
||||
game = models.ForeignKey("Game", on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
platform = models.ForeignKey("Platform", on_delete=models.CASCADE)
|
||||
year_released = models.IntegerField(default=datetime.today().year)
|
||||
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Purchase(models.Model):
|
||||
PHYSICAL = "ph"
|
||||
DIGITAL = "di"
|
||||
DIGITALUPGRADE = "du"
|
||||
RENTED = "re"
|
||||
BORROWED = "bo"
|
||||
TRIAL = "tr"
|
||||
DEMO = "de"
|
||||
PIRATED = "pi"
|
||||
OWNERSHIP_TYPES = [
|
||||
(PHYSICAL, "Physical"),
|
||||
(DIGITAL, "Digital"),
|
||||
(DIGITALUPGRADE, "Digital Upgrade"),
|
||||
(RENTED, "Rented"),
|
||||
(BORROWED, "Borrowed"),
|
||||
(TRIAL, "Trial"),
|
||||
(DEMO, "Demo"),
|
||||
(PIRATED, "Pirated"),
|
||||
]
|
||||
|
||||
edition = models.ForeignKey("Edition", on_delete=models.CASCADE)
|
||||
platform = models.ForeignKey("Platform", on_delete=models.CASCADE)
|
||||
date_purchased = models.DateField()
|
||||
date_refunded = models.DateField(blank=True, null=True)
|
||||
price = models.IntegerField(default=0)
|
||||
price_currency = models.CharField(max_length=3, default="USD")
|
||||
ownership_type = models.CharField(
|
||||
max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.game} ({self.platform})"
|
||||
platform_info = self.platform
|
||||
if self.platform != self.edition.platform:
|
||||
platform_info = f"{self.edition.platform} version on {self.platform}"
|
||||
return f"{self.edition} ({platform_info}, {self.edition.year_released}, {self.get_ownership_type_display()})"
|
||||
|
||||
|
||||
class Platform(models.Model):
|
||||
@ -48,6 +86,7 @@ class Session(models.Model):
|
||||
timestamp_end = models.DateTimeField(blank=True, null=True)
|
||||
duration_manual = models.DurationField(blank=True, null=True, default=timedelta(0))
|
||||
duration_calculated = models.DurationField(blank=True, null=True)
|
||||
device = models.ForeignKey("Device", on_delete=models.CASCADE, null=True)
|
||||
note = models.TextField(blank=True, null=True)
|
||||
|
||||
objects = SessionQuerySet.as_manager()
|
||||
@ -85,3 +124,23 @@ class Session(models.Model):
|
||||
else:
|
||||
self.duration_calculated = timedelta(0)
|
||||
super(Session, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class Device(models.Model):
|
||||
PC = "pc"
|
||||
CONSOLE = "co"
|
||||
HANDHELD = "ha"
|
||||
MOBILE = "mo"
|
||||
SBC = "sbc"
|
||||
DEVICE_TYPES = [
|
||||
(PC, "PC"),
|
||||
(CONSOLE, "Console"),
|
||||
(HANDHELD, "Handheld"),
|
||||
(MOBILE, "Mobile"),
|
||||
(SBC, "Single-board computer"),
|
||||
]
|
||||
name = models.CharField(max_length=255)
|
||||
type = models.CharField(max_length=3, choices=DEVICE_TYPES, default=PC)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.get_type_display()})"
|
||||
|
BIN
games/static/icons/edition_black.png
Normal file
After Width: | Height: | Size: 292 KiB |
BIN
games/static/icons/edition_white.png
Normal file
After Width: | Height: | Size: 321 KiB |
BIN
games/static/icons/game_black.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
games/static/icons/game_white.png
Normal file
After Width: | Height: | Size: 306 KiB |
BIN
games/static/icons/purchase_black.png
Normal file
After Width: | Height: | Size: 312 KiB |
BIN
games/static/icons/purchase_white.png
Normal file
After Width: | Height: | Size: 364 KiB |
BIN
games/static/icons/schedule.png
Normal file
After Width: | Height: | Size: 42 KiB |
@ -3,11 +3,14 @@
|
||||
{% block title %}{{ title }}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data" class="mx-auto">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<table class="mx-auto">
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form.as_p }}
|
||||
|
||||
<input type="submit" value="Submit"/>
|
||||
{{ form.as_table }}
|
||||
<tr>
|
||||
<td><input type="submit" value="Submit"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
{% endblock content %}
|
@ -17,8 +17,8 @@
|
||||
<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 'index' %}" class="flex items-center">
|
||||
<span class="text-4xl">⌚</span>
|
||||
<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">
|
||||
@ -27,10 +27,14 @@
|
||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_game' %}">New Game</a></li>
|
||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_platform' %}">New Platform</a></li>
|
||||
{% if game_available and platform_available %}
|
||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_edition' %}">New Edition</a></li>
|
||||
{% endif %}
|
||||
{% if edition_available %}
|
||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_purchase' %}">New Purchase</a></li>
|
||||
{% endif %}
|
||||
{% if purchase_available %}
|
||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_session' %}">New Session</a></li>
|
||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'add_device' %}">New Device</a></li>
|
||||
{% endif %}
|
||||
{% if session_count > 0 %}
|
||||
<li><a class="block py-2 pl-3 pr-4 hover:underline" href="{% url 'list_sessions' %}">All Sessions</a></li>
|
||||
|
@ -1,6 +1,8 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Sessions{% endblock title %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="text-center text-xl mb-4 dark:text-slate-400">
|
||||
@ -10,15 +12,33 @@
|
||||
{% if dataset.count >= 1 %}
|
||||
<div class="mb-4">Total playtime: {{ total_duration }} over {{ dataset.count }} sessions.</div>
|
||||
{% endif %}
|
||||
{% if purchase or platform or game %}
|
||||
<a class="text-red-400 inline" href="{% url 'list_sessions' %}"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 inline">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</a><span>Filtering by "{% firstof purchase platform game %}"</span>
|
||||
{% if purchase %}<a class="dark:text-white hover:underline block" href="{% url 'list_sessions_by_game' purchase.game.id %}">See all platforms</a>{% endif %}
|
||||
{% if purchase or platform or edition or game or ownership_type %}
|
||||
<span class="block">
|
||||
<a class="text-red-400 inline" href="{% url 'list_sessions' %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 inline">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</a>
|
||||
{% if purchase %}
|
||||
Filtering by purchase "{{ purchase }}"
|
||||
(<a href="{% url 'edit_purchase' purchase.id %}" class="hover:underline dark:text-white">Edit</a>)
|
||||
{% elif platform %}
|
||||
Filtering by purchase "{{ platform }}"
|
||||
(<a href="{% url 'edit_platform' platform.id %}" class="hover:underline dark:text-white">Edit</a>)
|
||||
{% elif game %}
|
||||
Filtering by purchase "{{ game }}"
|
||||
(<a href="{% url 'edit_game' game.id %}" class="hover:underline dark:text-white">Edit</a>)
|
||||
{% elif edition %}
|
||||
Filtering by purchase "{{ edition }}"
|
||||
(<a href="{% url 'edit_edition' edition.id %}" class="hover:underline dark:text-white">Edit</a>)
|
||||
{% elif ownership_type %}
|
||||
Filtering by ownership type "{{ ownership_type }}"
|
||||
{% endif%}
|
||||
</span>
|
||||
{% if purchase %}<a class="dark:text-white hover:underline block" href="{% url 'list_sessions_by_edition' purchase.edition.id %}">See all platforms</a>{% endif %}
|
||||
{% endif %}
|
||||
{% if dataset.count >= 1 %}
|
||||
<a class="clear-both" href="{% url 'start_session' last.purchase.id %}">
|
||||
<a href="{% url 'start_session' last.id %}">
|
||||
<button type="button" title="Track last tracked" class="mt-10 py-1 px-2 bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-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 rounded-lg ">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="self-center w-6 h-6 inline">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
|
||||
@ -28,16 +48,38 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="session-table" class="gap-4 shadow rounded-xl max-w-screen-lg mx-auto dark:bg-slate-700 p-2 justify-center">
|
||||
<div class="dark:border-white dark:text-slate-300 text-lg">Name</div>
|
||||
<div id="session-table" class="gap-4 shadow rounded-xl max-w-screen-2xl mx-auto dark:bg-slate-700 p-2 justify-center">
|
||||
<div class="dark:border-white dark:text-slate-300 text-lg">Purchase</div>
|
||||
<div class="dark:border-white dark:text-slate-300 text-lg">Platform</div>
|
||||
<div class="dark:border-white dark:text-slate-300 text-lg text-center">Start</div>
|
||||
<div class="dark:border-white dark:text-slate-300 text-lg text-center">End</div>
|
||||
<div class="dark:border-white dark:text-slate-300 text-lg">Duration</div>
|
||||
<div class="dark:border-white dark:text-slate-300 text-lg text-right">Manage</div>
|
||||
{% for data in dataset %}
|
||||
<div class="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap"><a class="hover:underline" href="{% url 'list_sessions_by_purchase' data.purchase.id %}">{{ data.purchase.game }}</a></div>
|
||||
<div class="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap"><a class="hover:underline" href="{% url 'list_sessions_by_platform' data.purchase.platform.id %}">{{ data.purchase.platform }}</a></div>
|
||||
<div class="purchase-name">
|
||||
<span class="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">{{ data.purchase.edition }} <span class="dark:text-slate-400">(<a class="hover:underline" href="{% url 'list_sessions_by_ownership_type' data.purchase.ownership_type %}">{{ data.purchase.get_ownership_type_display }}</a>)</span></span>
|
||||
|
||||
<span>
|
||||
<a href="{% url 'list_sessions_by_game' data.purchase.edition.game.id %}">
|
||||
<img src="{% static 'icons/game_white.png' %}" width="32" class="inline" alt="Filter by this game" title="Filter by this game" />
|
||||
</a>
|
||||
<a href="{% url 'list_sessions_by_edition' data.purchase.edition.id %}">
|
||||
<img src="{% static 'icons/edition_white.png' %}" width="32" class="inline" alt="Filter by this edition" title="Filter by this edition" />
|
||||
</a>
|
||||
<a href="{% url 'list_sessions_by_purchase' data.purchase.id %}">
|
||||
<img src="{% static 'icons/purchase_white.png' %}" width="32" class="inline" alt="Filter by this purchase" title="Filter by this purchase" />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
<a class="hover:underline" href="{% url 'list_sessions_by_platform' data.purchase.platform.id %}">
|
||||
{% if data.purchase.platform != data.purchase.edition.platform %}
|
||||
{{data.purchase.edition.platform}} on {{ data.purchase.platform }}
|
||||
{% else %}
|
||||
{{ data.purchase.platform }}
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="dark:text-slate-400 text-center">{{ data.timestamp_start | date:"d/m/Y H:i" }}</div>
|
||||
<div class="dark:text-slate-400 text-center">
|
||||
{% if data.unfinished %}
|
||||
@ -62,12 +104,14 @@
|
||||
</button>
|
||||
</a>
|
||||
{% endif %}
|
||||
<button type="button" title="Edit" class="py-1 px-2 flex justify-center items-center bg-blue-600 hover:bg-blue-700 focus:ring-blue-500 focus:ring-offset-blue-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 href="{% url 'edit_session' data.id %}">
|
||||
<button type="button" title="Edit" class="py-1 px-2 flex justify-center items-center bg-blue-600 hover:bg-blue-700 focus:ring-blue-500 focus:ring-offset-blue-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>
|
||||
<a href="{% url 'delete_session' data.id %}">
|
||||
<button type="button" edit="Delete" class="py-1 px-2 flex justify-center items-center bg-red-600 hover:bg-red-700 focus:ring-red-500 focus:ring-offset-blue-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">
|
||||
|
@ -4,6 +4,12 @@ from games import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.index, name="index"),
|
||||
path(
|
||||
"list-sessions/recent",
|
||||
views.list_sessions,
|
||||
{"filter": "recent"},
|
||||
name="list_sessions_recent",
|
||||
),
|
||||
path("add-game/", views.add_game, name="add_game"),
|
||||
path("add-platform/", views.add_platform, name="add_platform"),
|
||||
path("add-session/", views.add_session, name="add_session"),
|
||||
@ -13,7 +19,7 @@ urlpatterns = [
|
||||
name="update_session",
|
||||
),
|
||||
path(
|
||||
"start-session/<int:purchase_id>",
|
||||
"start-session/<int:last_session_id>",
|
||||
views.start_session,
|
||||
name="start_session",
|
||||
),
|
||||
@ -23,6 +29,13 @@ urlpatterns = [
|
||||
name="delete_session",
|
||||
),
|
||||
path("add-purchase/", views.add_purchase, name="add_purchase"),
|
||||
path("add-edition/", views.add_edition, name="add_edition"),
|
||||
path("edit-edition/<int:edition_id>", views.edit_edition, name="edit_edition"),
|
||||
path("edit-game/<int:game_id>", views.edit_game, name="edit_game"),
|
||||
path("edit-platform/<int:platform_id>", views.edit_platform, name="edit_platform"),
|
||||
path("add-device/", views.add_device, name="add_device"),
|
||||
path("edit-session/<int:session_id>", views.edit_session, name="edit_session"),
|
||||
path("edit-purchase/<int:purchase_id>", views.edit_purchase, name="edit_purchase"),
|
||||
path("list-sessions/", views.list_sessions, name="list_sessions"),
|
||||
path(
|
||||
"list-sessions/by-purchase/<int:purchase_id>",
|
||||
@ -42,4 +55,16 @@ urlpatterns = [
|
||||
{"filter": "game"},
|
||||
name="list_sessions_by_game",
|
||||
),
|
||||
path(
|
||||
"list-sessions/by-edition/<int:edition_id>",
|
||||
views.list_sessions,
|
||||
{"filter": "edition"},
|
||||
name="list_sessions_by_edition",
|
||||
),
|
||||
path(
|
||||
"list-sessions/by-ownership/<str:ownership_type>",
|
||||
views.list_sessions,
|
||||
{"filter": "ownership_type"},
|
||||
name="list_sessions_by_ownership_type",
|
||||
),
|
||||
]
|
||||
|
140
games/views.py
@ -1,4 +1,4 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from common.plots import playtime_over_time_chart
|
||||
@ -6,13 +6,21 @@ from common.time import now as now_with_tz
|
||||
from django.conf import settings
|
||||
from django.shortcuts import redirect, render
|
||||
|
||||
from .forms import GameForm, PlatformForm, PurchaseForm, SessionForm
|
||||
from .models import Game, Platform, Purchase, Session
|
||||
from .forms import (
|
||||
GameForm,
|
||||
PlatformForm,
|
||||
PurchaseForm,
|
||||
SessionForm,
|
||||
EditionForm,
|
||||
DeviceForm,
|
||||
)
|
||||
from .models import Game, Platform, Purchase, Session, Edition
|
||||
|
||||
|
||||
def model_counts(request):
|
||||
return {
|
||||
"game_available": Game.objects.count() != 0,
|
||||
"edition_available": Edition.objects.count() != 0,
|
||||
"platform_available": Platform.objects.count() != 0,
|
||||
"purchase_available": Purchase.objects.count() != 0,
|
||||
"session_count": Session.objects.count(),
|
||||
@ -47,8 +55,75 @@ def update_session(request, session_id=None):
|
||||
return redirect("list_sessions")
|
||||
|
||||
|
||||
def start_session(request, purchase_id=None):
|
||||
session = SessionForm({"purchase": purchase_id, "timestamp_start": now_with_tz()})
|
||||
def edit_session(request, session_id=None):
|
||||
context = {}
|
||||
session = Session.objects.get(id=session_id)
|
||||
form = SessionForm(request.POST or None, instance=session)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect("list_sessions")
|
||||
context["title"] = "Edit Session"
|
||||
context["form"] = form
|
||||
return render(request, "add.html", context)
|
||||
|
||||
|
||||
def edit_purchase(request, purchase_id=None):
|
||||
context = {}
|
||||
purchase = Purchase.objects.get(id=purchase_id)
|
||||
form = PurchaseForm(request.POST or None, instance=purchase)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect("list_sessions")
|
||||
context["title"] = "Edit Purchase"
|
||||
context["form"] = form
|
||||
return render(request, "add.html", context)
|
||||
|
||||
|
||||
def edit_game(request, game_id=None):
|
||||
context = {}
|
||||
purchase = Game.objects.get(id=game_id)
|
||||
form = GameForm(request.POST or None, instance=purchase)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect("list_sessions")
|
||||
context["title"] = "Edit Game"
|
||||
context["form"] = form
|
||||
return render(request, "add.html", context)
|
||||
|
||||
|
||||
def edit_platform(request, platform_id=None):
|
||||
context = {}
|
||||
purchase = Platform.objects.get(id=platform_id)
|
||||
form = PlatformForm(request.POST or None, instance=purchase)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect("list_sessions")
|
||||
context["title"] = "Edit Platform"
|
||||
context["form"] = form
|
||||
return render(request, "add.html", context)
|
||||
|
||||
|
||||
def edit_edition(request, edition_id=None):
|
||||
context = {}
|
||||
edition = Edition.objects.get(id=edition_id)
|
||||
form = EditionForm(request.POST or None, instance=edition)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect("list_sessions")
|
||||
context["title"] = "Edit Edition"
|
||||
context["form"] = form
|
||||
return render(request, "add.html", context)
|
||||
|
||||
|
||||
def start_session(request, last_session_id: int):
|
||||
last_session = Session.objects.get(id=last_session_id)
|
||||
session = SessionForm(
|
||||
{
|
||||
"purchase": last_session.purchase.id,
|
||||
"timestamp_start": now_with_tz(),
|
||||
"device": last_session.device,
|
||||
}
|
||||
)
|
||||
session.save()
|
||||
return redirect("list_sessions")
|
||||
|
||||
@ -59,8 +134,17 @@ def delete_session(request, session_id=None):
|
||||
return redirect("list_sessions")
|
||||
|
||||
|
||||
def list_sessions(request, filter="", purchase_id="", platform_id="", game_id=""):
|
||||
def list_sessions(
|
||||
request,
|
||||
filter="",
|
||||
purchase_id="",
|
||||
platform_id="",
|
||||
game_id="",
|
||||
edition_id="",
|
||||
ownership_type: str = "",
|
||||
):
|
||||
context = {}
|
||||
context["title"] = "Sessions"
|
||||
|
||||
if filter == "purchase":
|
||||
dataset = Session.objects.filter(purchase=purchase_id)
|
||||
@ -68,9 +152,20 @@ def list_sessions(request, filter="", purchase_id="", platform_id="", game_id=""
|
||||
elif filter == "platform":
|
||||
dataset = Session.objects.filter(purchase__platform=platform_id)
|
||||
context["platform"] = Platform.objects.get(id=platform_id)
|
||||
elif filter == "edition":
|
||||
dataset = Session.objects.filter(purchase__edition=edition_id)
|
||||
context["edition"] = Edition.objects.get(id=edition_id)
|
||||
elif filter == "game":
|
||||
dataset = Session.objects.filter(purchase__game=game_id)
|
||||
dataset = Session.objects.filter(purchase__edition__game=game_id)
|
||||
context["game"] = Game.objects.get(id=game_id)
|
||||
elif filter == "ownership_type":
|
||||
dataset = Session.objects.filter(purchase__ownership_type=ownership_type)
|
||||
context["ownership_type"] = dict(Purchase.OWNERSHIP_TYPES)[ownership_type]
|
||||
elif filter == "recent":
|
||||
dataset = Session.objects.filter(
|
||||
timestamp_start__gte=datetime.now() - timedelta(days=30)
|
||||
).order_by("-timestamp_start")
|
||||
context["title"] = "Last 30 days"
|
||||
else:
|
||||
# by default, sort from newest to oldest
|
||||
dataset = Session.objects.all().order_by("-timestamp_start")
|
||||
@ -85,7 +180,7 @@ def list_sessions(request, filter="", purchase_id="", platform_id="", game_id=""
|
||||
# cannot use dataset[0] here because that might be only partial QuerySet
|
||||
context["last"] = Session.objects.all().order_by("timestamp_start").last()
|
||||
# charts are always oldest->newest
|
||||
if Session.objects.count() >= 2:
|
||||
if dataset.count() >= 2:
|
||||
context["chart"] = playtime_over_time_chart(dataset.order_by("timestamp_start"))
|
||||
|
||||
return render(request, "list_sessions.html", context)
|
||||
@ -117,6 +212,18 @@ def add_game(request):
|
||||
return render(request, "add.html", context)
|
||||
|
||||
|
||||
def add_edition(request):
|
||||
context = {}
|
||||
form = EditionForm(request.POST or None)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect("index")
|
||||
|
||||
context["form"] = form
|
||||
context["title"] = "Add New Edition"
|
||||
return render(request, "add.html", context)
|
||||
|
||||
|
||||
def add_platform(request):
|
||||
context = {}
|
||||
form = PlatformForm(request.POST or None)
|
||||
@ -129,8 +236,17 @@ def add_platform(request):
|
||||
return render(request, "add.html", context)
|
||||
|
||||
|
||||
def index(request):
|
||||
def add_device(request):
|
||||
context = {}
|
||||
context["total_duration"] = Session().duration_sum
|
||||
context["title"] = "Index"
|
||||
return render(request, "index.html", context)
|
||||
form = DeviceForm(request.POST or None)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect("index")
|
||||
|
||||
context["form"] = form
|
||||
context["title"] = "Add New Device"
|
||||
return render(request, "add.html", context)
|
||||
|
||||
|
||||
def index(request):
|
||||
return redirect("list_sessions_recent")
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/typography": "^0.5.9"
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"tailwindcss": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
54
poetry.lock
generated
@ -220,21 +220,6 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
argon2 = ["argon2-cffi (>=19.1.0)"]
|
||||
bcrypt = ["bcrypt"]
|
||||
|
||||
[[package]]
|
||||
name = "django-cors-headers"
|
||||
version = "3.13.0"
|
||||
description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "django-cors-headers-3.13.0.tar.gz", hash = "sha256:f9dc6b4e3f611c3199700b3e5f3398c28757dcd559c2f82932687f3d0443cfdf"},
|
||||
{file = "django_cors_headers-3.13.0-py3-none-any.whl", hash = "sha256:37e42883b5f1f2295df6b4bba96eb2417a14a03270cb24b2a07f021cd4487cf4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Django = ">=3.2"
|
||||
|
||||
[[package]]
|
||||
name = "django-extensions"
|
||||
version = "3.2.1"
|
||||
@ -250,36 +235,6 @@ files = [
|
||||
[package.dependencies]
|
||||
Django = ">=3.2"
|
||||
|
||||
[[package]]
|
||||
name = "django-rest-framework"
|
||||
version = "0.1.0"
|
||||
description = "alias."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "django-rest-framework-0.1.0.tar.gz", hash = "sha256:47a8f496fa69e3b6bd79f68dd7a1527d907d6b77f009e9db7cf9bb21cc565e4a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
djangorestframework = "*"
|
||||
|
||||
[[package]]
|
||||
name = "djangorestframework"
|
||||
version = "3.14.0"
|
||||
description = "Web APIs for Django, made easy."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"},
|
||||
{file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
django = ">=3.0"
|
||||
pytz = "*"
|
||||
|
||||
[[package]]
|
||||
name = "djhtml"
|
||||
version = "1.5.2"
|
||||
@ -873,13 +828,6 @@ files = [
|
||||
{file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"},
|
||||
{file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"},
|
||||
{file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"},
|
||||
{file = "Pillow-9.4.0-2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0"},
|
||||
{file = "Pillow-9.4.0-2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f"},
|
||||
{file = "Pillow-9.4.0-2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c"},
|
||||
{file = "Pillow-9.4.0-2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848"},
|
||||
{file = "Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1"},
|
||||
{file = "Pillow-9.4.0-2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33"},
|
||||
{file = "Pillow-9.4.0-2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9"},
|
||||
{file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"},
|
||||
{file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"},
|
||||
{file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"},
|
||||
@ -1349,4 +1297,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "7a7ba3831802cf91b722b956817a0606b7b8f97724b1a23e5e581657fb34ea19"
|
||||
content-hash = "bf02e951b8c14fbe6f2709b6df7f9664f99ad5fbf7195d8e7b18c8574d00e683"
|
||||
|
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "timetracker"
|
||||
version = "1.0.0"
|
||||
version = "1.0.3"
|
||||
description = "A simple time tracker."
|
||||
authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"]
|
||||
license = "GPL"
|
||||
@ -14,8 +14,6 @@ gunicorn = "^20.1.0"
|
||||
uvicorn = "^0.20.0"
|
||||
pandas = "^1.5.2"
|
||||
matplotlib = "^3.6.3"
|
||||
django-rest-framework = "^0.1.0"
|
||||
django-cors-headers = "^3.13.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^22.12.0"
|
||||
|
14
tailwind.config.js
Normal file
@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
darkMode: 'class',
|
||||
content: ["./**/*.{html,js}"],
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
},
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/typography'),
|
||||
require('@tailwindcss/forms')
|
||||
],
|
||||
}
|
@ -12,7 +12,6 @@ https://docs.djangoproject.com/en/4.1/ref/settings/
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from corsheaders.defaults import default_headers, default_methods
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
@ -39,8 +38,6 @@ INSTALLED_APPS = [
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"rest_framework",
|
||||
"corsheaders",
|
||||
]
|
||||
|
||||
if DEBUG:
|
||||
@ -48,7 +45,6 @@ if DEBUG:
|
||||
INSTALLED_APPS.append("django.contrib.admin")
|
||||
|
||||
MIDDLEWARE = [
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
@ -153,24 +149,3 @@ if _csrf_trusted_origins:
|
||||
CSRF_TRUSTED_ORIGINS = _csrf_trusted_origins.split(",")
|
||||
else:
|
||||
CSRF_TRUSTED_ORIGINS = []
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_PERMISSION_CLASSES": [
|
||||
"rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly"
|
||||
]
|
||||
}
|
||||
|
||||
FRONTEND_ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", "frontend", "dist"))
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
CORS_ALLOW_HEADERS = list(default_headers) + [
|
||||
"Accept-Language",
|
||||
"Connection",
|
||||
"Host",
|
||||
"Origin",
|
||||
"Referer",
|
||||
"Sec-Fetch-Dest",
|
||||
"Sec-Fetch-Mode",
|
||||
"Sec-Fetch-Site",
|
||||
]
|
||||
|
@ -17,64 +17,10 @@ from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from django.views.generic import RedirectView
|
||||
from rest_framework import routers, serializers, viewsets
|
||||
from games.models import Game, Purchase, Platform, Session
|
||||
|
||||
|
||||
class GameSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Game
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class PlatformSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class PurchaseSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Purchase
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class SessionSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Session
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class GameViewSet(viewsets.ModelViewSet):
|
||||
queryset = Game.objects.all()
|
||||
serializer_class = GameSerializer
|
||||
|
||||
|
||||
class PlatformViewSet(viewsets.ModelViewSet):
|
||||
queryset = Platform.objects.all()
|
||||
serializer_class = PlatformSerializer
|
||||
|
||||
|
||||
class PurchaseViewSet(viewsets.ModelViewSet):
|
||||
queryset = Purchase.objects.all()
|
||||
serializer_class = PurchaseSerializer
|
||||
|
||||
|
||||
class SessionViewSet(viewsets.ModelViewSet):
|
||||
queryset = Session.objects.all()
|
||||
serializer_class = SessionSerializer
|
||||
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r"games", GameViewSet)
|
||||
router.register(r"platforms", PlatformViewSet)
|
||||
router.register(r"purchases", PurchaseViewSet)
|
||||
router.register(r"sessions", SessionViewSet)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("api/", include(router.urls)),
|
||||
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
|
||||
path("", RedirectView.as_view(url="/tracker")),
|
||||
path("tracker/", include("games.urls")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
|