Introduce magic

This commit is contained in:
Aarni Koskela
2024-10-03 13:53:45 +03:00
parent aeed56da6e
commit a156879993
6 changed files with 262 additions and 64 deletions

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
magic.json

View File

@@ -4,15 +4,27 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Koodiklinikka nimilappu generaattori</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
<script src="magic.js"></script>
</head>
<body onload="handleBoot()">
<div class="laput">
<button class="lisalappu" onclick="handleAdd()">+</button>
<button class="lisalappu" onclick="handleClickAdd()">+</button>
<div class="hakulappu">
<input
type="search"
oninput="handleSearch(event)"
placeholder="Hae nimellä..."
/>
<div id="hakutulokset"></div>
</div>
</div>
<template id="lappu-template">
<div class="lappu" ondblclick="handleYeet(event)">
<div class="lappu">
<button class="delete-btn" onclick="handleYeet(event)">
&times; Poista
</button>
<div style="text-align: center">
<figure>
<input type="file" onchange="handleUpload(event)" />

101
magic.js Normal file
View File

@@ -0,0 +1,101 @@
let magicProm = null;
function toBase64(buffer) {
const buf = new Uint8Array(buffer);
return btoa(String.fromCharCode(...buf));
}
function fromBase64(str) {
return Uint8Array.from([...atob(str)].map((c) => c.charCodeAt(0)));
}
async function sha256(message) {
const msgBuffer = new TextEncoder().encode(message);
return await crypto.subtle.digest("SHA-256", msgBuffer);
}
async function aesCbcDecrypt(base64Bytes, key) {
const decKey = await crypto.subtle.importKey("raw", key, "AES-CBC", true, [
"decrypt",
]);
const fullBuf = fromBase64(base64Bytes);
const iv = fullBuf.slice(0, 16);
const ciphertext = fullBuf.slice(16);
return crypto.subtle.decrypt({ name: "AES-CBC", iv }, decKey, ciphertext);
}
async function loadMagic() {
if (!magicProm) {
magicProm = fetch("./magic.json")
.then((r) => {
if (!r.ok) {
throw new Error(`Failed to load magic.json`);
}
return r.json();
})
.then((data) => new Map(data));
}
return magicProm;
}
async function doSearch(search) {
const res = [];
const db = await loadMagic();
const ents = db.get(toBase64(await sha256("kk2024~" + search)));
if (!ents) return [];
let decKeyBits = new Array(search.length).fill(search).join("x");
const decKey = await sha256(decKeyBits);
for (const ent of ents) {
const plain = new TextDecoder().decode(await aesCbcDecrypt(ent, decKey));
const json = JSON.parse(plain);
if (json.im) {
json.im = json.im.replace("^se", "https://avatars.slack-edge.com/");
}
res.push(json);
}
return res;
}
function handleAddFromSearchResult(event) {
event.preventDefault();
const { im, rn, dn } = event.target.dataset;
const el = addCard();
el.querySelector(".username").innerText = dn || "???";
el.querySelector(".nimi").innerText = rn || "???";
if (im) el.querySelector("img").src = im;
slideCardIn(el);
}
function rand(a, b) {
return a + Math.random() * (b - a);
}
async function handleSearch(event) {
const search = event.target.value.trim();
document.getElementById("hakutulokset").innerHTML = "";
if (!search) return;
let results = await doSearch(search);
let i = 0;
for (const { im, rn, dn } of results) {
const el = document.createElement("a");
Object.assign(el.dataset, { im, rn, dn });
el.innerText = [rn, dn].filter(Boolean).join(" / ");
el.onclick = handleAddFromSearchResult;
el.href = "#";
el.classList.add("hakutulos");
document.getElementById("hakutulokset").appendChild(el);
let transform = `rotate(${rand(-90, 90)}deg) translate(${rand(-50, 50)}%, ${rand(-50, 50)}%)`;
el.animate(
[
{ opacity: 0, transform },
{ opacity: 1, transform: "" },
],
{
duration: 300,
delay: i++ * 150,
fill: "both",
easing: "ease-out",
},
);
}
}

1
magic.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -30,22 +30,60 @@ const handleDragOver = (e) => {
e.preventDefault();
};
const handleAdd = () => {
function slideCardIn(node) {
node.animate(
[
{ opacity: 0, transform: "translateY(-10%)" },
{ opacity: 1, transform: "" },
],
{
duration: 200,
fill: "both",
easing: "ease-out",
},
);
}
function slideCardOut(node) {
return new Promise((resolve) => {
const anim = node.animate(
[
{ opacity: 1, transform: "" },
{ opacity: 0, transform: "translateY(40%)" },
],
{
duration: 200,
fill: "both",
easing: "ease-in",
},
);
anim.onfinish = resolve;
});
}
function addCard() {
const laput = document.querySelector(".laput");
const template = document.querySelector("#lappu-template");
const lisalappuButton = document.querySelector(".lisalappu");
const clone = template.content.cloneNode(true);
laput.insertBefore(clone, lisalappuButton);
const clone = template.content.firstElementChild.cloneNode(true);
return laput.insertBefore(clone, lisalappuButton);
}
const handleClickAdd = () => {
const node = addCard();
slideCardIn(node);
};
const handleYeet = (event) => {
const lappu = event.target;
if (lappu.classList.contains("lappu")) {
async function handleYeet(event) {
const lappu = event.target.closest(".lappu");
if (lappu) {
await slideCardOut(lappu);
lappu.parentNode.removeChild(lappu);
event.preventDefault();
}
};
}
const handleBoot = () => {
handleAdd();
addCard();
document.querySelector(".hakulappu").hidden = !window.crypto.subtle;
};

View File

@@ -4,11 +4,8 @@
box-sizing: border-box;
padding: 0;
margin: 0;
}
body {
margin: 10mm;
font-family: Inter,
font-family:
Inter,
system-ui,
-apple-system,
BlinkMacSystemFont,
@@ -22,15 +19,22 @@ body {
sans-serif;
}
body {
margin: 10mm;
}
.laput {
display: flex;
flex-wrap: wrap;
gap: 5mm;
}
.lisalappu {
.laput > * {
width: 85mm;
height: 54mm;
}
.lisalappu {
color: #aaa;
background: transparent;
border: 1px dashed #aaa;
@@ -42,8 +46,36 @@ body {
background: #f0f0f0;
}
.hakulappu {
border: 1px dashed #aaa;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.hakulappu a {
color: #2475a6;
}
.hakulappu input {
padding: 0.5em;
}
#hakutulokset {
text-align: center;
font-size: 80%;
padding: 0.5em;
}
.hakutulos {
display: block;
padding: 2px;
}
@media print {
.lisalappu {
.lisalappu,
.hakulappu {
display: none;
}
@@ -57,13 +89,12 @@ body {
}
.lappu {
width: 85mm;
height: 54mm;
border: 1px solid black;
padding: 10mm;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.lappu figure {
@@ -95,3 +126,17 @@ body {
font-size: 21px;
margin-top: 1rem;
}
.delete-btn {
position: absolute;
left: 0.1em;
top: 0.1em;
padding: 0.25em;
display: none;
border: 1px dotted crimson;
background: transparent;
cursor: pointer;
}
.lappu:hover .delete-btn {
display: block;
}