serve a static website from your pds

Improve appearance

+20
src/components/Button.svelte
··· 1 + <script lang="ts"> 2 + import type { Snippet } from "svelte"; 3 + 4 + interface Props { 5 + children: Snippet; 6 + } 7 + 8 + let { children }: Props = $props(); 9 + </script> 10 + 11 + <button class="button"> 12 + {@render children()} 13 + </button> 14 + 15 + <style> 16 + .button { 17 + border: none; 18 + border-radius: var(--radius-md); 19 + } 20 + </style>
+38
src/lib/date.ts
··· 1 + export function getRelativeTime(date: Date): string { 2 + const now = new Date(); 3 + const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); 4 + 5 + // future dates 6 + if (diffInSeconds < 0) { 7 + const absDiff = Math.abs(diffInSeconds); 8 + 9 + if (absDiff < 60) return "in a few seconds"; 10 + if (absDiff < 3600) return `in ${Math.floor(absDiff / 60)} minutes`; 11 + if (absDiff < 86400) return `in ${Math.floor(absDiff / 3600)} hours`; 12 + if (absDiff < 2592000) return `in ${Math.floor(absDiff / 86400)} days`; 13 + if (absDiff < 31536000) return `in ${Math.floor(absDiff / 2592000)} months`; 14 + return `in ${Math.floor(absDiff / 31536000)} years`; 15 + } 16 + 17 + // past dates 18 + if (diffInSeconds < 60) return "just now"; 19 + if (diffInSeconds < 3600) { 20 + const minutes = Math.floor(diffInSeconds / 60); 21 + return `${minutes} minute${minutes === 1 ? "" : "s"} ago`; 22 + } 23 + if (diffInSeconds < 86400) { 24 + const hours = Math.floor(diffInSeconds / 3600); 25 + return `${hours} hour${hours === 1 ? "" : "s"} ago`; 26 + } 27 + if (diffInSeconds < 2592000) { 28 + const days = Math.floor(diffInSeconds / 86400); 29 + return `${days} day${days === 1 ? "" : "s"} ago`; 30 + } 31 + if (diffInSeconds < 31536000) { 32 + const months = Math.floor(diffInSeconds / 2592000); 33 + return `${months} month${months === 1 ? "" : "s"} ago`; 34 + } 35 + 36 + const years = Math.floor(diffInSeconds / 31536000); 37 + return `${years} year${years === 1 ? "" : "s"} ago`; 38 + }
+16 -1
src/routes/+layout.svelte
··· 8 8 <link rel="icon" href={favicon} /> 9 9 </svelte:head> 10 10 11 - {@render children?.()} 11 + <div class="app"> 12 + {@render children?.()} 13 + </div> 12 14 13 15 <style> 14 16 :global { 15 17 @import "~/styles/reset.css"; 16 18 @import "~/styles/theme.css"; 19 + } 20 + 21 + :global(html) { 22 + font-family: var(--font-body); 23 + } 24 + 25 + .app { 26 + --app-max-width: 40rem; 27 + --app-inline-padding: 24px; 28 + position: relative; 29 + max-width: var(--app-max-width); 30 + margin: 0 auto; 31 + padding: 0 var(--app-inline-padding); 17 32 } 18 33 </style>
+23 -2
src/routes/~/+layout.svelte
··· 19 19 20 20 <header class="header"> 21 21 <h1><a href="/~/">athost</a></h1> 22 - <button popovertarget="menu">menu</button> 22 + <button class="menubutton" popovertarget="menu"> 23 + <img class="avatar" src={data.profile.avatar} alt="" width={24} /> 24 + <span>@{data.profile.displayName}</span> 25 + </button> 23 26 <div id="menu" class="menu" popover> 24 - <p>hello, {data.name}</p> 25 27 <button onclick={logOut}>log out</button> 26 28 </div> 27 29 </header> ··· 32 34 33 35 <style> 34 36 .header { 37 + position: relative; 35 38 display: flex; 36 39 justify-content: space-between; 37 40 align-items: center; 41 + } 42 + 43 + .menubutton { 44 + display: flex; 45 + align-items: center; 46 + } 47 + 48 + .menu { 49 + &:popover-open { 50 + position: absolute; 51 + inset: unset; 52 + right: calc(50% - var(--app-max-width) / 2 + var(--app-inline-padding)); 53 + margin-top: 4rem; 54 + } 55 + } 56 + 57 + .avatar { 58 + border-radius: 50%; 38 59 } 39 60 </style>
+2 -2
src/routes/~/+layout.ts
··· 14 14 15 15 const atp = client(session); 16 16 17 - const { displayName } = await atp.getProfile(did); 17 + const profile = await atp.getProfile(did); 18 18 19 - return { session, pds: session.info.aud, did, name: displayName }; 19 + return { session, pds: session.info.aud, did, profile }; 20 20 } catch (e) { 21 21 console.error(e); 22 22 redirect(303, "/");
+2 -1
src/routes/~/+page.svelte
··· 2 2 import { invalidate } from "$app/navigation"; 3 3 4 4 import { client } from "~/lib/oauth"; 5 + import Button from "~/components/Button.svelte"; 5 6 6 7 let { data } = $props(); 7 8 ··· 23 24 24 25 <form onsubmit={createWebsite}> 25 26 <input type="text" name="rkey" minlength={1} maxlength={512} pattern="[A-Za-z0-9.\-]+" /> 26 - <button>create</button> 27 + <Button>create</Button> 27 28 </form> 28 29 29 30 <ul>
+78 -46
src/routes/~/sites/[name]/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { goto, invalidate } from "$app/navigation"; 3 + import { getRelativeTime } from "~/lib/date"; 3 4 4 5 import { client } from "~/lib/oauth"; 5 6 ··· 37 38 } 38 39 </script> 39 40 40 - <header class="header"> 41 - <h2>{params.name}</h2> 42 - </header> 41 + <div class="detail"> 42 + <header class="header"> 43 + <h2>{params.name}</h2> 44 + </header> 43 45 44 - <details> 45 - <summary> 46 - <span>{data.record.value.description || data.record.cid}</span> 47 - <time>{data.record.value.createdAt}</time> 48 - </summary> 49 - <ul> 50 - {#each Object.entries(data.record.value.assets) as [path, file]} 51 - <li> 52 - <span>{path}</span> 53 - <a 54 - target="_blank" 55 - href="{data.pds}xrpc/com.atproto.sync.getBlob?did={data.did}&cid={file.ref.$link}">open</a 56 - > 57 - </li> 58 - {/each} 59 - </ul> 60 - </details> 46 + <details class="deploy"> 47 + <summary class="summary"> 48 + <span>{data.record.value.description || data.record.cid}</span> 49 + <time datetime={data.record.value.createdAt}> 50 + {getRelativeTime(new Date(data.record.value.createdAt))} 51 + </time> 52 + </summary> 53 + <ul> 54 + {#each Object.entries(data.record.value.assets) as [path, file]} 55 + <li> 56 + <span>{path}</span> 57 + <a 58 + target="_blank" 59 + href="{data.pds}xrpc/com.atproto.sync.getBlob?did={data.did}&cid={file.ref.$link}" 60 + >open</a 61 + > 62 + </li> 63 + {/each} 64 + </ul> 65 + </details> 61 66 62 - <h3>upload</h3> 63 - <form onsubmit={deployBundle}> 64 - <input type="hidden" name="rkey" value={params.name} /> 65 - <label> 66 - <span>description</span> 67 - <input name="description" /> 68 - </label> 69 - <input type="file" name="files" webkitdirectory /> 70 - <button>upload</button> 71 - </form> 67 + <section> 68 + <h3>Upload</h3> 69 + <form onsubmit={deployBundle}> 70 + <input type="hidden" name="rkey" value={params.name} /> 71 + <label> 72 + <span>description</span> 73 + <input name="description" /> 74 + </label> 75 + <input type="file" name="files" webkitdirectory /> 76 + <button>upload</button> 77 + </form> 78 + </section> 72 79 73 - <h3>settings</h3> 74 - <form> 75 - <label> 76 - <span>not found</span> 77 - <input name="notfound" /> 78 - </label> 79 - <button>save</button> 80 - </form> 80 + <section> 81 + <h3>Settings</h3> 82 + <form> 83 + <label> 84 + <span>not found</span> 85 + <input name="notfound" /> 86 + </label> 87 + <button>save</button> 88 + </form> 89 + </section> 81 90 82 - <h3>danger zone</h3> 83 - <form onsubmit={deleteBundle}> 84 - <label> 85 - <span>site name</span> 86 - <input name="rkey" pattern={params.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} /> 87 - </label> 88 - <button>delete</button> 89 - </form> 91 + <section> 92 + <h3>Danger Zone</h3> 93 + <form onsubmit={deleteBundle}> 94 + <label> 95 + <span>site name</span> 96 + <input name="rkey" pattern={params.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} /> 97 + </label> 98 + <button>delete</button> 99 + </form> 100 + </section> 101 + </div> 102 + 103 + <style> 104 + .detail { 105 + display: flex; 106 + flex-direction: column; 107 + gap: var(--size-10); 108 + } 109 + 110 + .deploy { 111 + border: var(--size-border) solid black; 112 + border-radius: var(--radius-md); 113 + } 114 + 115 + .summary { 116 + display: flex; 117 + justify-content: space-between; 118 + align-items: center; 119 + padding: var(--size-3) var(--size-4); 120 + } 121 + </style>
src/styles/bricolage-grotesque-variable.woff2

This is a binary file and will not be displayed.

src/styles/supreme-variable.woff2

This is a binary file and will not be displayed.

+37 -3
src/styles/theme.css
··· 1 1 @font-face { 2 - font-family: "Supreme"; 3 - src: url("./supreme-variable.woff2") format("woff2"); 2 + font-family: "Bricolage Grotesque"; 3 + src: url("./bricolage-grotesque-variable.woff2") format("woff2"); 4 4 font-weight: 100 1000; 5 5 } 6 6 7 7 :root { 8 - font-family: "Supreme", sans-serif; 8 + /* fonts */ 9 + --font-body: "Bricolage Grotesque", sans-serif; 10 + 11 + /* sizes */ 12 + --size-border: 1px; 13 + --size-outline: 3px; 14 + --size-1: 2px; 15 + --size-2: 4px; 16 + --size-3: 8px; 17 + --size-4: 12px; 18 + --size-5: 16px; 19 + --size-6: 20px; 20 + --size-7: 24px; 21 + --size-8: 28px; 22 + --size-9: 32px; 23 + --size-10: 48px; 24 + 25 + /* corner radii */ 26 + --radius-sm: 2px; 27 + --radius-md: 6px; 28 + --radius-lg: 8px; 29 + --radius-xl: 12px; 30 + } 31 + 32 + @supports (corner-shape: squircle) { 33 + * { 34 + corner-shape: squircle; 35 + } 36 + 37 + :root { 38 + --radius-sm: 4px; 39 + --radius-md: 12px; 40 + --radius-lg: 16px; 41 + --radius-xl: 24px; 42 + } 9 43 }