Created
May 25, 2020 00:13
-
-
Save zootella/aea0e857af0be05469ce7b399617ba75 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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