diff --git a/.gitignore b/.gitignore
index 72edaf7..c7406f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,5 @@ output/
# the world isn't ready yet
views/projects/item/wavetapper
-views/projects/item/dice_2
\ No newline at end of file
+views/projects/item/dice_2
+views/projects/item/text
\ No newline at end of file
diff --git a/views/projects/item/fediverse-madness/index.html b/views/projects/item/fediverse-madness/index.html
new file mode 100644
index 0000000..c6f7c7c
--- /dev/null
+++ b/views/projects/item/fediverse-madness/index.html
@@ -0,0 +1,141 @@
+
+
+
+
+ Fediverse Madness - abtmtr.link
+
+
+
+
+
+
+
+
+
+
+
+ Followers list
+ Retrieving followers... Please wait.
+
+
+ S.
+ Av.
+ User
+ Handle
+
+
+
+ Select all
+ Select none
+ Select random
+ Deselect random
+
+ Load more
+ Done
+
+
+ Following list
+ Retrieving following... Please wait.
+
+
+ S.
+ Av.
+ User
+ Handle
+
+
+
+ Select all
+ Select none
+ Select random
+ Deselect random
+
+ Load more
+ Done
+
+
+ Fight!
+ Bracket Level ...
+ Round ...
+
+
+
+
+
+
+
+
+
+
+
+ Please wait...
+ Please wait...
+
+
+ Winner
+ Congratulations, ...
+
+
+
+
+ Bracket
+
+
+
+ Play Again
+
+
+
+
+
+
+
+
+
diff --git a/views/projects/item/fediverse-madness/script.js b/views/projects/item/fediverse-madness/script.js
new file mode 100644
index 0000000..e69de29
diff --git a/views/projects/item/fediverse-madness/scripts/game.js b/views/projects/item/fediverse-madness/scripts/game.js
new file mode 100644
index 0000000..2e932cc
--- /dev/null
+++ b/views/projects/item/fediverse-madness/scripts/game.js
@@ -0,0 +1,476 @@
+function escapeHtml(unsafe)
+{
+ return unsafe
+ .replace(/<\/?script\/?>/g, "")
+ .replace(/<\/?img\/?>/g, "")
+ .replace(/<\/?iframe\/?>/g, "")
+ .replace(/<\/?xml\/?>/g, "")
+ .replace(/<\/?audio\/?>/g, "")
+ .replace(/<\/?video\/?>/g, "")
+ .replace(/<\/?object\/?>/g, "");
+ }
+
+const el_id_user = document.querySelector("#user");
+const el_id_instance = document.querySelector("#instance");
+const el_id_whoami = document.querySelector("#whoami");
+const el_id_submitwhoamiFollowers = document.querySelector("#submitwhoami_followers");
+const el_id_submitwhoamiFollowing = document.querySelector("#submitwhoami_following");
+const el_id_errorwhoami = document.querySelector("#errorwhoami");
+let gamemodeFollowers;
+
+el_id_user.addEventListener("input", (e) => {
+ console.log(e);
+ // User types @ Chrome autofill
+ if (e.data === "@" || e.data === undefined) {
+ el_id_user.value =
+ el_id_user.value.replace(/@/gim, "");
+ el_id_instance.focus();
+ }
+})
+
+async function verify() {
+ const username = el_id_user.value.replace(/[^a-z0-9_]/gim, "");
+ const instance = el_id_instance.value.replace(/@/gim, "");
+ el_id_user.value = username;
+ el_id_instance.value = instance;
+ if (username.length < 1) return null;
+ if (instance.length < 1) return null;
+ let user_req;
+ try {
+ user_req = await fetch(`https://${instance}/api/v1/accounts/lookup?acct=${username}`);
+ } catch (err) {
+ return null;
+ }
+ const user_json = await user_req.json();
+ if (user_req.ok) return {
+ USER_ID: user_json.id,
+ INSTANCE: instance
+ };
+ else return null;
+}
+
+el_id_submitwhoamiFollowers.addEventListener("click", async (e) => {
+ el_id_errorwhoami.innerHTML = "";
+ const result = await verify();
+ if (result == null) {
+ el_id_errorwhoami.innerHTML = "Invalid user!";
+ return false;
+ }
+ game = {
+ ...game,
+ ...result
+ };
+
+ gamemodeFollowers = true;
+ getFollowers();
+})
+
+el_id_submitwhoamiFollowing.addEventListener("click", async (e) => {
+ el_id_errorwhoami.innerHTML = "";
+ const result = await verify();
+ if (result == null) {
+ el_id_errorwhoami.innerHTML = "Invalid user!";
+ return false;
+ }
+ game = {
+ ...game,
+ ...result
+ };
+
+ gamemodeFollowers = false;
+ getFollowing();
+})
+
+// Followers
+
+const el_id_followers = document.querySelector("#followers");
+const el_id_loadingfollowers = document.querySelector("#loadingfollowers");
+const el_id_listfollowers = document.querySelector("#listfollowers");
+const el_id_listfollowersvalues = document.querySelector("#listfollowersvalues");
+const el_id_selectallfollowers = document.querySelector("#selectallfollowers");
+const el_id_selectrandomfollowers = document.querySelector("#selectrandomfollowers");
+const el_id_deselectrandomfollowers = document.querySelector("#deselectrandomfollowers");
+const el_id_selectnofollowers = document.querySelector("#selectnofollowers");
+const el_id_morefollowers = document.querySelector("#morefollowers");
+const el_id_submitfollowers = document.querySelector("#submitfollowers");
+let selectboxes = [];
+let userList = [];
+let lastId = "";
+let selectedUsers = [];
+
+function renderNameHTML(name, user) {
+ return escapeHtml(name).replace(/:([a-z0-9_-]+?):/gim, (m, p1) => {
+ const emoji = user.emojis.find(x => x.shortcode == p1);
+ if (emoji == null) return null;
+ return ` `;
+ });
+}
+
+async function getFollowers(dontLoadNew = false) {
+ el_id_whoami.hidden = true;
+ el_id_followers.hidden = false;
+ el_id_submitfollowers.disabled = true;
+ el_id_morefollowers.disabled = true;
+ let res;
+ let out;
+ if (!dontLoadNew) {
+ el_id_loadingfollowers.innerHTML = `Retrieving followers... Please wait.`;
+ res = await fetch(`https://${game.INSTANCE}/api/v1/accounts/${game.USER_ID}/followers?limit=68${lastId.length > 0 ? `&max_id=${lastId}` : ""}`);
+ out = await res.json();
+ }
+ el_id_submitfollowers.disabled = false;
+ el_id_morefollowers.disabled = false;
+ if (dontLoadNew) return false;
+ if (out.length < 1) {
+ el_id_loadingfollowers.innerHTML = `No followers found.`;
+ return false;
+ }
+ el_id_listfollowers.hidden = false;
+ el_id_listfollowers.innerHTML += out.reduce((pv, cuser) => {
+ return pv + `
+
+
+
+
+ ${renderNameHTML(cuser.display_name, cuser)}
+
+
+ @${cuser.fqn}
+ `
+ }, "");
+ userList.push(...out.map(user => ({
+ fqn: user.fqn,
+ avatar: user.avatar,
+ bot: user.bot,
+ created_at: user.created_at,
+ display_name: user.display_name,
+ emojis: user.emojis,
+ fields: user.fields,
+ id: user.id,
+ note: user.note,
+ username: user.url,
+ username: user.username
+ })));
+ lastId = userList.at(-1).id;
+ selectboxes = Array.from(el_id_listfollowers.querySelectorAll(".follower_checkbox"));
+ selectboxes.forEach(x => x.addEventListener("change", checkSelectedAmtFollowers));
+ checkSelectedAmtFollowers();
+}
+
+el_id_selectallfollowers.addEventListener("click", () => {
+ selectboxes.forEach(x => x.checked = true);
+ checkSelectedAmtFollowers();
+})
+
+el_id_selectrandomfollowers.addEventListener("click", () => {
+ const unselectedPick = selectboxes.filter(x => !x.checked);
+ const selected = unselectedPick[Math.floor(Math.random() * (unselectedPick.length - 1))];
+ if (selected == null) return;
+ selected.checked = true;
+ checkSelectedAmtFollowers();
+})
+
+el_id_deselectrandomfollowers.addEventListener("click", () => {
+ const selectedPick = selectboxes.filter(x => x.checked);
+ const selected = selectedPick[Math.floor(Math.random() * (selectedPick.length - 1))];
+ if (selected == null) return;
+ selected.checked = false;
+ checkSelectedAmtFollowers();
+})
+
+el_id_selectnofollowers.addEventListener("click", () => {
+ selectboxes.forEach(x => x.checked = false);
+ checkSelectedAmtFollowers();
+})
+
+el_id_morefollowers.addEventListener("click", () => getFollowers());
+
+el_id_submitfollowers.addEventListener("click", () => {
+ console.log(selectedUsers);
+ selectboxes.forEach(({checked}, i) => {
+ if (checked) selectedUsers.push(userList.at(i));
+ });
+ if (selectedUsers.length < 2) {
+ selectedUsers = [];
+ el_id_loadingfollowers.innerHTML = `Selected user count is less than 2! (${selectboxes.filter(x => x.checked).length} selected)`;
+ return;
+ }
+ sortSelectedUsers();
+})
+
+function checkSelectedAmtFollowers() {
+ el_id_loadingfollowers.innerHTML = `${selectboxes.filter(x => x.checked).length} selected`;
+}
+
+// Following
+
+const el_id_following = document.querySelector("#following");
+const el_id_loadingfollowing = document.querySelector("#loadingfollowing");
+const el_id_listfollowing = document.querySelector("#listfollowing");
+const el_id_listfollowingvalues = document.querySelector("#listfollowingvalues");
+const el_id_selectallfollowing = document.querySelector("#selectallfollowing");
+const el_id_selectrandomfollowing = document.querySelector("#selectrandomfollowing");
+const el_id_deselectrandomfollowing = document.querySelector("#deselectrandomfollowing");
+const el_id_selectnofollowing = document.querySelector("#selectnofollowing");
+const el_id_morefollowing = document.querySelector("#morefollowing");
+const el_id_submitfollowing = document.querySelector("#submitfollowing");
+
+async function getFollowing(dontLoadNew = false) {
+ el_id_whoami.hidden = true;
+ el_id_following.hidden = false;
+ el_id_submitfollowing.disabled = true;
+ el_id_morefollowing.disabled = true;
+ let res;
+ let out;
+ if (!dontLoadNew) {
+ el_id_loadingfollowing.innerHTML = `Retrieving following... Please wait.`;
+ res = await fetch(`https://${game.INSTANCE}/api/v1/accounts/${game.USER_ID}/following?limit=68${lastId.length > 0 ? `&max_id=${lastId}` : ""}`);
+ out = await res.json();
+ }
+ el_id_submitfollowing.disabled = false;
+ el_id_morefollowing.disabled = false;
+ if (dontLoadNew) return false;
+ if (out.length < 1) {
+ el_id_loadingfollowing.innerHTML = `No following found.`;
+ return false;
+ }
+ el_id_listfollowing.hidden = false;
+ el_id_listfollowing.innerHTML += out.reduce((pv, cuser) => {
+ return pv + `
+
+
+
+
+ ${renderNameHTML(cuser.display_name, cuser)}
+
+
+ @${cuser.fqn}
+ `
+ }, "");
+ userList.push(...out.map(user => ({
+ fqn: user.fqn,
+ avatar: user.avatar,
+ bot: user.bot,
+ created_at: user.created_at,
+ display_name: user.display_name,
+ emojis: user.emojis,
+ fields: user.fields,
+ id: user.id,
+ note: user.note,
+ username: user.url,
+ username: user.username
+ })));
+ lastId = userList.at(-1).id;
+ selectboxes = Array.from(el_id_listfollowing.querySelectorAll(".follower_checkbox"));
+ selectboxes.forEach(x => x.addEventListener("change", checkSelectedAmtFollowing));
+ checkSelectedAmtFollowing();
+}
+
+el_id_selectallfollowing.addEventListener("click", () => {
+ selectboxes.forEach(x => x.checked = true);
+ checkSelectedAmtFollowing();
+})
+
+el_id_selectrandomfollowing.addEventListener("click", () => {
+ const unselectedPick = selectboxes.filter(x => !x.checked);
+ const selected = unselectedPick[Math.floor(Math.random() * (unselectedPick.length - 1))];
+ if (selected == null) return;
+ selected.checked = true;
+ checkSelectedAmtFollowing();
+})
+
+el_id_deselectrandomfollowing.addEventListener("click", () => {
+ const selectedPick = selectboxes.filter(x => x.checked);
+ const selected = selectedPick[Math.floor(Math.random() * (selectedPick.length - 1))];
+ if (selected == null) return;
+ selected.checked = false;
+ checkSelectedAmtFollowing();
+})
+
+el_id_selectnofollowing.addEventListener("click", () => {
+ selectboxes.forEach(x => x.checked = false);
+ checkSelectedAmtFollowing();
+})
+
+el_id_morefollowing.addEventListener("click", () => getFollowing());
+
+el_id_submitfollowing.addEventListener("click", () => {
+ console.log(selectedUsers);
+ selectboxes.forEach(({checked}, i) => {
+ if (checked) selectedUsers.push(userList.at(i));
+ });
+ if (selectedUsers.length < 2) {
+ selectedUsers = [];
+ el_id_loadingfollowing.innerHTML = `Selected user count is less than 2! (${selectboxes.filter(x => x.checked).length} selected)`;
+ return;
+ }
+ sortSelectedUsers();
+})
+
+function checkSelectedAmtFollowing() {
+ el_id_loadingfollowing.innerHTML = `${selectboxes.filter(x => x.checked).length} selected`;
+}
+
+// Brackets
+
+function shuf(array) {
+ var count = array.length,
+ randomnumber,
+ temp;
+ while (count) {
+ randomnumber = Math.random() * count-- | 0;
+ temp = array[count];
+ array[count] = array[randomnumber];
+ array[randomnumber] = temp
+ }
+ return array;
+}
+
+function chunk(arr, size) {
+ return Array.from({ length: Math.ceil(arr.length / size) }, (v, i) =>
+ arr.slice(i * size, i * size + size)
+ );
+}
+
+const el_id_game = document.querySelector("#game");
+const el_id_gameUserOne = document.querySelector("#gameUserOne");
+const el_id_gameUserOneImage = document.querySelector("#gameUserOneImage");
+const el_id_gameUserOneName = document.querySelector("#gameUserOneName");
+const el_id_gameUserOneId = document.querySelector("#gameUserOneId");
+const el_id_gameUserOneNote = document.querySelector("#gameUserOneNote");
+const el_id_gameUserOneFields = document.querySelector("#gameUserOneFields");
+const el_id_gameUserTwo = document.querySelector("#gameUserTwo");
+const el_id_gameUserTwoImage = document.querySelector("#gameUserTwoImage");
+const el_id_gameUserTwoName = document.querySelector("#gameUserTwoName");
+const el_id_gameUserTwoId = document.querySelector("#gameUserTwoId");
+const el_id_gameUserTwoNote = document.querySelector("#gameUserTwoNote");
+const el_id_gameUserTwoFields = document.querySelector("#gameUserTwoFields");
+const el_id_gameSubmitLeft = document.querySelector("#gameSubmitLeft");
+const el_id_gameSubmitRight = document.querySelector("#gameSubmitRight");
+const el_id_gameBracketLevel = document.querySelector("#gameBracketLevel");
+const el_id_gameBracketFight = document.querySelector("#gameBracketFight");
+
+let state = [];
+let curDepth = 0;
+let curFight = 0;
+
+function sortSelectedUsers() {
+ if (gamemodeFollowers)
+ el_id_followers.hidden = true;
+ else
+ el_id_following.hidden = true;
+ el_id_game.hidden = false;
+ state.push(chunk(shuf(selectedUsers), 2));
+ state.push([]);
+ prepareGameStage();
+}
+
+function prepareGameStage() {
+ el_id_gameSubmitLeft.disabled = true;
+ el_id_gameSubmitRight.disabled = true;
+ el_id_gameSubmitLeft.innerHTML = "Please wait...";
+ el_id_gameSubmitRight.innerHTML = "Please wait...";
+ el_id_gameUserOneImage.src = "";
+ el_id_gameUserTwoImage.src = "";
+ el_id_gameBracketLevel.innerHTML = "Bracket Level " + (curDepth + 1);
+ el_id_gameBracketFight.innerHTML = "Round " + (curFight + 1);
+ const curSubStage = state[curDepth][curFight];
+ el_id_gameUserOneImage.src = curSubStage[0].avatar;
+ el_id_gameUserOneName.innerHTML = renderNameHTML(escapeHtml(curSubStage[0].display_name), curSubStage[0]);
+ el_id_gameUserOneId.innerHTML = "@" + curSubStage[0].fqn;
+ el_id_gameUserOneNote.innerHTML = escapeHtml(curSubStage[0].note);
+ el_id_gameUserOneFields.innerHTML = curSubStage[0].fields.reduce((pv, fields) => pv + `${escapeHtml(fields.name)} ${escapeHtml(fields.value)} `, "");
+ el_id_gameUserTwoImage.src = curSubStage[1].avatar;
+ el_id_gameUserTwoName.innerHTML = renderNameHTML(escapeHtml(curSubStage[1].display_name), curSubStage[1]);
+ el_id_gameUserTwoId.innerHTML = "@" + curSubStage[1].fqn;
+ el_id_gameUserTwoNote.innerHTML = escapeHtml(curSubStage[1].note);
+ el_id_gameUserTwoFields.innerHTML = curSubStage[1].fields.reduce((pv, fields) => pv + `${escapeHtml(fields.name)} ${escapeHtml(fields.value)} `, "");
+ el_id_gameSubmitLeft.disabled = false;
+ el_id_gameSubmitRight.disabled = false;
+ el_id_gameSubmitLeft.innerHTML = "Pick " + "@" + curSubStage[0].fqn;
+ el_id_gameSubmitRight.innerHTML = "Pick " + "@" + curSubStage[1].fqn;
+}
+
+function addNewThingy(thingy) {
+ if (state[curDepth + 1] == null) state.push([]);
+ if (curFight % 2 == 0)
+ state[curDepth + 1][Math.floor(curFight / 2)] = [];
+ state[curDepth + 1][Math.floor(curFight / 2)].push(thingy);
+}
+
+el_id_gameSubmitLeft.addEventListener("click", () => {
+ addNewThingy(state[curDepth][curFight][0]);
+ nextStage();
+})
+
+el_id_gameSubmitRight.addEventListener("click", () => {
+ addNewThingy(state[curDepth][curFight][1]);
+ nextStage();
+})
+
+function nextStage() {
+ if (curFight >= state[curDepth].length - 1) {
+ curDepth++;
+ curFight = 0;
+ } else curFight++;
+ console.log(state, curFight, curDepth);
+ if (state[curDepth][curFight].length < 2) {
+ if (state[curDepth].length < 2) {
+ endGame();
+ return;
+ }
+ addNewThingy(state[curDepth][curFight][0]);
+ curDepth++;
+ curFight = 0;
+ }
+ prepareGameStage();
+}
+
+// End
+
+const el_id_winner = document.querySelector("#winner");
+const el_id_winnerUser = document.querySelector("#winnerUser");
+const el_id_winnerUserImage = document.querySelector("#winnerUserImage");
+const el_id_winnerUserName = document.querySelector("#winnerUserName");
+const el_id_winnerUserId = document.querySelector("#winnerUserId");
+const el_id_winnerUserNote = document.querySelector("#winnerUserNote");
+const el_id_winnerUserFields = document.querySelector("#winnerUserFields");
+const el_id_winnermessage = document.querySelector("#winnermessage");
+const el_id_winnerusers = document.querySelector("#winnerusers");
+const el_id_winnerPlayAgain = document.querySelector("#winnerPlayAgain");
+
+function endGame() {
+ el_id_game.hidden = true;
+ el_id_winner.hidden = false;
+ const winningPlayer = state[curDepth][curFight][0];
+ el_id_winnerUserImage.src = winningPlayer.avatar;
+ el_id_winnerUserName.innerHTML = renderNameHTML(escapeHtml(winningPlayer.display_name), winningPlayer);
+ el_id_winnerUserId.innerHTML = "@" + winningPlayer.fqn;
+ el_id_winnerUserNote.innerHTML = escapeHtml(winningPlayer.note);
+ el_id_winnerUserFields.innerHTML = winningPlayer.fields.reduce((pv, fields) => pv + `${escapeHtml(fields.name)} ${escapeHtml(fields.value)} `, "");
+
+ el_id_winnermessage.innerHTML = `Congratulations, ${renderNameHTML(escapeHtml(winningPlayer.display_name), winningPlayer)}! You won the bracket!`;
+ el_id_winnerusers.innerHTML = `${state.reduce((pv, cs) => {
+ return pv + `
${cs.reduce((pvus, cus) => {
+ return pvus + cus.reduce((pvu, cu) => {
+ return pvu + `
+
+
${renderNameHTML(escapeHtml(cu.display_name), cu)}
+
@${cu.fqn}
+
`
+ }, "")
+ }, "")}
`
+ }, "")}
`
+}
+
+el_id_winnerPlayAgain.addEventListener("click", (x) => {
+ el_id_winner.hidden = true;
+ state = [];
+ selectedUsers = [];
+ curDepth = 0;
+ curFight = 0;
+ if (gamemodeFollowers)
+ getFollowers(true);
+ else
+ getFollowing(true);
+})
\ No newline at end of file
diff --git a/views/projects/item/fediverse-madness/style.css b/views/projects/item/fediverse-madness/style.css
new file mode 100644
index 0000000..220142c
--- /dev/null
+++ b/views/projects/item/fediverse-madness/style.css
@@ -0,0 +1,78 @@
+.userinstance {
+ border: 0;
+ margin: 0;
+ padding: 0;
+ vertical-align: middle;
+}
+
+.userinputbox {
+ border: var(--border-width) var(--border-style) var(--border-color);
+ border-radius: var(--border-radius);
+ padding-inline: 0.25em;
+ padding-block: 0.25em;
+}
+
+.userinputbox * {
+ vertical-align: 0%;
+}
+
+#listfollowers, #listfollowing {
+ display: inline-block;
+ position: relative;
+ width: 100%;
+ max-height: 31.25em;
+ overflow:auto;
+}
+
+#listfollowers #listfollowersvalues,
+#listfollowing #listfollowingvalues {
+ position: sticky;
+ top: 0;
+ background-color: var(--background-color);
+}
+
+.followers_namelabel,
+.following_namelabel {
+ max-width: 20em;
+}
+
+.gridUser {
+ display: grid;
+ grid-template-columns: 128px auto;
+ overflow: auto;
+ gap: 0.5em;
+}
+
+.gridUsersVert {
+ display: grid;
+ overflow: auto;
+ gap: 0.5em;
+}
+
+.flexUsersVert {
+ display: flex;
+ flex-direction: column;
+ align-items: safe center;
+ overflow: auto;
+ gap: 0.5em;
+}
+
+.flexUser {
+ display: flex;
+ flex-wrap: nowrap;
+ justify-content: space-evenly;
+ width: 100%;
+ gap: 0.5em;
+}
+
+.flexUserUser {
+ text-align: center;
+ width: 256px;
+ min-width: 256px;
+ overflow: hidden;
+ word-wrap: break-word;
+}
+
+.flexUserUser * {
+ word-wrap: break-word;
+}
\ No newline at end of file
diff --git a/views/projects/public/projects.json b/views/projects/public/projects.json
index bebe7db..1a25b3b 100644
--- a/views/projects/public/projects.json
+++ b/views/projects/public/projects.json
@@ -5,6 +5,20 @@
"normalize": "#808080"
},
"items": [
+ {
+ "name": "Fediverse Madness",
+ "date": 1710804730000,
+ "description": [
+ "A March Madness style bracket-based competition for the entities on the Fediverse.",
+ "Compate your friends! Have a meltdown-sized crisis over who you like more!",
+ "This project will make you do it.",
+ "SERIOUS TRIGGER WARNING: uncomfortable comparisons."
+ ],
+ "url": "/projects/item/fediverse-madness/",
+ "tags": [
+ "normalize"
+ ]
+ },
{
"name": "LastFMDownloader",
"date": 1708382160000,