commit a2fcc6ba953f7a584accb0d5ab1d146c9973a129 Author: Victor Westerlund Date: Thu Mar 13 15:20:35 2025 +0100 initial commit diff --git a/assets/media/coffee.svg b/assets/media/coffee.svg new file mode 100644 index 0000000..473a183 --- /dev/null +++ b/assets/media/coffee.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/media/icon.png b/assets/media/icon.png new file mode 100644 index 0000000..834d11f Binary files /dev/null and b/assets/media/icon.png differ diff --git a/assets/media/icon512_maskable.png b/assets/media/icon512_maskable.png new file mode 100644 index 0000000..ea0373e Binary files /dev/null and b/assets/media/icon512_maskable.png differ diff --git a/assets/media/icon512_rounded.png b/assets/media/icon512_rounded.png new file mode 100644 index 0000000..37b9d3a Binary files /dev/null and b/assets/media/icon512_rounded.png differ diff --git a/assets/media/sfx.m4a b/assets/media/sfx.m4a new file mode 100644 index 0000000..f0cf8fa Binary files /dev/null and b/assets/media/sfx.m4a differ diff --git a/assets/script.mjs b/assets/script.mjs new file mode 100644 index 0000000..a2bc3c0 --- /dev/null +++ b/assets/script.mjs @@ -0,0 +1,70 @@ +import { default as API } from "https://cdn.jsdelivr.net/npm/reflect-client@2.0.1/build/Reflect.js"; + +const STORAGE_KEY = "key"; +const STORAGE_URL = "url"; + +function error(message) { + // Create wrapper for error messages + if (!document.querySelector("div#error")) { + const wrapper = document.createElement("div"); + wrapper.id = "error"; + + // Remove wrapper element button + const closeButton = document.createElement("button"); + closeButton.innerText = "Close error messages"; + closeButton.addEventListener("click", () => wrapper.remove()); + + wrapper.appendChild(closeButton); + document.body.insertAdjacentElement("afterbegin", wrapper); + } + + const element = document.createElement("pre"); + element.innerText = message; + + document.querySelector("div#error").insertAdjacentElement("afterbegin", element); +} + +// Set logged in state on load if credentials exist +if (localStorage.getItem(STORAGE_URL) && localStorage.getItem(STORAGE_KEY)) { + document.body.classList.add("loggedin"); + + // Set login form credentials with values from storage + document.querySelector(`[name="${STORAGE_URL}"]`).value = localStorage.getItem(STORAGE_URL); + document.querySelector(`[name="${STORAGE_KEY}"]`).value = localStorage.getItem(STORAGE_KEY); +} + +// Login form +document.querySelector("form").addEventListener("submit", (event) => { + event.preventDefault(); + + // Set credentials in localStroage + const form = new FormData(event.target); + localStorage.setItem(STORAGE_URL, form.get(STORAGE_URL)); + localStorage.setItem(STORAGE_KEY, form.get(STORAGE_KEY)); + + document.body.classList.add("loggedin"); +}); + +// The Big Black Coffee Button +document.querySelector("button#coffee").addEventListener("click", async () => { + try { + // Attempt to pour some coffee + const url = new URL(localStorage.getItem(STORAGE_URL)); + const resp = await new API(url.origin, localStorage.getItem(STORAGE_KEY)).call(url.pathname).post(); + + // Bail out and show error error if we didn't get a 2xx response code + if (!resp.ok) { + return error(await resp.text()); + } + } catch (message) { + // Bail out if something went wrong with the API client + return error(message.toString()); + } + + // Play a fun little effect on success + const sfx = document.querySelector("audio"); + sfx.addEventListener("playing", () => document.body.classList.add("coffeup"), { once: true }); + sfx.addEventListener("ended", () => document.body.classList.remove("coffeup"), { once: true }); + + return sfx.play(); +}); \ No newline at end of file diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..d880519 --- /dev/null +++ b/assets/style.css @@ -0,0 +1,54 @@ +body { + font-family: monospace; + max-width: 1000px; + margin: auto; +} + +body:not(.loggedin) div:not(#login), +body.loggedin div#login { + display: none; +} + +form fieldset { + display: grid; +} + +div#error { + padding: 1em; + background-color: mistyrose; + margin-bottom: 2em; +} + +div#error pre { + padding: 1em; + background-color: salmon; +} + +button#coffee { + display: block; + margin: auto; + width: 20em; + height: 20em; + border-radius: 100%; + border: solid 1em black; + background-color: transparent; + box-sizing: border-box; + padding: 3em; +} + +button#coffee * { + pointer-events: none; +} + +body.coffeup button#coffee { + pointer-events: none; + border-color: burlywood; + animation: coffeup 300ms ease-in-out; +} + +@keyframes coffeup { + 0% { transform: rotate(0); } + 25% { transform: rotate(-10deg); } + 75% { transform: rotate(10deg); } + 100% { transform: rotate(0); } +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..5f3a804 --- /dev/null +++ b/index.html @@ -0,0 +1,39 @@ + + + + + + Big Black Coffee Button + + + + + +
+

Big Black Coffee Button

+
+
+ Credentials + + + + + + + +

Credentials will be saved in localStorage until cleared

+ + +
+
+
+
+ + +
+ + + + \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..3fc60d5 --- /dev/null +++ b/manifest.json @@ -0,0 +1,28 @@ +{ + "theme_color": "#deb887", + "background_color": "#ffffff", + "icons": [ + { + "purpose": "maskable", + "sizes": "512x512", + "src": "assets/media/icon512_maskable.png", + "type": "image/png" + }, + { + "purpose": "any", + "sizes": "512x512", + "src": "assets/media/icon512_rounded.png", + "type": "image/png" + } + ], + "orientation": "portrait", + "display": "standalone", + "dir": "auto", + "lang": "en-US", + "name": "Big Black Coffee Button", + "short_name": "BBCB", + "start_url": "https://bbcb.srv.vlw.se", + "scope": "https://bbcb.srv.vlw.se", + "id": "https://bbcb.srv.vlw.se", + "description": "One big button for updating api.vlw.se/coffee" +} \ No newline at end of file