@postpostscript/readmeManager: Edit Val Readmes With Persistent Drafts
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);
}