Skip to content

Instantly share code, notes, and snippets.

@zootella
Created May 25, 2020 00:13
Show Gist options
  • Save zootella/aea0e857af0be05469ce7b399617ba75 to your computer and use it in GitHub Desktop.
Save zootella/aea0e857af0be05469ce7b399617ba75 to your computer and use it in GitHub Desktop.
<!doctype html>
<html>
<head>
<title>blah4</title>
<style>
body {
max-width: 680px;
margin: 0 auto;
}
micro-composer {
display: block;
width: 500px;
}
micro-composer p {
margin: 0 0 5px;
}
micro-composer textarea,
micro-composer input {
padding: 5px;
}
micro-composer input {
margin-right: 10px;
border: 1px solid #bbb;
}
micro-composer textarea {
width: 100%;
min-height: 60px;
box-sizing: border-box;
}
micro-feed {
display: block;
margin: 1em 0;
}
micro-post {
display: grid;
grid-gap: 0;
grid-template-columns: 60px 1fr;
grid-template-rows: auto auto;
margin-bottom: 20px;
}
micro-post h1 { font-size: 21px; }
micro-post h2 { font-size: 20px; }
micro-post h3 { font-size: 19px; }
micro-post h4 { font-size: 18px; }
micro-post h5 { font-size: 17px; }
micro-post h6 { font-size: 16px; }
micro-post .thumb {
grid-row-start: 1;
grid-row-end: 3;
}
micro-post .thumb img {
width: 50px;
height: 50px;
object-fit: cover;
}
micro-post .meta {
margin-bottom: 5px;
}
micro-post .content {
min-height: 26px;
}
micro-post iframe.content {
width: 100%;
}
micro-post .content > * {
margin: 0;
}
micro-post .content > * + * {
margin-top: 1rem;
}
micro-post .content img,
micro-post .content video {
max-width: 100%;
}
</style>
</head>
<body>
<h1>blah4</h1>
<p>Refactored blahbity-blog to learn how it works</p>
<p>The official "Blahbity Blog" app:</p>
<p><i>hyper://a8e9bd0f4df60ed5246a1b1f53d51a1feaeb1315266f769ac218436f12fda830/</i></p>
<p>This hyperdrive, where it's refactored, commented a little and likely incorectly, and made a single file:</p>
<p><i>hyper://302232bfa3d6001fe1ecb8cf4ee68d73f300d8ca9bc4c6ecfde08b137a125f63/</i></p>
<p>Notes:</p>
<ul>
<li>single file so you can edit the code without having to navigate Beaker away from the functioning page</li>
<li>it would be great to do everything without <i>location.reload()</i>, as this clears the Dev Tools Console</li>
<li>it's awesome that the Blahbity Blog example is so short and simple, and the wrapper function around document.createElement is great, but, I think <i>React</i> could work in here as well, and without a bundler (I may try that next)</li>
</ul>
<script>
//load our own profile
var profile = undefined;//no profile
try {
profile = JSON.parse(localStorage.profile);//see if we can read a profile
} catch (e) { console.debug(e); }
// ---- <micro-composer> ----
class MicroComposer extends HTMLElement {
async connectedCallback() {
//no profile yet
if (!profile) {
//add the button to let the user choose a profile
this.append(element("button", {click: this.onClickChangeProfile.bind(this)}, ["Select a profile to post with"]));
//user has chosen their profile
} else {
//add the html to write a post
this.append(
element("form", {submit: this.onSubmit}, [
element("p", {}, [
element("textarea", {name: "content", required: true, placeholder: "Enter your post here"}, [])
]),
element("p", {}, [
element("input", {name: "filename", placeholder: "Post filename (optional)"}, []),
element("button", {type: "submit"}, ["Post a new blog entry"]),
" ",
element("small", {}, [
element("a", {href: "#", click: this.onClickChangeProfile.bind(this)}, ["Change profile"])
])
])
])
);
}
}
//the user wrote a blog entry and clicked the submit button
async onSubmit(e) {
e.preventDefault();//standard
//read the contents of form elements
var filename = e.target.filename.value;
var content = e.target.content.value;
if (!filename) filename = `${Date.now()}.md`;//set a default filename unless the user typed one
if (filename.indexOf(".") === -1) filename += ".md"//default markdown extension
//make the "/microblog/" folder in our profile.url hyperdrive
await beaker.hyperdrive.drive(profile.url).mkdir("/microblog/").catch(e => undefined);
//and then write the new blog entry there
await beaker.hyperdrive.drive(profile.url).writeFile("/microblog/" + filename, content);
//refresh the page, this clears out the console, though
location.reload();
}
//the user clicked to change their profile, that link beside the post button
async onClickChangeProfile(e) {
e.preventDefault();//standard
profile = await beaker.contacts.requestProfile();//beaker shows the box, you think
localStorage.profile = JSON.stringify(profile);//copy that into local storage
location.reload();//refresh with the new profile
}
}
customElements.define("micro-composer", MicroComposer);
// ---- <micro-feed> ----
//now lets define the <micro-feed> tag
class MicroFeed extends HTMLElement {
connectedCallback() {
this.initialLoad();
}
async initialLoad() {
if (!localStorage.profile) {
return
}
this.textContent = "Loading..."
try {
var posts = await loadFeed(({label, progress}) => {
this.textContent = `${label} (${(progress * 100)|0}%)`
})
} catch (e) {
this.textContent = e.toString()
console.debug(`Unable to query /microblog/`, e)
return
}
this.textContent = ""
for (let post of posts) {
this.append(new MicroPost(post))
}
}
}
customElements.define("micro-feed", MicroFeed);
/*
takes a callback function that well call with progress updates
list all the files in our friends microblog folder
return a list of the 30 most recent
*/
async function loadFeed(progressCallback = () => {}) {
//get our list of contacts, including ourselves
var contacts = await getContacts();
if (contacts.length === 0) return [];//no contacts, return a blank feed
var numLoaded = 0;//how many friends weve loaded so far
var numToLoad = contacts.length;//how many friends were going to load
progressCallback({label: "Querying...", progress: 0});
//do a directory listing in the microblog folder of all our contacts hyperdrives
var feedsFiles = await Promise.all(contacts.map(async contact => {
//here were doing a directory listing from our hyperdrive
let files = await beaker.hyperdrive.query({
path: "/microblog/*",//everything in the microblog folder
drive: contact.url,//our friends hyperdrive
sort: "ctime",//sort the results by creation time?
reverse: true,//biggest most recent at the top
limit: 30//just 30 results please
})
//report progress that we listed one more friends hyperdrive
progressCallback({label: "Querying...", progress: (++numLoaded) / numToLoad})
//return the friends files
return files;
}));
//Promise.all returns a single promise, feedsFiles
var files = feedsFiles.flat();//not sure
files.sort((a, b) => b.stat.ctime - a.stat.ctime);//sort by file date, "stat" is file metadata
//slice out the first (most recent) 30, on each call this inline function
return files.slice(0, 30).map(file => ({
author: contacts.find(c => c.url === file.drive),//go from hyperdrive hash to author name
url: file.url,
filename: file.url.split("/").filter(Boolean).pop(),//split, not sure on filter Boolean
ctime: file.stat.ctime,//creation time
content: undefined//well fill this in later?
}));
}
//get a list of our beaker contacts, including ourselves
async function getContacts() {
//get the full list of contacts in the address book
var contacts = await beaker.contacts.list();
//if we loaded our profile, but we cant seem to find our url in the contacts we got back
if (profile && !contacts.find(c => c.url === profile.url)) {
contacts.push(profile);//add our profile to the contacts list manually
}
//return our list of contacts that includes ourselves
return contacts;
}
// ---- <micro-post> ----
//now lets define the <micro-post> tag
class MicroPost extends HTMLElement {
constructor(post) {
super();
this.load(post);
}
async load(post) {
this.append(
element("a", {class: "thumb", href: post.author.url}, [
element("img", {src: `${post.author.url}thumb`}, [])
])
);
let day = (new Date(post.ctime)).toLocaleDateString();
this.append(
element("div", {class: "meta"}, [
element("a", {href: post.url, title: post.filename}, [post.filename]),
" ",
day
])
);
var contentDiv = element("div", {class: "content"}, ["Loading..."]);
this.append(contentDiv);
try {
await loadPost(post);
contentDiv.innerHTML = "";
if (post.content.img) {
contentDiv.append(
element("img", {src: post.content.img}, [])
);
} else if (post.content.video) {
contentDiv.append(
element("video", {controls: true}, [
element("source", {src: post.content.video}, [])
])
);
} else if (post.content.audio) {
contentDiv.append(
element("audio", {controls: true}, [
element("source", {src: post.content.audio}, [])
])
);
} else if (post.content.iframe) {
contentDiv.append(
element(
"iframe",
{
class: "content",
csp: `default-src 'self' 'unsafe-inline';`,
sandbox: `allow-forms allow-scripts allow-popups allow-popups-to-escape-sandbox`,
src: post.content.iframe
},
[])
);
} else if (post.content.html) {
contentDiv.innerHTML = post.content.html;
} else if (post.content.txt) {
contentDiv.append(
element("pre", {}, [post.content.txt])
);
}
} catch (e) {
console.error("Failed to render", post);
console.error(e);
return;
}
}
}
customElements.define("micro-post", MicroPost);
async function loadPost(post) {
try {
if (/\.(png|jpe?g|gif|svg)$/i.test(post.url)) {
post.content = {img: post.url};
} else if (/\.(mp4|webm|mov)/i.test(post.url)) {
post.content = {video: post.url};
} else if (/\.(mp3|ogg)/i.test(post.url)) {
post.content = {audio: post.url};
} else {
let txt = await beaker.hyperdrive.readFile(post.url)
if (/\.html?$/i.test(post.url)) {
post.content = {iframe: post.url};
} else if (/\.md$/i.test(post.url)) {
post.content = {html: beaker.markdown.toHTML(txt)};
} else {
post.content = {txt: txt};
}
}
} catch (e) {
console.error("Failed to read", post.url);
console.error(e);
}
}
/*
heres the element() function, which lets you easily build a new HTML element
tag is a html tag name, like "img"
attributes is an object with keys and values are like src as in <img src="v1">
children is an array of elements made with other calls to element()
*/
function element(tag, attributes, children) {
//create a new HTML element
var e = document.createElement(tag);//e is the element well fill and return
//attach the given attributes
for (let k in attributes) {//"for in" because attributes is an object
if (typeof attributes[k] === "function") {
e.addEventListener(k, attributes[k]);
} else {
e.setAttribute(k, attributes[k]);
}
}
//append the given child elements
for (let child of children) e.append(child);//"for of" because children is an array
return e;//return the element we made
}
</script>
<p><i>micro-composer:</i></p>
<micro-composer></micro-composer>
<p><i>micro-feed:</i></p>
<micro-feed></micro-feed>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment