{
+ 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 = (
+
+
+
+ | Label |
+ Video Link |
+
+
+
+ {rows}
+
+
+
)
+})
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 ()
+}
+
+viewRouter.get("/match/:id/:tab", (ctx) => {
+ const page = (
+
+
+
+
+
+ )
+ 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)
+})