serve a static website from your pds

Improve UI

-1
src/lib/index.ts
··· 1 - // place files you want to import through the `$lib` alias in this folder.
+7
src/routes/+layout.svelte
··· 9 9 </svelte:head> 10 10 11 11 {@render children?.()} 12 + 13 + <style> 14 + :global { 15 + @import "~/styles/reset.css"; 16 + @import "~/styles/theme.css"; 17 + } 18 + </style>
+6 -2
src/routes/~/+layout.svelte
··· 18 18 </script> 19 19 20 20 <header class="header"> 21 - <h1><a href="/~/">hello, {data.name}</a></h1> 22 - <button onclick={logOut}>log out</button> 21 + <h1><a href="/~/">athost</a></h1> 22 + <button popovertarget="menu">menu</button> 23 + <div id="menu" class="menu" popover> 24 + <p>hello, {data.name}</p> 25 + <button onclick={logOut}>log out</button> 26 + </div> 23 27 </header> 24 28 25 29 <div class="body">
+2 -2
src/routes/~/+layout.ts
··· 12 12 export const load: LayoutLoad = async () => { 13 13 try { 14 14 const did = localStorage.getItem("did") as any; 15 - const session = await getSession(did, { allowStale: true }); 15 + const session = await getSession(did); 16 16 17 17 const handler = new OAuthUserAgent(session); 18 18 const rpc = new Client({ handler }); ··· 23 23 24 24 if (isXRPCErrorPayload(data)) throw new Error("couldn't load profile"); 25 25 26 - return { session, did, name: data.displayName }; 26 + return { session, pds: session.info.aud, did, name: data.displayName }; 27 27 } catch (e) { 28 28 console.error(e); 29 29 redirect(303, "/");
+4 -4
src/routes/~/+page.svelte
··· 15 15 const form = e.currentTarget; 16 16 const formdata = new FormData(e.currentTarget); 17 17 18 - const name = formdata.get("name"); 19 - if (typeof name !== "string") throw new Error("invalid name"); 18 + const rkey = formdata.get("rkey"); 19 + if (typeof rkey !== "string") throw new Error("invalid rkey"); 20 20 21 21 const record = { 22 22 assets: [], ··· 24 24 }; 25 25 26 26 await rpc.post("com.atproto.repo.createRecord", { 27 - input: { repo: data.did, collection: "com.jakelazaroff.test", rkey: name, record }, 27 + input: { repo: data.did, collection: "com.jakelazaroff.test", rkey, record }, 28 28 }); 29 29 await invalidate("collection:com.jakelazaroff.test"); 30 30 form.reset(); ··· 32 32 </script> 33 33 34 34 <form onsubmit={createWebsite}> 35 - <input type="text" name="name" minlength={1} maxlength={512} pattern="[A-Za-z0-9.\-]+" /> 35 + <input type="text" name="rkey" minlength={1} maxlength={512} pattern="[A-Za-z0-9.\-]+" /> 36 36 <button>create</button> 37 37 </form> 38 38
+44 -13
src/routes/~/sites/[name]/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { invalidate } from "$app/navigation"; 2 + import { goto, invalidate } from "$app/navigation"; 3 3 4 4 import type {} from "@atcute/atproto"; 5 5 import { isXRPCErrorPayload } from "@atcute/client"; ··· 10 10 11 11 const rpc = client(data.session); 12 12 13 - async function deleteWebsite(rkey: string) { 13 + async function deleteBundle(e: SubmitEvent & { currentTarget: HTMLFormElement }) { 14 + e.preventDefault(); 15 + const form = e.currentTarget; 16 + const formdata = new FormData(form); 17 + 18 + const rkey = formdata.get("rkey"); 19 + if (typeof rkey !== "string") throw new Error("invalid rkey"); 20 + 14 21 await rpc.post("com.atproto.repo.deleteRecord", { 15 22 input: { repo: data.did, collection: "com.jakelazaroff.test", rkey }, 16 23 }); 24 + goto("/~/"); 17 25 } 18 26 19 27 async function deployBundle(e: SubmitEvent & { currentTarget: HTMLFormElement }) { ··· 21 29 const form = e.currentTarget; 22 30 const formdata = new FormData(form); 23 31 24 - const name = formdata.get("name"); 25 - if (typeof name !== "string") throw new Error("invalid name"); 32 + const rkey = formdata.get("rkey"); 33 + if (typeof rkey !== "string") throw new Error("invalid rkey"); 34 + 35 + let description = formdata.get("description"); 36 + if (typeof description !== "string" || !description) description = "Uploaded on website"; 26 37 27 38 const files = formdata.getAll("files"); 28 39 const assets: { path: string; file: any }[] = []; ··· 38 49 }); 39 50 } 40 51 41 - const record = { assets, createdAt: new Date().toISOString() }; 52 + const record = { description, assets, createdAt: new Date().toISOString() }; 42 53 await rpc.post("com.atproto.repo.putRecord", { 43 - input: { repo: data.did, collection: "com.jakelazaroff.test", rkey: name, record }, 54 + input: { repo: data.did, collection: "com.jakelazaroff.test", rkey, record }, 44 55 }); 45 56 if (isXRPCErrorPayload(data)) throw new Error("couldn't deploy"); 57 + 58 + invalidate(`rkey:${rkey}`); 46 59 } 47 60 </script> 48 61 49 62 <header class="header"> 50 63 <h2>{params.name}</h2> 51 - <button onclick={() => deleteWebsite(params.name)}>delete</button> 52 64 </header> 53 65 54 - <ul> 55 - {#each data.record.value.assets as asset} 56 - <li>{asset.path}</li> 57 - {/each} 58 - </ul> 66 + <details> 67 + <summary> 68 + <span>{data.record.value.description || data.record.cid}</span> 69 + <time>{data.record.value.createdAt}</time> 70 + </summary> 71 + <ul> 72 + {#each data.record.value.assets as asset} 73 + <li> 74 + <span>{asset.path}</span> 75 + <a 76 + target="_blank" 77 + href="{data.pds}xrpc/com.atproto.sync.getBlob?did={data.did}&cid={asset.file.ref.$link}" 78 + >open</a 79 + > 80 + </li> 81 + {/each} 82 + </ul> 83 + </details> 59 84 60 85 <form onsubmit={deployBundle}> 61 - <input type="hidden" name="name" value={params.name} /> 86 + <input type="hidden" name="rkey" value={params.name} /> 87 + <input name="description" /> 62 88 <input type="file" name="files" /> 63 89 <button>upload</button> 64 90 </form> 91 + 92 + <form onsubmit={deleteBundle}> 93 + <input name="rkey" pattern={params.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} /> 94 + <button>delete</button> 95 + </form>
+1 -1
src/routes/~/sites/[name]/+page.ts
··· 9 9 configure(); 10 10 11 11 export const load: PageLoad = async ({ parent, params, depends }) => { 12 - depends("collection:com.jakelazaroff.test"); 12 + depends(`rkey:${params.name}`); 13 13 14 14 try { 15 15 const { did, session } = await parent();
+60
src/styles/reset.css
··· 1 + *, 2 + *::before, 3 + *::after { 4 + box-sizing: border-box; 5 + } 6 + 7 + * { 8 + margin: 0; 9 + padding: 0; 10 + } 11 + 12 + html { 13 + scrollbar-gutter: stable; 14 + } 15 + 16 + body { 17 + line-height: 1.5; 18 + } 19 + 20 + :where(img, picture, video, canvas, svg) { 21 + display: block; 22 + max-inline-size: 100%; 23 + } 24 + 25 + :where(input, button, textarea, select) { 26 + font: inherit; 27 + letter-spacing: inherit; 28 + word-spacing: inherit; 29 + color: currentColor; 30 + } 31 + 32 + :where(p, h1, h2, h3, h4, h5, h6) { 33 + overflow-wrap: break-word; 34 + } 35 + 36 + :where(ol, ul) { 37 + list-style: none; 38 + } 39 + 40 + :not([class]) { 41 + &:where(h1, h2, h3, h4, h5, h6) { 42 + margin-block: 0.75em; 43 + line-height: 1.25; 44 + text-wrap: balance; 45 + letter-spacing: -0.05ch; 46 + } 47 + 48 + &:where(p, ol, ul) { 49 + margin-block: 1em; 50 + } 51 + 52 + &:where(ol, ul) { 53 + padding-inline-start: 1.5em; 54 + list-style: revert; 55 + } 56 + 57 + &:where(li) { 58 + margin-block: 0.5em; 59 + } 60 + }
src/styles/supreme-variable.woff2

This is a binary file and will not be displayed.

+9
src/styles/theme.css
··· 1 + @font-face { 2 + font-family: "Supreme"; 3 + src: url("./supreme-variable.woff2") format("woff2"); 4 + font-weight: 100 1000; 5 + } 6 + 7 + :root { 8 + font-family: "Supreme", sans-serif; 9 + }