From 22489242c2491cea3484f511af1c96c33cbc5ac8 Mon Sep 17 00:00:00 2001 From: didier amyot Date: Mon, 20 Nov 2023 07:38:50 -0500 Subject: [PATCH] first commit --- .gitignore | 2 + import_map.json | 1 + src/auth.ts | 3 + src/bundler.ts | 70 ++++++++++++++++++++++ src/client.ts | 150 ++++++++++++++++++++++++++++++++++++++++++++++++ src/server.ts | 89 ++++++++++++++++++++++++++++ src/shared.ts | 11 ++++ 7 files changed, 326 insertions(+) create mode 100644 .gitignore create mode 100644 import_map.json create mode 100644 src/auth.ts create mode 100644 src/bundler.ts create mode 100644 src/client.ts create mode 100644 src/server.ts create mode 100644 src/shared.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b01ae8d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +.idea diff --git a/import_map.json b/import_map.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/import_map.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..33c1f31 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,3 @@ +export function verifyAuth(auth: string): boolean{ + return auth.length === 4; +} \ No newline at end of file diff --git a/src/bundler.ts b/src/bundler.ts new file mode 100644 index 0000000..fd32089 --- /dev/null +++ b/src/bundler.ts @@ -0,0 +1,70 @@ +/** + * This part of the code is based on the way `fresh` is bundling the code. + * See https://github.com/denoland/fresh/blob/main/src/server/bundle.ts + */ + +// @deno-types="https://deno.land/x/esbuild@v0.14.51/mod.d.ts" +import * as esbuildNative from "https://deno.land/x/esbuild@v0.14.51/mod.js"; +import * as esbuildWasm from "https://deno.land/x/esbuild@v0.14.51/wasm.js"; +import { denoPlugin } from "https://deno.land/x/esbuild_deno_loader@0.5.2/mod.ts"; + +const esbuild: typeof esbuildWasm = Deno.run === undefined + ? esbuildWasm + : esbuildNative; + +let esbuildInitialized = false; +async function verifyEsbuildInitialized() { + if (esbuildInitialized) { + return; + } + console.log(new Date(), "start initializing esbuild"); + if (Deno.run === undefined) { + const wasmURL = new URL("./esbuild@v0.14.51.wasm", import.meta.url).href; + const resp = await fetch(wasmURL); + const wasm = new Response(resp.body, { + headers: { "Content-Type": "application/wasm" }, + }); + const wasmModule = await WebAssembly.compileStreaming(wasm); + await esbuild.initialize({ + wasmModule, + worker: false, + }); + } else { + await esbuild.initialize({}); + } + esbuildInitialized = true; + console.log(new Date(), "esbuild initialized"); +} + +export async function createBundle( + entryPoint: URL, + importMapURL: URL, + isDev?: boolean, +) { + await verifyEsbuildInitialized(); + console.log(new Date(), "Bundling", entryPoint.href); + const absWorkingDir = Deno.cwd(); + const res = await esbuild.build({ + bundle: true, + entryPoints: [entryPoint.href], + platform: "neutral", + treeShaking: true, + minify: !isDev, + sourcemap: isDev ? "inline" : false, + outfile: "", + absWorkingDir, + outdir: ".", + target: ["chrome99", "firefox99", "safari15"], + write: false, + format: "esm", + plugins: [denoPlugin({ importMapURL })], + splitting: false, + jsx: "automatic", + jsxImportSource: "react", + }); + if (res.errors.length > 0) { + throw res.errors; + } + console.log(new Date(), "Finished bundling", entryPoint.href); + return res.outputFiles[0].text; +} diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..3fe984a --- /dev/null +++ b/src/client.ts @@ -0,0 +1,150 @@ +import m from "https://esm.sh/mithril@2.2.2" +import {verifyAuth} from "./auth.ts"; +import {Scores, Team} from "./shared.ts"; + +class ProgressBar { + + closestFivePercent(n: number): number{ + let ret = 0; + let distRet = 101; + for(let i = 0; i < 101; i += 5){ + if(Math.abs(i - n) < distRet) { + ret = i; + distRet = Math.abs(i - n); + } + } + return ret; + } + + computeClickedScored(event: any){ + const total = event.target.clientWidth; + const score = event.x - event.target.offsetLeft; + return this.closestFivePercent(( score / total ) * 100); + } + view({attrs}){ + const onclickFn = (e) => { + attrs.scores[attrs.key] = this.computeClickedScored(e); + attrs.onchange(); + } + const value = attrs.scores[attrs.key]; + return m("progress.progress.is-primary", + {value: value, max: 100, onclick: onclickFn, title: `${value}%`}, + `${value}%`) + } +} + + +const scores: Scores = { + readme: 25, + diagrams: 50, + convinced: 75 +}; + +const teams: Team[] = [ + {id: 8787, name: "Les chacals de laval", scores: {...scores}}, + {id: 18167, name: "Les rapides de blainville", scores: {...scores}}, + {id: 17165, name: "Les pas-beaux du plateau", scores: {...scores}}, +] + +class VoteView { + + voteFor(team: Team, title: string, key: string, onchange: () => void){ + return m("div.columns", + m("div.column", title, ":"), + m("div.column.is-two-thirds", + m(new ProgressBar(), {scores: team.scores, key: key, onchange: onchange}))); + } + + view(vnode){ + const teamId = vnode.attrs.teamId; + const onchange = vnode.attrs.onchange ?? function(){}; + const team = teams.find(team => team.id.toString() === teamId); + return [ + this.voteFor(team, "Qualité du README", 'readme', onchange), + this.voteFor(team, "Qualité des diagrammes", 'diagrams', onchange), + this.voteFor(team, "Êtes-vous convaincu?", 'convinced', onchange)] + } +} + +class Submit{ + view(){ + return m("div.columns", + m("div.column", + m(m.route.Link, {class: "button", href: "/voter-home/"}, `Votez!`), + )); + } +} + +class MainVoteView{ + + async persistTeams(): Promise{ + const params: RequestInit = { + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(teams) + } + const resp = await fetch("/api/v1/vote/kkk", params); + console.log(resp.status) + } + + view(vnode){ + const name = "Les chacals de laval" + return m("div.columns.is-centered", + m("div.column.is-two-thirds", + m("h1.title.is-1", m(m.route.Link, {href: "/"}, `Votons - ${name}`)), + m(new VoteView(), {teamId: vnode.attrs.teamId, onchange: this.persistTeams}), + m(new Submit()))); + } +} + +class MainView{ + createTeamRow(team){ + return m('tr', [ + m('td', m(m.route.Link, {href: `${m.route.get()}vote/${team.id}/`}, team.name)), + m('td', team.scores.readme), + m('td', team.scores.diagrams), + m('td', team.scores.convinced), + ]) + } + + votesTable(teams){ + return m('table.table.is-striped.is-fullwidth', + m('thead', + m('tr', ['Équipe', 'Readme', 'Diagrammes', 'Idée'].map(title => m('th', title)))), + m('tbody', teams.map(team => this.createTeamRow(team)))) + } + view(){ + return m("div.columns.is-centered", + m("div.column.is-two-thirds", + this.votesTable(teams))) + } +} + +class PasscodeView{ + + verifyPasscode(event): void { + let password = event.target.value; + if(verifyAuth(password)){ + window.localStorage.setItem("userId", password); + m.route.set(`/voter-home/`); + } + } + + view(){ + const change = (e) => this.verifyPasscode(e); + return m('div.hero.is-fullheight', + m("div.hero-body.has-text-centered", + m("div.container", + m("div.box", m("input.input", {placeholder: "passcode", onkeyup: change}))))); + } +} + + + +m.route(window.document.body, "/", { + "/": PasscodeView, + "/voter-home/": MainView, + "/voter-home/vote/:teamId/": MainVoteView +}) \ No newline at end of file diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..a16f5b6 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,89 @@ +import { Application, Router } from "https://deno.land/x/oak/mod.ts"; +import {createBundle} from "./bundler.ts"; +import {Context} from "https://deno.land/x/oak@v12.2.0/context.ts"; +import {Team} from "./shared.ts"; + +const router = new Router(); + +const clientBundle = createBundle( + new URL("./client.ts", import.meta.url), + new URL("../import_map.json", import.meta.url), + false, +); + + +router.get("/", (context) => { + context.response.body = ` + + + + + title + + + + + + + `; +}); + +function getUserIndex(userId: string, ballotId: string, questionId: string){ + return ["tatoo-vote", + "v1", + "results", + "user", + userId, + "ballot", + ballotId, + "question", + questionId] +} + +function persistTeams(userId: any, ballots: Team[]) { + const kv = await Deno.openKv(); + for(const ballot of ballots){ + for(const question of Object.keys(ballot.scores)){ + const k = getUserIndex(userId, ballot.id, question); + const v = {value: ballot.scores[question]}; + await kv.set(k, v); + } + + } +} + +router.post("/api/v1/vote/:userid", async (ctx: Context)=> { + const userId = ctx?.params?.userid??""; + if(!ctx.request.hasBody){ + ctx.throw(415); + } + const body = await ctx.request.body().value; + const teams = body as Team[]; + await persistTeams(userId, teams); + console.log(body); + ctx.response.body = userId; + +}); + +router.get("/client.js", async (context): Promise => { + context.response.body = await createBundle( + new URL("./client.ts", import.meta.url), + new URL("../import_map.json", import.meta.url), + false, + ); + context.response.type = "application/javascript"; +}); + + + +const app = new Application(); +app.use(router.routes()); + + +app.use((ctx) => { + ctx.response.body = "Hello World!"; +}); + +await app.listen({ port: 3333 }); + + diff --git a/src/shared.ts b/src/shared.ts new file mode 100644 index 0000000..63f9d9d --- /dev/null +++ b/src/shared.ts @@ -0,0 +1,11 @@ +export type Scores = { + readme: number, + diagrams: number, + convinced: number, +}; + +export type Team = { + id: number, + name: string, + scores: Scores, +} \ No newline at end of file