first commit! 🎉

This commit is contained in:
ari melody 2024-06-16 07:31:16 +01:00
commit 8dc8190cdf
Signed by: ari
GPG key ID: CF99829C92678188
12 changed files with 1327 additions and 0 deletions

BIN
public/avatar/ari.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

240
public/css/style.css Normal file
View file

@ -0,0 +1,240 @@
:root {
--fg0: #eee;
--bg0: #080808;
--bg1: #101010;
--bg2: #121212;
--accent: #b7fd49;
}
body {
margin: 0;
padding: 0;
background-color: var(--bg0);
color: var(--fg0);
font-family: "Inter", sans-serif;
}
#feed {
width: 720px;
margin: 0 auto;
background-color: var(--bg1);
}
.post-container {
padding: 20px 32px;
border-bottom: 1px solid #8884;
transition: background-color .1s;
}
.post-container:hover {
background-color: var(--bg2);
}
.post-context {
margin-bottom: 8px;
padding-left: 58px;
display: flex;
flex-direction: row;
align-items: center;
color: var(--accent);
opacity: .8;
transition: opacity .1s;
}
.post-container:hover .post-context {
opacity: 1;
}
.post-context-icon {
margin-right: 4px;
}
.post-context a,
.post-context a:visited,
.post-header-container a,
.post-header-container a:visited {
color: inherit;
text-decoration: none;
}
.post-context a:hover,
.post-header-container a:hover {
text-decoration: underline;
}
.post-context-time {
margin-left: auto;
}
article.post { /* ... */ }
.post-header-container {
display: flex;
flex-direction: row;
}
.post-avatar-container {
margin-right: 12px;
}
.post-avatar {
border-radius: 8px;
box-shadow: 2px 2px #0004;
/* transition: transform .2s ease-out; */
}
/* .post-avatar:hover { */
/* transform: scale(1.1); */
/* } */
.post-header {
display: flex;
flex-grow: 1;
flex-direction: row;
}
.post-info {
margin-left: auto;
}
.post-user-info a {
display: block;
}
.post-body {
margin-top: 8px;
}
.post-media-container {
margin-top: 8px;
display: grid;
grid-gap: 8px;
}
.post-media-container[data-count="1"] {
grid-template-rows: 1fr;
}
.post-media-container[data-count="2"] {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
}
.post-media-container[data-count="3"] {
grid-template-columns: 1fr .5fr;
grid-template-rows: 1fr 1fr;
}
.post-media-container[data-count="4"] {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
.post-media {
border-radius: 12px;
background-color: #000;
overflow: hidden;
}
.post-media a {
width: 100%;
height: 100%;
display: block;
cursor: zoom-in;
}
.post-media a img {
width: 100%;
height: 100%;
display: block;
object-fit: contain;
}
.post-media-container > :nth-child(1) {
grid-column: 1/2;
grid-row: 1/2;
}
.post-media-container[data-count="3"] > :nth-child(1) {
grid-row: 1/3;
}
.post-media-container > :nth-child(2) {
grid-column: 2/2;
grid-row: 1/2;
}
.post-media-container > :nth-child(3) {
grid-column: 1/2;
grid-row: 2/2;
}
.post-media-container[data-count="3"] > :nth-child(3) {
grid-column: 2/2;
grid-row: 2/2;
}
.post-media-container > :nth-child(4) {
grid-column: 2/2;
grid-row: 2/2;
}
.post-container footer {
opacity: .8;
transition: opacity .1s;
}
.post-container:hover footer {
opacity: 1;
}
.post-reactions {
margin-top: 8px;
}
button.reaction {
padding: 6px 8px;
font-size: 1em;
background: none;
color: inherit;
border: none;
border-radius: 8px;
/* transition: transform .1s ease-out; */
}
button.reaction:hover,
.post-actions button:hover {
/* transform: scale(1.1); */
background: #8881;
}
button.reaction:active,
.post-actions button:active {
/* transform: scale(.95); */
background: #0001;
}
button.reaction.active,
.post-actions button.active {
background: var(--accent);
color: var(--bg0);
}
.post-actions {
margin-top: 8px;
}
.post-actions button {
padding: 6px 8px;
font-size: 1em;
background: none;
color: inherit;
border: none;
border-radius: 8px;
transition: transform .1s ease-out;
}
.post-actions button .count {
opacity: .5;
}

107
public/index.html Normal file
View file

@ -0,0 +1,107 @@
<!DOCTYPE html>
<!--
experimenting with post layouts here!
don't expect anything too flashy ;3
ari melody, 2024
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<link href="css/style.css" rel="stylesheet">
<script type="application/javascript" src="script/main.mjs" defer></script>
</head>
<body>
<audio id="sound-success" src="sound/success.wav"></audio>
<header>
</header>
<main>
<div id="feed">
<!--
<div class="post-container" aria-label="ari; hello world!~; 02:12:06">
<div class="post-context">
<span class="post-context-icon">🔁</span>
<span class="post-context-action">
<a href="/@ari">ari 💫</a> boosted this post.
</span>
<span class="post-context-time">
<time title="6/3/2024, 2:12:06 AM">2m ago</time>
</span>
</div>
<article class="post">
<div class="post-header-container">
<a href="/@ari" class="post-avatar-container">
<img src="avatar/ari.jpg" alt="" width="48" height="48" class="post-avatar" loading="lazy" decoding="async">
</a>
<header class="post-header">
<div class="post-user-info">
<a href="/@ari" class="name">ari 💫</a>
<span class="username">@ari</span>
</div>
<div class="post-info">
<a href="/post/21c892b23701" class="created-at">
<time title="6/3/2024, 2:11:58 AM">10m ago</time>
</a>
</div>
</header>
</div>
<div class="post-body">
<span class="post-content">hello world!~</span>
<div class="post-media-container" data-count="3">
<div class="post-media image">
<a href="media/ariyeah-button.png">
<img src="media/ariyeah-button.png" alt="custom miiverse &quot;yeah!&quot; button" loading="lazy" decoding="async">
</a>
</div>
<div class="post-media image">
<a href="media/beer.jpg">
<img src="media/beer.jpg" alt="barney calhoun with beer" loading="lazy" decoding="async">
</a>
</div>
<div class="post-media image">
<a href="media/duck.jpg">
<img src="media/duck.jpg" alt="big rubber duck" loading="lazy" decoding="async">
</a>
</div>
</div>
</div>
<footer class="post-footer">
<div class="post-reactions">
<button type="button" class="reaction">
<span></span>
<span class="count">52</span>
</button>
</div>
<div class="post-actions">
<button type="button" class="reply" aria-label="Reply" title="Reply">
<span>🗨️</span>
<span class="count">7</span>
</button>
<button type="button" class="boost" aria-label="Boost" title="Boost">
<span>🔁</span>
<span class="count">13</span>
</button>
<button type="button" class="favourite" aria-label="Favourite" title="Favourite">
<span></span>
</button>
<button type="button" class="react" aria-label="React" title="React">
<span>😃</span>
</button>
<button type="button" class="quote" aria-label="Quote" title="Quote">
<span>🗣️</span>
</button>
<button type="button" class="more" aria-label="More" title="More">
<span>🛠️</span>
</button>
</div>
</footer>
</article>
</div>
-->
</div>
</main>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
public/media/beer.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/media/duck.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

302
public/script/main.mjs Normal file
View file

@ -0,0 +1,302 @@
const aria_safe_regex = /([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF]|[\r])/g;
const INSTANCE_URL = "soc.arimelody.me";
const sounds = {
"default": new Audio("sound/log.ogg"),
"post": new Audio("sound/success.ogg"),
"boost": new Audio("sound/hello.ogg"),
};
const actors = {
"@ari": {
"url": "https://soc.arimelody.me/@ari",
"name": "ari 💫",
"avatar": "avatar/ari.jpg",
}
};
const test_post = {
"context": {
"type": "boost",
"by": "@ari",
"at": 1718513838624,
},
"author": "@ari",
"url": "/post/21c892b23701",
"at": 1718513988384,
"content": "hello world!~",
"media": [
{ "url": "media/ariyeah-button.png", "alt": "custom miiverse \"yeah!\" button" },
{ "url": "media/beer.jpg", "alt": "barney calhoun with beer" },
{ "url": "media/duck.jpg", "alt": "big rubber duck" },
],
"replies": 7,
"boosts": 13,
"reactions": [
{ "react": "⭐", "count": "52" },
{ "react": "❤️", "count": "9" },
],
};
const feed = document.getElementById("feed");
function render_post(data) {
// TODO: please god just use or make a library to build this
const actor = actors[data.author];
if (!actor) return;
const date = new Date(data.at);
const post = document.createElement("article");
post.classList.add("post-container");
post.ariaLabel = actor.name.replace(aria_safe_regex, "").trim() + "; " + data.content + "; " + date.toLocaleTimeString();
if (data.context && data.context.by && actors[data.context.by]) {
// post context
const post_context = document.createElement("div");
post_context.classList.add("post-context");
if (data.context.type == "boost") {
const post_context_icon = document.createElement("span");
post_context_icon.classList.add("post-context-icon");
post_context_icon.innerText = "🔁";
post_context.appendChild(post_context_icon);
const post_context_action = document.createElement("span");
post_context_action.classList.add("post-context-action");
const actor = actors[data.context.by];
post_context_action.innerHTML = `boosted by <a href="${actor.url}">${actor.name}</a>`;
post_context.appendChild(post_context_action);
const post_context_time = document.createElement("span");
post_context_time.classList.add("post-context-time");
post_context_time.innerHTML = `<time>${new Date(data.context.at).toLocaleString()}</time>`;
post_context.appendChild(post_context_time);
}
post.appendChild(post_context);
}
// the actual post
// article.post
const post_article = document.createElement("article");
const post_header_container = document.createElement("div");
post_header_container.classList.add("post-header-container");
const post_avatar_container = document.createElement("a");
post_avatar_container.classList.add("post-avatar-container");
post_avatar_container.href = actor.url;
const post_avatar = document.createElement("img");
post_avatar.classList.add("post-avatar");
post_avatar.src = actor.avatar;
post_avatar.alt = "";
post_avatar.width = 48;
post_avatar.height = 48;
post_avatar.loading = "lazy";
post_avatar.decoding = "async";
post_avatar_container.appendChild(post_avatar);
post_header_container.appendChild(post_avatar_container);
const post_header = document.createElement("header");
post_header.classList.add("post-header");
const post_user_info = document.createElement("div");
post_user_info.classList.add("post-user-info");
const post_user_info_name = document.createElement("a");
post_user_info_name.classList.add("name");
post_user_info_name.href = actor.url;
post_user_info_name.innerText = actor.name
post_user_info.appendChild(post_user_info_name);
const post_user_info_username = document.createElement("span");
post_user_info_username.classList.add("username");
post_user_info_username.href = actor.url;
post_user_info_username.innerText = data.author
post_user_info.appendChild(post_user_info_username);
post_header.appendChild(post_user_info);
const post_info = document.createElement("div");
post_info.classList.add("post-info");
const post_info_time = document.createElement("a");
post_info_time.classList.add("created-at");
const post_date = new Date(data.at);
post_info_time.innerHTML = `<time title=${post_date.toLocaleString()}>${post_date.toLocaleString()}</time>`;
post_info_time.href = post.url;
post_info.appendChild(post_info_time);
post_header.appendChild(post_info);
post_header_container.appendChild(post_header);
post_article.appendChild(post_header_container);
const post_body = document.createElement("div");
post_body.classList.add("post-body");
const post_content = document.createElement("span");
post_content.classList.add("post-content");
post_content.innerText = data.content;
post_body.appendChild(post_content);
const media_container = document.createElement("div");
media_container.classList.add("post-media-container");
media_container.dataset.count = data.media.length;
data.media.forEach(media => {
const media_item = document.createElement("div");
media_item.classList.add("post-media");
const link = document.createElement("a");
link.href = media.url;
const source = document.createElement("img");
source.src = media.url;
source.alt = media.alt;
source.loading = "lazy";
source.decoding = "async";
link.appendChild(source);
media_item.appendChild(link);
media_container.appendChild(media_item);
});
post_body.appendChild(media_container);
post_article.appendChild(post_body);
const post_footer = document.createElement("footer");
post_footer.classList.add("post-footer");
const post_reactions = document.createElement("div");
post_reactions.classList.add("post-reactions");
data.reactions.forEach(reaction => {
const btn = document.createElement("button");
btn.classList.add("reaction");
btn.type = "button";
const emote = document.createElement("span");
emote.innerText = reaction.react;
btn.appendChild(emote);
const count = document.createElement("span");
count.classList.add("count");
count.innerText = reaction.count;
btn.appendChild(count);
post_reactions.appendChild(btn);
});
post_footer.appendChild(post_reactions);
const post_actions = document.createElement("div");
post_actions.classList.add("post-actions");
const reply_button = document.createElement("button");
reply_button.type = "button";
reply_button.ariaLabel = "Reply";
reply_button.title = "Reply";
reply_button.innerHTML = `<span>🗨️</span><span class="count">${data.replies}</count>`;
post_actions.appendChild(reply_button);
const boost_button = document.createElement("button");
boost_button.type = "button";
boost_button.ariaLabel = "Boost";
boost_button.title = "Boost";
boost_button.innerHTML = `<span>🔁</span><span class="count">${data.boosts}</count>`;
post_actions.appendChild(boost_button);
const fav_button = document.createElement("button");
fav_button.type = "button";
fav_button.ariaLabel = "Favourite";
fav_button.title = "Favourite";
fav_button.innerText = "⭐";
post_actions.appendChild(fav_button);
const react_button = document.createElement("button");
react_button.type = "button";
react_button.ariaLabel = "React";
react_button.title = "React";
react_button.innerText = "😃";
post_actions.appendChild(react_button);
const quote_button = document.createElement("button");
quote_button.type = "button";
quote_button.ariaLabel = "Quote";
quote_button.title = "Quote";
quote_button.innerText = "🗣️";
post_actions.appendChild(quote_button);
const more_button = document.createElement("button");
more_button.type = "button";
more_button.ariaLabel = "More";
more_button.title = "More";
more_button.innerText = "⚒️";
post_actions.appendChild(more_button);
post_footer.appendChild(post_actions);
post_article.appendChild(post_footer);
post.appendChild(post_article);
return post;
};
function hook_post_listeners(post) {
post.querySelectorAll("button").forEach(button => {
button.addEventListener("click", () => {
if (button.classList.contains("reaction")) {
toggle_reaction(button);
}
switch (button.ariaLabel) {
case "Reply":
play_sound("post");
break;
case "Boost":
play_sound("boost");
break;
case "Favourite":
post.querySelectorAll("button.reaction").forEach(reaction => {
if (!reaction.innerText.startsWith("⭐")) return;
toggle_reaction(reaction);
});
play_sound();
break;
default:
play_sound();
break;
}
});
});
}
function toggle_reaction(reaction) {
const was_active = reaction.classList.contains("active");
reaction.classList.toggle("active");
const count = reaction.querySelector(".count");
count.innerText = Number(count.innerText) + (was_active ? -1 : 1);
}
function load_content() {
for (let i = 0; i < 10; i++) {
const post = render_post(test_post);
feed.appendChild(post);
hook_post_listeners(post);
}
}
function play_sound(name) {
if (!name) name = "default";
const sound = sounds[name];
if (!sound) {
console.warn(`Attempted to play sound "${name}", which does not exist!`);
return;
}
sound.pause();
sound.currentTime = 0;
sound.play();
}
feed.querySelectorAll(".post-container").forEach(post => {
hook_post_listeners(post);
});
load_content();
document.addEventListener("scroll", event => {
while (window.innerHeight + window.scrollY >= document.body.offsetHeight - 1000) {
load_content();
}
});

BIN
public/sound/hello.ogg Normal file

Binary file not shown.

BIN
public/sound/log.ogg Normal file

Binary file not shown.

BIN
public/sound/success.ogg Normal file

Binary file not shown.