Compare commits

...

12 Commits

26 changed files with 532 additions and 194 deletions

View File

@ -1,18 +1,12 @@
all: css migrate all: migrate
initialize: npm css migrate sethookdir loadplatforms initialize: npm migrate sethookdir loadplatforms
HTMLFILES := $(shell find games/templates -type f) HTMLFILES := $(shell find games/templates -type f)
npm: npm:
npm install npm install
css: input.css
npx tailwindcss -i ./input.css -o ./games/static/base.css
css-dev: css
npx tailwindcss -i ./input.css -o ./games/static/base.css --watch
makemigrations: makemigrations:
poetry run python manage.py makemigrations poetry run python manage.py makemigrations

View File

@ -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;
}

17
frontend/.eslintrc.cjs Normal file
View File

@ -0,0 +1,17 @@
module.exports = {
env: {
browser: true,
es2021: true
},
extends: ["eslint/recommended", "plugin:react/recommended", "plugin:prettier/recommended"],
overrides: [],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module"
},
plugins: ["react"],
rules: {},
parserOptions: {
ecmaFeatures: { jsx: true }
}
};

9
frontend/.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"semi": true,
"tabWidth": 2,
"printWidth": 100,
"singleQuote": true,
"trailingComma": "none",
"bracketSameLine": false,
"singleAttributePerLine": true
}

View File

@ -2,11 +2,15 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <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" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title> <!-- 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> </head>
<body> <body class="dark">
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.jsx"></script> <script type="module" src="/src/main.jsx"></script>
</body> </body>

View File

@ -9,13 +9,25 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "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": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0",
"react-router-dom": "^6.7.0",
"tailwindcss": "^3.2.4"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.0.26", "@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9", "@types/react-dom": "^18.0.9",
"@vitejs/plugin-react": "^3.0.0", "@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" "vite": "^4.0.0"
} }
} }

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,41 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@ -1,34 +1,20 @@
import { useState } from 'react' import "./App.css";
import reactLogo from './assets/react.svg' import Nav from "./components/Nav";
import './App.css' import { Outlet, useLoaderData } from "react-router-dom";
function App() { function App() {
const [count, setCount] = useState(0)
return ( return (
<div className="App"> <>
<div> <div className="dark:bg-gray-800 min-h-screen">
<a href="https://vitejs.dev" target="_blank"> <Nav />
<img src="/vite.svg" className="logo" alt="Vite logo" /> <Outlet />
</a>
<a href="https://reactjs.org" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div> </div>
<h1>Vite + React</h1> {/* {% load version %} */}
<div className="card"> {/* <span className="fixed left-2 bottom-2 text-xs text-slate-300 dark:text-slate-600">{% version %} ({% version_date %})</span> */}
<button onClick={() => setCount((count) => count + 1)}> </>
count is {count} );
</button>
<p>
Edit <code>src/App.jsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</div>
)
} }
export default App export default App;

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,71 @@
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;

View File

@ -0,0 +1,162 @@
export default function SessionList() {
const data = [
{
"url": "http://localhost:8000/api/sessions/25/",
"timestamp_start": "2020-01-01T00:00:00+01:00",
"timestamp_end": null,
"duration_manual": "12:00:00",
"duration_calculated": "00:00:00",
"note": "",
"purchase": "http://localhost:8000/api/purchases/3/"
},
{
"url": "http://localhost:8000/api/sessions/26/",
"timestamp_start": "2022-12-31T15:25:00+01:00",
"timestamp_end": "2022-12-31T17:25:00+01:00",
"duration_manual": "00:00:00",
"duration_calculated": "02:00:00",
"note": "",
"purchase": "http://localhost:8000/api/purchases/2/"
},
{
"url": "http://localhost:8000/api/sessions/27/",
"timestamp_start": "2023-01-01T23:00:00+01:00",
"timestamp_end": "2023-01-02T00:28:00+01:00",
"duration_manual": "00:00:00",
"duration_calculated": "01:28:00",
"note": "",
"purchase": "http://localhost:8000/api/purchases/3/"
},
{
"url": "http://localhost:8000/api/sessions/28/",
"timestamp_start": "2023-01-02T22:08:00+01:00",
"timestamp_end": "2023-01-03T01:08:00+01:00",
"duration_manual": "00:00:00",
"duration_calculated": "03:00:00",
"note": "",
"purchase": "http://localhost:8000/api/purchases/3/"
},
{
"url": "http://localhost:8000/api/sessions/29/",
"timestamp_start": "2023-01-03T22:36:00+01:00",
"timestamp_end": "2023-01-04T00:12:00+01:00",
"duration_manual": "00:00:00",
"duration_calculated": "01:36:00",
"note": "",
"purchase": "http://localhost:8000/api/purchases/3/"
},
{
"url": "http://localhost:8000/api/sessions/30/",
"timestamp_start": "2023-01-04T20:35:00+01:00",
"timestamp_end": "2023-01-04T22:36:00+01:00",
"duration_manual": "00:00:00",
"duration_calculated": "02:01:00",
"note": "",
"purchase": "http://localhost:8000/api/purchases/3/"
},
{
"url": "http://localhost:8000/api/sessions/31/",
"timestamp_start": "2023-01-06T18:48:00+01:00",
"timestamp_end": "2023-01-06T23:39:00+01:00",
"duration_manual": "00:00:00",
"duration_calculated": "04:51:00",
"note": "",
"purchase": "http://localhost:8000/api/purchases/3/"
},
{
"url": "http://localhost:8000/api/sessions/32/",
"timestamp_start": "2023-01-07T23:49:00+01:00",
"timestamp_end": "2023-01-08T01:43:00+01:00",
"duration_manual": "00:00:00",
"duration_calculated": "01:54:00",
"note": "",
"purchase": "http://localhost:8000/api/purchases/3/"
},
{
"url": "http://localhost:8000/api/sessions/33/",
"timestamp_start": "2023-01-08T16:21:00+01:00",
"timestamp_end": "2023-01-08T18:27:00+01:00",
"duration_manual": "00:00:00",
"duration_calculated": "02:06:00",
"note": "",
"purchase": "http://localhost:8000/api/purchases/3/"
},
{
"url": "http://localhost:8000/api/sessions/34/",
"timestamp_start": "2023-01-08T19:04:00+01:00",
"timestamp_end": "2023-01-08T22:03:00+01:00",
"duration_manual": "00:00:00",
"duration_calculated": "02:59:00",
"note": "",
"purchase": "http://localhost:8000/api/purchases/3/"
},
{
"url": "http://localhost:8000/api/sessions/35/",
"timestamp_start": "2023-01-09T19:35:48+01:00",
"timestamp_end": "2023-01-09T22:13:20.519058+01:00",
"duration_manual": "00:00:00",
"duration_calculated": "02:37:32.519058",
"note": "",
"purchase": "http://localhost:8000/api/purchases/3/"
},
{
"url": "http://localhost:8000/api/sessions/36/",
"timestamp_start": "2023-01-10T15:50:12+01:00",
"timestamp_end": "2023-01-10T17:03:45.424429+01:00",
"duration_manual": "00:00:00",
"duration_calculated": "01:13:33.424429",
"note": "",
"purchase": "http://localhost:8000/api/purchases/4/"
}
]
const header = ["url", "timestamp_start", "timestamp_end", "duration_manual", "duration_calculated", "note", "purchase"]
// const header = ["Name", "Platform", "Start", "End", "Duration", "Manage"]
return (
<>
<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>
</>
)
}

View File

@ -0,0 +1,16 @@
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>
)
}

View File

@ -1,70 +1,22 @@
:root { @tailwind base;
font-family: Inter, Avenir, Helvetica, Arial, sans-serif; @tailwind components;
font-size: 16px; @tailwind utilities;
line-height: 24px;
font-weight: 400;
color-scheme: light dark; form label {
color: rgba(255, 255, 255, 0.87); @apply dark:text-slate-400;
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
} }
a { form input,
font-weight: 500; select,
color: #646cff; textarea {
text-decoration: inherit; @apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
}
a:hover {
color: #535bf2;
} }
body { #session-table {
margin: 0; display: grid;
display: flex; grid-template-columns: 3fr 1fr repeat(2, 2fr) 0.5fr 1fr;
place-items: center;
min-width: 320px;
min-height: 100vh;
} }
h1 { #button-container button {
font-size: 3.2em; @apply mx-1;
line-height: 1.1; }
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@ -2,9 +2,33 @@ import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App' import App from './App'
import './index.css' 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( ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode> <React.StrictMode>
<App /> <RouterProvider router={router} />
</React.StrictMode>, </React.StrictMode>,
) )

View File

@ -0,0 +1,17 @@
export async function api(url) {
const response = await fetch(url);
if (response.ok) {
const jsonValue = await response.json();
return Promise.resolve(jsonValue);
} else {
return Promise.reject('Response was not OK.');
}
}
export async function getSession(sessionId) {
return await api(`/api/sessions/${sessionId}/`);
}
export async function getSessionList() {
return await api(`/api/sessions/`);
}

View File

@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: "class",
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
fontFamily: {
sans: ["Inter", "sans-serif"],
},
extend: {},
},
plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
};

View File

@ -1,12 +1,15 @@
import { defineConfig } from "vite"; import { defineConfig } from 'vite';
import react from "@vitejs/plugin-react"; import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
proxy: { proxy: {
"/": "http://localhost:8001", '/api': {
}, target: 'http://127.0.0.1:8000',
}, changeOrigin: true
}
}
}
}); });

0
games/serializers.py Normal file
View File

View File

@ -1,7 +1,6 @@
{ {
"devDependencies": { "dependencies": {
"@tailwindcss/forms": "^0.5.3", "@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8", "@tailwindcss/typography": "^0.5.9"
"tailwindcss": "^3.2.4"
} }
} }

54
poetry.lock generated
View File

@ -220,6 +220,21 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
argon2 = ["argon2-cffi (>=19.1.0)"] argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"] 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]] [[package]]
name = "django-extensions" name = "django-extensions"
version = "3.2.1" version = "3.2.1"
@ -235,6 +250,36 @@ files = [
[package.dependencies] [package.dependencies]
Django = ">=3.2" 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]] [[package]]
name = "djhtml" name = "djhtml"
version = "1.5.2" version = "1.5.2"
@ -828,6 +873,13 @@ files = [
{file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"}, {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-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-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_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-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"}, {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"},
@ -1297,4 +1349,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "bf02e951b8c14fbe6f2709b6df7f9664f99ad5fbf7195d8e7b18c8574d00e683" content-hash = "7a7ba3831802cf91b722b956817a0606b7b8f97724b1a23e5e581657fb34ea19"

View File

@ -14,6 +14,8 @@ gunicorn = "^20.1.0"
uvicorn = "^0.20.0" uvicorn = "^0.20.0"
pandas = "^1.5.2" pandas = "^1.5.2"
matplotlib = "^3.6.3" matplotlib = "^3.6.3"
django-rest-framework = "^0.1.0"
django-cors-headers = "^3.13.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
black = "^22.12.0" black = "^22.12.0"

View File

@ -1,14 +0,0 @@
module.exports = {
darkMode: 'class',
content: ["./**/*.{html,js}"],
theme: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
extend: {},
},
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms')
],
}

View File

@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/4.1/ref/settings/
import os import os
from pathlib import Path from pathlib import Path
from corsheaders.defaults import default_headers, default_methods
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@ -38,6 +39,8 @@ INSTALLED_APPS = [
"django.contrib.sessions", "django.contrib.sessions",
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"rest_framework",
"corsheaders",
] ]
if DEBUG: if DEBUG:
@ -45,6 +48,7 @@ if DEBUG:
INSTALLED_APPS.append("django.contrib.admin") INSTALLED_APPS.append("django.contrib.admin")
MIDDLEWARE = [ MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
@ -149,3 +153,24 @@ if _csrf_trusted_origins:
CSRF_TRUSTED_ORIGINS = _csrf_trusted_origins.split(",") CSRF_TRUSTED_ORIGINS = _csrf_trusted_origins.split(",")
else: else:
CSRF_TRUSTED_ORIGINS = [] 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",
]

View File

@ -17,10 +17,64 @@ from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.views.generic import RedirectView 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 = [ urlpatterns = [
path("", RedirectView.as_view(url="/tracker/list-sessions")), path("api/", include(router.urls)),
path("tracker/", include("games.urls")), path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
] ]
if settings.DEBUG: if settings.DEBUG: