diff --git a/.forgejo/workflows/publish.yaml b/.forgejo/workflows/publish.yaml index d192d28..b8f7077 100644 --- a/.forgejo/workflows/publish.yaml +++ b/.forgejo/workflows/publish.yaml @@ -23,5 +23,4 @@ jobs: make - name: Deploy run: | - npm install netlify-cli --save-dev - npx netlify deploy --dir target --prod \ No newline at end of file + netlify deploy --dir target --prod \ No newline at end of file diff --git a/.gitignore b/.gitignore index 188ebd1..2f7896d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ target/ -.vscode/ -.idea/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 780b388..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,11 +0,0 @@ -# Webtools - -This is a static site for a set of tools available on the web. The root of the site is located in "site", with index.html holding a list of available tools. Within each subdirectory of "site" is a separate tool. Each tool is self-contained with it's own index.html, CSS and JavaScript. Multipes of these files can be added if necessary. Note that each HTML has a standard header, with a typical title convension and importing "pico.min.css". - -Some tools can include a WASM file for more complex tasks. The WASM targets are built from Go files, with each cmd located as a sub directory in the "cmds" directory. Each one is built as a standalone WASM target. - -To build: - -- Run `build.wasm` to build the WASM targets. Additional targets will need to be added to this Make target -- Run `build` to build both the WASM and prepare the site. The site will be available in a "target" directory. -- Run `dev` to build the site and run a dev HTML server to test the site on port 8000. The dev server can be killed by sending a SIGINT signal. diff --git a/Makefile b/Makefile index d3f1b85..d9bdbe1 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,6 @@ build.wasm: GOOS=js GOARCH=wasm go build -o target/wasm/clocks.wasm ./cmds/clocks GOOS=js GOARCH=wasm go build -o target/wasm/gotemplate.wasm ./cmds/gotemplate GOOS=js GOARCH=wasm go build -o target/wasm/timestamps.wasm ./cmds/timestamps - GOOS=js GOARCH=wasm go build -o target/wasm/android-icons.wasm ./cmds/android-icons cp $(GOROOT)/lib/wasm/wasm_exec.js target/wasm/. .Phony: build.site diff --git a/cmds/android-icons/main.go b/cmds/android-icons/main.go deleted file mode 100644 index cb3ddc8..0000000 --- a/cmds/android-icons/main.go +++ /dev/null @@ -1,144 +0,0 @@ -//go:build js - -package main - -import ( - "archive/zip" - "bytes" - "encoding/base64" - "fmt" - "image" - "image/png" - "strings" - "syscall/js" - - "golang.org/x/image/draw" -) - -type density struct { - dir string - size int -} - -var densities = []density{ - {"mipmap-mdpi", 48}, - {"mipmap-hdpi", 72}, - {"mipmap-xhdpi", 96}, - {"mipmap-xxhdpi", 144}, - {"mipmap-xxxhdpi", 192}, -} - -func main() { - js.Global().Set("prepareZip", js.FuncOf(prepareZip)) - <-make(chan struct{}) -} - -func prepareZip(this js.Value, args []js.Value) interface{} { - // args[0] is an array of {name: string, data: string (base64 PNG data)} - files := args[0] - length := files.Length() - - if length == 0 { - js.Global().Call("alert", "No files selected.") - return nil - } - - var buf bytes.Buffer - zw := zip.NewWriter(&buf) - - for i := 0; i < length; i++ { - file := files.Index(i) - name := file.Get("name").String() - dataURL := file.Get("data").String() - - // Strip data URL prefix (data:image/png;base64,) - commaIdx := strings.Index(dataURL, ",") - if commaIdx < 0 { - setStatus(fmt.Sprintf("Invalid data for %s", name)) - return nil - } - b64Data := dataURL[commaIdx+1:] - - imgData, err := base64.StdEncoding.DecodeString(b64Data) - if err != nil { - setStatus(fmt.Sprintf("Failed to decode %s: %v", name, err)) - return nil - } - - src, err := png.Decode(bytes.NewReader(imgData)) - if err != nil { - setStatus(fmt.Sprintf("Failed to decode PNG %s: %v", name, err)) - return nil - } - - // Strip extension from name for the base name - baseName := name - if strings.HasSuffix(strings.ToLower(baseName), ".png") { - baseName = baseName[:len(baseName)-4] - } - - for _, d := range densities { - resized := resizeImage(src, d.size) - - var pngBuf bytes.Buffer - if err := png.Encode(&pngBuf, resized); err != nil { - setStatus(fmt.Sprintf("Failed to encode %s/%s.png: %v", d.dir, baseName, err)) - return nil - } - - path := fmt.Sprintf("%s/%s.png", d.dir, baseName) - w, err := zw.Create(path) - if err != nil { - setStatus(fmt.Sprintf("Failed to create zip entry %s: %v", path, err)) - return nil - } - if _, err := w.Write(pngBuf.Bytes()); err != nil { - setStatus(fmt.Sprintf("Failed to write zip entry %s: %v", path, err)) - return nil - } - } - } - - if err := zw.Close(); err != nil { - setStatus(fmt.Sprintf("Failed to finalize zip: %v", err)) - return nil - } - - // Convert zip bytes to a JS Uint8Array and trigger download - zipBytes := buf.Bytes() - jsArray := js.Global().Get("Uint8Array").New(len(zipBytes)) - js.CopyBytesToJS(jsArray, zipBytes) - - // Create Blob and download - blobParts := js.Global().Get("Array").New() - blobParts.Call("push", jsArray) - blobOpts := js.Global().Get("Object").New() - blobOpts.Set("type", "application/zip") - blob := js.Global().Get("Blob").New(blobParts, blobOpts) - - url := js.Global().Get("URL").Call("createObjectURL", blob) - anchor := js.Global().Get("document").Call("createElement", "a") - anchor.Set("href", url) - anchor.Set("download", "android-icons.zip") - js.Global().Get("document").Get("body").Call("appendChild", anchor) - anchor.Call("click") - anchor.Call("remove") - js.Global().Get("URL").Call("revokeObjectURL", url) - - setStatus(fmt.Sprintf("Done! Prepared icons for %d image(s).", length)) - return nil -} - -func resizeImage(src image.Image, size int) image.Image { - dst := image.NewNRGBA(image.Rect(0, 0, size, size)) - draw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil) - return dst -} - -func setStatus(msg string) { - querySelector("#status").Set("innerText", msg) -} - -func querySelector(query string) js.Value { - return js.Global().Get("document").Call("querySelector", query) -} diff --git a/go.mod b/go.mod index 4e30240..7f220ff 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,5 @@ module github.com/lmika/webtools -go 1.25.0 +go 1.24.3 -require ( - github.com/alecthomas/participle/v2 v2.1.4 // indirect - golang.org/x/image v0.37.0 // indirect -) +require github.com/alecthomas/participle/v2 v2.1.4 // indirect diff --git a/go.sum b/go.sum index 8e1c4f0..bff288a 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,2 @@ github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U= github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI= -golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA= -golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= diff --git a/site/android-icons/index.html b/site/android-icons/index.html deleted file mode 100644 index d30965e..0000000 --- a/site/android-icons/index.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - Android Icon Resizer - Tools - - - - -
-
-

Android Icon Resizer

-

Resize PNG images to Android mipmap density buckets. Everything runs in your browser.

-
-
-
-
- - -
-
-
-

Circle Crop Preview

- -
-
- -
-
-
- - - diff --git a/site/android-icons/main.js b/site/android-icons/main.js deleted file mode 100644 index 056050b..0000000 --- a/site/android-icons/main.js +++ /dev/null @@ -1,136 +0,0 @@ -import "/wasm/wasm_exec.js"; - -const go = new Go(); -let wasmReady = false; - -WebAssembly.instantiateStreaming(fetch("/wasm/android-icons.wasm"), go.importObject) - .then((result) => { - go.run(result.instance); - wasmReady = true; - }); - -const fileInput = document.getElementById("file-input"); -const previewArea = document.getElementById("preview-area"); -const prepareBtn = document.getElementById("prepare-btn"); -const statusEl = document.getElementById("status"); -const circlePreview = document.getElementById("circle-preview"); -const circleCanvas = document.getElementById("circle-canvas"); - -let loadedFiles = []; - -function renderCirclePreview() { - if (loadedFiles.length === 0) { - circlePreview.style.display = "none"; - return; - } - - const nameLower = (f) => f.name.toLowerCase(); - - let bgFile = loadedFiles.find((f) => /back/.test(nameLower(f))); - let fgFile = loadedFiles.find((f) => !/back/.test(nameLower(f)) && !/mono/.test(nameLower(f))); - - // Fallbacks: if only one image or no "back" image, use the first file - if (!bgFile && !fgFile) { - bgFile = loadedFiles[0]; - } else if (!bgFile) { - bgFile = fgFile; - fgFile = null; - } - - const size = 192; - circleCanvas.width = size; - circleCanvas.height = size; - const ctx = circleCanvas.getContext("2d"); - ctx.clearRect(0, 0, size, size); - - // Clip to circle - ctx.save(); - ctx.beginPath(); - ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2); - ctx.closePath(); - ctx.clip(); - - const drawLayer = (src) => { - return new Promise((resolve) => { - const img = new Image(); - img.onload = () => { - ctx.drawImage(img, 0, 0, size, size); - resolve(); - }; - img.onerror = resolve; - img.src = src; - }); - }; - - drawLayer(bgFile.data).then(() => { - if (fgFile && fgFile !== bgFile) { - return drawLayer(fgFile.data); - } - }).then(() => { - ctx.restore(); - - // Draw border ring - ctx.beginPath(); - ctx.arc(size / 2, size / 2, size / 2 - 1.5, 0, Math.PI * 2); - ctx.closePath(); - ctx.strokeStyle = "rgba(128, 128, 128, 0.5)"; - ctx.lineWidth = 3; - ctx.stroke(); - - circlePreview.style.display = "block"; - }); -} - -fileInput.addEventListener("change", () => { - const files = Array.from(fileInput.files); - loadedFiles = []; - previewArea.innerHTML = ""; - circlePreview.style.display = "none"; - - if (files.length === 0) { - prepareBtn.disabled = true; - return; - } - - files.forEach((file) => { - const reader = new FileReader(); - reader.onload = (e) => { - const dataURL = e.target.result; - loadedFiles.push({ name: file.name, data: dataURL }); - - const item = document.createElement("div"); - item.className = "preview-item"; - - const img = document.createElement("img"); - img.src = dataURL; - - const label = document.createElement("span"); - label.textContent = file.name; - - item.appendChild(img); - item.appendChild(label); - previewArea.appendChild(item); - - if (loadedFiles.length === files.length) { - prepareBtn.disabled = false; - renderCirclePreview(); - } - }; - reader.readAsDataURL(file); - }); -}); - -prepareBtn.addEventListener("click", () => { - if (!wasmReady) { - statusEl.textContent = "WASM module is still loading, please wait..."; - return; - } - if (loadedFiles.length === 0) { - return; - } - statusEl.textContent = "Preparing..."; - // Pass file data to Go WASM - setTimeout(() => { - prepareZip(loadedFiles); - }, 50); -}); diff --git a/site/android-icons/style.css b/site/android-icons/style.css deleted file mode 100644 index 1aea483..0000000 --- a/site/android-icons/style.css +++ /dev/null @@ -1,54 +0,0 @@ -#preview-area { - display: flex; - flex-wrap: wrap; - gap: 1rem; - margin: 1rem 0; -} - -.preview-item { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.25rem; -} - -.preview-item img { - width: 96px; - height: 96px; - object-fit: contain; - border: 1px solid var(--pico-muted-border-color); - border-radius: 4px; - background: repeating-conic-gradient(#eee 0% 25%, #fff 0% 50%) 50% / 16px 16px; -} - -.preview-item span { - font-size: 0.8rem; - color: var(--pico-muted-color); - word-break: break-all; - max-width: 96px; - text-align: center; -} - -.actions { - margin: 1rem 0; -} - -#circle-preview { - display: none; - margin: 1rem 0; -} - -#circle-preview h3 { - margin-bottom: 0.5rem; -} - -#circle-canvas { - background: repeating-conic-gradient(#eee 0% 25%, #fff 0% 50%) 50% / 16px 16px; - border-radius: 50%; - box-shadow: 0 0 0 3px var(--pico-muted-border-color); -} - -#status { - margin-top: 0.5rem; - font-style: italic; -} diff --git a/site/finska/index.html b/site/finska/index.html deleted file mode 100644 index b68d16b..0000000 --- a/site/finska/index.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - Finska Score Card - Tools - - - - - -

Finska Scorecard

- -
- - - - - - - - - - - - - - - - - -
- - Team A - - - - - Team B - - -
-
- -
-
-
- -
-
- -
- - -
-
- - \ No newline at end of file diff --git a/site/finska/scripts/controllers.js b/site/finska/scripts/controllers.js deleted file mode 100644 index acb321f..0000000 --- a/site/finska/scripts/controllers.js +++ /dev/null @@ -1,253 +0,0 @@ -import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"; -import { Scorecard, getStoreDAO } from "./models.js"; - -const storeDao = getStoreDAO(); - -export class FinskaScorecardController extends Controller { - static targets = ["score1Input", "score2Input", "scoreTable", "team1Header", "team2Header"]; - static values = { - maxScore: Number, - overflowScoreTo: Number - } - - connect() { - let rules = { - maxScore: Math.floor(this.maxScoreValue), - overflowScoreTo: Math.floor(this.overflowScoreToValue) - }; - - this._undoStack = []; - this._scorecard = storeDao.loadOrCreate(rules); - this._loadTeamNames(); - this.updateTable(); - } - - _loadTeamNames() { - const stored1 = localStorage.getItem('finska-team1-name'); - const stored2 = localStorage.getItem('finska-team2-name'); - this._team1Name = stored1 || 'Team A'; - this._team2Name = stored2 || 'Team B'; - this._renderTeamHeader(this.team1HeaderTarget, this._team1Name, 'editTeam1'); - this._renderTeamHeader(this.team2HeaderTarget, this._team2Name, 'editTeam2'); - } - - _renderTeamHeader(td, name, editAction) { - td.innerHTML = ` - ${this._escapeHtml(name)} - - `; - } - - _renderEditHeader(td, currentName, teamNum) { - td.innerHTML = ` - - - - `; - td.querySelector('input').focus(); - td.querySelector('input').select(); - } - - editTeam1() { - this._renderEditHeader(this.team1HeaderTarget, this._team1Name, '1'); - } - - editTeam2() { - this._renderEditHeader(this.team2HeaderTarget, this._team2Name, '2'); - } - - confirmTeam1() { - const input = this.team1HeaderTarget.querySelector('input'); - const newName = input.value.trim() || 'Team A'; - this._team1Name = newName; - localStorage.setItem('finska-team1-name', newName); - this._renderTeamHeader(this.team1HeaderTarget, this._team1Name, 'editTeam1'); - } - - confirmTeam2() { - const input = this.team2HeaderTarget.querySelector('input'); - const newName = input.value.trim() || 'Team B'; - this._team2Name = newName; - localStorage.setItem('finska-team2-name', newName); - this._renderTeamHeader(this.team2HeaderTarget, this._team2Name, 'editTeam2'); - } - - cancelTeam1() { - this._renderTeamHeader(this.team1HeaderTarget, this._team1Name, 'editTeam1'); - } - - cancelTeam2() { - this._renderTeamHeader(this.team2HeaderTarget, this._team2Name, 'editTeam2'); - } - - editKeyDown1(e) { - this._editKeyDown(e, this.confirmTeam1.bind(this), this.cancelTeam1.bind(this)); - } - - editKeyDown2(e) { - this._editKeyDown(e, this.confirmTeam2.bind(this), this.cancelTeam2.bind(this)); - } - - _editKeyDown(e, confirmFn, cancelFn) { - if (e.key === 'Enter') { - e.preventDefault(); - confirmFn(); - } else if (e.key === 'Escape') { - e.preventDefault(); - cancelFn(); - } - } - - _escapeHtml(str) { - const div = document.createElement('div'); - div.textContent = str; - return div.innerHTML; - } - - _escapeAttr(str) { - return str.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); - } - - updateTable() { - let tableBody = this.scoreTableTarget; - let tableRows = tableBody.querySelectorAll("tr"); - let pairs = this._scorecard.pairs(); - - for (let pairIndex = 0; pairIndex < pairs.length; pairIndex++) { - let tableRow; - if (pairIndex >= tableRows.length) { - tableRow = this._appendRow(); - } else { - tableRow = tableRows[pairIndex]; - } - - this._updateRow(tableRow, pairs[pairIndex]) - } - - // Remove any extra rows - for (let i = pairs.length; i < tableRows.length; i++) { - tableBody.removeChild(tableRows[i]); - } - - console.log(JSON.stringify(this._scorecard.toJson())); - } - - _updateRow(tableRow, pair) { - let tds = tableRow.querySelectorAll("td"); - - this._updateCell(pair.p1, tds[0], tds[1]); - this._updateCell(pair.p2, tds[2], tds[3]); - } - - _updateCell(score, scoreCell, totalCell) { - scoreCell.classList.value = ""; - totalCell.classList.value = ""; - - if (score != null) { - scoreCell.textContent = score.score; - totalCell.textContent = score.total; - - if (score.score === 0) { - scoreCell.classList.add("score-foul"); - totalCell.classList.add("score-foul"); - } else if (score.wasOverflow) { - scoreCell.classList.add("score-overflow"); - totalCell.classList.add("score-overflow"); - } else if (score.wasWin) { - scoreCell.classList.add("score-win"); - totalCell.classList.add("score-win"); - } - } else { - scoreCell.textContent = ""; - totalCell.textContent = ""; - } - } - - _appendRow() { - let newRow = document.createElement("tr"); - newRow.classList.add("score-entry"); - - for (let i = 0; i < 4; i++) { - newRow.appendChild(document.createElement("td")); - } - - this.scoreTableTarget.appendChild(newRow); - return newRow; - } - - addScore1() { - this._addScore(this.score1InputTarget, this.score2InputTarget, - this._scorecard.addPlayer1Score.bind(this._scorecard), - this._scorecard.removeLastPlayer1Score.bind(this._scorecard)); - } - - addScore2() { - this._addScore(this.score2InputTarget, this.score1InputTarget, - this._scorecard.addPlayer2Score.bind(this._scorecard), - this._scorecard.removeLastPlayer2Score.bind(this._scorecard)); - - } - - _addScore(inputElem, focusToInputElem, addScoreFn, queueUndoFn) { - let score = parseInt(inputElem.value); - if (isNaN(score)) { - score = 0; - } - - addScoreFn(score); - this._undoStack.push(queueUndoFn); - - inputElem.value = ""; - - storeDao.save(this._scorecard); - this.updateTable(); - - focusToInputElem.focus(); - } - - score1KeyDown(e) { - this._handleKeyDown(e, this.addScore1.bind(this)); - } - - score2KeyDown(e) { - this._handleKeyDown(e, this.addScore2.bind(this)); - } - - _handleKeyDown(e, addScoreFn) { - if (e.key === "Enter") { - e.preventDefault(); - addScoreFn(); - } - } - - undoLast() { - if (this._undoStack.length === 0) { - return; - } - if (!confirm("Really undo last move?")) { - return; - } - - (this._undoStack.pop())(); - - storeDao.save(this._scorecard); - this.updateTable(); - } - - resetAll() { - if (!confirm("Really reset?")) { - return; - } - - this._scorecard.reset(); - storeDao.clear(); - this._undoStack = []; - this._team1Name = 'Team A'; - this._team2Name = 'Team B'; - localStorage.removeItem('finska-team1-name'); - localStorage.removeItem('finska-team2-name'); - this._renderTeamHeader(this.team1HeaderTarget, this._team1Name, 'editTeam1'); - this._renderTeamHeader(this.team2HeaderTarget, this._team2Name, 'editTeam2'); - this.updateTable(); - } -} diff --git a/site/finska/scripts/main.js b/site/finska/scripts/main.js deleted file mode 100644 index 34ece68..0000000 --- a/site/finska/scripts/main.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Application } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"; -import { FinskaScorecardController } from "./controllers.js" - -window.Stimulus = Application.start(); - -window.Stimulus.register('finska-scorecard', FinskaScorecardController); - diff --git a/site/finska/scripts/models.js b/site/finska/scripts/models.js deleted file mode 100644 index 321122d..0000000 --- a/site/finska/scripts/models.js +++ /dev/null @@ -1,152 +0,0 @@ -export class ScoreEntry { - constructor(score, total, foul, wasOverflow, wasWin) { - this.score = score; - this.total = total; - this.fouls = foul; - this.wasOverflow = wasOverflow; - this.wasWin = wasWin; - } -}; - -export class Scorecard { - - constructor(rules) { - this.rules = rules; - this.reset(); - } - - reset() { - this._player1Scores = []; - this._player2Scores = []; - } - - addPlayer1Score(newScore) { - this._addScore(this._player1Scores, newScore); - } - - removeLastPlayer1Score() { - this._player1Scores.pop(); - } - - addPlayer2Score(newScore) { - this._addScore(this._player2Scores, newScore); - } - - removeLastPlayer2Score() { - this._player2Scores.pop(); - } - - _addScore(playerScores, newScore) { - let lastEntry; - if (playerScores.length === 0) { - lastEntry = new ScoreEntry(0, 0, 0, false, false); - } else { - lastEntry = playerScores[playerScores.length - 1]; - } - - let newEntry = this._newEntryFromPrevious(newScore, lastEntry); - - playerScores.push(newEntry); - } - - length() { - return Math.max(this._player1Scores.length, this._player2Scores.length); - } - - pairs() { - let pairs = []; - - for (let i = 0; i < this.length(); i++) { - pairs.push({ - p1: (i < this._player1Scores.length ? this._player1Scores[i] : null), - p2: (i < this._player2Scores.length ? this._player2Scores[i] : null), - }) - } - - return pairs; - } - - _newEntryFromPrevious(score, previousScoreEntry) { - if (previousScoreEntry === null) { - return new ScoreEntry(score, score, (score === 0 ? 1 : 0)); - } - - let wasOverflow = false, wasWin = false; - let newTotal = previousScoreEntry.total + score; - - if (newTotal === this.rules.maxScore) { - wasWin = true; - } else if (newTotal > this.rules.maxScore) { - newTotal = this.rules.overflowScoreTo; - wasOverflow = true; - } - - let newFouls = previousScoreEntry.foul; - if (score === 0) { - newFouls++; - } - - return new ScoreEntry(score, newTotal, newFouls, wasOverflow, wasWin); - } - - toJson() { - return { - "version": 1, - "rules": this.rules, - "p1": { "scores": this._player1Scores.map(p => p.score) }, - "p2": { "scores": this._player2Scores.map(p => p.score) }, - }; - } - - static fromJson(o) { - let scorecard = new Scorecard(o.rules); - o["p1"]["scores"].forEach(x => scorecard.addPlayer1Score(x)); - o["p2"]["scores"].forEach(x => scorecard.addPlayer2Score(x)); - return scorecard; - } -} - -class StoreDAO { - - constructor(localStorage) { - this._localStorage = localStorage; - } - - save(scoreCard) { - this._localStorage.setItem('finska-scorecard', JSON.stringify(scoreCard.toJson())); - } - - loadOrCreate(rules) { - try { - console.log("Loading scorecard"); - let scoreCardJson = this._localStorage.getItem('finska-scorecard'); - if (scoreCardJson !== null) { - return Scorecard.fromJson(JSON.parse(scoreCardJson)); - } - } catch (e) { - console.log(`Could not restore game: ${e}`); - } - - return new Scorecard(rules); - } - - clear() { - this._localStorage.removeItem('finska-scorecard'); - } -} - -class DummyStoreDAO { - save(scoreCard) { } - loadOrCreate(rules) { - return new Scorecard(rules); - } - clear() { } -} - -export function getStoreDAO() { - if (!!window.localStorage) { - return new StoreDAO(window.localStorage); - } else { - return new DummyStoreDAO(); - } -} \ No newline at end of file diff --git a/site/finska/style.css b/site/finska/style.css deleted file mode 100644 index 9bf0432..0000000 --- a/site/finska/style.css +++ /dev/null @@ -1,84 +0,0 @@ -tfoot input { - border-width: 1px; -} - -.score-foul { - color: #861D13; - background-color: #F8DCD6; - font-weight: bold; -} - -.score-overflow { - color: #5B4200; - background-color: #FCEFD9; - font-weight: bold; -} - -.score-win { - color: #394D00; - background-color: #DEFC85; - font-weight: bold; -} - -.team-header { - display: flex; - align-items: center; - justify-content: center; - gap: 0.25rem; -} - -.team-header .team-name { - font-weight: bold; -} - -.team-header .edit-btn { - background: none; - border: none; - cursor: pointer; - padding: 0.1rem 0.3rem; - font-size: 0.9rem; - margin: 0; - width: auto; - line-height: 1; - opacity: 0.5; -} - -.team-header .edit-btn:hover { - opacity: 1; -} - -.team-header-edit { - display: flex; - align-items: center; - justify-content: center; - gap: 0.25rem; -} - -.team-header-edit input { - border-width: 1px; - margin: 0; - padding: 0.2rem 0.4rem; - font-size: 0.9rem; - width: 6rem; - text-align: center; -} - -.team-header-edit .confirm-btn, -.team-header-edit .cancel-btn { - background: none; - border: none; - cursor: pointer; - padding: 0.1rem 0.3rem; - font-size: 1rem; - margin: 0; - width: auto; - line-height: 1; -} - -.team-header-edit .confirm-btn { - color: #2e7d32; -} - -.team-header-edit .cancel-btn { - color: #c62828; -} diff --git a/site/freelens-logo/script.js b/site/freelens-logo/script.js index 7be5111..0404e63 100644 --- a/site/freelens-logo/script.js +++ b/site/freelens-logo/script.js @@ -43,7 +43,6 @@ const availableWidth = width - PADDING * 2; const availableHeight = height - PADDING * 2; const halfHeight = availableHeight / 2; - const threeQuartersHeight = availableHeight * 3 / 4; ctx.fillStyle = 'white'; ctx.textAlign = 'center'; @@ -58,7 +57,7 @@ ctx.font = `bold ${fontSize}px sans-serif`; ctx.fillText(lower, width / 2, height - PADDING - halfHeight / 2 + 4); } else { - let fontSize = fitText(upper, availableWidth, threeQuartersHeight); + let fontSize = fitText(upper, availableWidth, halfHeight); ctx.font = `bold ${fontSize}px sans-serif`; ctx.fillText(upper, width / 2, height / 2); } diff --git a/site/gradient-image/index.html b/site/gradient-image/index.html deleted file mode 100644 index 2c1179c..0000000 --- a/site/gradient-image/index.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - Gradient Image - Tools - - - - -
-
-

Gradient Image

-

Generate gradient images as downloadable PNGs

-
-
-
-
- - - - - - - - - - - - - - -
-
- - -
-
- - - diff --git a/site/gradient-image/main.js b/site/gradient-image/main.js deleted file mode 100644 index 4271b7e..0000000 --- a/site/gradient-image/main.js +++ /dev/null @@ -1,70 +0,0 @@ -const PREVIEW_SIZE = 256; - -const fromColorEl = document.getElementById("from-color"); -const toColorEl = document.getElementById("to-color"); -const gradientTypeEl = document.getElementById("gradient-type"); -const rotationEl = document.getElementById("rotation"); -const rotationValueEl = document.getElementById("rotation-value"); -const imageSizeEl = document.getElementById("image-size"); -const canvas = document.getElementById("preview-canvas"); -const downloadBtn = document.getElementById("download-btn"); - -function drawGradient(ctx, size) { - const fromColor = fromColorEl.value; - const toColor = toColorEl.value; - const type = gradientTypeEl.value; - const rotation = parseInt(rotationEl.value); - - const cx = size / 2; - const cy = size / 2; - - let gradient; - if (type === "radial") { - const radius = size / 2; - gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius); - } else { - const angle = (rotation * Math.PI) / 180; - const len = size / 2; - const dx = Math.cos(angle) * len; - const dy = Math.sin(angle) * len; - gradient = ctx.createLinearGradient(cx - dx, cy - dy, cx + dx, cy + dy); - } - - gradient.addColorStop(0, fromColor); - gradient.addColorStop(1, toColor); - - ctx.fillStyle = gradient; - ctx.fillRect(0, 0, size, size); -} - -function renderPreview() { - canvas.width = PREVIEW_SIZE; - canvas.height = PREVIEW_SIZE; - const ctx = canvas.getContext("2d"); - drawGradient(ctx, PREVIEW_SIZE); -} - -function download() { - const size = parseInt(imageSizeEl.value); - const offscreen = document.createElement("canvas"); - offscreen.width = size; - offscreen.height = size; - const ctx = offscreen.getContext("2d"); - drawGradient(ctx, size); - - const link = document.createElement("a"); - link.download = `gradient-${size}x${size}.png`; - link.href = offscreen.toDataURL("image/png"); - link.click(); -} - -fromColorEl.addEventListener("input", renderPreview); -toColorEl.addEventListener("input", renderPreview); -gradientTypeEl.addEventListener("input", renderPreview); -rotationEl.addEventListener("input", () => { - rotationValueEl.textContent = rotationEl.value; - renderPreview(); -}); -downloadBtn.addEventListener("click", download); - -renderPreview(); diff --git a/site/gradient-image/style.css b/site/gradient-image/style.css deleted file mode 100644 index bfe7c47..0000000 --- a/site/gradient-image/style.css +++ /dev/null @@ -1,22 +0,0 @@ -.controls { - display: flex; - flex-direction: column; -} - -.preview { - display: flex; - flex-direction: column; - align-items: center; - gap: 1rem; -} - -#preview-canvas { - width: 256px; - height: 256px; - border: 1px solid var(--pico-muted-border-color); - border-radius: 4px; -} - -#download-btn { - width: 100%; -} diff --git a/site/hex-color/index.html b/site/hex-color/index.html deleted file mode 100644 index 921b912..0000000 --- a/site/hex-color/index.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - Hex Color Converter - Tools - - - - - -
-
-

Hex Color Converter

-

Convert hex color values to normalized RGB components

-
-
-
- - - - - - - - - - - - - - - - -
- - diff --git a/site/hex-color/script.js b/site/hex-color/script.js deleted file mode 100644 index 07852ed..0000000 --- a/site/hex-color/script.js +++ /dev/null @@ -1,116 +0,0 @@ -async function addHexFromClipboard() { - try { - const text = await navigator.clipboard.readText(); - const hex = text.trim(); - - // Parse hex color - const color = parseHexColor(hex); - if (!color) { - alert('Invalid hex color in clipboard. Expected format: #RRGGBB or #RRGGBBAA'); - return; - } - - // Add row to table - addColorRow(hex, color); - - // Show table if hidden - document.getElementById('colorTable').classList.remove('hidden'); - } catch (err) { - alert('Failed to read from clipboard: ' + err.message); - } -} - -document.getElementById('addHexBtn').addEventListener('click', addHexFromClipboard); - -document.getElementById('showHiddenBtn').addEventListener('click', () => { - const rows = document.querySelectorAll('#colorTableBody tr.hidden'); - rows.forEach(row => row.classList.remove('hidden')); - updateShowHiddenButton(); -}); - -document.addEventListener('keydown', (e) => { - if (e.key === 'p' || e.key === 'P') { - addHexFromClipboard(); - } -}); - -function parseHexColor(hex) { - // Remove leading hash if present - const cleanHex = hex.startsWith('#') ? hex.slice(1) : hex; - - // Validate hex string (6 or 8 characters) - if (!/^[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/.test(cleanHex)) { - return null; - } - - // Parse components - const r = parseInt(cleanHex.slice(0, 2), 16) / 255; - const g = parseInt(cleanHex.slice(2, 4), 16) / 255; - const b = parseInt(cleanHex.slice(4, 6), 16) / 255; - const a = cleanHex.length === 8 ? parseInt(cleanHex.slice(6, 8), 16) / 255 : 1.0; - - return { r, g, b, a, hex: '#' + cleanHex }; -} - -function addColorRow(originalHex, color) { - const tbody = document.getElementById('colorTableBody'); - const row = tbody.insertRow(); - - // Preview cell - const previewCell = row.insertCell(); - const preview = document.createElement('div'); - preview.style.width = '40px'; - preview.style.height = '40px'; - preview.style.backgroundColor = `rgba(${color.r * 255}, ${color.g * 255}, ${color.b * 255}, ${color.a})`; - preview.style.border = '1px solid #ccc'; - preview.style.borderRadius = '4px'; - previewCell.appendChild(preview); - - // Hex cell - const hexCell = row.insertCell(); - hexCell.textContent = color.hex; - - // Normalized components cell - const normalizedCell = row.insertCell(); - const normalizedText = `${color.r.toFixed(1)}, ${color.g.toFixed(1)}, ${color.b.toFixed(1)}, ${color.a.toFixed(1)}`; - normalizedCell.textContent = normalizedText; - - // Action cell with copy and hide buttons - const actionCell = row.insertCell(); - - const copyBtn = document.createElement('button'); - copyBtn.textContent = 'Copy'; - copyBtn.className = 'secondary'; - copyBtn.addEventListener('click', () => { - navigator.clipboard.writeText(normalizedText).then(() => { - const originalText = copyBtn.textContent; - copyBtn.textContent = 'Copied!'; - setTimeout(() => { - copyBtn.textContent = originalText; - }, 1500); - }).catch(err => { - alert('Failed to copy: ' + err.message); - }); - }); - actionCell.appendChild(copyBtn); - - const hideBtn = document.createElement('button'); - hideBtn.textContent = 'Hide'; - hideBtn.className = 'secondary'; - hideBtn.addEventListener('click', () => { - row.classList.add('hidden'); - updateShowHiddenButton(); - }); - actionCell.appendChild(hideBtn); -} - -function updateShowHiddenButton() { - const hiddenRows = document.querySelectorAll('#colorTableBody tr.hidden'); - const showHiddenBtn = document.getElementById('showHiddenBtn'); - - if (hiddenRows.length > 0) { - showHiddenBtn.classList.remove('hidden'); - } else { - showHiddenBtn.classList.add('hidden'); - } -} diff --git a/site/hex-color/style.css b/site/hex-color/style.css deleted file mode 100644 index a5b0997..0000000 --- a/site/hex-color/style.css +++ /dev/null @@ -1,20 +0,0 @@ -.hidden { - display: none; -} - -#colorTable { - margin-top: 2rem; -} - -#colorTable button { - margin: 0; - margin-right: 0.5rem; -} - -#colorTable td { - vertical-align: middle; -} - -#showHiddenBtn { - margin-top: 1rem; -} diff --git a/site/image-inner-resize/index.html b/site/image-inner-resize/index.html deleted file mode 100644 index 521be20..0000000 --- a/site/image-inner-resize/index.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - Image Inner Resize - Tools - - - - -
-
-

Image Inner Resize

-

Scale an image within its original dimensions, centered on a transparent background

-
-
-
-
- - - - - -
-
- - -
-
- - - diff --git a/site/image-inner-resize/main.js b/site/image-inner-resize/main.js deleted file mode 100644 index a22fc52..0000000 --- a/site/image-inner-resize/main.js +++ /dev/null @@ -1,57 +0,0 @@ -const fileInput = document.getElementById("file-input"); -const scaleEl = document.getElementById("scale"); -const scaleValueEl = document.getElementById("scale-value"); -const canvas = document.getElementById("preview-canvas"); -const downloadBtn = document.getElementById("download-btn"); - -let sourceImage = null; - -function render() { - if (!sourceImage) return; - - const w = sourceImage.naturalWidth; - const h = sourceImage.naturalHeight; - const scale = parseInt(scaleEl.value) / 100; - - canvas.width = w; - canvas.height = h; - const ctx = canvas.getContext("2d"); - ctx.clearRect(0, 0, w, h); - - const sw = w * scale; - const sh = h * scale; - const sx = (w - sw) / 2; - const sy = (h - sh) / 2; - - ctx.drawImage(sourceImage, sx, sy, sw, sh); -} - -fileInput.addEventListener("change", () => { - const file = fileInput.files[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = (e) => { - const img = new Image(); - img.onload = () => { - sourceImage = img; - downloadBtn.disabled = false; - render(); - }; - img.src = e.target.result; - }; - reader.readAsDataURL(file); -}); - -scaleEl.addEventListener("input", () => { - scaleValueEl.textContent = scaleEl.value; - render(); -}); - -downloadBtn.addEventListener("click", () => { - if (!sourceImage) return; - const link = document.createElement("a"); - link.download = "resized.png"; - link.href = canvas.toDataURL("image/png"); - link.click(); -}); diff --git a/site/image-inner-resize/style.css b/site/image-inner-resize/style.css deleted file mode 100644 index dbaa0f5..0000000 --- a/site/image-inner-resize/style.css +++ /dev/null @@ -1,23 +0,0 @@ -.controls { - display: flex; - flex-direction: column; -} - -.preview { - display: flex; - flex-direction: column; - align-items: center; - gap: 1rem; -} - -#preview-canvas { - max-width: 100%; - max-height: 512px; - border: 1px solid var(--pico-muted-border-color); - border-radius: 4px; - background: repeating-conic-gradient(#eee 0% 25%, #fff 0% 50%) 50% / 16px 16px; -} - -#download-btn { - width: 100%; -} diff --git a/site/index.html b/site/index.html index 437258e..c71b5cc 100644 --- a/site/index.html +++ b/site/index.html @@ -24,16 +24,6 @@
  • Gradient Bands
  • Two-letter Country Codes
  • Timestamp Converter
  • -
  • Generic Scorecard - 2 Players
  • -
  • Generic Scorecard - 4 Players
  • -
  • Mahjong Scorecard
  • -
  • Finska Scorecard
  • -
  • Mental Arithmatic Game
  • -
  • Neon Snake: vibe-coded by Google Gemini
  • -
  • Hex Color Converter
  • -
  • Android Icon Resizer
  • -
  • Gradient Image
  • -
  • Image Inner Resize
  • diff --git a/site/mahjong/guide.html b/site/mahjong/guide.html index 3133de2..43ceeb7 100644 --- a/site/mahjong/guide.html +++ b/site/mahjong/guide.html @@ -32,92 +32,66 @@ m Mahjong - 20 ps Pung of simples - 4 pt Pung of terminals - 8 ph Pung of honours - 8 xps Exposed pung of simples - 2 xpt Exposed pung of terminals - 4 xph Exposed pung of honours - 4 ks Kong of simples - 16 kt Kong of terminals - 32 kh Kong of honours - 32 xks Exposed kong of simples - 8 xkt Exposed kong of terminals - 16 xkh Exposed kong of honours - 16 pd Pair of dragons - 2 pw Pair of winds (either player or prevailing) - 2 b Bonus (flower or season) - 2 - - - p - Point (used for adjusting scores) - 1 - - - d - Penalty (used for adjusting scores) - -1 diff --git a/site/mahjong/index.html b/site/mahjong/index.html index 3ac7778..e1dce3f 100644 --- a/site/mahjong/index.html +++ b/site/mahjong/index.html @@ -78,21 +78,15 @@
    +

    +
    -
    -
    -
    -
    +
    -
    -
    - Notation Guide -
    - diff --git a/site/mahjong/main.js b/site/mahjong/main.js index 584bcec..f9ba03c 100644 --- a/site/mahjong/main.js +++ b/site/mahjong/main.js @@ -2,10 +2,13 @@ import { Application, Controller } from "https://unpkg.com/@hotwired/stimulus/di import { GameState, getGameState, setGameState, calculateScore } from "./models.js"; function emitGameStateEvent(details) { - let ev = new CustomEvent("gamestatechanged", { detail: details }); - window.dispatchEvent(ev); + // window.setTimeout(() => { + let ev = new CustomEvent("gamestatechanged", { detail: details }); + window.dispatchEvent(ev); + // }, 1); } + window.Stimulus = Application.start(); Stimulus.register("scorecard", class extends Controller { @@ -15,6 +18,8 @@ Stimulus.register("scorecard", class extends Controller { } connect() { + // this._rebuildTable(); + // this.element.classList.remove("hidden"); } handleGameState(ev) { @@ -70,8 +75,8 @@ Stimulus.register("scorecard", class extends Controller { let td = document.createElement("td"); td.textContent = c.s; if (c.w) { - td.textContent += " 🀄"; td.classList.add("winner"); + td.textContent += " 🏆"; } tr.append(td); } @@ -89,7 +94,7 @@ Stimulus.register("scorecard", class extends Controller { this.element.querySelector("table.scorecard").replaceWith(tbl); - this.prevailingTarget.textContent = `Prevailing: ${gameState.prevailing}`; + this.prevailingTarget.textContent = `Prevailing: ${gameState.prevaling}`; } }); @@ -97,6 +102,7 @@ Stimulus.register("newgame", class extends Controller { static targets = ["playerName"]; connect() { + this.element.classList.remove("hidden"); } handleGameState(ev) { @@ -127,9 +133,10 @@ Stimulus.register("newgame", class extends Controller { }); Stimulus.register("endround", class extends Controller { - static targets = ["form", "input", "wind", "prevailing"]; + static targets = ["form", "input", "wind"]; connect() { + // this.element.classList.remove("hidden"); } handleGameState(ev) { @@ -141,7 +148,6 @@ Stimulus.register("endround", class extends Controller { break; case "endround": this._prepForms(); - this._updatePreview(); this.element.classList.remove("hidden"); break; } @@ -149,10 +155,7 @@ Stimulus.register("endround", class extends Controller { updatePreview(ev) { ev.preventDefault(); - this._updatePreview(); - } - _updatePreview() { let scoreExprs = this._readScoreExprs(); let nextRound = getGameState().determineNextRound(scoreExprs); @@ -160,23 +163,15 @@ Stimulus.register("endround", class extends Controller { let previewElem = this.element.querySelector(`.preview[data-player="${i}"]`); previewElem.textContent = nextRound.roundScores[i].score; if (parseInt(i) === nextRound.roundWinner) { - previewElem.classList.add("winner"); - previewElem.textContent += " 🀄"; - } else { - previewElem.classList.remove("winner"); + previewElem.textContent += " 🏆"; } } if (nextRound.windsBump) { - this.windTarget.textContent = ` will be East next round`; + this.windTarget.textContent = `${getGameState().players[nextRound.nextRoundEast].name} will be East next round`; } else { - this.windTarget.textContent = ` will remain East next round`; + this.windTarget.textContent = `${getGameState().players[nextRound.nextRoundEast].name} will remain East next round`; } - let nextWindNameElem = document.createElement("strong"); - nextWindNameElem.textContent = getGameState().players[nextRound.nextRoundEast].name; - this.windTarget.prepend(nextWindNameElem); - - this.prevailingTarget.textContent = `Next prevailing: ${nextRound.nextPrevailing}`; } goBack(ev) { @@ -229,13 +224,4 @@ Stimulus.register("endround", class extends Controller { } return scoreExprs; } -}); - -document.addEventListener("DOMContentLoaded", () => { - let game = getGameState(); - if (game !== null) { - emitGameStateEvent({mode: "startgame"}); - } else { - emitGameStateEvent({mode: "newgame"}); - } -}) \ No newline at end of file +}); \ No newline at end of file diff --git a/site/mahjong/models.js b/site/mahjong/models.js index 6789bc4..48d1762 100644 --- a/site/mahjong/models.js +++ b/site/mahjong/models.js @@ -7,24 +7,22 @@ const windDistributions = [ ]; const scoreTokens = { - 'm': 20, - 'ps': 4, - 'pt': 8, - 'ph': 8, - 'xps': 2, - 'xpt': 4, - 'xph': 4, - 'ks': 16, - 'kt': 32, - 'kh': 32, - 'xks': 8, - 'xkt': 16, - 'xkh': 16, - 'pd': 2, - 'pw': 2, - 'b': 2, - 'p': 1, - 'd': -1, + 'm': 1, + 'ps': 1, + 'pt': 1, + 'ph': 1, + 'xps': 1, + 'xpt': 1, + 'xph': 1, + 'ks': 1, + 'kt': 1, + 'kh': 1, + 'xks': 1, + 'xkt': 1, + 'xkh': 1, + 'pd': 1, + 'pw': 1, + 'b': 1, } function parseTokens(str) { @@ -69,7 +67,7 @@ export class GameState { this.players = players; this.rounds = rounds; this.bumpWind = 0; - this.prevailing = "E"; + this.prevaling = "E"; } static newGame(playerNames) { @@ -80,21 +78,7 @@ export class GameState { } let rounds = []; - let gs = new GameState(players, rounds); - gs.save(); - return gs; - } - - static load() { - let json = localStorage.getItem('mahjong-scorecard'); - if (!json) { - return null; - } - let o = JSON.parse(json); - let gs = new GameState(o.p, o.r); - gs.bumpWind = o.b; - gs.prevailing = o.w; - return gs; + return new GameState(players, rounds); } determineNextRound(playerScoreExprs) { @@ -108,7 +92,7 @@ export class GameState { } let windsBump = roundWinner !== currentEast; - let nextPrevailing = this.prevailing; + let nextPrevailing = this.prevaling; if (windsBump) { nextPrevailing = windDistributions[3][((this.bumpWind + 1) / this.players.length)|0]; } @@ -149,24 +133,14 @@ export class GameState { this.players[pi].wind = windDistribution[i]; } - this.prevailing = nr.nextPrevailing; + this.prevaling = nr.nextPrevailing; if (nr.windsBump) { this.bumpWind++; } - this.save(); - } - - save() { - localStorage.setItem('mahjong-scorecard', JSON.stringify({ - p: this.players, - r: this.rounds, - b: this.bumpWind, - w: this.prevailing, - })); } } -let gameState = GameState.load(); +let gameState = new GameState(); export function getGameState() { return gameState; diff --git a/site/mahjong/style.css b/site/mahjong/style.css index 4f1eed1..1940ccb 100644 --- a/site/mahjong/style.css +++ b/site/mahjong/style.css @@ -1,7 +1,3 @@ -.container { - margin-block-end: 20px; -} - .hidden { display: none; } @@ -22,41 +18,4 @@ gap: 10px; justify-content: space-between; align-items: center; - margin-block-end: 12px; -} - -@media (max-width: 600px) { - .btns { - flex-direction: column; - gap: 20px; - } - - .btns :nth-child(1) { - align-self: start; - } - - .btns :nth-child(2) { - align-self: end; - } -} - -.winner { - color: #9B2318; - font-weight: bold; -} - - -th { - background: #E2E2E2; -} - -@media (prefers-color-scheme: dark) { - th { - background: #474747; - } - - .winner { - color: #F06048; - font-weight: bold; - } } \ No newline at end of file diff --git a/site/mental-arithmatic/index.html b/site/mental-arithmatic/index.html deleted file mode 100644 index 16c4321..0000000 --- a/site/mental-arithmatic/index.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - Mental Arithmatic - Tools - - - - - -
    -
    -
    -

    Fear of All Sums

    -

    A simple mental arithmetic game

    -
    -
    - -
    -

    Simple Sums

    -

    A two number sum with each term up to two digits.

    - -
    -
    -
    -
    - - - - \ No newline at end of file diff --git a/site/mental-arithmatic/scripts/countdown.js b/site/mental-arithmatic/scripts/countdown.js deleted file mode 100644 index dbdb79e..0000000 --- a/site/mental-arithmatic/scripts/countdown.js +++ /dev/null @@ -1,35 +0,0 @@ -import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"; - -export class CountdownController extends Controller { - static targets = ["countdown", "fakeinput"]; - - start() { - this.element.classList.remove('hidden'); - - this._countdown = 3; - this.countdownTarget.innerText = this._countdown; - - this._tickInterval = window.setInterval(this._tick.bind(this), 1000); - window.setTimeout(() => { - this.fakeinputTarget.focus(); - }, 1); - } - - _tick() { - this._countdown -= 1; - - if (this._countdown === 0) { - this.countdownTarget.innerText = "GO!"; - } else if (this._countdown < 0) { - window.clearInterval(this._tickInterval); - this._startActualGame(); - } else { - this.countdownTarget.innerText = this._countdown; - } - } - - _startActualGame() { - this.element.classList.add('hidden'); - window.dispatchEvent(new CustomEvent("startGame")); - } -} \ No newline at end of file diff --git a/site/mental-arithmatic/scripts/game.js b/site/mental-arithmatic/scripts/game.js deleted file mode 100644 index fe495b9..0000000 --- a/site/mental-arithmatic/scripts/game.js +++ /dev/null @@ -1,103 +0,0 @@ -import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"; - -export class GameController extends Controller { - static targets = [ - "clock", - "score", - "problem", - "indicators", - "answer", - ]; - - start() { - this.startGame(); - this.element.classList.remove('hidden'); - } - - submitAnswer(ev) { - ev.preventDefault(); - - this._checkAnswer(); - } - - startGame() { - this._clock = 120; - this._score = 0; - - this._generateProblem(); - this._startClock(); - - this._updateClock(); - } - - _startClock() { - this._clockInterval = window.setInterval(() => { - if (this._clock <= 0) { - this._gameOver(); - return; - } - - this._clock -= 1; - this._updateClock(); - }, 1000); - } - - _gameOver() { - window.clearInterval(this._clockInterval); - - this.element.classList.add('hidden'); - window.dispatchEvent(new CustomEvent("endGame")); - } - - _generateProblem() { - const num1 = Math.floor(Math.random() * 99) + 1; - const num2 = Math.floor(Math.random() * 99) + 1; - - this._problem = [num1, num2]; - this._answer = num1 + num2; - - this.indicatorsTarget.classList.remove('right', 'wrong'); - this.problemTarget.innerHTML = ''; - - for (let i = 0; i < this._problem.length; i++) { - const div = document.createElement('div'); - - if (i === this._problem.length - 1) { - div.textContent = '+ ' + this._problem[i]; - } else { - div.textContent = this._problem[i]; - } - this.problemTarget.appendChild(div); - } - - this.answerTarget.value = ""; - - window.setTimeout(() => { - this.answerTarget.focus(); - }, 1); - } - - _checkAnswer() { - let isRight = parseInt(this.answerTarget.value) === this._answer; - let delay = 500; - - if (isRight) { - this._score += 1; - this.indicatorsTarget.classList.add('right'); - } else { - this.indicatorsTarget.classList.add('wrong'); - this.answerTarget.value = this._answer; - delay = 800; - } - - this.scoreTarget.textContent = this._score; - - window.setTimeout(() => { this._generateProblem(); }, delay); - } - - _updateClock() { - let m = Math.floor(this._clock / 60); - let s = this._clock % 60; - this.clockTarget.textContent = `${m}:${s < 10 ? '0' : ''}${s}`; - } -} \ No newline at end of file diff --git a/site/mental-arithmatic/scripts/main.js b/site/mental-arithmatic/scripts/main.js deleted file mode 100644 index 05b427d..0000000 --- a/site/mental-arithmatic/scripts/main.js +++ /dev/null @@ -1,10 +0,0 @@ -import { Application } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"; -import { WelcomeController } from "./welcome.js" -import { CountdownController } from "./countdown.js" -import { GameController } from "./game.js" - -window.Stimulus = Application.start(); - -window.Stimulus.register('welcome', WelcomeController); -window.Stimulus.register('countdown', CountdownController); -window.Stimulus.register('game', GameController); \ No newline at end of file diff --git a/site/mental-arithmatic/scripts/welcome.js b/site/mental-arithmatic/scripts/welcome.js deleted file mode 100644 index 9fc549e..0000000 --- a/site/mental-arithmatic/scripts/welcome.js +++ /dev/null @@ -1,35 +0,0 @@ -import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"; - -export class WelcomeController extends Controller { - static targets = ["simpleHighScores"]; - - connect() { - this._buildHighScores(); - } - - startGame(ev) { - ev.preventDefault(); - - this.element.classList.add('hidden'); - window.dispatchEvent(new CustomEvent("startCountdown")); - } - - gameEnded(ev) { - this.element.classList.remove('hidden'); - } - - _buildHighScores() { - let scores = { - last: 12, - high: 10, - streak: 3 - }; - - let newEls = [ - `🏆 ${scores.last}`, - `đŸ”Ĩ ${scores.streak}`, - `â†Šī¸ ${scores.high}` - ]; - this.simpleHighScoresTarget.innerHTML = newEls.join(''); - } -} \ No newline at end of file diff --git a/site/mental-arithmatic/style.css b/site/mental-arithmatic/style.css deleted file mode 100644 index ce22cc0..0000000 --- a/site/mental-arithmatic/style.css +++ /dev/null @@ -1,122 +0,0 @@ -.hidden { - display: none !important; -} - -body { - --pico-form-element-disabled-opacity: 1.0; -} - -.game-card { - display: flex; - flex-direction: column; - min-height: 50vh; -} - -.welcome-card .game-variant footer { - display: flex; - align-items: center; - justify-content: space-between; -} - -.welcome-card .game-variant .high-scores { - display: flex; - gap: 10px; -} - -.countdown-timer { - font-size: 3em; - text-align: center; - width: 100%; -} - -input.fake { - position: fixed; - top: -600px; -} - -.game-card header, -.game-card footer { - display: flex; - justify-content: space-between; - - font-size: 2em; - margin-block: 12px; - margin-inline: 20px; -} - -.game-card main { - flex-grow: 1; - flex-shrink: 1; - - display: flex; - align-items: center; - - padding-block-start: 40px; -} - -.game-card .question { - margin: auto; - width: 60vw; - - text-align: right; - font-size: 3em; -} - -.game-card .answer { - border-block: 2px solid black; - padding-block: 2px; - position: relative; -} - -.game-card .answer.right { - border-color: #4EB31B; - color: #4EB31B; -} - -.game-card .answer.wrong { - border-color: #EE402E; - color: #EE402E; -} - -.game-card .answer .indicator { - display: none; - position: absolute; - top: 0; - bottom: 0; - left: 0; -} - -.game-card .answer.right .indicator.right { - display: block; -} - -.game-card .answer.wrong .indicator.wrong { - display: block; -} - -.game-card .question input { - border-inline: none; - border-radius: 0; - box-shadow: none; - - border: none; - - margin: 0; - padding: 0; - - text-align: right; - font-size: 1em; - height: 1em; -} - -.game-card .question input:focus { - border-none: none; -} - -.game-card .answer.right input { - color: green; -} - -.game-card .answer.wrong input { - color: red; -} \ No newline at end of file diff --git a/site/neon-snake/index.html b/site/neon-snake/index.html deleted file mode 100644 index 4537aa1..0000000 --- a/site/neon-snake/index.html +++ /dev/null @@ -1,451 +0,0 @@ - - - - - - Neon Snake - - - - -
    - -
    - - -
    -
    - SCORE: 000 - HI: 000 -
    -
    - -
    -

    NEON SNAKE

    -

    USE ARROW KEYS OR SWIPE

    - -
    - - -
    - -
    Desktop: Arrows/WASD/IJKL | Mobile: Swipe
    - - - - \ No newline at end of file diff --git a/site/scorecard-2p/index.html b/site/scorecard-2p/index.html deleted file mode 100644 index 0bf3b74..0000000 --- a/site/scorecard-2p/index.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - Score Card - Tools - - - - - -
    -
    -

    Scorecard

    -

    2 Players

    -
    -
    - - -
    - - - - - - - - - - - - - - - - - -
    Player APlayer B
    -
    - -
    -
    -
    - -
    -
    - -
    - - -
    -
    - - \ No newline at end of file diff --git a/site/scorecard-2p/main.js b/site/scorecard-2p/main.js deleted file mode 100644 index e69de29..0000000 diff --git a/site/scorecard-2p/scripts/controllers.js b/site/scorecard-2p/scripts/controllers.js deleted file mode 100644 index d06c0a5..0000000 --- a/site/scorecard-2p/scripts/controllers.js +++ /dev/null @@ -1,137 +0,0 @@ -import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"; -import { Scorecard, getStoreDAO } from "./models.js"; - -const storeDao = getStoreDAO(); - -export class FinskaScorecardController extends Controller { - static targets = ["score1Input", "score2Input", "scoreTable"]; - - connect() { - this._undoStack = []; - this._scorecard = storeDao.loadOrCreate(); - this.updateTable(); - } - - updateTable() { - let tableBody = this.scoreTableTarget; - let tableRows = tableBody.querySelectorAll("tr"); - let pairs = this._scorecard.pairs(); - - for (let pairIndex = 0; pairIndex < pairs.length; pairIndex++) { - let tableRow; - if (pairIndex >= tableRows.length) { - tableRow = this._appendRow(); - } else { - tableRow = tableRows[pairIndex]; - } - - this._updateRow(tableRow, pairs[pairIndex]) - } - - // Remove any extra rows - for (let i = pairs.length; i < tableRows.length; i++) { - tableBody.removeChild(tableRows[i]); - } - - console.log(JSON.stringify(this._scorecard.toJson())); - } - - _updateRow(tableRow, pair) { - let tds = tableRow.querySelectorAll("td"); - - this._updateCell(pair.p1, tds[0], tds[1]); - this._updateCell(pair.p2, tds[2], tds[3]); - } - - _updateCell(score, scoreCell, totalCell) { - scoreCell.classList.value = ""; - totalCell.classList.value = ""; - - if (score != null) { - scoreCell.textContent = score.score; - totalCell.textContent = score.total; - } else { - scoreCell.textContent = ""; - totalCell.textContent = ""; - } - } - - _appendRow() { - let newRow = document.createElement("tr"); - newRow.classList.add("score-entry"); - - for (let i = 0; i < 4; i++) { - newRow.appendChild(document.createElement("td")); - } - - this.scoreTableTarget.appendChild(newRow); - return newRow; - } - - addScore1() { - this._addScore(this.score1InputTarget, this.score2InputTarget, - this._scorecard.addPlayer1Score.bind(this._scorecard), - this._scorecard.removeLastPlayer1Score.bind(this._scorecard)); - } - - addScore2() { - this._addScore(this.score2InputTarget, this.score1InputTarget, - this._scorecard.addPlayer2Score.bind(this._scorecard), - this._scorecard.removeLastPlayer2Score.bind(this._scorecard)); - - } - - _addScore(inputElem, focusToInputElem, addScoreFn, queueUndoFn) { - let score = parseInt(inputElem.value); - if (isNaN(score)) { - score = 0; - } - - addScoreFn(score); - this._undoStack.push(queueUndoFn); - - inputElem.value = ""; - - storeDao.save(this._scorecard); - this.updateTable(); - - focusToInputElem.focus(); - } - - score1KeyDown(e) { - this._handleKeyDown(e, this.addScore1.bind(this)); - } - - score2KeyDown(e) { - this._handleKeyDown(e, this.addScore2.bind(this)); - } - - _handleKeyDown(e, addScoreFn) { - if (e.key === "Enter") { - e.preventDefault(); - addScoreFn(); - } - } - - undoLast() { - if (this._undoStack.length === 0) { - return; - } - - (this._undoStack.pop())(); - - storeDao.save(this._scorecard); - this.updateTable(); - } - - resetAll() { - if (!confirm("Really reset?")) { - return; - } - - this._scorecard.reset(); - storeDao.clear(); - this._undoStack = []; - this.updateTable(); - } -} diff --git a/site/scorecard-2p/scripts/main.js b/site/scorecard-2p/scripts/main.js deleted file mode 100644 index 34ece68..0000000 --- a/site/scorecard-2p/scripts/main.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Application } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"; -import { FinskaScorecardController } from "./controllers.js" - -window.Stimulus = Application.start(); - -window.Stimulus.register('finska-scorecard', FinskaScorecardController); - diff --git a/site/scorecard-2p/scripts/models.js b/site/scorecard-2p/scripts/models.js deleted file mode 100644 index 64cd910..0000000 --- a/site/scorecard-2p/scripts/models.js +++ /dev/null @@ -1,133 +0,0 @@ -export class ScoreEntry { - constructor(score, total) { - this.score = score; - this.total = total; - } -}; - -export class Scorecard { - - constructor() { - this.reset(); - } - - reset() { - this._player1Scores = []; - this._player2Scores = []; - } - - addPlayer1Score(newScore) { - this._addScore(this._player1Scores, newScore); - } - - removeLastPlayer1Score() { - this._player1Scores.pop(); - } - - addPlayer2Score(newScore) { - this._addScore(this._player2Scores, newScore); - } - - removeLastPlayer2Score() { - this._player2Scores.pop(); - } - - _addScore(playerScores, newScore) { - let lastEntry; - if (playerScores.length === 0) { - lastEntry = new ScoreEntry(0, 0, 0, false, false); - } else { - lastEntry = playerScores[playerScores.length - 1]; - } - - let newEntry = this._newEntryFromPrevious(newScore, lastEntry); - - playerScores.push(newEntry); - } - - length() { - return Math.max(this._player1Scores.length, this._player2Scores.length); - } - - pairs() { - let pairs = []; - - for (let i = 0; i < this.length(); i++) { - pairs.push({ - p1: (i < this._player1Scores.length ? this._player1Scores[i] : null), - p2: (i < this._player2Scores.length ? this._player2Scores[i] : null), - }) - } - - return pairs; - } - - _newEntryFromPrevious(score, previousScoreEntry) { - if (previousScoreEntry === null) { - return new ScoreEntry(score, score, (score === 0 ? 1 : 0)); - } - - let newTotal = previousScoreEntry.total + score; - return new ScoreEntry(score, newTotal); - } - - toJson() { - return { - "version": 1, - "p1": { "scores": this._player1Scores.map(p => p.score) }, - "p2": { "scores": this._player2Scores.map(p => p.score) }, - }; - } - - static fromJson(o) { - let scorecard = new Scorecard(); - o["p1"]["scores"].forEach(x => scorecard.addPlayer1Score(x)); - o["p2"]["scores"].forEach(x => scorecard.addPlayer2Score(x)); - return scorecard; - } -} - -class StoreDAO { - - constructor(localStorage) { - this._localStorage = localStorage; - } - - save(scoreCard) { - this._localStorage.setItem('generic-scorecard-2p', JSON.stringify(scoreCard.toJson())); - } - - loadOrCreate() { - try { - console.log("Loading scorecard"); - let scoreCardJson = this._localStorage.getItem('generic-scorecard-2p'); - if (scoreCardJson !== null) { - return Scorecard.fromJson(JSON.parse(scoreCardJson)); - } - } catch (e) { - console.log(`Could not restore game: ${e}`); - } - - return new Scorecard(); - } - - clear() { - this._localStorage.removeItem('generic-scorecard-2p'); - } -} - -class DummyStoreDAO { - save(scoreCard) { } - loadOrCreate() { - return new Scorecard(); - } - clear() { } -} - -export function getStoreDAO() { - if (!!window.localStorage) { - return new StoreDAO(window.localStorage); - } else { - return new DummyStoreDAO(); - } -} \ No newline at end of file diff --git a/site/scorecard-2p/style.css b/site/scorecard-2p/style.css deleted file mode 100644 index e2cd398..0000000 --- a/site/scorecard-2p/style.css +++ /dev/null @@ -1,21 +0,0 @@ -tfoot input { - border-width: 1px; -} - -.score-foul { - color: #861D13; - background-color: #F8DCD6; - font-weight: bold; -} - -.score-overflow { - color: #5B4200; - background-color: #FCEFD9; - font-weight: bold; -} - -.score-win { - color: #394D00; - background-color: #DEFC85; - font-weight: bold; -} \ No newline at end of file diff --git a/site/scorecard-4p/index.html b/site/scorecard-4p/index.html deleted file mode 100644 index d495e38..0000000 --- a/site/scorecard-4p/index.html +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - Score Card - Tools - - - - - -
    -
    -

    Scorecard

    -

    4 Players

    -
    -
    - - -
    - - - - - - - - - - - - - - - - - - - - - - - -
    Player APlayer BPlayer CPlayer D
    -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    - -
    - - -
    -
    - - \ No newline at end of file diff --git a/site/scorecard-4p/scripts/controllers.js b/site/scorecard-4p/scripts/controllers.js deleted file mode 100644 index 5159f4f..0000000 --- a/site/scorecard-4p/scripts/controllers.js +++ /dev/null @@ -1,158 +0,0 @@ -import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"; -import { Scorecard, getStoreDAO } from "./models.js"; - -const storeDao = getStoreDAO(); - -export class FinskaScorecardController extends Controller { - static targets = ["score1Input", "score2Input", "score3Input", "score4Input", "scoreTable"]; - - connect() { - this._undoStack = []; - this._scorecard = storeDao.loadOrCreate(); - this.updateTable(); - } - - updateTable() { - let tableBody = this.scoreTableTarget; - let tableRows = tableBody.querySelectorAll("tr"); - let pairs = this._scorecard.pairs(); - - for (let pairIndex = 0; pairIndex < pairs.length; pairIndex++) { - let tableRow; - if (pairIndex >= tableRows.length) { - tableRow = this._appendRow(); - } else { - tableRow = tableRows[pairIndex]; - } - - this._updateRow(tableRow, pairs[pairIndex]) - } - - // Remove any extra rows - for (let i = pairs.length; i < tableRows.length; i++) { - tableBody.removeChild(tableRows[i]); - } - - console.log(JSON.stringify(this._scorecard.toJson())); - } - - _updateRow(tableRow, pair) { - let tds = tableRow.querySelectorAll("td"); - - this._updateCell(pair.p1, tds[0], tds[1]); - this._updateCell(pair.p2, tds[2], tds[3]); - this._updateCell(pair.p3, tds[4], tds[5]); - this._updateCell(pair.p4, tds[6], tds[7]); - } - - _updateCell(score, scoreCell, totalCell) { - scoreCell.classList.value = ""; - totalCell.classList.value = ""; - - if (score != null) { - scoreCell.textContent = score.score; - totalCell.textContent = score.total; - } else { - scoreCell.textContent = ""; - totalCell.textContent = ""; - } - } - - _appendRow() { - let newRow = document.createElement("tr"); - newRow.classList.add("score-entry"); - - for (let i = 0; i < 8; i++) { - newRow.appendChild(document.createElement("td")); - } - - this.scoreTableTarget.appendChild(newRow); - return newRow; - } - - addScore1() { - this._addScore(this.score1InputTarget, this.score2InputTarget, - this._scorecard.addPlayer1Score.bind(this._scorecard), - this._scorecard.removeLastPlayer1Score.bind(this._scorecard)); - } - - addScore2() { - this._addScore(this.score2InputTarget, this.score3InputTarget, - this._scorecard.addPlayer2Score.bind(this._scorecard), - this._scorecard.removeLastPlayer2Score.bind(this._scorecard)); - } - - addScore3() { - this._addScore(this.score3InputTarget, this.score4InputTarget, - this._scorecard.addPlayer3Score.bind(this._scorecard), - this._scorecard.removeLastPlayer3Score.bind(this._scorecard)); - } - - addScore4() { - this._addScore(this.score4InputTarget, this.score1InputTarget, - this._scorecard.addPlayer4Score.bind(this._scorecard), - this._scorecard.removeLastPlayer4Score.bind(this._scorecard)); - } - - _addScore(inputElem, focusToInputElem, addScoreFn, queueUndoFn) { - let score = parseInt(inputElem.value); - if (isNaN(score)) { - score = 0; - } - - addScoreFn(score); - this._undoStack.push(queueUndoFn); - - inputElem.value = ""; - - storeDao.save(this._scorecard); - this.updateTable(); - - focusToInputElem.focus(); - } - - score1KeyDown(e) { - this._handleKeyDown(e, this.addScore1.bind(this)); - } - - score2KeyDown(e) { - this._handleKeyDown(e, this.addScore2.bind(this)); - } - - score3KeyDown(e) { - this._handleKeyDown(e, this.addScore3.bind(this)); - } - - score4KeyDown(e) { - this._handleKeyDown(e, this.addScore4.bind(this)); - } - - _handleKeyDown(e, addScoreFn) { - if (e.key === "Enter") { - e.preventDefault(); - addScoreFn(); - } - } - - undoLast() { - if (this._undoStack.length === 0) { - return; - } - - (this._undoStack.pop())(); - - storeDao.save(this._scorecard); - this.updateTable(); - } - - resetAll() { - if (!confirm("Really reset?")) { - return; - } - - this._scorecard.reset(); - storeDao.clear(); - this._undoStack = []; - this.updateTable(); - } -} diff --git a/site/scorecard-4p/scripts/main.js b/site/scorecard-4p/scripts/main.js deleted file mode 100644 index 34ece68..0000000 --- a/site/scorecard-4p/scripts/main.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Application } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"; -import { FinskaScorecardController } from "./controllers.js" - -window.Stimulus = Application.start(); - -window.Stimulus.register('finska-scorecard', FinskaScorecardController); - diff --git a/site/scorecard-4p/scripts/models.js b/site/scorecard-4p/scripts/models.js deleted file mode 100644 index 2472177..0000000 --- a/site/scorecard-4p/scripts/models.js +++ /dev/null @@ -1,158 +0,0 @@ -export class ScoreEntry { - constructor(score, total) { - this.score = score; - this.total = total; - } -}; - -export class Scorecard { - - constructor() { - this.reset(); - } - - reset() { - this._player1Scores = []; - this._player2Scores = []; - this._player3Scores = []; - this._player4Scores = []; - } - - addPlayer1Score(newScore) { - this._addScore(this._player1Scores, newScore); - } - - removeLastPlayer1Score() { - this._player1Scores.pop(); - } - - addPlayer2Score(newScore) { - this._addScore(this._player2Scores, newScore); - } - - removeLastPlayer2Score() { - this._player2Scores.pop(); - } - - addPlayer3Score(newScore) { - this._addScore(this._player3Scores, newScore); - } - - removeLastPlayer3Score() { - this._player3Scores.pop(); - } - - addPlayer4Score(newScore) { - this._addScore(this._player4Scores, newScore); - } - - removeLastPlayer4Score() { - this._player4Scores.pop(); - } - - _addScore(playerScores, newScore) { - let lastEntry; - if (playerScores.length === 0) { - lastEntry = new ScoreEntry(0, 0, 0, false, false); - } else { - lastEntry = playerScores[playerScores.length - 1]; - } - - let newEntry = this._newEntryFromPrevious(newScore, lastEntry); - - playerScores.push(newEntry); - } - - length() { - return Math.max(this._player1Scores.length, this._player2Scores.length, - this._player3Scores.length, this._player4Scores.length); - } - - pairs() { - let pairs = []; - - for (let i = 0; i < this.length(); i++) { - pairs.push({ - p1: (i < this._player1Scores.length ? this._player1Scores[i] : null), - p2: (i < this._player2Scores.length ? this._player2Scores[i] : null), - p3: (i < this._player3Scores.length ? this._player3Scores[i] : null), - p4: (i < this._player4Scores.length ? this._player4Scores[i] : null), - }) - } - - return pairs; - } - - _newEntryFromPrevious(score, previousScoreEntry) { - if (previousScoreEntry === null) { - return new ScoreEntry(score, score, (score === 0 ? 1 : 0)); - } - - let newTotal = previousScoreEntry.total + score; - return new ScoreEntry(score, newTotal); - } - - toJson() { - return { - "version": 1, - "p1": { "scores": this._player1Scores.map(p => p.score) }, - "p2": { "scores": this._player2Scores.map(p => p.score) }, - "p3": { "scores": this._player3Scores.map(p => p.score) }, - "p4": { "scores": this._player4Scores.map(p => p.score) }, - }; - } - - static fromJson(o) { - let scorecard = new Scorecard(); - o["p1"]["scores"].forEach(x => scorecard.addPlayer1Score(x)); - o["p2"]["scores"].forEach(x => scorecard.addPlayer2Score(x)); - if (o["p3"]) o["p3"]["scores"].forEach(x => scorecard.addPlayer3Score(x)); - if (o["p4"]) o["p4"]["scores"].forEach(x => scorecard.addPlayer4Score(x)); - return scorecard; - } -} - -class StoreDAO { - - constructor(localStorage) { - this._localStorage = localStorage; - } - - save(scoreCard) { - this._localStorage.setItem('generic-scorecard-4p', JSON.stringify(scoreCard.toJson())); - } - - loadOrCreate() { - try { - console.log("Loading scorecard"); - let scoreCardJson = this._localStorage.getItem('generic-scorecard-4p'); - if (scoreCardJson !== null) { - return Scorecard.fromJson(JSON.parse(scoreCardJson)); - } - } catch (e) { - console.log(`Could not restore game: ${e}`); - } - - return new Scorecard(); - } - - clear() { - this._localStorage.removeItem('generic-scorecard-4p'); - } -} - -class DummyStoreDAO { - save(scoreCard) { } - loadOrCreate() { - return new Scorecard(); - } - clear() { } -} - -export function getStoreDAO() { - if (!!window.localStorage) { - return new StoreDAO(window.localStorage); - } else { - return new DummyStoreDAO(); - } -} \ No newline at end of file diff --git a/site/scorecard-4p/style.css b/site/scorecard-4p/style.css deleted file mode 100644 index e2cd398..0000000 --- a/site/scorecard-4p/style.css +++ /dev/null @@ -1,21 +0,0 @@ -tfoot input { - border-width: 1px; -} - -.score-foul { - color: #861D13; - background-color: #F8DCD6; - font-weight: bold; -} - -.score-overflow { - color: #5B4200; - background-color: #FCEFD9; - font-weight: bold; -} - -.score-win { - color: #394D00; - background-color: #DEFC85; - font-weight: bold; -} \ No newline at end of file