@postpostscript/readmeManager: Edit Val Readmes With Persistent Drafts

edit this readme

image.png

image.png

Todo:

  • Upload images
  • Autosave/save without reloading page
  • Ctrl+S
  • Multiple draft versions
  • Switch to dark codemirror theme which has markdown styling
  • Allow for checking checkboxes in preview
    • View with just the preview
  • Ability to favorite vals on the Home page
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/** @jsxImportSource https://esm.sh/hono/jsx */
import { fetchPaginatedData } from "https://esm.town/v/nbbaier/fetchPaginatedData?v=49";
import { api } from "https://esm.town/v/pomdtr/api?v=12";
import { extractValInfo } from "https://esm.town/v/pomdtr/extractValInfo?v=26";
import { authMiddlewareCookie, type HonoEnv } from "https://esm.town/v/postpostscript/authMiddleware";
import { html } from "https://esm.town/v/postpostscript/html";
import { updateValReadme } from "https://esm.town/v/postpostscript/updateValReadme";
import { API_URL } from "https://esm.town/v/std/API_URL?v=5";
import { blob } from "https://esm.town/v/std/blob?v=11";
import { type Context, Hono } from "npm:hono";
const app = new Hono<HonoEnv>();
app.use(
"*",
authMiddlewareCookie({
optional: true,
}),
);
export const DRAFT_PREFIX = "readmeManager:draft";
const styles = `
img {
max-width: 100%;
}
`;
app.all("/:author/:name", async (c) => {
const { pathname } = new URL(c.req.url);
const author = c.req.param("author");
const name = c.req.param("name");
let { id, readme } = await api(`/v1/alias/${author}/${name}`);
const blobID = `${DRAFT_PREFIX}:${id}`;
let draft;
if (c.get("auth")) {
if (c.req.method === "POST") {
const formData = await c.req.formData();
const submitType = formData.get("submit");
if (formData.get("readme")) {
if (submitType === "publish") {
await updateValReadme(id, formData.get("readme"));
return c.redirect(`/${author}/${name}`);
} else {
await blob.setJSON(blobID, formData.get("readme"));
return c.redirect(`/${author}/${name}?draft=1`);
}
}
} else {
draft = await blob.getJSON(blobID);
}
if (c.req.query("draft") && draft) {
readme = draft;
}
}
return c.html(
<html data-bs-theme="dark">
<head>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
crossorigin="anonymous"
/>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.4.0/github-markdown.min.css"
rel="stylesheet"
/>
<script
type="module"
src="https://cdn.jsdelivr.net/npm/code-mirror-web-component@0.0.16/dist/code-mirror.js"
>
</script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script
type="module"
dangerouslySetInnerHTML={{
__html: html`
import debounce from 'https://esm.sh/debounce';
async function render(text) {
window.result.innerHTML = await marked.parse(text);
document.getElementById('readme-input').value = text
}
document.addEventListener("DOMContentLoaded", () => {
const $editor = document.createElement('code-mirror')
$editor.setAttribute('id', 'editor')
$editor.setAttribute('theme', 'dracula')
$editor.setAttribute('language', 'markdown')
$editor.setAttribute('code', decodeURIComponent("${encodeURIComponent(readme ?? "")}"))
$editor.setAttribute('class', 'h-100')
$editor.addEventListener("code-change", (e) => {
render(e.detail.code)
})
render($editor.getAttribute('code'));
document.getElementById('editor-col').appendChild($editor)
})

Expanded search endpoint

This is a mirrored version of the Val Town API /search/vals endpoint, meant mostly for use with pomdtr's vscode extension. It allows for more sophisticated filtering of the results of a search.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { fetchPaginatedData } from "https://esm.town/v/nbbaier/fetchPaginatedData";
import { paginatedResponse } from "https://esm.town/v/nbbaier/paginatedResponse";
import { Hono } from "npm:hono";
const app = new Hono();
app.get("/", async c => {
const req = c.req;
const params = req.queries();
const endpoint = `https://api.val.town/v1/search/vals?query=${params.q}`;
// TODO figure out a way to pass a user's valtown here when via the vscode extension
const data = await fetchPaginatedData(endpoint);
// TODO add more filters (search params)
let vals = data;
if (params.name) { vals = vals.filter(val => val.name.includes(params.name)); }
if (params.user) { vals = vals.filter(val => val.author.username === `@${params.user}`); }
if (params.privacy) { vals = vals.filter(val => val.privacy === params.privacy); }
// TODO add sorting
return paginatedResponse(req, vals);
});
export default app.fetch;

Empty Val Utils

Handy utility functions to see if you have vals with no code lying around your account and to delete them (is you want to).

Usage

listEmptyVals

Create valimport {listEmptyVals } from "https://esm.town/v/nbbaier/deleteEmptyVals"; console.log(await listEmptyVals(<user_id>))

deleteEmptyVals

Create valimport {listEmptyVals } from "https://esm.town/v/nbbaier/deleteEmptyVals"; await deleteEmptyVals(<user_id>)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { fetchPaginatedData } from "https://esm.town/v/nbbaier/fetchPaginatedData";
import { deleteVal } from "https://esm.town/v/neverstew/deleteVal";
export async function listEmptyVals(id: string) {
const token = Deno.env.get("valtown");
const res = await fetchPaginatedData(`https://api.val.town/v1/users/${id}/vals`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.filter((v) => (v.code.length === 0)).map(v => v.name);
}
export async function deleteEmptyVals(id: string) {
const token = Deno.env.get("valtown");
const res = await fetchPaginatedData(`https://api.val.town/v1/users/${id}/vals`, {
headers: { Authorization: `Bearer ${token}` },
});
const vals = res.filter((v) => (v.code.length === 0)).forEach(
async v => {
try {
const result = await deleteVal({
token,
id: v.id,
});
if (result.status === 204) {
console.log(`Successfully deleted val: ${v.name}`);
}
} catch (error) {
console.log(error);
}
},
);
}

filterVals

This val exports a utility function that returns a list of all a user's val, filtered by a callback function.

Example

Get the names of all your private vals:

Create valimport { filterVals } from "https://esm.town/v/nbbaier/filterVals"; const id = <user_id> const vals = await filterVals(id, (v) => v.privacy === "private") const privateValNames = vals.map(v => v.name);
1
2
3
4
5
6
7
8
9
import { fetchPaginatedData } from "https://esm.town/v/nbbaier/fetchPaginatedData";
export async function filterVals(id: string, filter: (value: any, index: number, array: any[]) => unknown) {
const token = Deno.env.get("valtown");
const res = await fetchPaginatedData(`https://api.val.town/v1/users/${id}/vals`, {
headers: { Authorization: `Bearer ${token}` },
});
return res.filter(filter);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { fetchPaginatedData } from "https://esm.town/v/nbbaier/fetchPaginatedData";
import { paginatedResponse } from "https://esm.town/v/nbbaier/paginatedResponse";
import { API_URL } from "https://esm.town/v/std/API_URL?v=5";
const id = Deno.env.get("USER_ID");
const vals = await fetchPaginatedData(
`${API_URL}/v1/users/${id}/vals`,
{
headers: { Authorization: `Bearer ${Deno.env.get("valtown")}` },
},
);
export default async function(req: Request): Promise<Response> {
return Response.json({ url: `${API_URL}/v1/users/${id}/vals` }); // paginatedResponse(req, vals, 100);
}
1
Next