diff --git a/frontend/main.js b/frontend/main.js index 0eeec6e..9ea08b1 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -2,12 +2,6 @@ import m from "https://esm.sh/mithril@2.2.2" import colormap from "https://esm.sh/colormap@2.3.2" import {range} from "./util.js"; - -// const second = 1000; - -// const canvasWidth = Number.parseFloat(document.querySelector("svg").getAttribute("width")); -// const canvasHeight = Number.parseFloat(document.querySelector("svg").getAttribute("height")); - function gaussianRandom(mean=0, stdev=1) { const u = 1 - Math.random(); // Converting [0,1) to (0,1] const v = Math.random(); @@ -45,28 +39,6 @@ function* keyFrames(duration, framePerSeconds){ } } -// const duration = 60; -// const timeline = anime.timeline({ -// duration: duration * second, -// autoplay: false, -// easing: 'linear'}); -// -// for(let i = 1; i <= 12; i++){ -// timeline.add({ -// targets: `#rect-${i}`, -// keyframes: Array.from(keyFrames(duration, 30)), -// }, 0) -// } - - -// document.querySelector('#play').onclick = timeline.play; -// document.querySelector('#pause').onclick = timeline.pause; -// document.querySelector('#seek').onclick = function(){ -// const seekVal = Number.parseFloat(second * document.querySelector('#seek-value').value); -// timeline.pause(); -// timeline.seek(seekVal); -// } - function generatePoints(points, frames){ const lst = []; @@ -105,7 +77,15 @@ function getCourtLines(){ m("line", {x1: 600, x2: 600, y1: 0, y2: 450, class: "return-line"}) ] } + + class WorkingTabs{ + #datasets = [] + + async oninit(){ + const url = "/dodgeball-tracker/outputs/index.json" + this.#datasets = (await m.request({method: "GET", url: url}))?.index ?? []; + } tabs(){ return m("div.tabs", @@ -115,16 +95,14 @@ class WorkingTabs{ } info(){ - const options = [ - m("option", "one"), - m("option", "two"), - ] + const options = this.#datasets.map(ds => m('option', {value: ds.id}, ds.label)); return m("div.field", m("label.label", "Dataset"), m("div.control", m("div.select", m("select", options)))); } + view(){ return m("div", this.tabs(), diff --git a/frontend/server.tsx b/frontend/server.tsx new file mode 100644 index 0000000..e69de29 diff --git a/visualizer/deno.json b/visualizer/deno.json new file mode 100644 index 0000000..7b3fa7f --- /dev/null +++ b/visualizer/deno.json @@ -0,0 +1,14 @@ +{ + "imports": { + "preact": "https://esm.sh/preact@10.19.2", + "preact/": "https://esm.sh/preact@10.19.2/", + "@preact/signals": "https://esm.sh/*@preact/signals@1.2.1", + "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.0", + "$std/": "https://deno.land/std@0.208.0/" + }, + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "preact" + } + +} diff --git a/visualizer/public/images/js_logo.svg b/visualizer/public/images/js_logo.svg new file mode 100644 index 0000000..9650ca7 --- /dev/null +++ b/visualizer/public/images/js_logo.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/visualizer/public/scripts/auth.js b/visualizer/public/scripts/auth.js new file mode 100644 index 0000000..10500d5 --- /dev/null +++ b/visualizer/public/scripts/auth.js @@ -0,0 +1,33 @@ +const sessionLocalStorageKey = 'dgt-session'; + +let cachedPathVariables = null; + +function getPathVariables(){ + if (cachedPathVariables !== null) { + return cachedPathVariables; + } + const raw = document.getElementById("url-placeholders")?.textContent || "{}" + cachedPathVariables = Object.entries(JSON.parse(raw)?.pathVariables || {}) + return cachedPathVariables + +} + +window.addEventListener('login', function (evt) { + localStorage.setItem(sessionLocalStorageKey, evt.detail.value); + document.location.href = "/"; +}); + +window.addEventListener('logout', function (evt) { + window.localStorage.removeItem(sessionLocalStorageKey); + document.location.href = "/login.html"; +}); + +window.addEventListener('htmx:configRequest', function(evt) { + evt.detail.headers['dgt-session'] = localStorage.getItem(sessionLocalStorageKey); + + function reducer(path, [key, value]){ + return path.replace(key, value) + } + + evt.detail.path = getPathVariables().reduce(reducer, evt.detail.path) +}); diff --git a/visualizer/server.ts b/visualizer/server.ts new file mode 100644 index 0000000..9e72fef --- /dev/null +++ b/visualizer/server.ts @@ -0,0 +1,9 @@ +import {app} from "./src/server.ts"; +import Logger from "https://deno.land/x/logger@v1.1.5/logger.ts"; + + + +const logger = new Logger() +const port = 8000 +logger.info(`Starting web server on port ${port}`) +await app.listen({ port }); \ No newline at end of file diff --git a/visualizer/src/api/mod.tsx b/visualizer/src/api/mod.tsx new file mode 100644 index 0000000..aa6c01b --- /dev/null +++ b/visualizer/src/api/mod.tsx @@ -0,0 +1,65 @@ +import {Router} from "https://deno.land/x/oak/mod.ts"; +import {FileMatchService} from "../services/matches.ts"; +import {ViewTabService} from "./tab.tsx"; + + +export const apiRouter = new Router( {prefix: '/api/v1'}); + +async function getMatchService(): Promise { + const f = "/home/didier/alldressed-programming/dodgeball-tracker/outputs/index.json" + return new FileMatchService(f) +} + +apiRouter.get('/ping', (ctx) => { + ctx.response.body = pong +}) + +apiRouter.get("/match/:id/title", async function(ctx){ + const service = await getMatchService() + const match = await service.getMatchInfoById(ctx.params.id) + ctx.response.body = (Match: {match.label}) +}); + +apiRouter.get("/match/:id/pane/info", async function(ctx){ + const service = await getMatchService() + const match = await service.getMatchInfoById(ctx.params.id) + const viewService = new ViewTabService(); + ctx.response.body = viewService.infoTab(match) +}); + +apiRouter.get("/match/:id/pane/player", async function(ctx){ + const service = await getMatchService() + const match = await service.getMatchInfoById(ctx.params.id) + const viewService = new ViewTabService(); + ctx.response.body = viewService.playerTab(match) +}); + +apiRouter.get("/match/:id/pane/player/data", async function(ctx){ + ctx.response.body = +}); + +apiRouter.get('/list-matches', async (ctx) => { + const matchService = await getMatchService() + const rows = []; + for await (const match of matchService.getAllMatchInfo()){ + rows.push( + + { match.label } + { match.originalVideoUrl } + + ) + } + ctx.response.body = (
+ + + + + + + + + {rows} + +
LabelVideo Link
+
) +}) diff --git a/visualizer/src/api/tab.tsx b/visualizer/src/api/tab.tsx new file mode 100644 index 0000000..c222215 --- /dev/null +++ b/visualizer/src/api/tab.tsx @@ -0,0 +1,73 @@ +import {MatchInfo} from "../services/matches.ts"; + +export class ViewTabService { + + infoTab(matchInfo: MatchInfo){ + return ( + + + + + + + + + + + + + + + + + + + +
Label{matchInfo.label}
Id{matchInfo.id}
Date{matchInfo.matchDate}
Video URL + {matchInfo.originalVideoUrl} +
) + + } + + playerTab(matchInfo: MatchInfo){ + return (
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
) + + } + +} \ No newline at end of file diff --git a/visualizer/src/login.ts b/visualizer/src/login.ts new file mode 100644 index 0000000..a2da694 --- /dev/null +++ b/visualizer/src/login.ts @@ -0,0 +1,25 @@ +import { Router } from "https://deno.land/x/oak/mod.ts"; + + +export const loginRouter = new Router(); + +export function isLoggedIn(ctx){ + return ctx.request.headers.get('dgt-session') +} + + +loginRouter.post("/api/v1/login", async (ctx)=> { + const username = (await ctx.request.body().value).get("username"); + ctx.response.headers.append("HX-Trigger", JSON.stringify({'login': username})); + ctx.response.body = username; +}); + +loginRouter.post("/api/v1/logout", (ctx)=> { + ctx.response.headers.append("HX-Trigger", "logout"); + ctx.response.body = ""; +}); + + + + + diff --git a/visualizer/src/server.ts b/visualizer/src/server.ts new file mode 100644 index 0000000..fc2155c --- /dev/null +++ b/visualizer/src/server.ts @@ -0,0 +1,45 @@ +import { Application } from "https://deno.land/x/oak/mod.ts"; +import staticFiles from "https://deno.land/x/static_files@1.1.6/mod.ts"; +import {loginRouter, isLoggedIn} from "./login.ts"; +import {apiRouter} from "./api/mod.tsx"; +import {viewRouter} from "./views/mod.tsx"; +import {renderToString} from "https://esm.sh/preact-render-to-string@5.1.19?deps=preact@10.5.15"; +import Logger from "https://deno.land/x/logger@v1.1.5/logger.ts"; + +const logger = new Logger(); +export const app = new Application(); + +function isReactComponent(x){ + return !!x && Object.hasOwn(x, "type") && Object.hasOwn(x, "props") +} + +async function reactRendererMiddleware(ctx, next){ + await next(); + if(isReactComponent(ctx.response.body)){ + logger.info("Converting react component to string.") + ctx.response.body = renderToString(ctx.response.body) + + } +} + +async function loggingMiddleware(ctx, next){ + if(!isLoggedIn(ctx)){ + logger.info("user not logged in.") + ctx.response.headers.append("HX-Redirect", "/login.html"); + return + } + logger.info('user logged in') + await next(); +} + + +app.use(loginRouter.routes()); +app.use(viewRouter.routes()); +app.use(staticFiles("public", {brotli: true, fetch: true})); +app.use(loggingMiddleware) +app.use(reactRendererMiddleware) +app.use(apiRouter.routes()); + + + + diff --git a/visualizer/src/services/matches.ts b/visualizer/src/services/matches.ts new file mode 100644 index 0000000..3bb1ed2 --- /dev/null +++ b/visualizer/src/services/matches.ts @@ -0,0 +1,41 @@ + +export type MatchInfo = { + id: string, + originalVideoUrl: string, + matchDate: string, + label: string +} + +export interface MatchService { + + getAllMatchInfo(): AsyncIterator; + getMatchInfoById(id: string): Promise; + +} + +export class FileMatchService implements MatchService{ + + readonly #indexFile: string + + constructor(indexFile: string) { + this.#indexFile = indexFile; + } + + async *getAllMatchInfo(): AsyncIterator{ + const data = await Deno.readTextFile(this.#indexFile) + const ret = JSON.parse(data) as {matches: MatchInfo[]} + yield* ret.matches + } + + async getMatchInfoById(id: string): Promise{ + for await(const mi of this.getAllMatchInfo()){ + if(mi.id === id){ + return mi + } + } + } + + + + +} \ No newline at end of file diff --git a/visualizer/src/views/base.tsx b/visualizer/src/views/base.tsx new file mode 100644 index 0000000..e9269bc --- /dev/null +++ b/visualizer/src/views/base.tsx @@ -0,0 +1,48 @@ +export function Head(){ + return ( + + + + JS Reading App + + + + + + ) +} + + +export function NavBar(){ + return () +} + +export function Body({pathVariables, children}){ + const urlPlaceHolders = { + pathVariables: Object.fromEntries(Object.entries(pathVariables ?? {}).map(([k, v]) => [`:${k}`, v])) + } + return ( + { children } + + + ) +} \ No newline at end of file diff --git a/visualizer/src/views/mod.tsx b/visualizer/src/views/mod.tsx new file mode 100644 index 0000000..930df5f --- /dev/null +++ b/visualizer/src/views/mod.tsx @@ -0,0 +1,124 @@ +import { Router } from "https://deno.land/x/oak/mod.ts"; +import {renderToString} from "https://esm.sh/preact-render-to-string@5.1.19?deps=preact@10.5.15"; +import * as base from './base.tsx'; +import {Head} from "./base.tsx"; +export const viewRouter = new Router(); + +function renderPageToString(page){ + return `${renderToString(page)}` +} + + +function Court(){ + const margin = 50; + const returnLine = {stroke: 'blue', strokeWidth: '2px'} + const courtBorder = {stroke: 'red', strokeWidth: '4px', fill: 'gray' } + const lines = [ + , + , + , + ] + return ( + + + { lines } + + + ) +} + +viewRouter.get("/match/:id/:tab", (ctx) => { + const page = ( + + + +
+ +
+
+
+

 

+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+
+
+ foo bar toto +
+
+
+
+
+ ) + ctx.response.body = renderPageToString(page) +}) + + +viewRouter.get("/index.html", (ctx) => { + const page = ( + + + +
+
+
+
+
+ ) + ctx.response.body = renderPageToString(page) +}) + +viewRouter.redirect("/", "/index.html") + +viewRouter.get("/login.html", (ctx) => { + const page = ( + + + +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ ) + ctx.response.body = renderPageToString(page) +})