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 + + + + + + + +
+
+

Fediverse Madness

+

Competitive bracket-based comparisons of Fediverse users.

+
+ +
+
+
+

Who are you?

+ + @ + + @ + + +
+
+
+ + +

+
+ + + + +
+ + + + + + + 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,