├── src
├── vite-env.d.ts
├── style.css
└── main.ts
├── vite.config.ts
├── .gitignore
├── tsconfig.json
├── package.json
├── public
├── postTemplate.html
├── indexTemplate.html
├── about.html
└── templateStyle.css.txt
├── index.html
├── plugin-connected.svg
├── plugin-connecting.svg
├── plugin-disconnected.svg
├── favicon.svg
├── logo-dark.svg
└── README.md
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
31 | My name is Jess Martin and this is 32 | my mumblr. 33 |
34 |37 | Mumblr is a microblog, in the vein 38 | of Tumblr. It's a tribute to a 39 | simpler time of blogging, a few steps away from the noise and 40 | platform paranoia of twitter. It's a quiet place to share my 41 | thoughts without distraction. 42 |
43 |
46 | Strange Software
47 | Also, mumblr is a way for me to learn about decentralized
48 | applications. The software that powers this blog,
49 | mumblr, is a
50 | little bizarre. If you'd like to learn more about it, I recommend
51 | you head to
52 | the README in GitHub.
55 |
33 |
34 | I've been livestreaming most of my work on mumblr. You can view past livestreams in [this playlist](https://www.youtube.com/playlist?list=PLR5cUEyS7wdhcv8v2KDOwRkyP9EPKOHmA).
35 |
36 | ## FAQ
37 |
38 | Q: What's IPFS?
39 |
40 | A: [IPFS](https://ipfs.io) stands for Interplanetary File System. It's a distributed, peer-to-peer, content-addressed storage protocol. If you want a simple analogy, it's BitTorrent meets the Internet Archive. Going a bit deeper, it's a powerful and future-proof way of storing content on the web.
41 |
42 | Q: Is the "m" in mumblr capitalized or lowercase?
43 |
44 | A: Mumblr prefers to be capitalized when at the beginning of a sentence. When occurring within a sentence, mumblr prefers to be lowercase.
45 |
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | body,
2 | html,
3 | form {
4 | margin: 0;
5 | padding: 0;
6 | }
7 |
8 | body {
9 | --ff: "Barlow", system-ui, "Helvetica", sans-serif;
10 |
11 | --bg: #d9d9d9;
12 | --frost: #e9e9e9;
13 | --paper: #f9f9f9;
14 | --purple: #4a4dd4;
15 | --fg: #222;
16 | --light: #777;
17 | }
18 |
19 | .block {
20 | padding: 8px 12px;
21 | }
22 |
23 | .light {
24 | color: var(--light);
25 | font-size: 0.825rem;
26 | }
27 |
28 | body {
29 | background: var(--bg);
30 | }
31 |
32 | html {
33 | font-size: 18px;
34 | }
35 |
36 | body,
37 | button,
38 | input[type="text"],
39 | input[type="tel"],
40 | input[type="email"],
41 | input[type="submit"],
42 | textarea {
43 | font-size: 1rem;
44 | color: var(--fg);
45 | font-family: var(--ff);
46 | border: 0;
47 | margin: 0;
48 | outline: 0;
49 | box-sizing: border-box;
50 | }
51 |
52 | input[type="text"],
53 | input[type="tel"],
54 | input[type="email"],
55 | input[type="submit"],
56 | textarea {
57 | transition: background-color 0.2s, box-shadow 0.2s, color 0.2s;
58 | }
59 |
60 | textarea {
61 | height: 100%;
62 | flex-grow: 1;
63 | }
64 |
65 | input,
66 | textarea {
67 | display: block;
68 | margin: 12px 0;
69 | padding: 10px;
70 | border-radius: 6px;
71 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
72 | overflow: hidden;
73 | }
74 |
75 | ul,
76 | ol {
77 | margin: 0;
78 | padding-left: 0;
79 | list-style: none;
80 | }
81 |
82 | input[type="submit"] {
83 | -webkit-appearance: none;
84 | }
85 |
86 | a,
87 | button,
88 | input[type="submit"] {
89 | color: var(--fg);
90 | text-decoration: none;
91 | cursor: pointer;
92 | }
93 |
94 | button:hover:enabled,
95 | button:focus,
96 | input[type="submit"].card:hover:enabled,
97 | input[type="submit"].card:focus {
98 | cursor: pointer;
99 | opacity: 0.75;
100 | }
101 |
102 | body {
103 | max-width: 840px;
104 | margin: 0 auto;
105 | }
106 |
107 | header {
108 | display: flex;
109 | flex-direction: row;
110 | justify-content: space-between;
111 | align-items: center;
112 | padding: 0 12px;
113 | }
114 |
115 | .title {
116 | font-weight: bold;
117 | float: left;
118 | }
119 |
120 | #connect-disconnect {
121 | float: right;
122 | margin-top: 23px;
123 | margin-left: 12px;
124 | background: #fff url("/logo-dark.svg") no-repeat top left;
125 | background-size: 300px 20px;
126 | background-position: top 10px left -55px;
127 | padding-right: 110px;
128 | color: orange;
129 | border-radius: 6px;
130 | }
131 |
132 | #connect-disconnect-icon {
133 | width: 35px;
134 | }
135 |
136 | #connecting-icon {
137 | width: 35px;
138 | animation-name: spin;
139 | animation-duration: 2000ms;
140 | animation-iteration-count: infinite;
141 | animation-timing-function: linear;
142 | }
143 |
144 | #build-button {
145 | float: right;
146 | margin-left: 12px;
147 | margin-top: 23px;
148 | }
149 |
150 | #build-button.success {
151 | background-color: #4a4dd4;
152 | color: #fff;
153 | }
154 |
155 | .visit-site-link {
156 | float: right;
157 | margin-left: 12px;
158 | margin-top: 35px;
159 | }
160 |
161 | .visit-site-link:hover {
162 | text-decoration: underline;
163 | color: #4a4dd4;
164 | }
165 |
166 | .subtitle {
167 | font-size: 1rem;
168 | font-weight: normal;
169 | color: var(--light);
170 | float: right;
171 | margin-top: 36px;
172 | }
173 |
174 | input.title-input {
175 | font-weight: 700;
176 | font-size: 1.25rem;
177 | }
178 |
179 | .title-input,
180 | .body-input {
181 | width: 100%;
182 | }
183 |
184 | input.title-input:focus,
185 | textarea.body-input:focus {
186 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
187 | }
188 |
189 | textarea.body-input {
190 | height: 20em;
191 | resize: none;
192 | }
193 |
194 | .post-button {
195 | width: 100%;
196 | background: var(--frost);
197 | font-weight: 700;
198 | }
199 |
200 | .post-button:not([disabled]):hover {
201 | background-color: var(--purple);
202 | color: var(--paper);
203 | }
204 |
205 | footer {
206 | margin: 12px 0;
207 | text-align: left;
208 | width: 100%;
209 | }
210 |
211 | footer p {
212 | color: #999;
213 | font-size: 0.825rem;
214 | }
215 |
216 | @keyframes spin {
217 | from {
218 | transform:rotate(0deg);
219 | }
220 | to {
221 | transform:rotate(360deg);
222 | }
223 | }
--------------------------------------------------------------------------------
/public/templateStyle.css.txt:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | }
5 |
6 | body {
7 | --primary-bg: #fdfeff;
8 | --primary-text: #111111;
9 | --secondary-bg: #eeeef3;
10 | --secondary-text: #9b9b9b;
11 | --hover-bg: #dde1e5;
12 | --active-bg: #cdcfd2;
13 |
14 | --dark-primary-bg: #141516;
15 | --dark-primary-text: #ebebeb;
16 | --dark-secondary-bg: #30373a;
17 | --dark-secondary-text: #a4a7a9;
18 | --dark-hover-bg: #474c50;
19 | --dark-active-bg: #626569;
20 | }
21 |
22 | .dark {
23 | --primary-bg: var(--dark-primary-bg);
24 | --primary-text: var(--dark-primary-text);
25 | --secondary-bg: var(--dark-secondary-bg);
26 | --secondary-text: var(--dark-secondary-text);
27 | --hover-bg: var(--dark-hover-bg);
28 | --active-bg: var(--dark-active-bg);
29 | }
30 |
31 | @media (prefers-color-scheme: dark) {
32 | body:not(.light) {
33 | --primary-bg: var(--dark-primary-bg);
34 | --primary-text: var(--dark-primary-text);
35 | --secondary-bg: var(--dark-secondary-bg);
36 | --secondary-text: var(--dark-secondary-text);
37 | --hover-bg: var(--dark-hover-bg);
38 | --active-bg: var(--dark-active-bg);
39 | }
40 | }
41 |
42 | body {
43 | font-family: system-ui, sans-serif;
44 | color: var(--primary-text);
45 | background: var(--primary-bg);
46 |
47 | display: flex;
48 | flex-direction: column;
49 | min-height: 100vh;
50 | border-bottom: 8px solid #111111;
51 | }
52 |
53 | input,
54 | button,
55 | textarea {
56 | font-size: 1em;
57 | padding: 0.5em 0.8em;
58 | color: var(--primary-text);
59 | font-family: system-ui, sans-serif;
60 | tab-size: 4;
61 | }
62 |
63 | input::placeholder,
64 | textarea::placeholder {
65 | color: var(--secondary-text);
66 | }
67 |
68 | header,
69 | h1,
70 | main {
71 | width: calc(100% - 32px);
72 | max-width: 860px;
73 | margin: 1em auto;
74 | }
75 |
76 | header {
77 | display: flex;
78 | flex-direction: row;
79 | align-items: center;
80 | justify-content: space-between;
81 | }
82 |
83 | header .logo {
84 | font-weight: bold;
85 | }
86 |
87 | nav {
88 | display: flex;
89 | flex-direction: row-reverse;
90 | align-items: center;
91 | gap: 1em;
92 | }
93 |
94 | header a,
95 | header button {
96 | display: inline;
97 | cursor: pointer;
98 | color: var(--primary-text);
99 | background: transparent;
100 | border: 0;
101 | border-radius: 0;
102 | text-decoration: none;
103 | padding: 0.5em 0;
104 | }
105 |
106 | header a:hover,
107 | header button:hover {
108 | text-decoration: underline;
109 | }
110 |
111 | h1 {
112 | margin-top: 0.75em;
113 | margin-bottom: 0.25em;
114 | line-height: 1.4em;
115 | }
116 |
117 | main {
118 | margin-bottom: 3em;
119 | }
120 |
121 | .about p,
122 | .about li {
123 | max-width: 64ch;
124 | line-height: 1.5em;
125 | }
126 |
127 | .about a {
128 | color: inherit;
129 | }
130 |
131 | .about a:hover {
132 | background: var(--hover-bg);
133 | }
134 |
135 | form {
136 | position: relative;
137 | margin-bottom: 2em;
138 | overflow: hidden;
139 | }
140 |
141 | form input,
142 | form textarea {
143 | display: block;
144 | border-radius: 6px;
145 | border: 0;
146 | background: var(--hover-bg);
147 | width: 100%;
148 | box-sizing: border-box;
149 | }
150 |
151 | form textarea {
152 | min-height: 50vh;
153 | line-height: 1.5em;
154 | resize: vertical;
155 | }
156 |
157 | form input:hover,
158 | form input:focus,
159 | form textarea:hover,
160 | form textarea:focus {
161 | outline: 0;
162 | }
163 |
164 | form button[type="submit"] {
165 | border-radius: 6px;
166 | border: 0;
167 | color: var(--primary-bg);
168 | background: var(--primary-text);
169 | margin-top: 0.5em;
170 | float: right;
171 | cursor: pointer;
172 | }
173 |
174 | form button[type="submit"]:hover {
175 | background: var(--secondary-text);
176 | }
177 |
178 | .pageControls {
179 | float: right;
180 | display: flex;
181 | flex-direction: row;
182 | align-items: center;
183 | justify-content: flex-end;
184 | gap: 0.75em;
185 | }
186 |
187 | .pageControls a {
188 | color: var(--primary-text);
189 | text-decoration: none;
190 | }
191 |
192 | .pageControls a:hover {
193 | text-decoration: underline;
194 | }
195 |
196 | a.pageButton {
197 | display: flex;
198 | align-items: center;
199 | justify-content: center;
200 | font-size: 1.5em;
201 | height: 1.5em;
202 | width: 1.5em;
203 | border-radius: 50%;
204 | background: var(--secondary-bg);
205 | }
206 |
207 | a.pageButton:hover {
208 | background: var(--hover-bg);
209 | text-decoration: none;
210 | }
211 |
212 | .message {
213 | font-style: italic;
214 | color: var(--secondary-text);
215 | margin-bottom: 2em;
216 | }
217 |
218 | .update {
219 | margin-bottom: 2.5em;
220 | word-break: break-word;
221 | }
222 |
223 | .update .update-t {
224 | font-size: 14px;
225 | color: var(--secondary-text);
226 | margin-bottom: -0.5em;
227 | }
228 |
229 | .update .update-t a {
230 | color: var(--secondary-text);
231 | text-decoration: none;
232 | }
233 |
234 | .update .update-t a:hover {
235 | text-decoration: underline;
236 | }
237 |
238 | .update-t .relativestamp {
239 | margin-bottom: 6px;
240 | }
241 |
242 | /* update Markdown */
243 |
244 | .update h1,
245 | .update h2,
246 | .update h3 {
247 | margin: 0.75em 0 0.5em 0;
248 | line-height: 1.4em;
249 | }
250 |
251 | .update h1 {
252 | font-size: 1.75em;
253 | }
254 |
255 | .update h2 {
256 | font-size: 1.5em;
257 | }
258 |
259 | .update h3 {
260 | font-size: 1.2em;
261 | }
262 |
263 | .update h4,
264 | .update h5,
265 | .update h6 {
266 | font-size: 1em;
267 | }
268 |
269 | .update p,
270 | .update li {
271 | line-height: 1.5em;
272 | max-width: 64ch;
273 | }
274 |
275 | .update strike {
276 | color: var(--secondary-text);
277 | }
278 |
279 | .update img {
280 | max-width: 100%;
281 | max-height: 500px;
282 | border-radius: 6px;
283 | }
284 |
285 | .update a {
286 | color: var(--primary-text);
287 | text-decoration: underline;
288 | }
289 |
290 | .update ul,
291 | .update ol {
292 | padding-left: 3ch;
293 | }
294 |
295 | .update pre,
296 | .update code {
297 | background: var(--hover-bg);
298 | font-size: 1em;
299 | font-family: "IBM Plex Mono", "Menlo", "Monaco", monospace;
300 | /*
301 | * In Safari (2021), word-break: break-word from `.update` combined with
302 | * certain contents of tags that do not begin lines with whitespace
303 | * break line-wrapping behavior. This inversion of word-break works around
304 | * that Safari bug (or at least, what appears to be a browser bug, as
305 | * Chrome does not reproduce).
306 | */
307 | word-break: initial;
308 | }
309 |
310 | .update pre {
311 | border-radius: 6px;
312 | box-sizing: border-box;
313 | padding: 12px 8px;
314 | overflow-x: auto;
315 | }
316 |
317 | .update code {
318 | padding: 1px 5px;
319 | border-radius: 6px;
320 | }
321 |
322 | .update pre code {
323 | padding: 0;
324 | }
325 |
326 | .update blockquote {
327 | margin: 0;
328 | border-left: 4px solid var(--active-bg);
329 | padding-left: 1em;
330 | display: block;
331 | }
332 |
333 | @media only screen and (min-width: 760px) {
334 | .update {
335 | display: flex;
336 | flex-direction: row;
337 | align-items: flex-start;
338 | justify-content: space-between;
339 | margin-bottom: 1.5em;
340 | }
341 | .update-t {
342 | flex-grow: 0;
343 | flex-shrink: 0;
344 | width: 134px;
345 | margin-top: 3px;
346 | }
347 | .update-s {
348 | width: 0;
349 | flex-grow: 1;
350 | flex-shrink: 1;
351 | }
352 | .update-s :first-child {
353 | margin-top: 0;
354 | }
355 | }
356 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import "./style.css";
2 | import * as wn from "webnative";
3 | import FileSystem from "webnative/fs/index";
4 | import { unified } from "unified";
5 | import remarkParse from "remark-parse";
6 | import remarkRehype from "remark-rehype";
7 | import rehypeStringify from "rehype-stringify";
8 | import remarkFrontmatter from "remark-frontmatter";
9 | import extract from "remark-extract-frontmatter";
10 | import { parse } from "yaml";
11 | import ConnectedIcon from "../plugin-connected.svg";
12 | import DisconnectedSvg from "../plugin-disconnected.svg";
13 | import { BaseLink } from "webnative/fs/types";
14 |
15 | const permissions = {
16 | app: {
17 | name: "mumblr",
18 | creator: "Jess Martin",
19 | },
20 | fs: {
21 | private: [wn.path.directory("Posts")], // This will be `private/Posts`
22 | public: [wn.path.directory("Posts"), wn.path.directory("Apps", "mumblr")], // This will be `public/Posts`
23 | },
24 | };
25 |
26 | const state = await wn
27 | .initialise({
28 | permissions: permissions,
29 | })
30 | .catch((err) => {
31 | switch (err) {
32 | case wn.InitialisationError.InsecureContext:
33 | // TODO: Notify the user that the app is not secure
34 | case wn.InitialisationError.UnsupportedBrowser:
35 | // TODO: Notify the user of the error
36 | }
37 | });
38 |
39 | // Give me maximum debuggage
40 | // TODO: Disable when in production
41 | wn.setup.debug({ enabled: true });
42 |
43 | let fs: FileSystem | undefined;
44 | const connectionStatus = document.querySelector(
45 | "#connect-disconnect"
46 | )!;
47 |
48 | switch (state?.scenario) {
49 | case wn.Scenario.AuthCancelled:
50 | console.log("Auth cancelled by user");
51 | break;
52 |
53 | case wn.Scenario.AuthSucceeded:
54 | // New permissions have been granted
55 | case wn.Scenario.Continuation:
56 | // Great success! We can now use the filesystem!
57 | console.log("Connected");
58 | connectionStatus.innerHTML = `
`;
59 | fs = state.fs; // Load the filesystem
60 | connectionStatus.addEventListener("click", async function () {
61 | wn.leave().then(() => {
62 | console.log("Disconnected");
63 | });
64 | });
65 | // Enable the UI
66 | document.querySelector("#post")!.disabled = false;
67 | document.querySelector("#body-input")!.disabled =
68 | false;
69 | break;
70 |
71 | case wn.Scenario.NotAuthorised:
72 | connectionStatus.innerHTML = `
`;
73 | connectionStatus.addEventListener("click", async function () {
74 | console.log("Redirected to lobby");
75 | wn.redirectToLobby(permissions);
76 | });
77 | console.log("Not connected");
78 | break;
79 | }
80 |
81 | const publishBtn = document.querySelector("#post")!;
82 |
83 | publishBtn.addEventListener("click", async function () {
84 | const body = document.querySelector(".body-input")!;
85 |
86 | // If body is blank, don't publish
87 | if (body.value.trim().length === 0) {
88 | return;
89 | }
90 |
91 | publishBtn.disabled = true;
92 | publishBtn.value = "Publishing...";
93 |
94 | const postTimestamp = Date.now();
95 | const frontmatter = `---
96 | postedAt: ${postTimestamp}
97 | ---\n`;
98 | const postContent = frontmatter + body.value;
99 |
100 | if (fs !== undefined) {
101 | const filePath = wn.path.file("public", "Posts", `${postTimestamp}.md`);
102 | await fs.add(filePath, postContent).then(() => {
103 | console.log("file saved");
104 | });
105 | await fs
106 | .publish()
107 | .then(() => {
108 | console.log("file system published");
109 | publishBtn.value = "Publish successful!";
110 | enablePublishBtn();
111 | })
112 | .catch((err) => {
113 | console.log(err);
114 | });
115 | } else {
116 | console.log("no file system");
117 | }
118 | });
119 |
120 | async function enablePublishBtn() {
121 | await new Promise((resolve) => setTimeout(resolve, 2000));
122 | publishBtn.value = "Publish";
123 | publishBtn.disabled = false;
124 | }
125 |
126 | const buildSiteButton =
127 | document.querySelector("#build-button")!;
128 |
129 | buildSiteButton.addEventListener("click", async function () {
130 | console.log("attempting to build site");
131 | buildSiteButton.disabled = true;
132 | buildSiteButton.value = "Building...";
133 |
134 | // Read the most recent Markdown files
135 | let markdownPosts = [];
136 | // TODO: Make this into a "break" statement
137 | if (fs !== undefined) {
138 | const linksObject = await fs.ls(wn.path.directory("public", "Posts"));
139 | const links = Object.entries(linksObject);
140 |
141 | // If file is a directory, skip it
142 | const files = await Promise.all(
143 | links.filter((link) => (link[1] as BaseLink).isFile)
144 | );
145 |
146 | const posts = await Promise.all(
147 | files.map(([name, _]) => {
148 | const filePath = wn.path.file("public", "Posts", name);
149 | return fs?.cat(filePath);
150 | })
151 | );
152 |
153 | // Convert the FileContentRaw, which is a uint8array to string
154 | const filesContents = [];
155 | const decoder = new TextDecoder();
156 | for (const post of posts) {
157 | filesContents.push(decoder.decode(post as Uint8Array));
158 | }
159 |
160 | // Convert the string to micromark
161 | const parsedPosts = await Promise.all(
162 | filesContents.map((content) => {
163 | return unified()
164 | .use(remarkParse)
165 | .use(remarkFrontmatter)
166 | .use(extract, { yaml: parse })
167 | .use(remarkRehype)
168 | .use(rehypeStringify)
169 | .process(content);
170 | })
171 | );
172 |
173 | // Ignore files that don't have postedAt
174 | markdownPosts = parsedPosts.filter((post) => {
175 | return post.data.postedAt !== undefined;
176 | });
177 |
178 | // Sort the files by postedAt
179 | markdownPosts.sort((a, b) => {
180 | return (
181 | new Date(b.data.postedAt as number).getTime() -
182 | new Date(a.data.postedAt as number).getTime()
183 | );
184 | });
185 | console.log(markdownPosts);
186 | // Build the HTML/CSS
187 | const parser = new DOMParser();
188 | const serializer = new XMLSerializer();
189 |
190 | // Load the template HTML file locally
191 | const indexTemplate = await fetch("/indexTemplate.html");
192 | const indexTemplateString = await indexTemplate.text();
193 | const indexTemplateDoc = parser.parseFromString(
194 | indexTemplateString,
195 | "text/html"
196 | );
197 |
198 | const postTemplate = await fetch("/postTemplate.html");
199 | const postTemplateString = await postTemplate.text();
200 |
201 | // Generate the HTML for each markdown post and insert into template html
202 | const blogPostsDiv = document.createElement("div");
203 | // iterate over the posts
204 | for await (const markdownPost of markdownPosts) {
205 | const postTimestamp = markdownPost.data.postedAt as number;
206 | const postDate = new Date(markdownPost.data.postedAt as number);
207 | const postDateString = postDate.toLocaleString("en-us", {
208 | year: "numeric",
209 | month: "numeric",
210 | day: "numeric",
211 | });
212 | // build up blogPostHtml
213 | const postDiv = document.createElement("div");
214 | postDiv.innerHTML = `
215 |
216 |
217 | ${postDateString}
218 | ${
219 | postDate.getHours() + ":" + postDate.getMinutes()
220 | }
221 |
222 |
223 | ${markdownPost.value}
224 |
225 |
226 | `;
227 |
228 | // Create a page for this post
229 | const postDoc = parser.parseFromString(postTemplateString, "text/html");
230 | const innerPostDiv = postDoc.querySelector("div.feed");
231 | innerPostDiv?.appendChild(postDiv);
232 |
233 | // Write this page to IPFS
234 | const updatesHtmlPath = wn.path.file(
235 | "public",
236 | "Apps",
237 | "mumblr",
238 | "updates",
239 | `${postTimestamp}`,
240 | "index.html"
241 | );
242 | const postDocString = serializer.serializeToString(postDoc);
243 | await fs.add(updatesHtmlPath, postDocString).then(() => {
244 | console.log("blog post added");
245 | });
246 |
247 | // Add the post to the HTML of all posts
248 | blogPostsDiv.appendChild(postDiv);
249 | }
250 | const feedDiv = indexTemplateDoc.querySelector("div.feed");
251 | feedDiv?.appendChild(blogPostsDiv);
252 |
253 | // Write the static site to IPFS
254 | // Write the template HTML to index.html
255 | const indexHtmlPath = wn.path.file(
256 | "public",
257 | "Apps",
258 | "mumblr",
259 | "index.html"
260 | );
261 | const templateDocString = serializer.serializeToString(indexTemplateDoc);
262 |
263 | await fs.add(indexHtmlPath, templateDocString).then(() => {
264 | console.log("index page added");
265 | });
266 |
267 | // Write the stylesheet to IPFS
268 | const stylesheet = await fetch("/templateStyle.css.txt");
269 | const stylesheetString = await stylesheet.text();
270 |
271 | const stylesheetPath = wn.path.file(
272 | "public",
273 | "Apps",
274 | "mumblr",
275 | "style.css"
276 | );
277 | await fs.add(stylesheetPath, stylesheetString).then(() => {
278 | console.log("stylesheet added");
279 | });
280 |
281 | // Write the about page to IPFS
282 | const aboutPage = await fetch("/about.html");
283 | const aboutPageString = await aboutPage.text();
284 |
285 | const aboutPagePath = wn.path.file(
286 | "public",
287 | "Apps",
288 | "mumblr",
289 | "about",
290 | "index.html"
291 | );
292 | await fs.add(aboutPagePath, aboutPageString).then(() => {
293 | console.log("about page added");
294 | });
295 |
296 | // Publish the static html to IPFS
297 | await fs
298 | .publish()
299 | .then(() => {
300 | console.log("file system published");
301 | })
302 | .catch((err) => {
303 | console.log(err);
304 | });
305 |
306 | // Update the UI and show a simple IPFS Web Gateway URL where the site can be viewed
307 | const username = await wn.authenticatedUsername();
308 | const ipfsUrl = `https://${username}.files.fission.name/p/Apps/mumblr/`;
309 |
310 | buildSiteButton.value = "Build successful!";
311 | buildSiteButton.classList.add("success");
312 | const visitSiteLink = document.createElement("a");
313 | visitSiteLink.href = ipfsUrl;
314 | visitSiteLink.innerText = "Visit Site";
315 | visitSiteLink.className = "visit-site-link";
316 | buildSiteButton.insertAdjacentHTML("afterend", visitSiteLink.outerHTML);
317 | await enableBuildSiteButton();
318 | }
319 | });
320 |
321 | async function enableBuildSiteButton() {
322 | await new Promise((resolve) => setTimeout(resolve, 5000));
323 | buildSiteButton.disabled = false;
324 | buildSiteButton.classList.remove("success");
325 | buildSiteButton.value = "Build";
326 | }
327 |
--------------------------------------------------------------------------------