hyperfixated on the popovers again

Changed files
+277 -153
src
+7
bun.lock
··· 7 7 "@astrojs/db": "^0.17.1", 8 8 "@astrojs/node": "^9.4.3", 9 9 "@atproto/api": "^0.16.7", 10 + "@floating-ui/dom": "^1.7.4", 10 11 "@fujocoded/authproto": "^0.0.4", 11 12 "@lucide/astro": "^0.542.0", 12 13 "@tailwindcss/vite": "^4.1.13", ··· 147 148 "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww=="], 148 149 149 150 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.9", "", { "os": "win32", "cpu": "x64" }, "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ=="], 151 + 152 + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], 153 + 154 + "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], 155 + 156 + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], 150 157 151 158 "@fujocoded/authproto": ["@fujocoded/authproto@0.0.4", "", { "dependencies": { "@astrojs/db": "^0.17.1", "@atproto/identity": "^0.4.8", "@atproto/oauth-client-node": "^0.3.3", "astro-integration-kit": "^0.19.0", "unstorage": "^1.16.1" }, "peerDependencies": { "astro": "^5.13.0" } }, "sha512-VoKfScLMaGLAOB6WKFsU7lEsCNvy98KW7MaPfqLKIbjg0MqWrbeyvCak4mGDjQUe96jm8DBMkd6b2W+T369PNA=="], 152 159
+1
package.json
··· 5 5 "@astrojs/db": "^0.17.1", 6 6 "@astrojs/node": "^9.4.3", 7 7 "@atproto/api": "^0.16.7", 8 + "@floating-ui/dom": "^1.7.4", 8 9 "@fujocoded/authproto": "^0.0.4", 9 10 "@lucide/astro": "^0.542.0", 10 11 "@tailwindcss/vite": "^4.1.13",
+22 -2
src/assets/styles/global.css
··· 1 1 @import "tailwindcss"; 2 - @plugin "daisyui"; 2 + @plugin "daisyui" { 3 + themes: all; 4 + /* add new themes here */ 5 + } 3 6 @plugin "@tailwindcss/typography"; 4 7 8 + /* default theme */ 5 9 @theme { 6 10 /* font tokens */ 7 11 --font-sans: var(--atkinson); ··· 26 30 --text-7xl: clamp(7.4506rem, 71.4115rem + -82.5302cqi, 52.8422rem); 27 31 --text-8xl: clamp(9.3132rem, 116.6654rem + -138.5189cqi, 85.4986rem); 28 32 --text-9xl: clamp(11.6415rem, 190.1667rem + -230.355cqi, 138.3368rem); 29 - } 33 + } 34 + 35 + @custom-variant dark (&:where( 36 + [data-theme=dark], 37 + [data-theme=dracula], 38 + [data-theme=synthwave], 39 + [data-theme=halloween], 40 + [data-theme=forest], 41 + [data-theme=aqua], 42 + [data-theme=black], 43 + [data-theme=luxury], 44 + [data-theme=business], 45 + [data-theme=night], 46 + [data-theme=coffee], 47 + [data-theme=sunset], 48 + [data-theme=abyss] 49 + ));
+13 -11
src/components/Dialog.astro
··· 13 13 <dialog 14 14 {id} 15 15 class:list={[ 16 - "m-auto", 17 - "rounded-box", 18 - "shadow" 19 - , className, 16 + "modal modal-bottom sm:modal-middle", 17 + className, 20 18 ]} 21 19 role={alert ? "alertdialog" : undefined} 22 20 closedby="any" 23 21 > 24 - <div class="card"> 25 - <header class="flex items-center justify-between"> 26 - <h1 class="card-title">{title}</h1> 22 + <div class="modal-box"> 23 + <header class="modal-header"> 24 + <h1 class="text-lg leading-none flex-1 pl-2">{title}</h1> 27 25 <form method="dialog"> 28 - <button aria-label="close" class="close"> 26 + <button aria-label="close" class="btn btn-error"> 29 27 <X /> 30 28 </button> 31 29 </form> 32 30 </header> 33 - 34 - <div class="card-body"> 31 + 32 + <div class="mt-14"> 35 33 <slot /> 36 34 </div> 37 35 </div> 38 36 </dialog> 39 37 40 38 <style> 41 - 39 + @reference "../assets/styles/global.css"; 40 + 41 + .modal-header { 42 + @apply absolute top-0 left-0 flex items-center justify-between p-2 container bg-accent text-accent-content; 43 + } 42 44 </style>
+62 -24
src/components/Popover.astro
··· 4 4 interface Props { 5 5 id?: string; 6 6 label: string; 7 - direction: "top" | "bottom"; 8 7 icon?: "info" | "warning" | "danger"; 9 8 title?: string; 10 9 class?: string; 11 10 } 12 11 13 - const { id, label, direction = "top", icon, title, class: className, ...rest } = Astro.props; 12 + const { id, label, icon, title, class: className, ...rest } = Astro.props; 14 13 --- 15 14 <!-- type button needs to be set here, otherwise it doesn't work inside forms --> 16 15 <button 17 16 type="button" 18 17 id={`${id}-trigger`} 19 18 class:list={[ 20 - "btn", 21 - "btn-xs", 19 + "btn btn-xs", 22 20 icon && ["btn-circle", "btn-ghost"], 23 21 icon && 24 22 (icon === "info") ? "text-info" : ··· 47 45 <div 48 46 {id} 49 47 class:list={[ 50 - "dropdown", 51 - "card", 52 - "bg-base-100", 53 - "w-72", 54 - "shadow", 55 48 "popover-content", 56 49 className, 57 50 ]} 58 - role="tooltip" {...rest} 59 - popover="auto" 51 + role="tooltip" 52 + popover="auto" 53 + {...rest} 60 54 > 61 55 <div class="card-body"> 62 56 {title && ( ··· 67 61 </div> 68 62 </div> 69 63 70 - <style define:vars={{ anchor: `--${id}-anchor`, direction }}> 64 + <style define:vars={{ anchor: `--${id}-anchor` }}> 65 + @reference "../assets/styles/global.css"; 66 + 71 67 .popover-btn { 72 - anchor-name: var(--anchor); 68 + @supports (anchor-name: var(--anchor)) { 69 + anchor-name: var(--anchor); 70 + } 73 71 } 74 72 75 73 .popover-content { 76 - position-anchor: var(--anchor); 77 - top: anchor(var(--direction)); 78 - left: anchor(center); 79 - transform: translateX(-50%); 80 - position-try-fallbacks: flip-block, flip-inline; 74 + @apply dropdown card mx-0 inset-auto bg-base-100 w-72 shadow; 75 + 76 + @supports (position-anchor: var(--anchor)) and (left: anchor(center)) { 77 + position-anchor: var(--anchor); 78 + left: anchor(center); 79 + transform: translateX(-50%); 80 + } 81 81 } 82 82 </style> 83 83 84 - <script define:vars={{ id }} is:inline> 85 - const trigger = document.getElementById(`${id}-trigger`); 86 - const popover = document.getElementById(id); 84 + <script> 85 + import { computePosition, autoUpdate, shift, flip } from "@floating-ui/dom"; 86 + const triggers = document.querySelectorAll(".popover-btn"); 87 + 88 + triggers.forEach(trigger => { 89 + const btn = trigger as HTMLButtonElement; 90 + // triggering button will always end with "-trigger" 91 + // so slice that from the id 92 + const id = btn.id.slice(0, -8); 93 + const popover = document.getElementById(`${id}`) as HTMLElement; 94 + 95 + btn.addEventListener("click", (e) => { 96 + e.preventDefault(); 97 + popover.togglePopover(); 98 + }); 87 99 88 - trigger.addEventListener("click", (e) => { 89 - e.preventDefault(); 90 - popover.togglePopover(); 100 + popover.addEventListener("toggle", (e) => { 101 + const cleanup = autoUpdate( 102 + btn, 103 + popover, 104 + () => { 105 + computePosition(btn, popover, { 106 + middleware: [ 107 + flip(), 108 + shift({ 109 + crossAxis: false, 110 + }), 111 + ], 112 + }).then(({ placement, middlewareData }) => { 113 + Object.assign(popover.style, { 114 + top: `anchor(${placement})`, 115 + ...(placement === "top") && { 116 + transform: (middlewareData.shift?.enabled.x) 117 + ? `translate(calc(-50% + ${middlewareData.shift.x}px), -100%)` 118 + : `translate(-50%, -100%)`, 119 + }, 120 + }); 121 + }); 122 + }); 123 + if (e.newState === "open") { 124 + cleanup; 125 + } else { 126 + cleanup(); 127 + } 128 + }); 91 129 }); 92 130 </script>
+44 -21
src/components/Settings.astro
··· 3 3 --- 4 4 <Dialog id="settings" title="User preferences"> 5 5 <form id="user-settings"> 6 - <label for="font-family">font family</label> 7 - <select name="fontFamily" id="font-family"> 8 - <option value="default">choose...</option> 9 - <option value="--serif">serif</option> 10 - <option value="--mono">monospaced</option> 11 - <option value="--sans">sans serif</option> 12 - <option value="--dyslexic">dyslexic</option> 13 - </select> 6 + <fieldset class="fieldset"> 7 + <label for="font-family">font family</label> 8 + <select class="select" name="fontFamily" id="font-family"> 9 + <option value="default">choose...</option> 10 + <option value="--font-serif">serif</option> 11 + <option value="--font-mono">monospaced</option> 12 + <option value="--font-sans">sans serif</option> 13 + <option value="--font-dyslexic">dyslexic</option> 14 + </select> 15 + </fieldset> 14 16 15 - <label for="font-size">text size</label> 16 - <input type="range" name="fontSize" id="font-size" min="-1" max="2" step="1" /> 17 + <fieldset class="fieldset"> 18 + <label for="font-size">text size</label> 19 + <input class="range" type="range" name="fontSize" id="font-size" min="-1" max="2" step="1" /> 20 + </fieldset> 17 21 18 - <label for="line-height">line height</label> 19 - <input type="range" name="lineHeight" id="line-height" min="1" max="2" step="0.05" /> 22 + <fieldset class="fieldset"> 23 + <label for="line-height">line height</label> 24 + <input class="range" type="range" name="lineHeight" id="line-height" min="1" max="2" step="0.05" /> 25 + </fieldset> 26 + 27 + <fieldset class="fieldset"> 28 + <label for="letter-spacing">letter spacing</label> 29 + <input class="range" type="range" name="letterSpacing" id="letter-spacing" min="0" max="0.1" step="0.01" /> 30 + </fieldset> 20 31 21 - <label for="letter-spacing">letter spacing</label> 22 - <input type="range" name="letterSpacing" id="letter-spacing" min="0" max="0.1" step="0.01" /> 23 - 24 - <label for="word-spacing">word spacing</label> 25 - <input type="range" name="wordSpacing" id="word-spacing" min="0" max="0.5" step="0.01" /> 32 + <fieldset class="fieldset"> 33 + <label for="word-spacing">word spacing</label> 34 + <input class="range" type="range" name="wordSpacing" id="word-spacing" min="0" max="0.5" step="0.01" /> 35 + </fieldset> 26 36 27 - <div id="test-area"> 37 + <div id="test-area" class="mt-4 text-(length:--size) leading-(--line-height) tracking-(--letter-spacing) line-clamp-4"> 28 38 <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Asperiores quae dolorum debitis vero nostrum nobis aspernatur ipsam sunt dolorem, eum ut corrupti unde commodi soluta natus repellendus totam animi adipisci.</p> 29 39 </div> 30 40 31 - <button id="confirm-settings">save</button> 41 + <div class="modal-action"> 42 + <button formmethod="dialog" value="default" class="btn btn-neutral">Cancel</button> 43 + <button id="confirm-settings" class="btn btn-primary">Save</button> 44 + </div> 32 45 </form> 33 46 </Dialog> 34 47 35 48 <style> 36 49 #test-area { 37 - --font: var(--body); 50 + --step--1: clamp(0.6953rem, 0.5707rem + 0.554vw, 1rem); 51 + --step-0: clamp(1.125rem, 1.0739rem + 0.2273vw, 1.25rem); 52 + --step-1: clamp(1.5625rem, 1.9257rem + -0.4686vw, 1.8203rem); 53 + --step-2: clamp(1.9531rem, 3.351rem + -1.8037vw, 2.9452rem); 54 + 55 + --font: var(--font-sans); 38 56 --size: var(--step-0); 39 57 --letter-spacing: 0em; 40 58 --word-spacing: 0em; ··· 55 73 const test = document.getElementById("test-area"); 56 74 57 75 form?.addEventListener("submit", (e) => { 58 - e.preventDefault(); 76 + const target = e.target as HTMLFormElement; 77 + if (target.nodeValue === null) { 78 + return; 79 + } else { 80 + e.preventDefault(); 81 + } 59 82 }); 60 83 61 84 inputs.forEach((input) => {
+30 -17
src/layouts/WorkPage.astro
··· 2 2 import Layout from "./Layout.astro"; 3 3 4 4 interface Props { 5 + slug: string; 5 6 title: string; 6 - has_previous: boolean; 7 - has_next: boolean; 7 + author: string; 8 + // tags: Tag[]; 9 + tags: any; 10 + createdAt: Date; 11 + updatedAt?: Date | null; 12 + comments?: boolean; 13 + previous?: boolean; 14 + next?: boolean; 8 15 } 9 16 10 - const { title, has_previous, has_next } = Astro.props; 17 + const { slug, title, author, tags, createdAt, updatedAt, comments, previous, next } = Astro.props; 11 18 --- 12 - <Layout title={title}> 13 - <a href="#workname-content">to content</a> 14 - 19 + <Layout title={title} skipLink="work-body"> 15 20 <nav id="work-menu"> 16 - {has_previous && ( 21 + {previous && ( 17 22 // chapterid - 1? 18 23 <a href="">previous chaptertitle</a> 19 24 )} 20 25 <!-- if theres more than one chapter, render this box --> 21 - <select name="workname-chapters" id="workname-chapters"> 26 + <select name="chapterSelect" id={`${slug}-chapters`}> 22 27 <option value="default" selected>Choose chapter...</option> 23 28 <!-- map each chapter here --> 24 29 </select> 25 30 26 - {has_next && ( 31 + {next && ( 27 32 // chapterid + 1 ? 28 33 <a href="">next chaptertitle</a> 29 34 )} 30 35 </nav> 31 36 32 - <main> 37 + <main id="work-body"> 33 38 <header> 34 39 <h1>{title}</h1> 35 - <h2>author name</h2> 40 + <h2>{author}</h2> 41 + <!-- replace this at some point --> 42 + {JSON.stringify(tags)} 43 + <time datetime={createdAt.toISOString()}>{createdAt}</time> 44 + {updatedAt && ( 45 + <time datetime={updatedAt.toISOString()}>{updatedAt}</time> 46 + )} 36 47 37 48 <div id="summary"> 38 49 summary 39 50 </div> 40 51 </header> 41 52 42 - <section id="workname-content"> 53 + <section id={`${slug}-content`} class="prose lg:prose-xl"> 43 54 <!-- if work has its own style, render it here somehow --> 44 55 <details> 45 56 <summary>Author's notes</summary> 46 57 this should include author's notes 47 58 </details> 48 - 59 + 49 60 <slot /> 50 61 </section> 51 62 52 - <aside id="workname-comments"> 53 - <!-- use bsky api to render comments here --> 54 - <!-- paginate this --> 55 - </aside> 63 + {comments && ( 64 + <aside id={`${slug}-comments`}> 65 + <!-- use bsky api to render comments here --> 66 + <!-- paginate this --> 67 + </aside> 68 + )} 56 69 </main> 57 70 </Layout>
+1 -1
src/pages/login.astro
··· 19 19 <fieldset class="fieldset mx-auto place-content-center max-w-md"> 20 20 <label class="fieldset-label" for="handle"> 21 21 Input your handle 22 - <Popover id="handle-help" icon="info" label="help" direction="bottom"> 22 + <Popover id="handle-help" icon="info" label="help"> 23 23 <h3>What's my handle?</h3> 24 24 <p>It'll look like a website URL without the <samp>https://</samp> or slashes, so a typical BlueSky handle will look something like: <b>alice.bsky.social</b>.</p> 25 25 <p>What yours will look like depends on whether you made a custom handle!</p>
+84 -61
src/pages/user/index.astro
··· 1 1 --- 2 2 import Layout from "@/layouts/Layout.astro"; 3 + import { Info } from "@lucide/astro"; 3 4 import { actions } from "astro:actions"; 4 5 import { db, eq, Users, Works } from "astro:db"; 5 6 import Dialog from "~/Dialog.astro"; 6 7 import Popover from "~/Popover.astro"; 8 + import Settings from "~/Settings.astro"; 7 9 8 10 const loggedInUser = Astro.locals.loggedInUser; 9 11 ··· 21 23 .from(Works) 22 24 .where(eq(Works.author, user?.userDid ?? loggedInUser.did)); 23 25 --- 24 - <Layout> 25 - <h1>User Settings</h1> 26 - <p>{loggedInUser?.handle}</p> 27 - 28 - <!-- registration will only happen in the below form! --> 29 - {(query.length === 0) && ( 30 - <> 31 - <h2>Connect account</h2> 32 - <div class="info"> 33 - <p>Right now, you aren't connected to the site. You can connect your BlueSky / self-hosted PDS account to this website to post a work.</p> 34 - <p>Please check out the Terms of Service, Privacy Policy, and Code of Conduct before connecting your account.</p> 35 - </div> 36 - <button id="trigger-confirm">Connect your PDS Account</button> 37 - 38 - <Dialog id="connect-account" title="Are you sure?"> 39 - <form action={actions.usersActions.addUser} method="post"> 40 - <label for="nickname">Nickname</label> 41 - <Popover id="nickname-info" label="info" icon="warning" direction="bottom"> 42 - <p>You can optionally set your nickname for this site. This is separate from your handle and acts as your identifier.</p> 43 - <p>Think of your handle as what you use to log in with, and your nickname as the name you want to publish your works under.</p> 44 - <h3>Important</h3> 45 - <p>If you do set a nickname, please make sure it's unique! Having two people with the same nickname would cause confusion, unfortunately.</p> 46 - </Popover> 47 - <input type="text" name="nickname" id="nickname" /> 26 + <Layout skipLink="user-profile"> 27 + <main id="user-profile"> 28 + <h1 class="text-xl">User Settings</h1> 29 + <p>{loggedInUser?.handle}</p> 30 + 31 + <!-- registration will only happen in the below form! --> 32 + {(query.length === 0) && ( 33 + <> 34 + <h2 class="text-lg">Connect account</h2> 35 + <div class="info"> 36 + <p>Right now, you aren't connected to the site. You can connect your BlueSky / self-hosted PDS account to this website to post a work.</p> 37 + <p>Please check out the Terms of Service, Privacy Policy, and Code of Conduct before connecting your account.</p> 38 + </div> 39 + <button id="trigger-confirm" class="btn btn-accent">Connect your PDS Account</button> 40 + 41 + <Dialog id="connect-account" title="Are you sure?"> 42 + <form action={actions.usersActions.addUser} method="post"> 43 + <fieldset class="fieldset"> 44 + <div class="flex gap-1"> 45 + <label for="nickname" class="label">Nickname</label> 46 + <Popover id="nickname-note" label="info" icon="warning" title="Important"> 47 + <p>If you do set a nickname, please make sure it's unique! Having two people with the same nickname would cause confusion, unfortunately.</p> 48 + </Popover> 49 + </div> 50 + <input class="input w-full" type="text" name="nickname" id="nickname" aria-describedby="nickname-info" /> 51 + <div id="nickname-info" class="alert"> 52 + <Info class="text-info" /> 53 + <div> 54 + <p>You can optionally set your nickname for this site.</p> 55 + <p>This is separate from your handle and acts similarly to a penname.</p> 56 + </div> 57 + </div> 58 + </fieldset> 48 59 49 - <button formmethod="dialog">Cancel</button> 50 - <button>Confirm</button> 51 - </form> 52 - </Dialog> 53 - </> 54 - )} 55 - 56 - {user && ( 57 - <> 58 - <h2>your nickname???</h2> 59 - <time datetime={user.joinedAt.toISOString()}>{user.joinedAt}</time> 60 - <p>{user.userDid}</p> 60 + <div class="modal-action"> 61 + <button class="btn btn-neutral" formmethod="dialog">Cancel</button> 62 + <button class="btn btn-primary">Confirm</button> 63 + </div> 64 + </form> 65 + </Dialog> 66 + </> 67 + )} 68 + 69 + {user && ( 70 + <> 71 + <h2>your nickname???</h2> 72 + <time datetime={user.joinedAt.toISOString()}>{user.joinedAt}</time> 73 + <p>{user.userDid}</p> 61 74 62 - {works && ( 63 - <section> 64 - <ul> 65 - {works.map(work => ( 66 - <article> 67 - <h3>{work.title}</h3> 75 + {works && ( 76 + <section> 77 + <ul> 78 + {works.map(work => ( 79 + <article> 80 + <h3>{work.title}</h3> 68 81 69 - <time datetime={work.createdAt.toISOString()}> 70 - {work.createdAt} 71 - </time> 72 - {work.updatedAt && ( 73 - <time datetime={work.updatedAt.toISOString()}> 74 - {work.updatedAt} 82 + <time datetime={work.createdAt.toISOString()}> 83 + {work.createdAt} 75 84 </time> 76 - )} 85 + {work.updatedAt && ( 86 + <time datetime={work.updatedAt.toISOString()}> 87 + {work.updatedAt} 88 + </time> 89 + )} 77 90 78 - <ul> 79 - {JSON.stringify(work.tags)} 80 - </ul> 81 - 82 - summary here 83 - </article> 84 - ))} 85 - </ul> 86 - </section> 87 - )} 88 - </> 89 - )} 91 + <ul> 92 + {JSON.stringify(work.tags)} 93 + </ul> 94 + 95 + summary here 96 + </article> 97 + ))} 98 + </ul> 99 + </section> 100 + )} 101 + </> 102 + )} 103 + 104 + <button class="btn btn-primary" id="trigger-settings">Set preferences</button> 105 + <Settings /> 106 + </main> 90 107 </Layout> 91 108 92 109 <script> 93 110 const trigger = document.getElementById("trigger-confirm"); 111 + const trigger2 = document.getElementById("trigger-settings"); 94 112 const confirmDialog = document.getElementById("connect-account") as HTMLDialogElement; 113 + const settingsDialog = document.getElementById("settings") as HTMLDialogElement; 95 114 96 115 trigger?.addEventListener("click", (_) => { 97 116 confirmDialog.showModal(); 117 + }); 118 + 119 + trigger2?.addEventListener("click", (_) => { 120 + settingsDialog.showModal(); 98 121 }); 99 122 </script>
+13 -16
src/pages/works/[workId].astro
··· 1 1 --- 2 - import Layout from "@/layouts/Layout.astro"; 2 + import WorkPage from "@/layouts/WorkPage.astro"; 3 3 import { didToHandle } from "@/lib/atproto"; 4 - import type { Tag } from "@/lib/types"; 5 4 import { db, eq, Users, Works } from "astro:db"; 6 5 7 6 const { workId } = Astro.params; ··· 16 15 return Astro.redirect("/not-found"); 17 16 } 18 17 --- 19 - <Layout> 20 - {work.map(async ({ Works, Users }) => ( 21 - <> 22 - <h1>{Works.title}</h1> 23 - <h2>{await didToHandle(Users.userDid)}</h2> 24 - <time datetime={Works.createdAt.toISOString()}>{Works.createdAt}</time> 25 - {(Works.tags as Tag[]).map(tag => ( 26 - <a href={tag.url}>{tag.label}</a> 27 - ))} 28 - 29 - <Fragment set:html={Works.content} /> 30 - </> 31 - ))} 32 - </Layout> 18 + {work.map(async ({ Works, Users }) => ( 19 + <WorkPage 20 + slug={Works.slug} 21 + title={Works.title} 22 + author={await didToHandle(Users.userDid)} 23 + createdAt={Works.createdAt} 24 + updatedAt={Works.updatedAt} 25 + tags={Works.tags} 26 + > 27 + <Fragment set:html={Works.content} /> 28 + </WorkPage> 29 + ))}