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 charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Koodiklinikka nimilappu generaattori</title> <title>Koodiklinikka nimilappu generaattori</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css" />
<script src="script.js"></script> <script src="script.js"></script>
<script src="magic.js"></script>
</head> </head>
<body onload="handleBoot()"> <body onload="handleBoot()">
<div class="laput"> <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> </div>
<template id="lappu-template"> <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"> <div style="text-align: center">
<figure> <figure>
<input type="file" onchange="handleUpload(event)" /> <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(); 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 laput = document.querySelector(".laput");
const template = document.querySelector("#lappu-template"); const template = document.querySelector("#lappu-template");
const lisalappuButton = document.querySelector(".lisalappu"); const lisalappuButton = document.querySelector(".lisalappu");
const clone = template.content.cloneNode(true); const clone = template.content.firstElementChild.cloneNode(true);
laput.insertBefore(clone, lisalappuButton); return laput.insertBefore(clone, lisalappuButton);
}
const handleClickAdd = () => {
const node = addCard();
slideCardIn(node);
}; };
const handleYeet = (event) => { async function handleYeet(event) {
const lappu = event.target; const lappu = event.target.closest(".lappu");
if (lappu.classList.contains("lappu")) { if (lappu) {
await slideCardOut(lappu);
lappu.parentNode.removeChild(lappu); lappu.parentNode.removeChild(lappu);
event.preventDefault(); event.preventDefault();
} }
}; }
const handleBoot = () => { const handleBoot = () => {
handleAdd(); addCard();
document.querySelector(".hakulappu").hidden = !window.crypto.subtle;
}; };

151
style.css
View File

@@ -1,14 +1,11 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
* { * {
box-sizing: border-box; box-sizing: border-box;
padding: 0; padding: 0;
margin: 0; margin: 0;
} font-family:
Inter,
body {
margin: 10mm;
font-family: Inter,
system-ui, system-ui,
-apple-system, -apple-system,
BlinkMacSystemFont, BlinkMacSystemFont,
@@ -22,76 +19,124 @@ body {
sans-serif; sans-serif;
} }
body {
margin: 10mm;
}
.laput { .laput {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 5mm; gap: 5mm;
}
.laput > * {
width: 85mm;
height: 54mm;
} }
.lisalappu { .lisalappu {
width: 85mm; color: #aaa;
height: 54mm; background: transparent;
color: #aaa; border: 1px dashed #aaa;
background: transparent; font-size: 3em;
border: 1px dashed #aaa; cursor: pointer;
font-size: 3em;
cursor: pointer;
} }
.lisalappu:hover { .lisalappu:hover {
background: #f0f0f0; 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 { @media print {
.lisalappu { .lisalappu,
display: none; .hakulappu {
} display: none;
}
body { body {
margin: unset; margin: unset;
} }
input { input {
display: none; display: none;
} }
} }
.lappu { .lappu {
width: 85mm; border: 1px solid black;
height: 54mm; padding: 10mm;
border: 1px solid black; display: flex;
padding: 10mm; align-items: center;
display: flex; justify-content: center;
align-items: center; position: relative;
justify-content: center;
} }
.lappu figure { .lappu figure {
position: relative; position: relative;
user-select: none; user-select: none;
width: 30%; width: 30%;
aspect-ratio: 1/1; aspect-ratio: 1/1;
margin: 0 auto; margin: 0 auto;
border-radius: 100%; border-radius: 100%;
overflow: hidden; overflow: hidden;
} }
.lappu figure input { .lappu figure input {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
opacity: 0; opacity: 0;
cursor: pointer; cursor: pointer;
} }
.lappu .username { .lappu .username {
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
opacity: 0.8; opacity: 0.8;
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.lappu .nimi { .lappu .nimi {
font-size: 21px; font-size: 21px;
margin-top: 1rem; 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;
} }