a tool for shared writing and social publishing

Merge branch 'main' into feature/tags

+8991 -3553
+6
actions/createPublicationDraft.ts
··· 11 11 redirectUser: false, 12 12 firstBlockType: "text", 13 13 }); 14 + let { data: publication } = await supabaseServerClient 15 + .from("publications") 16 + .select("*") 17 + .eq("uri", publication_uri) 18 + .single(); 19 + if (publication?.identity_did !== identity.atp_did) return; 14 20 15 21 await supabaseServerClient 16 22 .from("leaflets_in_publications")
+127
actions/deleteLeaflet.ts
··· 1 1 "use server"; 2 + import { refresh } from "next/cache"; 2 3 3 4 import { drizzle } from "drizzle-orm/node-postgres"; 4 5 import { ··· 9 10 import { eq } from "drizzle-orm"; 10 11 import { PermissionToken } from "src/replicache"; 11 12 import { pool } from "supabase/pool"; 13 + import { getIdentityData } from "./getIdentityData"; 14 + import { supabaseServerClient } from "supabase/serverClient"; 12 15 13 16 export async function deleteLeaflet(permission_token: PermissionToken) { 14 17 const client = await pool.connect(); 15 18 const db = drizzle(client); 19 + 20 + // Get the current user's identity 21 + let identity = await getIdentityData(); 22 + 23 + // Check publication and document ownership in one query 24 + let { data: tokenData } = await supabaseServerClient 25 + .from("permission_tokens") 26 + .select( 27 + ` 28 + id, 29 + leaflets_in_publications(publication, publications!inner(identity_did)), 30 + leaflets_to_documents(document, documents!inner(uri)) 31 + `, 32 + ) 33 + .eq("id", permission_token.id) 34 + .single(); 35 + 36 + if (tokenData) { 37 + // Check if leaflet is in a publication 38 + const leafletInPubs = tokenData.leaflets_in_publications || []; 39 + if (leafletInPubs.length > 0) { 40 + if (!identity) { 41 + throw new Error( 42 + "Unauthorized: You must be logged in to delete a leaflet in a publication", 43 + ); 44 + } 45 + const isOwner = leafletInPubs.some( 46 + (pub: any) => pub.publications.identity_did === identity.atp_did, 47 + ); 48 + if (!isOwner) { 49 + throw new Error( 50 + "Unauthorized: You must own the publication to delete this leaflet", 51 + ); 52 + } 53 + } 54 + 55 + // Check if there's a standalone published document 56 + const leafletDocs = tokenData.leaflets_to_documents || []; 57 + if (leafletDocs.length > 0) { 58 + if (!identity) { 59 + throw new Error( 60 + "Unauthorized: You must be logged in to delete a published leaflet", 61 + ); 62 + } 63 + for (let leafletDoc of leafletDocs) { 64 + const docUri = leafletDoc.documents?.uri; 65 + // Extract the DID from the document URI (format: at://did:plc:xxx/...) 66 + if (docUri && identity.atp_did && !docUri.includes(identity.atp_did)) { 67 + throw new Error( 68 + "Unauthorized: You must own the published document to delete this leaflet", 69 + ); 70 + } 71 + } 72 + } 73 + } 74 + 16 75 await db.transaction(async (tx) => { 17 76 let [token] = await tx 18 77 .select() ··· 32 91 .where(eq(permission_tokens.id, permission_token.id)); 33 92 }); 34 93 client.release(); 94 + 95 + refresh(); 96 + return; 97 + } 98 + 99 + export async function archivePost(token: string) { 100 + let identity = await getIdentityData(); 101 + if (!identity) throw new Error("No Identity"); 102 + 103 + // Archive on homepage 104 + await supabaseServerClient 105 + .from("permission_token_on_homepage") 106 + .update({ archived: true }) 107 + .eq("token", token) 108 + .eq("identity", identity.id); 109 + 110 + // Check if leaflet is in any publications where user is the creator 111 + let { data: leafletInPubs } = await supabaseServerClient 112 + .from("leaflets_in_publications") 113 + .select("publication, publications!inner(identity_did)") 114 + .eq("leaflet", token); 115 + 116 + if (leafletInPubs) { 117 + for (let pub of leafletInPubs) { 118 + if (pub.publications.identity_did === identity.atp_did) { 119 + await supabaseServerClient 120 + .from("leaflets_in_publications") 121 + .update({ archived: true }) 122 + .eq("leaflet", token) 123 + .eq("publication", pub.publication); 124 + } 125 + } 126 + } 127 + 128 + refresh(); 129 + return; 130 + } 131 + 132 + export async function unarchivePost(token: string) { 133 + let identity = await getIdentityData(); 134 + if (!identity) throw new Error("No Identity"); 135 + 136 + // Unarchive on homepage 137 + await supabaseServerClient 138 + .from("permission_token_on_homepage") 139 + .update({ archived: false }) 140 + .eq("token", token) 141 + .eq("identity", identity.id); 142 + 143 + // Check if leaflet is in any publications where user is the creator 144 + let { data: leafletInPubs } = await supabaseServerClient 145 + .from("leaflets_in_publications") 146 + .select("publication, publications!inner(identity_did)") 147 + .eq("leaflet", token); 148 + 149 + if (leafletInPubs) { 150 + for (let pub of leafletInPubs) { 151 + if (pub.publications.identity_did === identity.atp_did) { 152 + await supabaseServerClient 153 + .from("leaflets_in_publications") 154 + .update({ archived: false }) 155 + .eq("leaflet", token) 156 + .eq("publication", pub.publication); 157 + } 158 + } 159 + } 160 + 161 + refresh(); 35 162 return; 36 163 }
+4
actions/getIdentityData.ts
··· 17 17 identities( 18 18 *, 19 19 bsky_profiles(*), 20 + notifications(count), 20 21 publication_subscriptions(*), 21 22 custom_domains!custom_domains_identity_id_fkey(publication_domains(*), *), 22 23 home_leaflet:permission_tokens!identities_home_page_fkey(*, permission_token_rights(*, 23 24 entity_sets(entities(facts(*))) 24 25 )), 25 26 permission_token_on_homepage( 27 + archived, 26 28 created_at, 27 29 permission_tokens!inner( 28 30 id, 29 31 root_entity, 30 32 permission_token_rights(*), 33 + leaflets_to_documents(*, documents(*)), 31 34 leaflets_in_publications(*, publications(*), documents(*)) 32 35 ) 33 36 ) 34 37 )`, 35 38 ) 39 + .eq("identities.notifications.read", false) 36 40 .eq("id", auth_token) 37 41 .eq("confirmed", true) 38 42 .single()
+33
actions/publications/moveLeafletToPublication.ts
··· 1 + "use server"; 2 + 3 + import { getIdentityData } from "actions/getIdentityData"; 4 + import { supabaseServerClient } from "supabase/serverClient"; 5 + 6 + export async function moveLeafletToPublication( 7 + leaflet_id: string, 8 + publication_uri: string, 9 + metadata: { title: string; description: string }, 10 + entitiesToDelete: string[], 11 + ) { 12 + let identity = await getIdentityData(); 13 + if (!identity || !identity.atp_did) return null; 14 + let { data: publication } = await supabaseServerClient 15 + .from("publications") 16 + .select("*") 17 + .eq("uri", publication_uri) 18 + .single(); 19 + if (publication?.identity_did !== identity.atp_did) return; 20 + 21 + await supabaseServerClient.from("leaflets_in_publications").insert({ 22 + publication: publication_uri, 23 + leaflet: leaflet_id, 24 + doc: null, 25 + title: metadata.title, 26 + description: metadata.description, 27 + }); 28 + 29 + await supabaseServerClient 30 + .from("entities") 31 + .delete() 32 + .in("id", entitiesToDelete); 33 + }
-26
actions/publications/updateLeafletDraftMetadata.ts
··· 1 - "use server"; 2 - 3 - import { getIdentityData } from "actions/getIdentityData"; 4 - import { supabaseServerClient } from "supabase/serverClient"; 5 - 6 - export async function updateLeafletDraftMetadata( 7 - leafletID: string, 8 - publication_uri: string, 9 - title: string, 10 - description: string, 11 - ) { 12 - let identity = await getIdentityData(); 13 - if (!identity?.atp_did) return null; 14 - let { data: publication } = await supabaseServerClient 15 - .from("publications") 16 - .select() 17 - .eq("uri", publication_uri) 18 - .single(); 19 - if (!publication || publication.identity_did !== identity.atp_did) 20 - return null; 21 - await supabaseServerClient 22 - .from("leaflets_in_publications") 23 - .update({ title, description }) 24 - .eq("leaflet", leafletID) 25 - .eq("publication", publication_uri); 26 - }
+234 -56
actions/publishToPublication.ts
··· 23 23 PubLeafletBlocksIframe, 24 24 PubLeafletBlocksPage, 25 25 PubLeafletBlocksPoll, 26 + PubLeafletBlocksButton, 26 27 PubLeafletPollDefinition, 27 28 } from "lexicons/api"; 28 29 import { Block } from "components/Blocks/Block"; ··· 43 44 import { List, parseBlocksToList } from "src/utils/parseBlocksToList"; 44 45 import { getBlocksWithTypeLocal } from "src/hooks/queries/useBlocks"; 45 46 import { Lock } from "src/utils/lock"; 47 + import type { PubLeafletPublication } from "lexicons/api"; 48 + import { 49 + ColorToRGB, 50 + ColorToRGBA, 51 + } from "components/ThemeManager/colorToLexicons"; 52 + import { parseColor } from "@react-stately/color"; 46 53 47 54 export async function publishToPublication({ 48 55 root_entity, ··· 50 57 leaflet_id, 51 58 title, 52 59 description, 60 + entitiesToDelete, 53 61 }: { 54 62 root_entity: string; 55 - publication_uri: string; 63 + publication_uri?: string; 56 64 leaflet_id: string; 57 65 title?: string; 58 66 description?: string; 67 + entitiesToDelete?: string[]; 59 68 }) { 60 69 const oauthClient = await createOauthClient(); 61 70 let identity = await getIdentityData(); ··· 65 74 let agent = new AtpBaseClient( 66 75 credentialSession.fetchHandler.bind(credentialSession), 67 76 ); 68 - let { data: draft } = await supabaseServerClient 69 - .from("leaflets_in_publications") 70 - .select("*, publications(*), documents(*)") 71 - .eq("publication", publication_uri) 72 - .eq("leaflet", leaflet_id) 73 - .single(); 74 - if (!draft || identity.atp_did !== draft?.publications?.identity_did) 75 - throw new Error("No draft or not publisher"); 77 + 78 + // Check if we're publishing to a publication or standalone 79 + let draft: any = null; 80 + let existingDocUri: string | null = null; 81 + 82 + if (publication_uri) { 83 + // Publishing to a publication - use leaflets_in_publications 84 + let { data, error } = await supabaseServerClient 85 + .from("publications") 86 + .select("*, leaflets_in_publications(*, documents(*))") 87 + .eq("uri", publication_uri) 88 + .eq("leaflets_in_publications.leaflet", leaflet_id) 89 + .single(); 90 + console.log(error); 91 + 92 + if (!data || identity.atp_did !== data?.identity_did) 93 + throw new Error("No draft or not publisher"); 94 + draft = data.leaflets_in_publications[0]; 95 + existingDocUri = draft?.doc; 96 + } else { 97 + // Publishing standalone - use leaflets_to_documents 98 + let { data } = await supabaseServerClient 99 + .from("leaflets_to_documents") 100 + .select("*, documents(*)") 101 + .eq("leaflet", leaflet_id) 102 + .single(); 103 + draft = data; 104 + existingDocUri = draft?.document; 105 + } 106 + 107 + // Heuristic: Remove title entities if this is the first time publishing 108 + // (when coming from a standalone leaflet with entitiesToDelete passed in) 109 + if (entitiesToDelete && entitiesToDelete.length > 0 && !existingDocUri) { 110 + await supabaseServerClient 111 + .from("entities") 112 + .delete() 113 + .in("id", entitiesToDelete); 114 + } 115 + 76 116 let { data } = await supabaseServerClient.rpc("get_facts", { 77 117 root: root_entity, 78 118 }); 79 119 let facts = (data as unknown as Fact<Attribute>[]) || []; 80 120 81 - let { firstPageBlocks, pages } = await processBlocksToPages( 121 + let { pages } = await processBlocksToPages( 82 122 facts, 83 123 agent, 84 124 root_entity, ··· 87 127 88 128 let existingRecord = 89 129 (draft?.documents?.data as PubLeafletDocument.Record | undefined) || {}; 130 + 131 + // Extract theme for standalone documents (not for publications) 132 + let theme: PubLeafletPublication.Theme | undefined; 133 + if (!publication_uri) { 134 + theme = await extractThemeFromFacts(facts, root_entity, agent); 135 + } 136 + 90 137 let record: PubLeafletDocument.Record = { 138 + publishedAt: new Date().toISOString(), 139 + ...existingRecord, 91 140 $type: "pub.leaflet.document", 92 141 author: credentialSession.did!, 93 - publication: publication_uri, 94 - publishedAt: new Date().toISOString(), 95 - ...existingRecord, 142 + ...(publication_uri && { publication: publication_uri }), 143 + ...(theme && { theme }), 96 144 title: title || "Untitled", 97 145 description: description || "", 98 - pages: [ 99 - { 100 - $type: "pub.leaflet.pages.linearDocument", 101 - blocks: firstPageBlocks, 102 - }, 103 - ...pages.map((p) => { 104 - if (p.type === "canvas") { 105 - return { 106 - $type: "pub.leaflet.pages.canvas" as const, 107 - id: p.id, 108 - blocks: p.blocks as PubLeafletPagesCanvas.Block[], 109 - }; 110 - } else { 111 - return { 112 - $type: "pub.leaflet.pages.linearDocument" as const, 113 - id: p.id, 114 - blocks: p.blocks as PubLeafletPagesLinearDocument.Block[], 115 - }; 116 - } 117 - }), 118 - ], 146 + pages: pages.map((p) => { 147 + if (p.type === "canvas") { 148 + return { 149 + $type: "pub.leaflet.pages.canvas" as const, 150 + id: p.id, 151 + blocks: p.blocks as PubLeafletPagesCanvas.Block[], 152 + }; 153 + } else { 154 + return { 155 + $type: "pub.leaflet.pages.linearDocument" as const, 156 + id: p.id, 157 + blocks: p.blocks as PubLeafletPagesLinearDocument.Block[], 158 + }; 159 + } 160 + }), 119 161 }; 120 - let rkey = draft?.doc ? new AtUri(draft.doc).rkey : TID.nextStr(); 162 + 163 + // Keep the same rkey if updating an existing document 164 + let rkey = existingDocUri ? new AtUri(existingDocUri).rkey : TID.nextStr(); 121 165 let { data: result } = await agent.com.atproto.repo.putRecord({ 122 166 rkey, 123 167 repo: credentialSession.did!, ··· 126 170 validate: false, //TODO publish the lexicon so we can validate! 127 171 }); 128 172 173 + // Optimistically create database entries 129 174 await supabaseServerClient.from("documents").upsert({ 130 175 uri: result.uri, 131 176 data: record as Json, 132 177 }); 133 - await Promise.all([ 134 - //Optimistically put these in! 135 - supabaseServerClient.from("documents_in_publications").upsert({ 136 - publication: record.publication, 137 - document: result.uri, 138 - }), 139 - supabaseServerClient 140 - .from("leaflets_in_publications") 141 - .update({ 178 + 179 + if (publication_uri) { 180 + // Publishing to a publication - update both tables 181 + await Promise.all([ 182 + supabaseServerClient.from("documents_in_publications").upsert({ 183 + publication: publication_uri, 184 + document: result.uri, 185 + }), 186 + supabaseServerClient.from("leaflets_in_publications").upsert({ 142 187 doc: result.uri, 143 - }) 144 - .eq("leaflet", leaflet_id) 145 - .eq("publication", publication_uri), 146 - ]); 188 + leaflet: leaflet_id, 189 + publication: publication_uri, 190 + title: title, 191 + description: description, 192 + }), 193 + ]); 194 + } else { 195 + // Publishing standalone - update leaflets_to_documents 196 + await supabaseServerClient.from("leaflets_to_documents").upsert({ 197 + leaflet: leaflet_id, 198 + document: result.uri, 199 + title: title || "Untitled", 200 + description: description || "", 201 + }); 202 + 203 + // Heuristic: Remove title entities if this is the first time publishing standalone 204 + // (when entitiesToDelete is provided and there's no existing document) 205 + if (entitiesToDelete && entitiesToDelete.length > 0 && !existingDocUri) { 206 + await supabaseServerClient 207 + .from("entities") 208 + .delete() 209 + .in("id", entitiesToDelete); 210 + } 211 + } 147 212 148 213 return { rkey, record: JSON.parse(JSON.stringify(record)) }; 149 214 } ··· 168 233 169 234 let firstEntity = scan.eav(root_entity, "root/page")?.[0]; 170 235 if (!firstEntity) throw new Error("No root page"); 171 - let blocks = getBlocksWithTypeLocal(facts, firstEntity?.data.value); 172 - let b = await blocksToRecord(blocks, did); 173 - return { firstPageBlocks: b, pages }; 236 + 237 + // Check if the first page is a canvas or linear document 238 + let [pageType] = scan.eav(firstEntity.data.value, "page/type"); 239 + 240 + if (pageType?.data.value === "canvas") { 241 + // First page is a canvas 242 + let canvasBlocks = await canvasBlocksToRecord(firstEntity.data.value, did); 243 + pages.unshift({ 244 + id: firstEntity.data.value, 245 + blocks: canvasBlocks, 246 + type: "canvas", 247 + }); 248 + } else { 249 + // First page is a linear document 250 + let blocks = getBlocksWithTypeLocal(facts, firstEntity?.data.value); 251 + let b = await blocksToRecord(blocks, did); 252 + pages.unshift({ 253 + id: firstEntity.data.value, 254 + blocks: b, 255 + type: "doc", 256 + }); 257 + } 258 + 259 + return { pages }; 174 260 175 261 async function uploadImage(src: string) { 176 262 let data = await fetch(src); ··· 192 278 await Promise.all( 193 279 parsedBlocks.map(async (blockOrList) => { 194 280 if (blockOrList.type === "block") { 195 - let alignmentValue = 196 - scan.eav(blockOrList.block.value, "block/text-alignment")[0]?.data 197 - .value || "left"; 198 - let alignment = 281 + let alignmentValue = scan.eav( 282 + blockOrList.block.value, 283 + "block/text-alignment", 284 + )[0]?.data.value; 285 + let alignment: ExcludeString< 286 + PubLeafletPagesLinearDocument.Block["alignment"] 287 + > = 199 288 alignmentValue === "center" 200 289 ? "lex:pub.leaflet.pages.linearDocument#textAlignCenter" 201 290 : alignmentValue === "right" 202 291 ? "lex:pub.leaflet.pages.linearDocument#textAlignRight" 203 - : undefined; 292 + : alignmentValue === "justify" 293 + ? "lex:pub.leaflet.pages.linearDocument#textAlignJustify" 294 + : alignmentValue === "left" 295 + ? "lex:pub.leaflet.pages.linearDocument#textAlignLeft" 296 + : undefined; 204 297 let b = await blockToRecord(blockOrList.block, did); 205 298 if (!b) return []; 206 299 let block: PubLeafletPagesLinearDocument.Block = { ··· 446 539 }; 447 540 return block; 448 541 } 542 + if (b.type === "button") { 543 + let [text] = scan.eav(b.value, "button/text"); 544 + let [url] = scan.eav(b.value, "button/url"); 545 + if (!text || !url) return; 546 + let block: $Typed<PubLeafletBlocksButton.Main> = { 547 + $type: "pub.leaflet.blocks.button", 548 + text: text.data.value, 549 + url: url.data.value, 550 + }; 551 + return block; 552 + } 449 553 return; 450 554 } 451 555 ··· 547 651 } 548 652 return []; 549 653 } 654 + 655 + type ExcludeString<T> = T extends string 656 + ? string extends T 657 + ? never 658 + : T /* maybe literal, not the whole `string` */ 659 + : T; /* not a string */ 660 + 661 + async function extractThemeFromFacts( 662 + facts: Fact<any>[], 663 + root_entity: string, 664 + agent: AtpBaseClient, 665 + ): Promise<PubLeafletPublication.Theme | undefined> { 666 + let scan = scanIndexLocal(facts); 667 + let pageBackground = scan.eav(root_entity, "theme/page-background")?.[0]?.data 668 + .value; 669 + let cardBackground = scan.eav(root_entity, "theme/card-background")?.[0]?.data 670 + .value; 671 + let primary = scan.eav(root_entity, "theme/primary")?.[0]?.data.value; 672 + let accentBackground = scan.eav(root_entity, "theme/accent-background")?.[0] 673 + ?.data.value; 674 + let accentText = scan.eav(root_entity, "theme/accent-text")?.[0]?.data.value; 675 + let showPageBackground = !scan.eav( 676 + root_entity, 677 + "theme/card-border-hidden", 678 + )?.[0]?.data.value; 679 + let backgroundImage = scan.eav(root_entity, "theme/background-image")?.[0]; 680 + let backgroundImageRepeat = scan.eav( 681 + root_entity, 682 + "theme/background-image-repeat", 683 + )?.[0]; 684 + 685 + let theme: PubLeafletPublication.Theme = { 686 + showPageBackground: showPageBackground ?? true, 687 + }; 688 + 689 + if (pageBackground) 690 + theme.backgroundColor = ColorToRGBA(parseColor(`hsba(${pageBackground})`)); 691 + if (cardBackground) 692 + theme.pageBackground = ColorToRGBA(parseColor(`hsba(${cardBackground})`)); 693 + if (primary) theme.primary = ColorToRGB(parseColor(`hsba(${primary})`)); 694 + if (accentBackground) 695 + theme.accentBackground = ColorToRGB( 696 + parseColor(`hsba(${accentBackground})`), 697 + ); 698 + if (accentText) 699 + theme.accentText = ColorToRGB(parseColor(`hsba(${accentText})`)); 700 + 701 + // Upload background image if present 702 + if (backgroundImage?.data) { 703 + let imageData = await fetch(backgroundImage.data.src); 704 + if (imageData.status === 200) { 705 + let binary = await imageData.blob(); 706 + let blob = await agent.com.atproto.repo.uploadBlob(binary, { 707 + headers: { "Content-Type": binary.type }, 708 + }); 709 + 710 + theme.backgroundImage = { 711 + $type: "pub.leaflet.theme.backgroundImage", 712 + image: blob.data.blob, 713 + repeat: backgroundImageRepeat?.data.value ? true : false, 714 + ...(backgroundImageRepeat?.data.value && { 715 + width: backgroundImageRepeat.data.value, 716 + }), 717 + }; 718 + } 719 + } 720 + 721 + // Only return theme if at least one property is set 722 + if (Object.keys(theme).length > 1 || theme.showPageBackground !== true) { 723 + return theme; 724 + } 725 + 726 + return undefined; 727 + }
+1 -1
actions/subscriptions/sendPostToSubscribers.ts
··· 57 57 ) { 58 58 return; 59 59 } 60 - let domain = getCurrentDeploymentDomain(); 60 + let domain = await getCurrentDeploymentDomain(); 61 61 let res = await fetch("https://api.postmarkapp.com/email/batch", { 62 62 method: "POST", 63 63 headers: {
+6 -16
app/(home-pages)/discover/PubListing.tsx
··· 16 16 }, 17 17 ) => { 18 18 let record = props.record as PubLeafletPublication.Record; 19 - let theme = usePubTheme(record); 19 + let theme = usePubTheme(record.theme); 20 20 let backgroundImage = record?.theme?.backgroundImage?.image?.ref 21 21 ? blobRefToSrc( 22 22 record?.theme?.backgroundImage?.image?.ref, ··· 37 37 px-3 py-3 selected-outline 38 38 hover:outline-accent-contrast hover:border-accent-contrast 39 39 relative overflow-hidden`} 40 + style={{ 41 + backgroundImage: `url(${backgroundImage})`, 42 + backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 43 + backgroundSize: `${backgroundImageRepeat ? `${backgroundImageSize}px` : "cover"}`, 44 + }} 40 45 > 41 - {backgroundImage && ( 42 - <img 43 - src={backgroundImage} 44 - alt="" 45 - loading="lazy" 46 - fetchPriority="low" 47 - className="absolute inset-0 pointer-events-none" 48 - style={{ 49 - width: backgroundImageRepeat ? `${backgroundImageSize}px` : "100%", 50 - height: backgroundImageRepeat ? "auto" : "100%", 51 - objectFit: backgroundImageRepeat ? "none" : "cover", 52 - objectPosition: "center", 53 - }} 54 - /> 55 - )} 56 46 <div 57 47 className={`flex w-full flex-col justify-center text-center max-h-48 pt-4 pb-3 px-3 rounded-lg relative z-10 ${props.resizeHeight ? "" : "sm:h-48 h-full"} ${record.theme?.showPageBackground ? "bg-[rgba(var(--bg-page),var(--bg-page-alpha))] " : ""}`} 58 48 >
+75 -23
app/(home-pages)/discover/SortedPublicationList.tsx
··· 1 1 "use client"; 2 2 import Link from "next/link"; 3 - import { useState } from "react"; 3 + import { useState, useEffect, useRef } from "react"; 4 4 import { theme } from "tailwind.config"; 5 - import { PublicationsList } from "./page"; 6 5 import { PubListing } from "./PubListing"; 6 + import useSWRInfinite from "swr/infinite"; 7 + import { getPublications, type Cursor, type Publication } from "./getPublications"; 7 8 8 9 export function SortedPublicationList(props: { 9 - publications: PublicationsList; 10 + publications: Publication[]; 10 11 order: string; 12 + nextCursor: Cursor | null; 11 13 }) { 12 14 let [order, setOrder] = useState(props.order); 15 + 16 + const getKey = ( 17 + pageIndex: number, 18 + previousPageData: { publications: Publication[]; nextCursor: Cursor | null } | null, 19 + ) => { 20 + // Reached the end 21 + if (previousPageData && !previousPageData.nextCursor) return null; 22 + 23 + // First page, we don't have previousPageData 24 + if (pageIndex === 0) return ["discover-publications", order, null] as const; 25 + 26 + // Add the cursor to the key 27 + return ["discover-publications", order, previousPageData?.nextCursor] as const; 28 + }; 29 + 30 + const { data, error, size, setSize, isValidating } = useSWRInfinite( 31 + getKey, 32 + ([_, orderValue, cursor]) => { 33 + const orderParam = orderValue === "popular" ? "popular" : "recentlyUpdated"; 34 + return getPublications(orderParam, cursor); 35 + }, 36 + { 37 + fallbackData: order === props.order 38 + ? [{ publications: props.publications, nextCursor: props.nextCursor }] 39 + : undefined, 40 + revalidateFirstPage: false, 41 + }, 42 + ); 43 + 44 + const loadMoreRef = useRef<HTMLDivElement>(null); 45 + 46 + // Set up intersection observer to load more when trigger element is visible 47 + useEffect(() => { 48 + const observer = new IntersectionObserver( 49 + (entries) => { 50 + if (entries[0].isIntersecting && !isValidating) { 51 + const hasMore = data && data[data.length - 1]?.nextCursor; 52 + if (hasMore) { 53 + setSize(size + 1); 54 + } 55 + } 56 + }, 57 + { threshold: 0.1 }, 58 + ); 59 + 60 + if (loadMoreRef.current) { 61 + observer.observe(loadMoreRef.current); 62 + } 63 + 64 + return () => observer.disconnect(); 65 + }, [data, size, setSize, isValidating]); 66 + 67 + const allPublications = data ? data.flatMap((page) => page.publications) : []; 68 + 13 69 return ( 14 70 <div className="discoverHeader flex flex-col items-center "> 15 71 <SortButtons ··· 21 77 setOrder(o); 22 78 }} 23 79 /> 24 - <div className="discoverPubList flex flex-col gap-3 pt-6 w-full"> 25 - {props.publications 26 - ?.filter((pub) => pub.documents_in_publications.length > 0) 27 - ?.sort((a, b) => { 28 - if (order === "popular") { 29 - return ( 30 - b.publication_subscriptions[0].count - 31 - a.publication_subscriptions[0].count 32 - ); 33 - } 34 - const aDate = new Date( 35 - a.documents_in_publications[0]?.indexed_at || 0, 36 - ); 37 - const bDate = new Date( 38 - b.documents_in_publications[0]?.indexed_at || 0, 39 - ); 40 - return bDate.getTime() - aDate.getTime(); 41 - }) 42 - .map((pub) => <PubListing resizeHeight key={pub.uri} {...pub} />)} 80 + <div className="discoverPubList flex flex-col gap-3 pt-6 w-full relative"> 81 + {allPublications.map((pub) => ( 82 + <PubListing resizeHeight key={pub.uri} {...pub} /> 83 + ))} 84 + {/* Trigger element for loading more publications */} 85 + <div 86 + ref={loadMoreRef} 87 + className="absolute bottom-96 left-0 w-full h-px pointer-events-none" 88 + aria-hidden="true" 89 + /> 90 + {isValidating && ( 91 + <div className="text-center text-tertiary py-4"> 92 + Loading more publications... 93 + </div> 94 + )} 43 95 </div> 44 96 </div> 45 97 ); ··· 81 133 <div className="relative"> 82 134 <button 83 135 onClick={props.onClick} 84 - className={`text-sm bg-accent-1 text-accent-2 rounded-md px-[8px] py-0.5 border ${props.selected ? "border-accent-contrast font-bold" : "border-border-light"}`} 136 + className={`text-sm rounded-md px-[8px] font-bold py-0.5 border ${props.selected ? "border-accent-contrast bg-accent-1 text-accent-2 " : "bg-bg-page text-accent-contrast border-accent-contrast"}`} 85 137 > 86 138 {props.children} 87 139 </button>
+119
app/(home-pages)/discover/getPublications.ts
··· 1 + "use server"; 2 + 3 + import { supabaseServerClient } from "supabase/serverClient"; 4 + 5 + export type Cursor = { 6 + indexed_at?: string; 7 + count?: number; 8 + uri: string; 9 + }; 10 + 11 + export type Publication = Awaited< 12 + ReturnType<typeof getPublications> 13 + >["publications"][number]; 14 + 15 + export async function getPublications( 16 + order: "recentlyUpdated" | "popular" = "recentlyUpdated", 17 + cursor?: Cursor | null, 18 + ): Promise<{ publications: any[]; nextCursor: Cursor | null }> { 19 + const limit = 25; 20 + 21 + // Fetch all publications with their most recent document 22 + let { data: publications, error } = await supabaseServerClient 23 + .from("publications") 24 + .select( 25 + "*, documents_in_publications(*, documents(*)), publication_subscriptions(count)", 26 + ) 27 + .or( 28 + "record->preferences->showInDiscover.is.null,record->preferences->>showInDiscover.eq.true", 29 + ) 30 + .order("indexed_at", { 31 + referencedTable: "documents_in_publications", 32 + ascending: false, 33 + }) 34 + .limit(1, { referencedTable: "documents_in_publications" }); 35 + 36 + if (error) { 37 + console.error("Error fetching publications:", error); 38 + return { publications: [], nextCursor: null }; 39 + } 40 + 41 + // Filter out publications without documents 42 + const allPubs = (publications || []).filter( 43 + (pub) => pub.documents_in_publications.length > 0, 44 + ); 45 + 46 + // Sort on the server 47 + allPubs.sort((a, b) => { 48 + if (order === "popular") { 49 + const aCount = a.publication_subscriptions[0]?.count || 0; 50 + const bCount = b.publication_subscriptions[0]?.count || 0; 51 + if (bCount !== aCount) { 52 + return bCount - aCount; 53 + } 54 + // Secondary sort by uri for stability 55 + return b.uri.localeCompare(a.uri); 56 + } else { 57 + // recentlyUpdated 58 + const aDate = new Date( 59 + a.documents_in_publications[0]?.indexed_at || 0, 60 + ).getTime(); 61 + const bDate = new Date( 62 + b.documents_in_publications[0]?.indexed_at || 0, 63 + ).getTime(); 64 + if (bDate !== aDate) { 65 + return bDate - aDate; 66 + } 67 + // Secondary sort by uri for stability 68 + return b.uri.localeCompare(a.uri); 69 + } 70 + }); 71 + 72 + // Find cursor position and slice 73 + let startIndex = 0; 74 + if (cursor) { 75 + startIndex = allPubs.findIndex((pub) => { 76 + if (order === "popular") { 77 + const pubCount = pub.publication_subscriptions[0]?.count || 0; 78 + // Find first pub after cursor 79 + return ( 80 + pubCount < (cursor.count || 0) || 81 + (pubCount === cursor.count && pub.uri < cursor.uri) 82 + ); 83 + } else { 84 + const pubDate = pub.documents_in_publications[0]?.indexed_at || ""; 85 + // Find first pub after cursor 86 + return ( 87 + pubDate < (cursor.indexed_at || "") || 88 + (pubDate === cursor.indexed_at && pub.uri < cursor.uri) 89 + ); 90 + } 91 + }); 92 + // If not found, we're at the end 93 + if (startIndex === -1) { 94 + return { publications: [], nextCursor: null }; 95 + } 96 + } 97 + 98 + // Get the page 99 + const page = allPubs.slice(startIndex, startIndex + limit); 100 + 101 + // Create next cursor 102 + const nextCursor = 103 + page.length === limit && startIndex + limit < allPubs.length 104 + ? order === "recentlyUpdated" 105 + ? { 106 + indexed_at: page[page.length - 1].documents_in_publications[0]?.indexed_at, 107 + uri: page[page.length - 1].uri, 108 + } 109 + : { 110 + count: page[page.length - 1].publication_subscriptions[0]?.count || 0, 111 + uri: page[page.length - 1].uri, 112 + } 113 + : null; 114 + 115 + return { 116 + publications: page, 117 + nextCursor, 118 + }; 119 + }
+9 -21
app/(home-pages)/discover/page.tsx
··· 1 - import { supabaseServerClient } from "supabase/serverClient"; 2 1 import Link from "next/link"; 3 2 import { SortedPublicationList } from "./SortedPublicationList"; 4 3 import { Metadata } from "next"; 5 4 import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; 6 - 7 - export type PublicationsList = Awaited<ReturnType<typeof getPublications>>; 8 - async function getPublications() { 9 - let { data: publications, error } = await supabaseServerClient 10 - .from("publications") 11 - .select( 12 - "*, documents_in_publications(*, documents(*)), publication_subscriptions(count)", 13 - ) 14 - .or( 15 - "record->preferences->showInDiscover.is.null,record->preferences->>showInDiscover.eq.true", 16 - ) 17 - .order("indexed_at", { 18 - referencedTable: "documents_in_publications", 19 - ascending: false, 20 - }) 21 - .limit(1, { referencedTable: "documents_in_publications" }); 22 - return publications; 23 - } 5 + import { getPublications } from "./getPublications"; 24 6 25 7 export const metadata: Metadata = { 26 8 title: "Leaflet Discover", ··· 50 32 } 51 33 52 34 const DiscoverContent = async (props: { order: string }) => { 53 - let publications = await getPublications(); 35 + const orderValue = 36 + props.order === "popular" ? "popular" : "recentlyUpdated"; 37 + let { publications, nextCursor } = await getPublications(orderValue); 54 38 55 39 return ( 56 40 <div className="max-w-prose mx-auto w-full"> ··· 61 45 <Link href="/lish/createPub">make your own</Link>! 62 46 </p> 63 47 </div> 64 - <SortedPublicationList publications={publications} order={props.order} /> 48 + <SortedPublicationList 49 + publications={publications} 50 + order={props.order} 51 + nextCursor={nextCursor} 52 + /> 65 53 </div> 66 54 ); 67 55 };
+115 -8
app/(home-pages)/home/Actions/AccountSettings.tsx
··· 1 1 "use client"; 2 2 3 3 import { ActionButton } from "components/ActionBar/ActionButton"; 4 - import { Menu, MenuItem } from "components/Layout"; 5 4 import { mutate } from "swr"; 6 5 import { AccountSmall } from "components/Icons/AccountSmall"; 7 6 import { LogoutSmall } from "components/Icons/LogoutSmall"; 7 + import { Popover } from "components/Popover"; 8 + import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; 9 + import { SpeedyLink } from "components/SpeedyLink"; 10 + import { GoBackSmall } from "components/Icons/GoBackSmall"; 11 + import { useState } from "react"; 12 + import { ThemeSetterContent } from "components/ThemeManager/ThemeSetter"; 13 + import { useIsMobile } from "src/hooks/isMobile"; 8 14 9 - // it was going have a popover with a log out button 10 - export const AccountSettings = () => { 15 + export const AccountSettings = (props: { entityID: string }) => { 16 + let [state, setState] = useState<"menu" | "general" | "theme">("menu"); 17 + let isMobile = useIsMobile(); 18 + 11 19 return ( 12 - <Menu 20 + <Popover 13 21 asChild 22 + onOpenChange={() => setState("menu")} 23 + side={isMobile ? "top" : "right"} 24 + align={isMobile ? "center" : "start"} 25 + className={`max-w-xs w-[1000px] ${state === "theme" && "bg-white!"}`} 14 26 trigger={<ActionButton icon=<AccountSmall /> label="Settings" />} 15 27 > 16 - <MenuItem 17 - onSelect={async () => { 28 + {state === "general" ? ( 29 + <GeneralSettings backToMenu={() => setState("menu")} /> 30 + ) : state === "theme" ? ( 31 + <AccountThemeSettings 32 + entityID={props.entityID} 33 + backToMenu={() => setState("menu")} 34 + /> 35 + ) : ( 36 + <SettingsMenu state={state} setState={setState} /> 37 + )} 38 + </Popover> 39 + ); 40 + }; 41 + 42 + const SettingsMenu = (props: { 43 + state: "menu" | "general" | "theme"; 44 + setState: (s: typeof props.state) => void; 45 + }) => { 46 + let menuItemClassName = 47 + "menuItem -mx-[8px] text-left flex items-center justify-between hover:no-underline!"; 48 + 49 + return ( 50 + <div className="flex flex-col gap-0.5"> 51 + <AccountSettingsHeader state={"menu"} /> 52 + <button 53 + className={menuItemClassName} 54 + type="button" 55 + onClick={() => { 56 + props.setState("general"); 57 + }} 58 + > 59 + General 60 + <ArrowRightTiny /> 61 + </button> 62 + <button 63 + className={menuItemClassName} 64 + type="button" 65 + onClick={() => props.setState("theme")} 66 + > 67 + Account Theme 68 + <ArrowRightTiny /> 69 + </button> 70 + </div> 71 + ); 72 + }; 73 + 74 + const GeneralSettings = (props: { backToMenu: () => void }) => { 75 + return ( 76 + <div className="flex flex-col gap-0.5"> 77 + <AccountSettingsHeader 78 + state={"general"} 79 + backToMenuAction={() => props.backToMenu()} 80 + /> 81 + 82 + <button 83 + className="flex gap-2 font-bold" 84 + onClick={async () => { 18 85 await fetch("/api/auth/logout"); 19 86 mutate("identity", null); 20 87 }} 21 88 > 22 89 <LogoutSmall /> 23 90 Logout 24 - </MenuItem> 25 - </Menu> 91 + </button> 92 + </div> 93 + ); 94 + }; 95 + const AccountThemeSettings = (props: { 96 + entityID: string; 97 + backToMenu: () => void; 98 + }) => { 99 + return ( 100 + <div className="flex flex-col gap-0.5"> 101 + <AccountSettingsHeader 102 + state={"theme"} 103 + backToMenuAction={() => props.backToMenu()} 104 + /> 105 + <ThemeSetterContent entityID={props.entityID} home /> 106 + </div> 107 + ); 108 + }; 109 + export const AccountSettingsHeader = (props: { 110 + state: "menu" | "general" | "theme"; 111 + backToMenuAction?: () => void; 112 + }) => { 113 + return ( 114 + <div className="flex justify-between font-bold text-secondary bg-border-light -mx-3 -mt-2 px-3 py-2 mb-1"> 115 + {props.state === "menu" 116 + ? "Settings" 117 + : props.state === "general" 118 + ? "General" 119 + : props.state === "theme" 120 + ? "Account Theme" 121 + : ""} 122 + {props.backToMenuAction && ( 123 + <button 124 + type="button" 125 + onClick={() => { 126 + props.backToMenuAction && props.backToMenuAction(); 127 + }} 128 + > 129 + <GoBackSmall className="text-accent-contrast" /> 130 + </button> 131 + )} 132 + </div> 26 133 ); 27 134 };
+6 -5
app/(home-pages)/home/Actions/Actions.tsx
··· 1 1 "use client"; 2 2 import { ThemePopover } from "components/ThemeManager/ThemeSetter"; 3 3 import { CreateNewLeafletButton } from "./CreateNewButton"; 4 - import { HelpPopover } from "components/HelpPopover"; 4 + import { HelpButton } from "app/[leaflet_id]/actions/HelpButton"; 5 5 import { AccountSettings } from "./AccountSettings"; 6 6 import { useIdentityData } from "components/IdentityProvider"; 7 7 import { useReplicache } from "src/replicache"; ··· 13 13 return ( 14 14 <> 15 15 <CreateNewLeafletButton /> 16 - {identity ? <AccountSettings /> : <LoginActionButton />} 17 - {/*<HelpPopover noShortcuts />*/} 18 - <ThemePopover entityID={rootEntity} home /> 19 - <HelpPopover /> 16 + {identity ? ( 17 + <AccountSettings entityID={rootEntity} /> 18 + ) : ( 19 + <LoginActionButton /> 20 + )} 20 21 </> 21 22 ); 22 23 };
+2 -48
app/(home-pages)/home/Actions/CreateNewButton.tsx
··· 1 1 "use client"; 2 2 3 3 import { createNewLeaflet } from "actions/createNewLeaflet"; 4 - import { createNewLeafletFromTemplate } from "actions/createNewLeafletFromTemplate"; 5 4 import { ActionButton } from "components/ActionBar/ActionButton"; 6 5 import { AddTiny } from "components/Icons/AddTiny"; 7 6 import { BlockCanvasPageSmall } from "components/Icons/BlockCanvasPageSmall"; 8 7 import { BlockDocPageSmall } from "components/Icons/BlockDocPageSmall"; 9 - import { TemplateSmall } from "components/Icons/TemplateSmall"; 10 8 import { Menu, MenuItem } from "components/Layout"; 11 9 import { useIsMobile } from "src/hooks/isMobile"; 12 - import { create } from "zustand"; 13 - import { combine, createJSONStorage, persist } from "zustand/middleware"; 14 10 15 - export const useTemplateState = create( 16 - persist( 17 - combine( 18 - { 19 - templates: [] as { id: string; name: string }[], 20 - }, 21 - (set) => ({ 22 - removeTemplate: (template: { id: string }) => 23 - set((state) => { 24 - return { 25 - templates: state.templates.filter((t) => t.id !== template.id), 26 - }; 27 - }), 28 - addTemplate: (template: { id: string; name: string }) => 29 - set((state) => { 30 - if (state.templates.find((t) => t.id === template.id)) return state; 31 - return { templates: [...state.templates, template] }; 32 - }), 33 - }), 34 - ), 35 - { 36 - name: "home-templates", 37 - storage: createJSONStorage(() => localStorage), 38 - }, 39 - ), 40 - ); 41 11 export const CreateNewLeafletButton = (props: {}) => { 42 12 let isMobile = useIsMobile(); 43 - let templates = useTemplateState((s) => s.templates); 44 13 let openNewLeaflet = (id: string) => { 45 14 if (isMobile) { 46 15 window.location.href = `/${id}?focusFirstBlock`; ··· 51 20 return ( 52 21 <Menu 53 22 asChild 23 + side={isMobile ? "top" : "right"} 24 + align={isMobile ? "center" : "start"} 54 25 trigger={ 55 26 <ActionButton 56 27 id="new-leaflet-button" ··· 94 65 </div> 95 66 </div> 96 67 </MenuItem> 97 - {templates.length > 0 && ( 98 - <hr className="border-border-light mx-2 mb-0.5" /> 99 - )} 100 - {templates.map((t) => { 101 - return ( 102 - <MenuItem 103 - key={t.id} 104 - onSelect={async () => { 105 - let id = await createNewLeafletFromTemplate(t.id, false); 106 - if (!id.error) openNewLeaflet(id.id); 107 - }} 108 - > 109 - <TemplateSmall /> 110 - New {t.name} 111 - </MenuItem> 112 - ); 113 - })} 114 68 </Menu> 115 69 ); 116 70 };
+50 -66
app/(home-pages)/home/HomeLayout.tsx
··· 1 1 "use client"; 2 2 3 - import { getHomeDocs, HomeDoc } from "./storage"; 3 + import { getHomeDocs } from "./storage"; 4 4 import useSWR from "swr"; 5 5 import { 6 6 Fact, ··· 13 13 import type { Attribute } from "src/replicache/attributes"; 14 14 import { callRPC } from "app/api/rpc/client"; 15 15 import { StaticLeafletDataContext } from "components/PageSWRDataProvider"; 16 - import { HomeSmall } from "components/Icons/HomeSmall"; 17 16 import { 18 17 HomeDashboardControls, 19 18 DashboardLayout, ··· 22 21 } from "components/PageLayouts/DashboardLayout"; 23 22 import { Actions } from "./Actions/Actions"; 24 23 import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; 25 - import { Json } from "supabase/database.types"; 26 - import { useTemplateState } from "./Actions/CreateNewButton"; 27 - import { CreateNewLeafletButton } from "./Actions/CreateNewButton"; 28 - import { ActionButton } from "components/ActionBar/ActionButton"; 29 - import { AddTiny } from "components/Icons/AddTiny"; 30 - import { 31 - get_leaflet_data, 32 - GetLeafletDataReturnType, 33 - } from "app/api/rpc/[command]/get_leaflet_data"; 34 - import { useEffect, useRef, useState } from "react"; 35 - import { Input } from "components/Input"; 24 + import { GetLeafletDataReturnType } from "app/api/rpc/[command]/get_leaflet_data"; 25 + import { useState } from "react"; 36 26 import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; 37 27 import { 38 - ButtonPrimary, 39 - ButtonSecondary, 40 - ButtonTertiary, 41 - } from "components/Buttons"; 42 - import { AddSmall } from "components/Icons/AddSmall"; 43 - import { PublishIllustration } from "app/[leaflet_id]/publish/PublishIllustration/PublishIllustration"; 44 - import { PubListEmptyIllo } from "components/ActionBar/Publications"; 45 - import { theme } from "tailwind.config"; 46 - import Link from "next/link"; 47 - import { DiscoverIllo } from "./HomeEmpty/DiscoverIllo"; 48 - import { WelcomeToLeafletIllo } from "./HomeEmpty/WelcomeToLeafletIllo"; 49 - import { 50 28 DiscoverBanner, 51 29 HomeEmptyState, 52 30 PublicationBanner, 53 31 } from "./HomeEmpty/HomeEmpty"; 54 32 55 - type Leaflet = { 33 + export type Leaflet = { 56 34 added_at: string; 35 + archived?: boolean | null; 57 36 token: PermissionToken & { 58 37 leaflets_in_publications?: Exclude< 59 38 GetLeafletDataReturnType["result"]["data"], 60 39 null 61 40 >["leaflets_in_publications"]; 41 + leaflets_to_documents?: Exclude< 42 + GetLeafletDataReturnType["result"]["data"], 43 + null 44 + >["leaflets_to_documents"]; 62 45 }; 63 46 }; 64 47 ··· 89 72 let { identity } = useIdentityData(); 90 73 91 74 let hasPubs = !identity || identity.publications.length === 0 ? false : true; 92 - let hasTemplates = 93 - useTemplateState((s) => s.templates).length === 0 ? false : true; 75 + let hasArchived = 76 + identity && 77 + identity.permission_token_on_homepage.filter( 78 + (leaflet) => leaflet.archived === true, 79 + ).length > 0; 94 80 95 81 return ( 96 82 <DashboardLayout ··· 108 94 setSearchValueAction={setSearchValue} 109 95 hasBackgroundImage={hasBackgroundImage} 110 96 hasPubs={hasPubs} 111 - hasTemplates={hasTemplates} 97 + hasArchived={!!hasArchived} 112 98 /> 113 99 ), 114 100 content: ( ··· 148 134 ...identity.permission_token_on_homepage.reduce( 149 135 (acc, tok) => { 150 136 let title = 151 - tok.permission_tokens.leaflets_in_publications[0]?.title; 137 + tok.permission_tokens.leaflets_in_publications[0]?.title || 138 + tok.permission_tokens.leaflets_to_documents[0]?.title; 152 139 if (title) acc[tok.permission_tokens.root_entity] = title; 153 140 return acc; 154 141 }, ··· 168 155 ? identity.permission_token_on_homepage.map((ptoh) => ({ 169 156 added_at: ptoh.created_at, 170 157 token: ptoh.permission_tokens as PermissionToken, 158 + archived: ptoh.archived, 171 159 })) 172 160 : localLeaflets 173 161 .sort((a, b) => (a.added_at > b.added_at ? -1 : 1)) ··· 225 213 w-full 226 214 ${display === "grid" ? "grid auto-rows-max md:grid-cols-4 sm:grid-cols-3 grid-cols-2 gap-y-4 gap-x-4 sm:gap-x-6 sm:gap-y-5 grow" : "flex flex-col gap-2 pt-2"} `} 227 215 > 228 - {props.leaflets.map(({ token: leaflet, added_at }, index) => ( 216 + {props.leaflets.map(({ token: leaflet, added_at, archived }, index) => ( 229 217 <ReplicacheProvider 230 218 disablePull 231 219 initialFactsOnly={!!identity} ··· 239 227 value={{ 240 228 ...leaflet, 241 229 leaflets_in_publications: leaflet.leaflets_in_publications || [], 230 + leaflets_to_documents: leaflet.leaflets_to_documents || [], 242 231 blocked_by_admin: null, 243 232 custom_domain_routes: [], 244 233 }} 245 234 > 246 235 <LeafletListItem 247 - title={props?.titles?.[leaflet.root_entity] || "Untitled"} 248 - token={leaflet} 249 - draft={!!leaflet.leaflets_in_publications?.length} 250 - published={!!leaflet.leaflets_in_publications?.find((l) => l.doc)} 251 - publishedAt={ 252 - leaflet.leaflets_in_publications?.find((l) => l.doc)?.documents 253 - ?.indexed_at 254 - } 255 - leaflet_id={leaflet.root_entity} 236 + title={props?.titles?.[leaflet.root_entity]} 237 + archived={archived} 256 238 loggedIn={!!identity} 257 239 display={display} 258 240 added_at={added_at} ··· 281 263 282 264 let sortedLeaflets = leaflets.sort((a, b) => { 283 265 if (sort === "alphabetical") { 284 - if (titles[a.token.root_entity] === titles[b.token.root_entity]) { 266 + let titleA = titles[a.token.root_entity] ?? "Untitled"; 267 + let titleB = titles[b.token.root_entity] ?? "Untitled"; 268 + 269 + if (titleA === titleB) { 285 270 return a.added_at > b.added_at ? -1 : 1; 286 271 } else { 287 - return titles[a.token.root_entity].toLocaleLowerCase() > 288 - titles[b.token.root_entity].toLocaleLowerCase() 289 - ? 1 290 - : -1; 272 + return titleA.toLocaleLowerCase() > titleB.toLocaleLowerCase() ? 1 : -1; 291 273 } 292 274 } else { 293 275 return a.added_at === b.added_at ··· 300 282 } 301 283 }); 302 284 303 - let allTemplates = useTemplateState((s) => s.templates); 304 - let filteredLeaflets = sortedLeaflets.filter(({ token: leaflet }) => { 305 - let published = !!leaflet.leaflets_in_publications?.find((l) => l.doc); 306 - let drafts = !!leaflet.leaflets_in_publications?.length && !published; 307 - let docs = !leaflet.leaflets_in_publications?.length; 308 - let templates = !!allTemplates.find((t) => t.id === leaflet.id); 309 - // If no filters are active, show all 310 - if ( 311 - !filter.drafts && 312 - !filter.published && 313 - !filter.docs && 314 - !filter.templates 315 - ) 316 - return true; 285 + let filteredLeaflets = sortedLeaflets.filter( 286 + ({ token: leaflet, archived: archived }) => { 287 + let published = 288 + !!leaflet.leaflets_in_publications?.find((l) => l.doc) || 289 + !!leaflet.leaflets_to_documents?.find((l) => l.document); 290 + let drafts = !!leaflet.leaflets_in_publications?.length && !published; 291 + let docs = !leaflet.leaflets_in_publications?.length && !archived; 292 + // If no filters are active, show all 293 + if ( 294 + !filter.drafts && 295 + !filter.published && 296 + !filter.docs && 297 + !filter.archived 298 + ) 299 + return archived === false || archived === null || archived == undefined; 317 300 318 - return ( 319 - (filter.drafts && drafts) || 320 - (filter.published && published) || 321 - (filter.docs && docs) || 322 - (filter.templates && templates) 323 - ); 324 - }); 301 + return ( 302 + (filter.drafts && drafts) || 303 + (filter.published && published) || 304 + (filter.docs && docs) || 305 + (filter.archived && archived) 306 + ); 307 + }, 308 + ); 325 309 if (searchValue === "") return filteredLeaflets; 326 310 let searchedLeaflets = filteredLeaflets.filter(({ token: leaflet }) => { 327 311 return titles[leaflet.root_entity]
+29 -57
app/(home-pages)/home/LeafletList/LeafletInfo.tsx
··· 1 1 "use client"; 2 - import { PermissionToken } from "src/replicache"; 2 + import { useEntity } from "src/replicache"; 3 3 import { LeafletOptions } from "./LeafletOptions"; 4 - import Link from "next/link"; 5 - import { useState } from "react"; 6 - import { theme } from "tailwind.config"; 7 - import { TemplateSmall } from "components/Icons/TemplateSmall"; 8 4 import { timeAgo } from "src/utils/timeAgo"; 5 + import { usePageTitle } from "components/utils/UpdateLeafletTitle"; 6 + import { useLeafletPublicationStatus } from "components/PageSWRDataProvider"; 9 7 10 8 export const LeafletInfo = (props: { 11 9 title?: string; 12 - draft?: boolean; 13 - published?: boolean; 14 - token: PermissionToken; 15 - leaflet_id: string; 16 - loggedIn: boolean; 17 - isTemplate: boolean; 18 10 className?: string; 19 11 display: "grid" | "list"; 20 12 added_at: string; 21 - publishedAt?: string; 13 + archived?: boolean | null; 14 + loggedIn: boolean; 22 15 }) => { 23 - let [prefetch, setPrefetch] = useState(false); 16 + const pubStatus = useLeafletPublicationStatus(); 24 17 let prettyCreatedAt = props.added_at ? timeAgo(props.added_at) : ""; 18 + let prettyPublishedAt = pubStatus?.publishedAt 19 + ? timeAgo(pubStatus.publishedAt) 20 + : ""; 25 21 26 - let prettyPublishedAt = props.publishedAt ? timeAgo(props.publishedAt) : ""; 22 + // Look up root page first, like UpdateLeafletTitle does 23 + let firstPage = useEntity(pubStatus?.leafletId ?? "", "root/page")[0]; 24 + let entityID = firstPage?.data.value || pubStatus?.leafletId || ""; 25 + let titleFromDb = usePageTitle(entityID); 26 + 27 + let title = props.title ?? titleFromDb ?? "Untitled"; 27 28 28 29 return ( 29 30 <div 30 31 className={`leafletInfo w-full min-w-0 flex flex-col ${props.className}`} 31 32 > 32 33 <div className="flex justify-between items-center shrink-0 max-w-full gap-2 leading-tight overflow-hidden"> 33 - <Link 34 - onMouseEnter={() => setPrefetch(true)} 35 - onPointerDown={() => setPrefetch(true)} 36 - prefetch={prefetch} 37 - href={`/${props.token.id}`} 38 - className="no-underline sm:hover:no-underline text-primary grow min-w-0" 39 - > 40 - <h3 className="sm:text-lg text-base truncate w-full min-w-0"> 41 - {props.title} 42 - </h3> 43 - </Link> 34 + <h3 className="sm:text-lg text-base truncate w-full min-w-0"> 35 + {title} 36 + </h3> 44 37 <div className="flex gap-1 shrink-0"> 45 - {props.isTemplate && props.display === "list" ? ( 46 - <TemplateSmall 47 - fill={theme.colors["bg-page"]} 48 - className="text-tertiary" 49 - /> 50 - ) : null} 51 - <LeafletOptions 52 - leaflet={props.token} 53 - isTemplate={props.isTemplate} 54 - loggedIn={props.loggedIn} 55 - added_at={props.added_at} 56 - /> 38 + <LeafletOptions archived={props.archived} loggedIn={props.loggedIn} /> 57 39 </div> 58 40 </div> 59 - <Link 60 - onMouseEnter={() => setPrefetch(true)} 61 - onPointerDown={() => setPrefetch(true)} 62 - prefetch={prefetch} 63 - href={`/${props.token.id}`} 64 - className="no-underline sm:hover:no-underline text-primary w-full" 65 - > 66 - {props.draft || props.published ? ( 41 + <div className="flex gap-2 items-center"> 42 + {props.archived ? ( 43 + <div className="text-xs text-tertiary truncate">Archived</div> 44 + ) : pubStatus?.draftInPublication || pubStatus?.isPublished ? ( 67 45 <div 68 - className={`text-xs ${props.published ? "font-bold text-tertiary" : "text-tertiary"}`} 46 + className={`text-xs w-max grow truncate ${pubStatus?.isPublished ? "font-bold text-tertiary" : "text-tertiary"}`} 69 47 > 70 - {props.published 48 + {pubStatus?.isPublished 71 49 ? `Published ${prettyPublishedAt}` 72 50 : `Draft ${prettyCreatedAt}`} 73 51 </div> 74 52 ) : ( 75 - <div className="text-xs text-tertiary">{prettyCreatedAt}</div> 53 + <div className="text-xs text-tertiary grow w-max truncate"> 54 + {prettyCreatedAt} 55 + </div> 76 56 )} 77 - </Link> 78 - {props.isTemplate && props.display === "grid" ? ( 79 - <div className="absolute -top-2 right-1"> 80 - <TemplateSmall 81 - className="text-tertiary" 82 - fill={theme.colors["bg-page"]} 83 - /> 84 - </div> 85 - ) : null} 57 + </div> 86 58 </div> 87 59 ); 88 60 };
+45 -26
app/(home-pages)/home/LeafletList/LeafletListItem.tsx
··· 1 1 "use client"; 2 - import { PermissionToken } from "src/replicache"; 3 - import { useTemplateState } from "../Actions/CreateNewButton"; 4 2 import { LeafletListPreview, LeafletGridPreview } from "./LeafletPreview"; 5 3 import { LeafletInfo } from "./LeafletInfo"; 6 4 import { useState, useRef, useEffect } from "react"; 5 + import { SpeedyLink } from "components/SpeedyLink"; 6 + import { useLeafletPublicationStatus } from "components/PageSWRDataProvider"; 7 7 8 8 export const LeafletListItem = (props: { 9 - token: PermissionToken; 10 - leaflet_id: string; 9 + archived?: boolean | null; 11 10 loggedIn: boolean; 12 11 display: "list" | "grid"; 13 12 cardBorderHidden: boolean; 14 13 added_at: string; 15 - title: string; 16 - draft?: boolean; 17 - published?: boolean; 18 - publishedAt?: string; 14 + title?: string; 19 15 index: number; 20 16 isHidden: boolean; 21 17 showPreview?: boolean; 22 18 }) => { 23 - let isTemplate = useTemplateState( 24 - (s) => !!s.templates.find((t) => t.id === props.token.id), 25 - ); 26 - 19 + const pubStatus = useLeafletPublicationStatus(); 27 20 let [isOnScreen, setIsOnScreen] = useState(props.index < 16 ? true : false); 28 21 let previewRef = useRef<HTMLDivElement | null>(null); 29 22 ··· 45 38 return () => observer.disconnect(); 46 39 }, [previewRef]); 47 40 41 + const tokenId = pubStatus?.shareLink ?? ""; 42 + 48 43 if (props.display === "list") 49 44 return ( 50 45 <> 51 46 <div 52 47 ref={previewRef} 53 - className={`gap-3 w-full ${props.cardBorderHidden ? "" : "px-2 py-1 block-border hover:outline-border"}`} 48 + className={`relative flex gap-3 w-full 49 + ${props.isHidden ? "hidden" : "flex"} 50 + ${props.cardBorderHidden ? "" : "px-2 py-1 block-border hover:outline-border relative"}`} 54 51 style={{ 55 52 backgroundColor: props.cardBorderHidden 56 53 ? "transparent" 57 54 : "rgba(var(--bg-page), var(--bg-page-alpha))", 58 - 59 - display: props.isHidden ? "none" : "flex", 60 55 }} 61 56 > 62 - {props.showPreview && ( 63 - <LeafletListPreview isVisible={isOnScreen} {...props} /> 64 - )} 65 - <LeafletInfo isTemplate={isTemplate} {...props} /> 57 + <SpeedyLink 58 + href={`/${tokenId}`} 59 + className={`absolute w-full h-full top-0 left-0 no-underline hover:no-underline! text-primary`} 60 + /> 61 + {props.showPreview && <LeafletListPreview isVisible={isOnScreen} />} 62 + <LeafletInfo 63 + title={props.title} 64 + display={props.display} 65 + added_at={props.added_at} 66 + archived={props.archived} 67 + loggedIn={props.loggedIn} 68 + /> 66 69 </div> 67 70 {props.cardBorderHidden && ( 68 71 <hr ··· 77 80 return ( 78 81 <div 79 82 ref={previewRef} 80 - className={`leafletGridListItem relative 81 - flex flex-col gap-1 p-1 h-52 83 + className={` 84 + relative 85 + flex flex-col gap-1 p-1 h-52 w-full 82 86 block-border border-border! hover:outline-border 87 + ${props.isHidden ? "hidden" : "flex"} 83 88 `} 84 89 style={{ 85 90 backgroundColor: props.cardBorderHidden 86 91 ? "transparent" 87 92 : "rgba(var(--bg-page), var(--bg-page-alpha))", 88 - 89 - display: props.isHidden ? "none" : "flex", 90 93 }} 91 94 > 95 + <SpeedyLink 96 + href={`/${tokenId}`} 97 + className={`absolute w-full h-full top-0 left-0 no-underline hover:no-underline! text-primary`} 98 + /> 92 99 <div className="grow"> 93 - <LeafletGridPreview {...props} isVisible={isOnScreen} /> 100 + <LeafletGridPreview isVisible={isOnScreen} /> 94 101 </div> 95 102 <LeafletInfo 96 - isTemplate={isTemplate} 97 103 className="px-1 pb-0.5 shrink-0" 98 - {...props} 104 + title={props.title} 105 + display={props.display} 106 + added_at={props.added_at} 107 + archived={props.archived} 108 + loggedIn={props.loggedIn} 99 109 /> 100 110 </div> 101 111 ); 102 112 }; 113 + 114 + const LeafletLink = (props: { id: string; className: string }) => { 115 + return ( 116 + <SpeedyLink 117 + href={`/${props.id}`} 118 + className={`no-underline hover:no-underline! text-primary ${props.className}`} 119 + /> 120 + ); 121 + };
+301 -170
app/(home-pages)/home/LeafletList/LeafletOptions.tsx
··· 1 1 "use client"; 2 2 3 3 import { Menu, MenuItem } from "components/Layout"; 4 - import { useReplicache, type PermissionToken } from "src/replicache"; 5 - import { hideDoc } from "../storage"; 6 4 import { useState } from "react"; 7 - import { ButtonPrimary } from "components/Buttons"; 8 - import { useTemplateState } from "../Actions/CreateNewButton"; 9 - import { useSmoker, useToaster } from "components/Toast"; 10 - import { removeLeafletFromHome } from "actions/removeLeafletFromHome"; 11 - import { useIdentityData } from "components/IdentityProvider"; 5 + import { ButtonPrimary, ButtonTertiary } from "components/Buttons"; 6 + import { useToaster } from "components/Toast"; 7 + import { MoreOptionsVerticalTiny } from "components/Icons/MoreOptionsVerticalTiny"; 8 + import { DeleteSmall } from "components/Icons/DeleteSmall"; 9 + import { 10 + archivePost, 11 + deleteLeaflet, 12 + unarchivePost, 13 + } from "actions/deleteLeaflet"; 14 + import { ArchiveSmall } from "components/Icons/ArchiveSmall"; 15 + import { UnpublishSmall } from "components/Icons/UnpublishSmall"; 16 + import { 17 + deletePost, 18 + unpublishPost, 19 + } from "app/lish/[did]/[publication]/dashboard/deletePost"; 20 + import { ShareSmall } from "components/Icons/ShareSmall"; 12 21 import { HideSmall } from "components/Icons/HideSmall"; 13 - import { MoreOptionsTiny } from "components/Icons/MoreOptionsTiny"; 14 - import { TemplateRemoveSmall } from "components/Icons/TemplateRemoveSmall"; 15 - import { TemplateSmall } from "components/Icons/TemplateSmall"; 16 - import { MoreOptionsVerticalTiny } from "components/Icons/MoreOptionsVerticalTiny"; 17 - import { addLeafletToHome } from "actions/addLeafletToHome"; 22 + import { hideDoc } from "../storage"; 23 + 24 + import { 25 + useIdentityData, 26 + mutateIdentityData, 27 + } from "components/IdentityProvider"; 28 + import { 29 + usePublicationData, 30 + mutatePublicationData, 31 + } from "app/lish/[did]/[publication]/dashboard/PublicationSWRProvider"; 32 + import { ShareButton } from "app/[leaflet_id]/actions/ShareOptions"; 33 + import { useLeafletPublicationStatus } from "components/PageSWRDataProvider"; 18 34 19 35 export const LeafletOptions = (props: { 20 - leaflet: PermissionToken; 21 - isTemplate: boolean; 22 - loggedIn: boolean; 23 - added_at: string; 36 + archived?: boolean | null; 37 + loggedIn?: boolean; 24 38 }) => { 25 - let { mutate: mutateIdentity } = useIdentityData(); 26 - let [state, setState] = useState<"normal" | "template">("normal"); 39 + const pubStatus = useLeafletPublicationStatus(); 40 + let [state, setState] = useState<"normal" | "areYouSure">("normal"); 27 41 let [open, setOpen] = useState(false); 28 - let smoker = useSmoker(); 29 - let toaster = useToaster(); 42 + let { identity } = useIdentityData(); 43 + let isPublicationOwner = 44 + !!identity?.atp_did && !!pubStatus?.documentUri?.includes(identity.atp_did); 30 45 return ( 31 46 <> 32 47 <Menu ··· 38 53 }} 39 54 trigger={ 40 55 <div 41 - className="text-secondary shrink-0" 56 + className="text-secondary shrink-0 relative" 42 57 onClick={(e) => { 43 58 e.preventDefault; 44 59 e.stopPropagation; ··· 49 64 } 50 65 > 51 66 {state === "normal" ? ( 52 - <> 53 - {!props.isTemplate ? ( 54 - <MenuItem 55 - onSelect={(e) => { 56 - e.preventDefault(); 57 - setState("template"); 58 - }} 59 - > 60 - <TemplateSmall /> Add as Template 61 - </MenuItem> 62 - ) : ( 63 - <MenuItem 64 - onSelect={(e) => { 65 - useTemplateState.getState().removeTemplate(props.leaflet); 66 - let newLeafletButton = 67 - document.getElementById("new-leaflet-button"); 68 - if (!newLeafletButton) return; 69 - let rect = newLeafletButton.getBoundingClientRect(); 70 - smoker({ 71 - static: true, 72 - text: <strong>Removed template!</strong>, 73 - position: { 74 - y: rect.top, 75 - x: rect.right + 5, 76 - }, 77 - }); 78 - }} 79 - > 80 - <TemplateRemoveSmall /> Remove from Templates 81 - </MenuItem> 82 - )} 83 - <MenuItem 84 - onSelect={async () => { 85 - if (props.loggedIn) { 86 - mutateIdentity( 87 - (s) => { 88 - if (!s) return s; 89 - return { 90 - ...s, 91 - permission_token_on_homepage: 92 - s.permission_token_on_homepage.filter( 93 - (ptrh) => 94 - ptrh.permission_tokens.id !== props.leaflet.id, 95 - ), 96 - }; 97 - }, 98 - { revalidate: false }, 99 - ); 100 - await removeLeafletFromHome([props.leaflet.id]); 101 - mutateIdentity(); 102 - } else { 103 - hideDoc(props.leaflet); 104 - } 105 - toaster({ 106 - content: ( 107 - <div className="font-bold"> 108 - Doc removed!{" "} 109 - <UndoRemoveFromHomeButton 110 - leaflet={props.leaflet} 111 - added_at={props.added_at} 112 - /> 113 - </div> 114 - ), 115 - type: "success", 116 - }); 117 - }} 118 - > 119 - <HideSmall /> 120 - Remove from Home 121 - </MenuItem> 122 - </> 123 - ) : state === "template" ? ( 124 - <AddTemplateForm 125 - leaflet={props.leaflet} 126 - close={() => setOpen(false)} 127 - /> 67 + !props.loggedIn ? ( 68 + <LoggedOutOptions setState={setState} /> 69 + ) : pubStatus?.documentUri && isPublicationOwner ? ( 70 + <PublishedPostOptions setState={setState} /> 71 + ) : ( 72 + <DefaultOptions setState={setState} archived={props.archived} /> 73 + ) 74 + ) : state === "areYouSure" ? ( 75 + <DeleteAreYouSureForm backToMenu={() => setState("normal")} /> 128 76 ) : null} 129 77 </Menu> 130 78 </> 131 79 ); 132 80 }; 133 81 134 - const UndoRemoveFromHomeButton = (props: { 135 - leaflet: PermissionToken; 136 - added_at: string | undefined; 82 + const DefaultOptions = (props: { 83 + setState: (s: "areYouSure") => void; 84 + archived?: boolean | null; 137 85 }) => { 138 - let toaster = useToaster(); 139 - let { mutate } = useIdentityData(); 86 + const pubStatus = useLeafletPublicationStatus(); 87 + const toaster = useToaster(); 88 + const { setArchived } = useArchiveMutations(); 89 + const { identity } = useIdentityData(); 90 + const tokenId = pubStatus?.token.id; 91 + const itemType = pubStatus?.draftInPublication ? "Draft" : "Leaflet"; 92 + 93 + // Check if this is a published post/document and if user is the owner 94 + const isPublishedPostOwner = 95 + !!identity?.atp_did && !!pubStatus?.documentUri?.includes(identity.atp_did); 96 + const canDelete = !pubStatus?.documentUri || isPublishedPostOwner; 97 + 140 98 return ( 141 - <button 142 - onClick={async (e) => { 143 - await mutate( 144 - (identity) => { 145 - if (!identity) return; 146 - return { 147 - ...identity, 148 - permission_token_on_homepage: [ 149 - ...identity.permission_token_on_homepage, 150 - { 151 - created_at: props.added_at || new Date().toISOString(), 152 - permission_tokens: { 153 - ...props.leaflet, 154 - leaflets_in_publications: [], 155 - }, 156 - }, 157 - ], 158 - }; 159 - }, 160 - { revalidate: false }, 161 - ); 162 - await addLeafletToHome(props.leaflet.id); 163 - await mutate(); 99 + <> 100 + <EditLinkShareButton link={pubStatus?.shareLink ?? ""} /> 101 + <hr className="border-border-light" /> 102 + <MenuItem 103 + onSelect={async () => { 104 + if (!tokenId) return; 105 + setArchived(tokenId, !props.archived); 106 + 107 + if (!props.archived) { 108 + await archivePost(tokenId); 109 + toaster({ 110 + content: ( 111 + <div className="font-bold flex gap-2"> 112 + Archived {itemType}! 113 + <ButtonTertiary 114 + className="underline text-accent-2!" 115 + onClick={async () => { 116 + setArchived(tokenId, false); 117 + await unarchivePost(tokenId); 118 + toaster({ 119 + content: <div className="font-bold">Unarchived!</div>, 120 + type: "success", 121 + }); 122 + }} 123 + > 124 + Undo? 125 + </ButtonTertiary> 126 + </div> 127 + ), 128 + type: "success", 129 + }); 130 + } else { 131 + await unarchivePost(tokenId); 132 + toaster({ 133 + content: <div className="font-bold">Unarchived!</div>, 134 + type: "success", 135 + }); 136 + } 137 + }} 138 + > 139 + <ArchiveSmall /> 140 + {!props.archived ? " Archive" : "Unarchive"} {itemType} 141 + </MenuItem> 142 + {canDelete && ( 143 + <DeleteForeverMenuItem 144 + onSelect={(e) => { 145 + e.preventDefault(); 146 + props.setState("areYouSure"); 147 + }} 148 + /> 149 + )} 150 + </> 151 + ); 152 + }; 153 + 154 + const LoggedOutOptions = (props: { setState: (s: "areYouSure") => void }) => { 155 + const pubStatus = useLeafletPublicationStatus(); 156 + const toaster = useToaster(); 164 157 165 - toaster({ 166 - content: <div className="font-bold">Recovered Doc!</div>, 167 - type: "success", 168 - }); 169 - }} 170 - className="underline" 171 - > 172 - Undo? 173 - </button> 158 + return ( 159 + <> 160 + <EditLinkShareButton link={`/${pubStatus?.shareLink ?? ""}`} /> 161 + <hr className="border-border-light" /> 162 + <MenuItem 163 + onSelect={() => { 164 + if (pubStatus?.token) hideDoc(pubStatus.token); 165 + toaster({ 166 + content: <div className="font-bold">Removed from Home!</div>, 167 + type: "success", 168 + }); 169 + }} 170 + > 171 + <HideSmall /> 172 + Remove from Home 173 + </MenuItem> 174 + <DeleteForeverMenuItem 175 + onSelect={(e) => { 176 + e.preventDefault(); 177 + props.setState("areYouSure"); 178 + }} 179 + /> 180 + </> 174 181 ); 175 182 }; 176 183 177 - const AddTemplateForm = (props: { 178 - leaflet: PermissionToken; 179 - close: () => void; 184 + const PublishedPostOptions = (props: { 185 + setState: (s: "areYouSure") => void; 180 186 }) => { 181 - let [name, setName] = useState(""); 182 - let smoker = useSmoker(); 183 - return ( 184 - <div className="flex flex-col gap-2 px-3 py-1"> 185 - <label className="font-bold flex flex-col gap-1 text-secondary"> 186 - Template Name 187 - <input 188 - value={name} 189 - onChange={(e) => setName(e.target.value)} 190 - type="text" 191 - className=" text-primary font-normal border border-border rounded-md outline-hidden px-2 py-1 w-64" 192 - /> 193 - </label> 187 + const pubStatus = useLeafletPublicationStatus(); 188 + const toaster = useToaster(); 189 + const postLink = pubStatus?.postShareLink ?? ""; 190 + const isFullUrl = postLink.includes("http"); 194 191 195 - <ButtonPrimary 196 - onClick={() => { 197 - useTemplateState.getState().addTemplate({ 198 - name, 199 - id: props.leaflet.id, 192 + return ( 193 + <> 194 + <ShareButton 195 + text={ 196 + <div className="flex gap-2"> 197 + <ShareSmall /> 198 + Copy Post Link 199 + </div> 200 + } 201 + smokerText="Link copied!" 202 + id="get-link" 203 + link={postLink} 204 + fullLink={isFullUrl ? postLink : undefined} 205 + /> 206 + <hr className="border-border-light" /> 207 + <MenuItem 208 + onSelect={async () => { 209 + if (pubStatus?.documentUri) { 210 + await unpublishPost(pubStatus.documentUri); 211 + } 212 + toaster({ 213 + content: <div className="font-bold">Unpublished Post!</div>, 214 + type: "success", 200 215 }); 201 - let newLeafletButton = document.getElementById("new-leaflet-button"); 202 - if (!newLeafletButton) return; 203 - let rect = newLeafletButton.getBoundingClientRect(); 204 - smoker({ 205 - static: true, 206 - text: <strong>Added {name}!</strong>, 207 - position: { 208 - y: rect.top, 209 - x: rect.right + 5, 210 - }, 211 - }); 212 - props.close(); 213 216 }} 214 - className="place-self-end" 215 217 > 216 - Add Template 217 - </ButtonPrimary> 218 + <UnpublishSmall /> 219 + <div className="flex flex-col"> 220 + Unpublish Post 221 + <div className="text-tertiary text-sm font-normal!"> 222 + Move this post back into drafts 223 + </div> 224 + </div> 225 + </MenuItem> 226 + <DeleteForeverMenuItem 227 + onSelect={(e) => { 228 + e.preventDefault(); 229 + props.setState("areYouSure"); 230 + }} 231 + subtext="Post" 232 + /> 233 + </> 234 + ); 235 + }; 236 + 237 + const DeleteAreYouSureForm = (props: { backToMenu: () => void }) => { 238 + const pubStatus = useLeafletPublicationStatus(); 239 + const toaster = useToaster(); 240 + const { removeFromLists } = useArchiveMutations(); 241 + const tokenId = pubStatus?.token.id; 242 + 243 + const itemType = pubStatus?.documentUri 244 + ? "Post" 245 + : pubStatus?.draftInPublication 246 + ? "Draft" 247 + : "Leaflet"; 248 + 249 + return ( 250 + <div className="flex flex-col justify-center p-2 text-center"> 251 + <div className="text-primary font-bold"> Are you sure?</div> 252 + <div className="text-sm text-secondary"> 253 + This will delete it forever for everyone! 254 + </div> 255 + <div className="flex gap-2 mx-auto items-center mt-2"> 256 + <ButtonTertiary onClick={() => props.backToMenu()}> 257 + Nevermind 258 + </ButtonTertiary> 259 + <ButtonPrimary 260 + onClick={async () => { 261 + if (tokenId) removeFromLists(tokenId); 262 + if (pubStatus?.documentUri) { 263 + await deletePost(pubStatus.documentUri); 264 + } 265 + if (pubStatus?.token) deleteLeaflet(pubStatus.token); 266 + 267 + toaster({ 268 + content: <div className="font-bold">Deleted {itemType}!</div>, 269 + type: "success", 270 + }); 271 + }} 272 + > 273 + Delete it! 274 + </ButtonPrimary> 275 + </div> 218 276 </div> 219 277 ); 220 278 }; 279 + 280 + // Shared menu items 281 + const EditLinkShareButton = (props: { link: string }) => ( 282 + <ShareButton 283 + text={ 284 + <div className="flex gap-2"> 285 + <ShareSmall /> 286 + Copy Edit Link 287 + </div> 288 + } 289 + subtext="" 290 + smokerText="Link copied!" 291 + id="get-link" 292 + link={props.link} 293 + /> 294 + ); 295 + 296 + const DeleteForeverMenuItem = (props: { 297 + onSelect: (e: Event) => void; 298 + subtext?: string; 299 + }) => ( 300 + <MenuItem onSelect={props.onSelect}> 301 + <DeleteSmall /> 302 + {props.subtext ? ( 303 + <div className="flex flex-col"> 304 + Delete {props.subtext} 305 + <div className="text-tertiary text-sm font-normal!"> 306 + Unpublish AND delete 307 + </div> 308 + </div> 309 + ) : ( 310 + "Delete Forever" 311 + )} 312 + </MenuItem> 313 + ); 314 + 315 + // Helper to update archived state in both identity and publication data 316 + function useArchiveMutations() { 317 + const { mutate: mutatePub } = usePublicationData(); 318 + const { mutate: mutateIdentity } = useIdentityData(); 319 + 320 + return { 321 + setArchived: (tokenId: string, archived: boolean) => { 322 + mutateIdentityData(mutateIdentity, (data) => { 323 + const item = data.permission_token_on_homepage.find( 324 + (p) => p.permission_tokens?.id === tokenId, 325 + ); 326 + if (item) item.archived = archived; 327 + }); 328 + mutatePublicationData(mutatePub, (data) => { 329 + const item = data.publication?.leaflets_in_publications.find( 330 + (l) => l.permission_tokens?.id === tokenId, 331 + ); 332 + if (item) item.archived = archived; 333 + }); 334 + }, 335 + removeFromLists: (tokenId: string) => { 336 + mutateIdentityData(mutateIdentity, (data) => { 337 + data.permission_token_on_homepage = 338 + data.permission_token_on_homepage.filter( 339 + (p) => p.permission_tokens?.id !== tokenId, 340 + ); 341 + }); 342 + mutatePublicationData(mutatePub, (data) => { 343 + if (!data.publication) return; 344 + data.publication.leaflets_in_publications = 345 + data.publication.leaflets_in_publications.filter( 346 + (l) => l.permission_tokens?.id !== tokenId, 347 + ); 348 + }); 349 + }, 350 + }; 351 + }
+49 -112
app/(home-pages)/home/LeafletList/LeafletPreview.tsx
··· 3 3 ThemeBackgroundProvider, 4 4 ThemeProvider, 5 5 } from "components/ThemeManager/ThemeProvider"; 6 - import { 7 - PermissionToken, 8 - useEntity, 9 - useReferenceToEntity, 10 - } from "src/replicache"; 11 - import { useTemplateState } from "../Actions/CreateNewButton"; 6 + import { useEntity, useReferenceToEntity } from "src/replicache"; 12 7 import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; 13 8 import { LeafletContent } from "./LeafletContent"; 14 9 import { Tooltip } from "components/Tooltip"; 15 - import { useState } from "react"; 16 - import Link from "next/link"; 17 - import { SpeedyLink } from "components/SpeedyLink"; 10 + import { useLeafletPublicationStatus } from "components/PageSWRDataProvider"; 11 + import { CSSProperties } from "react"; 18 12 19 - export const LeafletListPreview = (props: { 20 - draft?: boolean; 21 - published?: boolean; 22 - isVisible: boolean; 23 - token: PermissionToken; 24 - leaflet_id: string; 25 - loggedIn: boolean; 26 - }) => { 27 - let root = 28 - useReferenceToEntity("root/page", props.leaflet_id)[0]?.entity || 29 - props.leaflet_id; 30 - let firstPage = useEntity(root, "root/page")[0]; 31 - let page = firstPage?.data.value || root; 13 + function useLeafletPreviewData() { 14 + const pubStatus = useLeafletPublicationStatus(); 15 + const leafletId = pubStatus?.leafletId ?? ""; 16 + const root = 17 + useReferenceToEntity("root/page", leafletId)[0]?.entity || leafletId; 18 + const firstPage = useEntity(root, "root/page")[0]; 19 + const page = firstPage?.data.value || root; 32 20 33 - let cardBorderHidden = useCardBorderHidden(root); 34 - let rootBackgroundImage = useEntity(root, "theme/card-background-image"); 35 - let rootBackgroundRepeat = useEntity( 21 + const cardBorderHidden = useCardBorderHidden(root); 22 + const rootBackgroundImage = useEntity(root, "theme/card-background-image"); 23 + const rootBackgroundRepeat = useEntity( 36 24 root, 37 25 "theme/card-background-image-repeat", 38 26 ); 39 - let rootBackgroundOpacity = useEntity( 27 + const rootBackgroundOpacity = useEntity( 40 28 root, 41 29 "theme/card-background-image-opacity", 42 30 ); 43 31 32 + const contentWrapperStyle: CSSProperties = cardBorderHidden 33 + ? {} 34 + : { 35 + backgroundImage: rootBackgroundImage 36 + ? `url(${rootBackgroundImage.data.src}), url(${rootBackgroundImage.data.fallback})` 37 + : undefined, 38 + backgroundRepeat: rootBackgroundRepeat ? "repeat" : "no-repeat", 39 + backgroundPosition: "center", 40 + backgroundSize: !rootBackgroundRepeat 41 + ? "cover" 42 + : rootBackgroundRepeat?.data.value / 3, 43 + opacity: 44 + rootBackgroundImage?.data.src && rootBackgroundOpacity 45 + ? rootBackgroundOpacity.data.value 46 + : 1, 47 + backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))", 48 + }; 49 + 50 + const contentWrapperClass = `leafletContentWrapper h-full sm:w-48 w-40 mx-auto overflow-clip ${!cardBorderHidden && "border border-border-light border-b-0 rounded-t-md"}`; 51 + 52 + return { root, page, cardBorderHidden, contentWrapperStyle, contentWrapperClass }; 53 + } 54 + 55 + export const LeafletListPreview = (props: { isVisible: boolean }) => { 56 + const { root, page, cardBorderHidden, contentWrapperStyle, contentWrapperClass } = 57 + useLeafletPreviewData(); 58 + 44 59 return ( 45 60 <Tooltip 46 61 open={true} ··· 77 92 <ThemeProvider local entityID={root} className="rounded-sm"> 78 93 <ThemeBackgroundProvider entityID={root}> 79 94 <div className="leafletPreview grow shrink-0 h-44 w-64 px-2 pt-2 sm:px-3 sm:pt-3 flex items-end pointer-events-none rounded-[2px] "> 80 - <div 81 - className={`leafletContentWrapper h-full sm:w-48 w-40 mx-auto overflow-clip ${!cardBorderHidden && "border border-border-light border-b-0 rounded-t-md"}`} 82 - style={ 83 - cardBorderHidden 84 - ? {} 85 - : { 86 - backgroundImage: rootBackgroundImage 87 - ? `url(${rootBackgroundImage.data.src}), url(${rootBackgroundImage.data.fallback})` 88 - : undefined, 89 - backgroundRepeat: rootBackgroundRepeat 90 - ? "repeat" 91 - : "no-repeat", 92 - backgroundPosition: "center", 93 - backgroundSize: !rootBackgroundRepeat 94 - ? "cover" 95 - : rootBackgroundRepeat?.data.value / 3, 96 - opacity: 97 - rootBackgroundImage?.data.src && rootBackgroundOpacity 98 - ? rootBackgroundOpacity.data.value 99 - : 1, 100 - backgroundColor: 101 - "rgba(var(--bg-page), var(--bg-page-alpha))", 102 - } 103 - } 104 - > 95 + <div className={contentWrapperClass} style={contentWrapperStyle}> 105 96 <LeafletContent entityID={page} isOnScreen={props.isVisible} /> 106 97 </div> 107 98 </div> ··· 111 102 ); 112 103 }; 113 104 114 - export const LeafletGridPreview = (props: { 115 - draft?: boolean; 116 - published?: boolean; 117 - token: PermissionToken; 118 - leaflet_id: string; 119 - loggedIn: boolean; 120 - isVisible: boolean; 121 - }) => { 122 - let root = 123 - useReferenceToEntity("root/page", props.leaflet_id)[0]?.entity || 124 - props.leaflet_id; 125 - let firstPage = useEntity(root, "root/page")[0]; 126 - let page = firstPage?.data.value || root; 105 + export const LeafletGridPreview = (props: { isVisible: boolean }) => { 106 + const { root, page, contentWrapperStyle, contentWrapperClass } = 107 + useLeafletPreviewData(); 127 108 128 - let cardBorderHidden = useCardBorderHidden(root); 129 - let rootBackgroundImage = useEntity(root, "theme/card-background-image"); 130 - let rootBackgroundRepeat = useEntity( 131 - root, 132 - "theme/card-background-image-repeat", 133 - ); 134 - let rootBackgroundOpacity = useEntity( 135 - root, 136 - "theme/card-background-image-opacity", 137 - ); 138 109 return ( 139 110 <ThemeProvider local entityID={root} className="w-full!"> 140 - <div className="border border-border-light rounded-md w-full h-full overflow-hidden relative"> 141 - <div className="relative w-full h-full"> 111 + <div className="border border-border-light rounded-md w-full h-full overflow-hidden "> 112 + <div className="w-full h-full"> 142 113 <ThemeBackgroundProvider entityID={root}> 143 114 <div 144 115 inert 145 - className="leafletPreview relative grow shrink-0 h-full w-full px-2 pt-2 sm:px-3 sm:pt-3 flex items-end pointer-events-none" 116 + className="leafletPreview grow shrink-0 h-full w-full px-2 pt-2 sm:px-3 sm:pt-3 flex items-end pointer-events-none" 146 117 > 147 - <div 148 - className={`leafletContentWrapper h-full sm:w-48 w-40 mx-auto overflow-clip ${!cardBorderHidden && "border border-border-light border-b-0 rounded-t-md"}`} 149 - style={ 150 - cardBorderHidden 151 - ? {} 152 - : { 153 - backgroundImage: rootBackgroundImage 154 - ? `url(${rootBackgroundImage.data.src}), url(${rootBackgroundImage.data.fallback})` 155 - : undefined, 156 - backgroundRepeat: rootBackgroundRepeat 157 - ? "repeat" 158 - : "no-repeat", 159 - backgroundPosition: "center", 160 - backgroundSize: !rootBackgroundRepeat 161 - ? "cover" 162 - : rootBackgroundRepeat?.data.value / 3, 163 - opacity: 164 - rootBackgroundImage?.data.src && rootBackgroundOpacity 165 - ? rootBackgroundOpacity.data.value 166 - : 1, 167 - backgroundColor: 168 - "rgba(var(--bg-page), var(--bg-page-alpha))", 169 - } 170 - } 171 - > 118 + <div className={contentWrapperClass} style={contentWrapperStyle}> 172 119 <LeafletContent entityID={page} isOnScreen={props.isVisible} /> 173 120 </div> 174 121 </div> 175 122 </ThemeBackgroundProvider> 176 123 </div> 177 - <LeafletPreviewLink id={props.token.id} /> 178 124 </div> 179 125 </ThemeProvider> 180 126 ); 181 127 }; 182 - 183 - const LeafletPreviewLink = (props: { id: string }) => { 184 - return ( 185 - <SpeedyLink 186 - href={`/${props.id}`} 187 - className={`hello no-underline sm:hover:no-underline text-primary absolute inset-0 w-full h-full bg-bg-test`} 188 - /> 189 - ); 190 - };
+2 -1
app/(home-pages)/home/page.tsx
··· 29 29 ...auth_res?.permission_token_on_homepage.reduce( 30 30 (acc, tok) => { 31 31 let title = 32 - tok.permission_tokens.leaflets_in_publications[0]?.title; 32 + tok.permission_tokens.leaflets_in_publications[0]?.title || 33 + tok.permission_tokens.leaflets_to_documents[0]?.title; 33 34 if (title) acc[tok.permission_tokens.root_entity] = title; 34 35 return acc; 35 36 },
+116
app/(home-pages)/looseleafs/LooseleafsLayout.tsx
··· 1 + "use client"; 2 + import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; 3 + import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; 4 + import { useState } from "react"; 5 + import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; 6 + import { Fact, PermissionToken } from "src/replicache"; 7 + import { Attribute } from "src/replicache/attributes"; 8 + import { Actions } from "../home/Actions/Actions"; 9 + import { callRPC } from "app/api/rpc/client"; 10 + import { useIdentityData } from "components/IdentityProvider"; 11 + import useSWR from "swr"; 12 + import { getHomeDocs } from "../home/storage"; 13 + import { Leaflet, LeafletList } from "../home/HomeLayout"; 14 + 15 + export const LooseleafsLayout = (props: { 16 + entityID: string | null; 17 + titles: { [root_entity: string]: string }; 18 + initialFacts: { 19 + [root_entity: string]: Fact<Attribute>[]; 20 + }; 21 + }) => { 22 + let [searchValue, setSearchValue] = useState(""); 23 + let [debouncedSearchValue, setDebouncedSearchValue] = useState(""); 24 + 25 + useDebouncedEffect( 26 + () => { 27 + setDebouncedSearchValue(searchValue); 28 + }, 29 + 200, 30 + [searchValue], 31 + ); 32 + 33 + let cardBorderHidden = !!useCardBorderHidden(props.entityID); 34 + return ( 35 + <DashboardLayout 36 + id="looseleafs" 37 + cardBorderHidden={cardBorderHidden} 38 + currentPage="looseleafs" 39 + defaultTab="home" 40 + actions={<Actions />} 41 + tabs={{ 42 + home: { 43 + controls: null, 44 + content: ( 45 + <LooseleafList 46 + titles={props.titles} 47 + initialFacts={props.initialFacts} 48 + cardBorderHidden={cardBorderHidden} 49 + searchValue={debouncedSearchValue} 50 + /> 51 + ), 52 + }, 53 + }} 54 + /> 55 + ); 56 + }; 57 + 58 + export const LooseleafList = (props: { 59 + titles: { [root_entity: string]: string }; 60 + initialFacts: { 61 + [root_entity: string]: Fact<Attribute>[]; 62 + }; 63 + searchValue: string; 64 + cardBorderHidden: boolean; 65 + }) => { 66 + let { identity } = useIdentityData(); 67 + let { data: initialFacts } = useSWR( 68 + "home-leaflet-data", 69 + async () => { 70 + if (identity) { 71 + let { result } = await callRPC("getFactsFromHomeLeaflets", { 72 + tokens: identity.permission_token_on_homepage.map( 73 + (ptrh) => ptrh.permission_tokens.root_entity, 74 + ), 75 + }); 76 + let titles = { 77 + ...result.titles, 78 + ...identity.permission_token_on_homepage.reduce( 79 + (acc, tok) => { 80 + let title = 81 + tok.permission_tokens.leaflets_in_publications[0]?.title || 82 + tok.permission_tokens.leaflets_to_documents[0]?.title; 83 + if (title) acc[tok.permission_tokens.root_entity] = title; 84 + return acc; 85 + }, 86 + {} as { [k: string]: string }, 87 + ), 88 + }; 89 + return { ...result, titles }; 90 + } 91 + }, 92 + { fallbackData: { facts: props.initialFacts, titles: props.titles } }, 93 + ); 94 + 95 + let leaflets: Leaflet[] = identity 96 + ? identity.permission_token_on_homepage 97 + .filter( 98 + (ptoh) => ptoh.permission_tokens.leaflets_to_documents.length > 0, 99 + ) 100 + .map((ptoh) => ({ 101 + added_at: ptoh.created_at, 102 + token: ptoh.permission_tokens as PermissionToken, 103 + })) 104 + : []; 105 + return ( 106 + <LeafletList 107 + defaultDisplay="list" 108 + searchValue={props.searchValue} 109 + leaflets={leaflets} 110 + titles={initialFacts?.titles || {}} 111 + cardBorderHidden={props.cardBorderHidden} 112 + initialFacts={initialFacts?.facts || {}} 113 + showPreview 114 + /> 115 + ); 116 + };
+47
app/(home-pages)/looseleafs/page.tsx
··· 1 + import { getIdentityData } from "actions/getIdentityData"; 2 + import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; 3 + import { Actions } from "../home/Actions/Actions"; 4 + import { Fact } from "src/replicache"; 5 + import { Attribute } from "src/replicache/attributes"; 6 + import { getFactsFromHomeLeaflets } from "app/api/rpc/[command]/getFactsFromHomeLeaflets"; 7 + import { supabaseServerClient } from "supabase/serverClient"; 8 + import { LooseleafsLayout } from "./LooseleafsLayout"; 9 + 10 + export default async function Home() { 11 + let auth_res = await getIdentityData(); 12 + 13 + let [allLeafletFacts] = await Promise.all([ 14 + auth_res 15 + ? getFactsFromHomeLeaflets.handler( 16 + { 17 + tokens: auth_res.permission_token_on_homepage.map( 18 + (r) => r.permission_tokens.root_entity, 19 + ), 20 + }, 21 + { supabase: supabaseServerClient }, 22 + ) 23 + : undefined, 24 + ]); 25 + 26 + let home_docs_initialFacts = allLeafletFacts?.result || {}; 27 + 28 + return ( 29 + <LooseleafsLayout 30 + entityID={auth_res?.home_leaflet?.root_entity || null} 31 + titles={{ 32 + ...home_docs_initialFacts.titles, 33 + ...auth_res?.permission_token_on_homepage.reduce( 34 + (acc, tok) => { 35 + let title = 36 + tok.permission_tokens.leaflets_in_publications[0]?.title || 37 + tok.permission_tokens.leaflets_to_documents[0]?.title; 38 + if (title) acc[tok.permission_tokens.root_entity] = title; 39 + return acc; 40 + }, 41 + {} as { [k: string]: string }, 42 + ), 43 + }} 44 + initialFacts={home_docs_initialFacts.facts || {}} 45 + /> 46 + ); 47 + }
+65
app/(home-pages)/notifications/CommentNotication.tsx
··· 1 + import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/BaseTextBlock"; 2 + import { 3 + AppBskyActorProfile, 4 + PubLeafletComment, 5 + PubLeafletDocument, 6 + PubLeafletPublication, 7 + } from "lexicons/api"; 8 + import { HydratedCommentNotification } from "src/notifications"; 9 + import { blobRefToSrc } from "src/utils/blobRefToSrc"; 10 + import { Avatar } from "components/Avatar"; 11 + import { CommentTiny } from "components/Icons/CommentTiny"; 12 + import { 13 + CommentInNotification, 14 + ContentLayout, 15 + Notification, 16 + } from "./Notification"; 17 + import { AtUri } from "@atproto/api"; 18 + 19 + export const CommentNotification = (props: HydratedCommentNotification) => { 20 + let docRecord = props.commentData.documents 21 + ?.data as PubLeafletDocument.Record; 22 + let commentRecord = props.commentData.record as PubLeafletComment.Record; 23 + let profileRecord = props.commentData.bsky_profiles 24 + ?.record as AppBskyActorProfile.Record; 25 + const displayName = 26 + profileRecord.displayName || 27 + props.commentData.bsky_profiles?.handle || 28 + "Someone"; 29 + const pubRecord = props.commentData.documents?.documents_in_publications[0] 30 + ?.publications?.record as PubLeafletPublication.Record | undefined; 31 + let docUri = new AtUri(props.commentData.documents?.uri!); 32 + let rkey = docUri.rkey; 33 + let did = docUri.host; 34 + 35 + const href = pubRecord 36 + ? `https://${pubRecord.base_path}/${rkey}?interactionDrawer=comments` 37 + : `/p/${did}/${rkey}?interactionDrawer=comments`; 38 + 39 + return ( 40 + <Notification 41 + timestamp={props.commentData.indexed_at} 42 + href={href} 43 + icon={<CommentTiny />} 44 + actionText={<>{displayName} commented on your post</>} 45 + content={ 46 + <ContentLayout postTitle={docRecord.title} pubRecord={pubRecord}> 47 + <CommentInNotification 48 + className="" 49 + avatar={ 50 + profileRecord?.avatar?.ref && 51 + blobRefToSrc( 52 + profileRecord?.avatar?.ref, 53 + props.commentData.bsky_profiles?.did || "", 54 + ) 55 + } 56 + displayName={displayName} 57 + index={[]} 58 + plaintext={commentRecord.plaintext} 59 + facets={commentRecord.facets} 60 + /> 61 + </ContentLayout> 62 + } 63 + /> 64 + ); 65 + };
+35
app/(home-pages)/notifications/FollowNotification.tsx
··· 1 + import { Avatar } from "components/Avatar"; 2 + import { Notification } from "./Notification"; 3 + import { HydratedSubscribeNotification } from "src/notifications"; 4 + import { blobRefToSrc } from "src/utils/blobRefToSrc"; 5 + import { AppBskyActorProfile, PubLeafletPublication } from "lexicons/api"; 6 + 7 + export const FollowNotification = (props: HydratedSubscribeNotification) => { 8 + const profileRecord = props.subscriptionData?.identities?.bsky_profiles 9 + ?.record as AppBskyActorProfile.Record; 10 + const displayName = 11 + profileRecord?.displayName || 12 + props.subscriptionData?.identities?.bsky_profiles?.handle || 13 + "Someone"; 14 + const pubRecord = props.subscriptionData?.publications 15 + ?.record as PubLeafletPublication.Record; 16 + const avatarSrc = 17 + profileRecord?.avatar?.ref && 18 + blobRefToSrc( 19 + profileRecord.avatar.ref, 20 + props.subscriptionData?.identity || "", 21 + ); 22 + 23 + return ( 24 + <Notification 25 + timestamp={props.created_at} 26 + href={`https://${pubRecord?.base_path}`} 27 + icon={<Avatar src={avatarSrc} displayName={displayName} tiny />} 28 + actionText={ 29 + <> 30 + {displayName} subscribed to {pubRecord?.name}! 31 + </> 32 + } 33 + /> 34 + ); 35 + };
+48
app/(home-pages)/notifications/MentionNotification.tsx
··· 1 + import { QuoteTiny } from "components/Icons/QuoteTiny"; 2 + import { ContentLayout, Notification } from "./Notification"; 3 + import { HydratedQuoteNotification } from "src/notifications"; 4 + import { PubLeafletDocument, PubLeafletPublication } from "lexicons/api"; 5 + import { AtUri } from "@atproto/api"; 6 + import { Avatar } from "components/Avatar"; 7 + 8 + export const QuoteNotification = (props: HydratedQuoteNotification) => { 9 + const postView = props.bskyPost.post_view as any; 10 + const author = postView.author; 11 + const displayName = author.displayName || author.handle || "Someone"; 12 + const docRecord = props.document.data as PubLeafletDocument.Record; 13 + const pubRecord = props.document.documents_in_publications[0]?.publications 14 + ?.record as PubLeafletPublication.Record | undefined; 15 + const docUri = new AtUri(props.document.uri); 16 + const rkey = docUri.rkey; 17 + const did = docUri.host; 18 + const postText = postView.record?.text || ""; 19 + 20 + const href = pubRecord 21 + ? `https://${pubRecord.base_path}/${rkey}` 22 + : `/p/${did}/${rkey}`; 23 + 24 + return ( 25 + <Notification 26 + timestamp={props.created_at} 27 + href={href} 28 + icon={<QuoteTiny />} 29 + actionText={<>{displayName} quoted your post</>} 30 + content={ 31 + <ContentLayout postTitle={docRecord.title} pubRecord={pubRecord}> 32 + <div className="flex gap-2 text-sm w-full"> 33 + <Avatar 34 + src={author.avatar} 35 + displayName={displayName} 36 + /> 37 + <pre 38 + style={{ wordBreak: "break-word" }} 39 + className="whitespace-pre-wrap text-secondary line-clamp-3 sm:line-clamp-6" 40 + > 41 + {postText} 42 + </pre> 43 + </div> 44 + </ContentLayout> 45 + } 46 + /> 47 + ); 48 + };
+116
app/(home-pages)/notifications/Notification.tsx
··· 1 + "use client"; 2 + import { Avatar } from "components/Avatar"; 3 + import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/BaseTextBlock"; 4 + import { PubLeafletPublication, PubLeafletRichtextFacet } from "lexicons/api"; 5 + import { timeAgo } from "src/utils/timeAgo"; 6 + import { useReplicache, useEntity } from "src/replicache"; 7 + 8 + export const Notification = (props: { 9 + icon: React.ReactNode; 10 + actionText: React.ReactNode; 11 + content?: React.ReactNode; 12 + timestamp: string; 13 + href: string; 14 + }) => { 15 + let { rootEntity } = useReplicache(); 16 + let cardBorderHidden = useEntity(rootEntity, "theme/card-border-hidden")?.data 17 + .value; 18 + 19 + // If compact mode, always hide border 20 + 21 + return ( 22 + <div 23 + className={`relative flex flex-col w-full pb-3 sm:pb-4 pt-2 ${ 24 + cardBorderHidden 25 + ? " first:pt-0! " 26 + : " block-border border-border! hover:outline-border sm:px-4 px-3 pl-2 sm:pl-3 " 27 + }`} 28 + style={{ 29 + backgroundColor: cardBorderHidden 30 + ? "transparent" 31 + : "rgba(var(--bg-page), var(--bg-page-alpha))", 32 + }} 33 + > 34 + <a 35 + href={props.href} 36 + className=" absolute top-0 bottom-0 left-0 right-0" 37 + /> 38 + <div className="flex justify-between items-center gap-3 w-full "> 39 + <div className={`flex flex-row gap-2 items-center grow w-full min-w-0`}> 40 + <div className="text-secondary shrink-0">{props.icon}</div> 41 + <div className={`text-secondary font-bold grow truncate min-w-0 }`}> 42 + {props.actionText} 43 + </div> 44 + </div> 45 + <div className={`text-tertiary shrink-0 min-w-8 text-sm`}> 46 + {timeAgo(props.timestamp)} 47 + </div> 48 + </div> 49 + {props.content && ( 50 + <div className="flex flex-row gap-2 mt-1 w-full"> 51 + <div className="w-4 shrink-0" /> 52 + {props.content} 53 + </div> 54 + )} 55 + </div> 56 + ); 57 + }; 58 + 59 + export const ContentLayout = (props: { 60 + children: React.ReactNode; 61 + postTitle: string; 62 + pubRecord?: PubLeafletPublication.Record; 63 + }) => { 64 + let { rootEntity } = useReplicache(); 65 + let cardBorderHidden = useEntity(rootEntity, "theme/card-border-hidden")?.data 66 + .value; 67 + 68 + return ( 69 + <div 70 + className={`border border-border-light rounded-md px-2 py-[6px] w-full ${cardBorderHidden ? "transparent" : "bg-bg-page"}`} 71 + > 72 + <div className="text-tertiary text-sm italic font-bold pb-1"> 73 + {props.postTitle} 74 + </div> 75 + {props.children} 76 + {props.pubRecord && ( 77 + <> 78 + <hr className="mt-3 mb-1 border-border-light" /> 79 + <a 80 + href={`https://${props.pubRecord.base_path}`} 81 + className="relative text-xs text-tertiary flex gap-[6px] items-center font-bold hover:no-underline!" 82 + > 83 + {props.pubRecord.name} 84 + </a> 85 + </> 86 + )} 87 + </div> 88 + ); 89 + }; 90 + 91 + type Facet = PubLeafletRichtextFacet.Main; 92 + export const CommentInNotification = (props: { 93 + avatar: string | undefined; 94 + displayName: string; 95 + plaintext: string; 96 + facets?: Facet[]; 97 + index: number[]; 98 + className?: string; 99 + }) => { 100 + return ( 101 + <div className=" flex gap-2 text-sm w-full "> 102 + <Avatar src={props.avatar} displayName={props.displayName} /> 103 + <pre 104 + style={{ wordBreak: "break-word" }} 105 + className={`whitespace-pre-wrap text-secondary line-clamp-3 sm:line-clamp-6 ${props.className}`} 106 + > 107 + <BaseTextBlock 108 + preview 109 + index={props.index} 110 + plaintext={props.plaintext} 111 + facets={props.facets} 112 + /> 113 + </pre> 114 + </div> 115 + ); 116 + };
+53
app/(home-pages)/notifications/NotificationList.tsx
··· 1 + "use client"; 2 + 3 + import { HydratedNotification } from "src/notifications"; 4 + import { CommentNotification } from "./CommentNotication"; 5 + import { useEffect, createContext } from "react"; 6 + import { markAsRead } from "./getNotifications"; 7 + import { ReplyNotification } from "./ReplyNotification"; 8 + import { useIdentityData } from "components/IdentityProvider"; 9 + import { FollowNotification } from "./FollowNotification"; 10 + import { QuoteNotification } from "./MentionNotification"; 11 + 12 + export function NotificationList({ 13 + notifications, 14 + compact, 15 + }: { 16 + notifications: HydratedNotification[]; 17 + compact?: boolean; 18 + }) { 19 + let { mutate } = useIdentityData(); 20 + useEffect(() => { 21 + setTimeout(async () => { 22 + await markAsRead(); 23 + mutate(); 24 + }, 500); 25 + }, []); 26 + 27 + if (notifications.length === 0) 28 + return ( 29 + <div className="w-full text-sm flex flex-col gap-1 container italic text-tertiary text-center sm:p-4 p-3"> 30 + <div className="text-base font-bold">no notifications yet...</div> 31 + Here, you&apos;ll find notifications about new follows, comments, 32 + mentions, and replies! 33 + </div> 34 + ); 35 + return ( 36 + <div className="max-w-prose mx-auto w-full"> 37 + <div className={`flex flex-col gap-2`}> 38 + {notifications.map((n) => { 39 + if (n.type === "comment") { 40 + if (n.parentData) return <ReplyNotification key={n.id} {...n} />; 41 + return <CommentNotification key={n.id} {...n} />; 42 + } 43 + if (n.type === "subscribe") { 44 + return <FollowNotification key={n.id} {...n} />; 45 + } 46 + if (n.type === "quote") { 47 + return <QuoteNotification key={n.id} {...n} />; 48 + } 49 + })} 50 + </div> 51 + </div> 52 + ); 53 + }
+88
app/(home-pages)/notifications/ReplyNotification.tsx
··· 1 + import { Avatar } from "components/Avatar"; 2 + import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/BaseTextBlock"; 3 + import { ReplyTiny } from "components/Icons/ReplyTiny"; 4 + import { 5 + CommentInNotification, 6 + ContentLayout, 7 + Notification, 8 + } from "./Notification"; 9 + import { HydratedCommentNotification } from "src/notifications"; 10 + import { 11 + PubLeafletComment, 12 + PubLeafletDocument, 13 + PubLeafletPublication, 14 + } from "lexicons/api"; 15 + import { AppBskyActorProfile, AtUri } from "@atproto/api"; 16 + import { blobRefToSrc } from "src/utils/blobRefToSrc"; 17 + 18 + export const ReplyNotification = (props: HydratedCommentNotification) => { 19 + let docRecord = props.commentData.documents 20 + ?.data as PubLeafletDocument.Record; 21 + let commentRecord = props.commentData.record as PubLeafletComment.Record; 22 + let profileRecord = props.commentData.bsky_profiles 23 + ?.record as AppBskyActorProfile.Record; 24 + const displayName = 25 + profileRecord.displayName || 26 + props.commentData.bsky_profiles?.handle || 27 + "Someone"; 28 + 29 + let parentRecord = props.parentData?.record as PubLeafletComment.Record; 30 + let parentProfile = props.parentData?.bsky_profiles 31 + ?.record as AppBskyActorProfile.Record; 32 + const parentDisplayName = 33 + parentProfile.displayName || 34 + props.parentData?.bsky_profiles?.handle || 35 + "Someone"; 36 + 37 + let docUri = new AtUri(props.commentData.documents?.uri!); 38 + let rkey = docUri.rkey; 39 + let did = docUri.host; 40 + const pubRecord = props.commentData.documents?.documents_in_publications[0] 41 + ?.publications?.record as PubLeafletPublication.Record | undefined; 42 + 43 + const href = pubRecord 44 + ? `https://${pubRecord.base_path}/${rkey}?interactionDrawer=comments` 45 + : `/p/${did}/${rkey}?interactionDrawer=comments`; 46 + 47 + return ( 48 + <Notification 49 + timestamp={props.commentData.indexed_at} 50 + href={href} 51 + icon={<ReplyTiny />} 52 + actionText={`${displayName} replied to your comment`} 53 + content={ 54 + <ContentLayout postTitle={docRecord.title} pubRecord={pubRecord}> 55 + <CommentInNotification 56 + className="" 57 + avatar={ 58 + parentProfile?.avatar?.ref && 59 + blobRefToSrc( 60 + parentProfile?.avatar?.ref, 61 + props.parentData?.bsky_profiles?.did || "", 62 + ) 63 + } 64 + displayName={parentDisplayName} 65 + index={[]} 66 + plaintext={parentRecord.plaintext} 67 + facets={parentRecord.facets} 68 + /> 69 + <div className="h-3 -mt-[1px] ml-[10px] border-l border-border" /> 70 + <CommentInNotification 71 + className="" 72 + avatar={ 73 + profileRecord?.avatar?.ref && 74 + blobRefToSrc( 75 + profileRecord?.avatar?.ref, 76 + props.commentData.bsky_profiles?.did || "", 77 + ) 78 + } 79 + displayName={displayName} 80 + index={[]} 81 + plaintext={commentRecord.plaintext} 82 + facets={commentRecord.facets} 83 + /> 84 + </ContentLayout> 85 + } 86 + /> 87 + ); 88 + };
+28
app/(home-pages)/notifications/getNotifications.ts
··· 1 + "use server"; 2 + import { getIdentityData } from "actions/getIdentityData"; 3 + import { hydrateNotifications } from "src/notifications"; 4 + import { supabaseServerClient } from "supabase/serverClient"; 5 + 6 + export async function getNotifications(limit?: number) { 7 + let identity = await getIdentityData(); 8 + if (!identity?.atp_did) return []; 9 + let query = supabaseServerClient 10 + .from("notifications") 11 + .select("*") 12 + .eq("recipient", identity.atp_did) 13 + .order("created_at", { ascending: false }); 14 + if (limit) query.limit(limit); 15 + let { data } = await query; 16 + let notifications = await hydrateNotifications(data || []); 17 + return notifications; 18 + } 19 + 20 + export async function markAsRead() { 21 + let identity = await getIdentityData(); 22 + if (!identity?.atp_did) return []; 23 + await supabaseServerClient 24 + .from("notifications") 25 + .update({ read: true }) 26 + .eq("recipient", identity.atp_did); 27 + return; 28 + }
+34 -1
app/(home-pages)/notifications/page.tsx
··· 1 + import { getIdentityData } from "actions/getIdentityData"; 2 + import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; 3 + import { redirect } from "next/navigation"; 4 + import { hydrateNotifications } from "src/notifications"; 5 + import { supabaseServerClient } from "supabase/serverClient"; 6 + import { CommentNotification } from "./CommentNotication"; 7 + import { NotificationList } from "./NotificationList"; 8 + 1 9 export default async function Notifications() { 2 - return <div>Notifications</div>; 10 + return ( 11 + <DashboardLayout 12 + id="discover" 13 + cardBorderHidden={true} 14 + currentPage="notifications" 15 + defaultTab="default" 16 + actions={null} 17 + tabs={{ 18 + default: { 19 + controls: null, 20 + content: <NotificationContent />, 21 + }, 22 + }} 23 + /> 24 + ); 3 25 } 26 + 27 + const NotificationContent = async () => { 28 + let identity = await getIdentityData(); 29 + if (!identity?.atp_did) return redirect("/home"); 30 + let { data } = await supabaseServerClient 31 + .from("notifications") 32 + .select("*") 33 + .eq("recipient", identity.atp_did); 34 + let notifications = await hydrateNotifications(data || []); 35 + return <NotificationList notifications={notifications} />; 36 + };
-98
app/[leaflet_id]/Actions.tsx
··· 1 - import { publishToPublication } from "actions/publishToPublication"; 2 - import { 3 - getBasePublicationURL, 4 - getPublicationURL, 5 - } from "app/lish/createPub/getPublicationURL"; 6 - import { ActionButton } from "components/ActionBar/ActionButton"; 7 - import { GoBackSmall } from "components/Icons/GoBackSmall"; 8 - import { PublishSmall } from "components/Icons/PublishSmall"; 9 - import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 10 - import { SpeedyLink } from "components/SpeedyLink"; 11 - import { useToaster } from "components/Toast"; 12 - import { DotLoader } from "components/utils/DotLoader"; 13 - import { useParams, useRouter } from "next/navigation"; 14 - import { useState } from "react"; 15 - import { useReplicache } from "src/replicache"; 16 - import { Json } from "supabase/database.types"; 17 - 18 - export const BackToPubButton = (props: { 19 - publication: { 20 - identity_did: string; 21 - indexed_at: string; 22 - name: string; 23 - record: Json; 24 - uri: string; 25 - }; 26 - }) => { 27 - return ( 28 - <SpeedyLink 29 - href={`${getBasePublicationURL(props.publication)}/dashboard`} 30 - className="hover:no-underline!" 31 - > 32 - <ActionButton 33 - icon={<GoBackSmall className="shrink-0" />} 34 - label="To Pub" 35 - /> 36 - </SpeedyLink> 37 - ); 38 - }; 39 - 40 - export const PublishButton = () => { 41 - let { data: pub } = useLeafletPublicationData(); 42 - let params = useParams(); 43 - let router = useRouter(); 44 - if (!pub?.doc) 45 - return ( 46 - <ActionButton 47 - primary 48 - icon={<PublishSmall className="shrink-0" />} 49 - label={"Publish!"} 50 - onClick={() => { 51 - router.push(`/${params.leaflet_id}/publish`); 52 - }} 53 - /> 54 - ); 55 - 56 - return <UpdateButton />; 57 - }; 58 - 59 - const UpdateButton = () => { 60 - let [isLoading, setIsLoading] = useState(false); 61 - let { data: pub, mutate } = useLeafletPublicationData(); 62 - let { permission_token, rootEntity } = useReplicache(); 63 - let toaster = useToaster(); 64 - 65 - return ( 66 - <ActionButton 67 - primary 68 - icon={<PublishSmall className="shrink-0" />} 69 - label={isLoading ? <DotLoader /> : "Update!"} 70 - onClick={async () => { 71 - if (!pub || !pub.publications) return; 72 - setIsLoading(true); 73 - let doc = await publishToPublication({ 74 - root_entity: rootEntity, 75 - publication_uri: pub.publications.uri, 76 - leaflet_id: permission_token.id, 77 - title: pub.title, 78 - description: pub.description, 79 - }); 80 - setIsLoading(false); 81 - mutate(); 82 - toaster({ 83 - content: ( 84 - <div> 85 - {pub.doc ? "Updated! " : "Published! "} 86 - <SpeedyLink 87 - href={`${getPublicationURL(pub.publications)}/${doc?.rkey}`} 88 - > 89 - link 90 - </SpeedyLink> 91 - </div> 92 - ), 93 - type: "success", 94 - }); 95 - }} 96 - /> 97 - ); 98 - };
+16 -20
app/[leaflet_id]/Footer.tsx
··· 4 4 import { Media } from "components/Media"; 5 5 import { ThemePopover } from "components/ThemeManager/ThemeSetter"; 6 6 import { Toolbar } from "components/Toolbar"; 7 - import { ShareOptions } from "components/ShareOptions"; 8 - import { HomeButton } from "components/HomeButton"; 7 + import { ShareOptions } from "app/[leaflet_id]/actions/ShareOptions"; 8 + import { HomeButton } from "app/[leaflet_id]/actions/HomeButton"; 9 + import { PublishButton } from "./actions/PublishButton"; 9 10 import { useEntitySetContext } from "components/EntitySetProvider"; 10 - import { HelpPopover } from "components/HelpPopover"; 11 + import { HelpButton } from "app/[leaflet_id]/actions/HelpButton"; 11 12 import { Watermark } from "components/Watermark"; 12 - import { BackToPubButton, PublishButton } from "./Actions"; 13 + import { BackToPubButton } from "./actions/BackToPubButton"; 13 14 import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 14 15 import { useIdentityData } from "components/IdentityProvider"; 15 16 ··· 36 37 /> 37 38 </div> 38 39 ) : entity_set.permissions.write ? ( 39 - pub?.publications && 40 - identity?.atp_did && 41 - pub.publications.identity_did === identity.atp_did ? ( 42 - <ActionFooter> 40 + <ActionFooter> 41 + {pub?.publications && 42 + identity?.atp_did && 43 + pub.publications.identity_did === identity.atp_did ? ( 43 44 <BackToPubButton publication={pub.publications} /> 44 - <PublishButton /> 45 - <ShareOptions /> 46 - <HelpPopover /> 47 - <ThemePopover entityID={props.entityID} /> 48 - </ActionFooter> 49 - ) : ( 50 - <ActionFooter> 45 + ) : ( 51 46 <HomeButton /> 52 - <ShareOptions /> 53 - <HelpPopover /> 54 - <ThemePopover entityID={props.entityID} /> 55 - </ActionFooter> 56 - ) 47 + )} 48 + 49 + <PublishButton entityID={props.entityID} /> 50 + <ShareOptions /> 51 + <ThemePopover entityID={props.entityID} /> 52 + </ActionFooter> 57 53 ) : ( 58 54 <div className="pb-2 px-2 z-10 flex justify-end"> 59 55 <Watermark mobile />
+12 -28
app/[leaflet_id]/Sidebar.tsx
··· 1 1 "use client"; 2 - import { ActionButton } from "components/ActionBar/ActionButton"; 3 2 import { Sidebar } from "components/ActionBar/Sidebar"; 4 3 import { useEntitySetContext } from "components/EntitySetProvider"; 5 - import { HelpPopover } from "components/HelpPopover"; 6 - import { HomeButton } from "components/HomeButton"; 4 + import { HelpButton } from "app/[leaflet_id]/actions/HelpButton"; 5 + import { HomeButton } from "app/[leaflet_id]/actions/HomeButton"; 7 6 import { Media } from "components/Media"; 8 7 import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 9 - import { ShareOptions } from "components/ShareOptions"; 8 + import { ShareOptions } from "app/[leaflet_id]/actions/ShareOptions"; 10 9 import { ThemePopover } from "components/ThemeManager/ThemeSetter"; 10 + import { PublishButton } from "./actions/PublishButton"; 11 11 import { Watermark } from "components/Watermark"; 12 - import { useUIState } from "src/useUIState"; 13 - import { BackToPubButton, PublishButton } from "./Actions"; 12 + import { BackToPubButton } from "./actions/BackToPubButton"; 14 13 import { useIdentityData } from "components/IdentityProvider"; 15 14 import { useReplicache } from "src/replicache"; 16 15 ··· 29 28 <div className="sidebarContainer flex flex-col justify-end h-full w-16 relative"> 30 29 {entity_set.permissions.write && ( 31 30 <Sidebar> 31 + <PublishButton entityID={rootEntity} /> 32 + <ShareOptions /> 33 + <ThemePopover entityID={rootEntity} /> 34 + <HelpButton /> 35 + <hr className="text-border" /> 32 36 {pub?.publications && 33 37 identity?.atp_did && 34 38 pub.publications.identity_did === identity.atp_did ? ( 35 - <> 36 - <PublishButton /> 37 - <ShareOptions /> 38 - <ThemePopover entityID={rootEntity} /> 39 - <HelpPopover /> 40 - <hr className="text-border" /> 41 - <BackToPubButton publication={pub.publications} /> 42 - </> 39 + <BackToPubButton publication={pub.publications} /> 43 40 ) : ( 44 - <> 45 - <ShareOptions /> 46 - <ThemePopover entityID={rootEntity} /> 47 - <HelpPopover /> 48 - <hr className="text-border" /> 49 - <HomeButton /> 50 - </> 41 + <HomeButton /> 51 42 )} 52 43 </Sidebar> 53 44 )} ··· 59 50 </Media> 60 51 ); 61 52 } 62 - 63 - const blurPage = () => { 64 - useUIState.setState(() => ({ 65 - focusedEntity: null, 66 - selectedBlocks: [], 67 - })); 68 - };
+27
app/[leaflet_id]/actions/BackToPubButton.tsx
··· 1 + import { getBasePublicationURL } from "app/lish/createPub/getPublicationURL"; 2 + import { ActionButton } from "components/ActionBar/ActionButton"; 3 + import { GoBackSmall } from "components/Icons/GoBackSmall"; 4 + import { SpeedyLink } from "components/SpeedyLink"; 5 + import { Json } from "supabase/database.types"; 6 + 7 + export const BackToPubButton = (props: { 8 + publication: { 9 + identity_did: string; 10 + indexed_at: string; 11 + name: string; 12 + record: Json; 13 + uri: string; 14 + }; 15 + }) => { 16 + return ( 17 + <SpeedyLink 18 + href={`${getBasePublicationURL(props.publication)}/dashboard`} 19 + className="hover:no-underline!" 20 + > 21 + <ActionButton 22 + icon={<GoBackSmall className="shrink-0" />} 23 + label="To Pub" 24 + /> 25 + </SpeedyLink> 26 + ); 27 + };
+427
app/[leaflet_id]/actions/PublishButton.tsx
··· 1 + "use client"; 2 + import { publishToPublication } from "actions/publishToPublication"; 3 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 4 + import { ActionButton } from "components/ActionBar/ActionButton"; 5 + import { 6 + PubIcon, 7 + PubListEmptyContent, 8 + PubListEmptyIllo, 9 + } from "components/ActionBar/Publications"; 10 + import { ButtonPrimary, ButtonTertiary } from "components/Buttons"; 11 + import { AddSmall } from "components/Icons/AddSmall"; 12 + import { LooseLeafSmall } from "components/Icons/LooseleafSmall"; 13 + import { PublishSmall } from "components/Icons/PublishSmall"; 14 + import { useIdentityData } from "components/IdentityProvider"; 15 + import { InputWithLabel } from "components/Input"; 16 + import { Menu, MenuItem } from "components/Layout"; 17 + import { 18 + useLeafletDomains, 19 + useLeafletPublicationData, 20 + } from "components/PageSWRDataProvider"; 21 + import { Popover } from "components/Popover"; 22 + import { SpeedyLink } from "components/SpeedyLink"; 23 + import { useToaster } from "components/Toast"; 24 + import { DotLoader } from "components/utils/DotLoader"; 25 + import { PubLeafletPublication } from "lexicons/api"; 26 + import { useParams, useRouter, useSearchParams } from "next/navigation"; 27 + import { useState, useMemo } from "react"; 28 + import { useIsMobile } from "src/hooks/isMobile"; 29 + import { useReplicache, useEntity } from "src/replicache"; 30 + import { Json } from "supabase/database.types"; 31 + import { 32 + useBlocks, 33 + useCanvasBlocksWithType, 34 + } from "src/hooks/queries/useBlocks"; 35 + import * as Y from "yjs"; 36 + import * as base64 from "base64-js"; 37 + import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment"; 38 + import { BlueskyLogin } from "app/login/LoginForm"; 39 + import { moveLeafletToPublication } from "actions/publications/moveLeafletToPublication"; 40 + import { AddTiny } from "components/Icons/AddTiny"; 41 + 42 + export const PublishButton = (props: { entityID: string }) => { 43 + let { data: pub } = useLeafletPublicationData(); 44 + let params = useParams(); 45 + let router = useRouter(); 46 + 47 + if (!pub) return <PublishToPublicationButton entityID={props.entityID} />; 48 + if (!pub?.doc) 49 + return ( 50 + <ActionButton 51 + primary 52 + icon={<PublishSmall className="shrink-0" />} 53 + label={"Publish!"} 54 + onClick={() => { 55 + router.push(`/${params.leaflet_id}/publish`); 56 + }} 57 + /> 58 + ); 59 + 60 + return <UpdateButton />; 61 + }; 62 + 63 + const UpdateButton = () => { 64 + let [isLoading, setIsLoading] = useState(false); 65 + let { data: pub, mutate } = useLeafletPublicationData(); 66 + let { permission_token, rootEntity } = useReplicache(); 67 + let { identity } = useIdentityData(); 68 + let toaster = useToaster(); 69 + 70 + return ( 71 + <ActionButton 72 + primary 73 + icon={<PublishSmall className="shrink-0" />} 74 + label={isLoading ? <DotLoader /> : "Update!"} 75 + onClick={async () => { 76 + if (!pub) return; 77 + setIsLoading(true); 78 + let doc = await publishToPublication({ 79 + root_entity: rootEntity, 80 + publication_uri: pub.publications?.uri, 81 + leaflet_id: permission_token.id, 82 + title: pub.title, 83 + description: pub.description, 84 + }); 85 + setIsLoading(false); 86 + mutate(); 87 + 88 + // Generate URL based on whether it's in a publication or standalone 89 + let docUrl = pub.publications 90 + ? `${getPublicationURL(pub.publications)}/${doc?.rkey}` 91 + : `https://leaflet.pub/p/${identity?.atp_did}/${doc?.rkey}`; 92 + 93 + toaster({ 94 + content: ( 95 + <div> 96 + {pub.doc ? "Updated! " : "Published! "} 97 + <SpeedyLink href={docUrl}>link</SpeedyLink> 98 + </div> 99 + ), 100 + type: "success", 101 + }); 102 + }} 103 + /> 104 + ); 105 + }; 106 + 107 + const PublishToPublicationButton = (props: { entityID: string }) => { 108 + let { identity } = useIdentityData(); 109 + let { permission_token } = useReplicache(); 110 + let query = useSearchParams(); 111 + console.log(query.get("publish")); 112 + let [open, setOpen] = useState(query.get("publish") !== null); 113 + 114 + let isMobile = useIsMobile(); 115 + identity && identity.atp_did && identity.publications.length > 0; 116 + let [selectedPub, setSelectedPub] = useState<string | undefined>(undefined); 117 + let router = useRouter(); 118 + let { title, entitiesToDelete } = useTitle(props.entityID); 119 + let [description, setDescription] = useState(""); 120 + 121 + return ( 122 + <Popover 123 + asChild 124 + open={open} 125 + onOpenChange={(o) => setOpen(o)} 126 + side={isMobile ? "top" : "right"} 127 + align={isMobile ? "center" : "start"} 128 + className="sm:max-w-sm w-[1000px]" 129 + trigger={ 130 + <ActionButton 131 + primary 132 + icon={<PublishSmall className="shrink-0" />} 133 + label={"Publish on ATP"} 134 + /> 135 + } 136 + > 137 + {!identity || !identity.atp_did ? ( 138 + <div className="-mx-2 -my-1"> 139 + <div 140 + className={`bg-[var(--accent-light)] w-full rounded-md flex flex-col text-center justify-center p-2 pb-4 text-sm`} 141 + > 142 + <div className="mx-auto pt-2 scale-90"> 143 + <PubListEmptyIllo /> 144 + </div> 145 + <div className="pt-1 font-bold">Publish on AT Proto</div> 146 + { 147 + <> 148 + <div className="pb-2 text-secondary text-xs"> 149 + Link a Bluesky account to start <br /> a publishing on AT 150 + Proto 151 + </div> 152 + 153 + <BlueskyLogin 154 + compact 155 + redirectRoute={`/${permission_token.id}?publish`} 156 + /> 157 + </> 158 + } 159 + </div> 160 + </div> 161 + ) : ( 162 + <div className="flex flex-col"> 163 + <PostDetailsForm 164 + title={title} 165 + description={description} 166 + setDescription={setDescription} 167 + /> 168 + <hr className="border-border-light my-3" /> 169 + <div> 170 + <PubSelector 171 + publications={identity.publications} 172 + selectedPub={selectedPub} 173 + setSelectedPub={setSelectedPub} 174 + /> 175 + </div> 176 + <hr className="border-border-light mt-3 mb-2" /> 177 + 178 + <div className="flex gap-2 items-center place-self-end"> 179 + {selectedPub !== "looseleaf" && selectedPub && ( 180 + <SaveAsDraftButton 181 + selectedPub={selectedPub} 182 + leafletId={permission_token.id} 183 + metadata={{ title: title, description }} 184 + entitiesToDelete={entitiesToDelete} 185 + /> 186 + )} 187 + <ButtonPrimary 188 + disabled={selectedPub === undefined} 189 + onClick={async (e) => { 190 + if (!selectedPub) return; 191 + e.preventDefault(); 192 + if (selectedPub === "create") return; 193 + 194 + // For looseleaf, navigate without publication_uri 195 + if (selectedPub === "looseleaf") { 196 + router.push( 197 + `${permission_token.id}/publish?title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}&entitiesToDelete=${encodeURIComponent(JSON.stringify(entitiesToDelete))}`, 198 + ); 199 + } else { 200 + router.push( 201 + `${permission_token.id}/publish?publication_uri=${encodeURIComponent(selectedPub)}&title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}&entitiesToDelete=${encodeURIComponent(JSON.stringify(entitiesToDelete))}`, 202 + ); 203 + } 204 + }} 205 + > 206 + Next{selectedPub === "create" && ": Create Pub!"} 207 + </ButtonPrimary> 208 + </div> 209 + </div> 210 + )} 211 + </Popover> 212 + ); 213 + }; 214 + 215 + const SaveAsDraftButton = (props: { 216 + selectedPub: string | undefined; 217 + leafletId: string; 218 + metadata: { title: string; description: string }; 219 + entitiesToDelete: string[]; 220 + }) => { 221 + let { mutate } = useLeafletPublicationData(); 222 + let { rep } = useReplicache(); 223 + let [isLoading, setIsLoading] = useState(false); 224 + 225 + return ( 226 + <ButtonTertiary 227 + onClick={async (e) => { 228 + if (!props.selectedPub) return; 229 + if (props.selectedPub === "create") return; 230 + e.preventDefault(); 231 + setIsLoading(true); 232 + await moveLeafletToPublication( 233 + props.leafletId, 234 + props.selectedPub, 235 + props.metadata, 236 + props.entitiesToDelete, 237 + ); 238 + await Promise.all([rep?.pull(), mutate()]); 239 + setIsLoading(false); 240 + }} 241 + > 242 + {isLoading ? <DotLoader /> : "Save as Draft"} 243 + </ButtonTertiary> 244 + ); 245 + }; 246 + 247 + const PostDetailsForm = (props: { 248 + title: string; 249 + description: string; 250 + setDescription: (d: string) => void; 251 + }) => { 252 + return ( 253 + <div className=" flex flex-col gap-1"> 254 + <div className="text-sm text-tertiary">Post Details</div> 255 + <div className="flex flex-col gap-2"> 256 + <InputWithLabel label="Title" value={props.title} disabled /> 257 + <InputWithLabel 258 + label="Description (optional)" 259 + textarea 260 + value={props.description} 261 + className="h-[4lh]" 262 + onChange={(e) => props.setDescription(e.currentTarget.value)} 263 + /> 264 + </div> 265 + </div> 266 + ); 267 + }; 268 + 269 + const PubSelector = (props: { 270 + selectedPub: string | undefined; 271 + setSelectedPub: (s: string) => void; 272 + publications: { 273 + identity_did: string; 274 + indexed_at: string; 275 + name: string; 276 + record: Json | null; 277 + uri: string; 278 + }[]; 279 + }) => { 280 + // HEY STILL TO DO 281 + // test out logged out, logged in but no pubs, and pubbed up flows 282 + 283 + return ( 284 + <div className="flex flex-col gap-1"> 285 + <div className="text-sm text-tertiary">Publish to…</div> 286 + {props.publications.length === 0 || props.publications === undefined ? ( 287 + <div className="flex flex-col gap-1"> 288 + <div className="flex gap-2 menuItem"> 289 + <LooseLeafSmall className="shrink-0" /> 290 + <div className="flex flex-col leading-snug"> 291 + <div className="text-secondary font-bold"> 292 + Publish as Looseleaf 293 + </div> 294 + <div className="text-tertiary text-sm font-normal"> 295 + Publish this as a one off doc to AT Proto 296 + </div> 297 + </div> 298 + </div> 299 + <div className="flex gap-2 px-2 py-1 "> 300 + <PublishSmall className="shrink-0 text-border" /> 301 + <div className="flex flex-col leading-snug"> 302 + <div className="text-border font-bold"> 303 + Publish to Publication 304 + </div> 305 + <div className="text-border text-sm font-normal"> 306 + Publish your writing to a blog on AT Proto 307 + </div> 308 + <hr className="my-2 drashed border-border-light border-dashed" /> 309 + <div className="text-tertiary text-sm font-normal "> 310 + You don't have any Publications yet.{" "} 311 + <a target="_blank" href="/lish/createPub"> 312 + Create one 313 + </a>{" "} 314 + to get started! 315 + </div> 316 + </div> 317 + </div> 318 + </div> 319 + ) : ( 320 + <div className="flex flex-col gap-1"> 321 + <PubOption 322 + selected={props.selectedPub === "looseleaf"} 323 + onSelect={() => props.setSelectedPub("looseleaf")} 324 + > 325 + <LooseLeafSmall /> 326 + Publish as Looseleaf 327 + </PubOption> 328 + <hr className="border-border-light border-dashed " /> 329 + {props.publications.map((p) => { 330 + let pubRecord = p.record as PubLeafletPublication.Record; 331 + return ( 332 + <PubOption 333 + key={p.uri} 334 + selected={props.selectedPub === p.uri} 335 + onSelect={() => props.setSelectedPub(p.uri)} 336 + > 337 + <> 338 + <PubIcon record={pubRecord} uri={p.uri} /> 339 + {p.name} 340 + </> 341 + </PubOption> 342 + ); 343 + })} 344 + <div className="flex items-center px-2 py-1 text-accent-contrast gap-2"> 345 + <AddTiny className="m-1 shrink-0" /> 346 + 347 + <a target="_blank" href="/lish/createPub"> 348 + Start a new Publication 349 + </a> 350 + </div> 351 + </div> 352 + )} 353 + </div> 354 + ); 355 + }; 356 + 357 + const PubOption = (props: { 358 + selected: boolean; 359 + onSelect: () => void; 360 + children: React.ReactNode; 361 + }) => { 362 + return ( 363 + <button 364 + className={`flex gap-2 menuItem font-bold text-secondary ${props.selected && "bg-[var(--accent-light)]! outline! outline-offset-1! outline-accent-contrast!"}`} 365 + onClick={() => { 366 + props.onSelect(); 367 + }} 368 + > 369 + {props.children} 370 + </button> 371 + ); 372 + }; 373 + 374 + let useTitle = (entityID: string) => { 375 + let rootPage = useEntity(entityID, "root/page")[0].data.value; 376 + let canvasBlocks = useCanvasBlocksWithType(rootPage).filter( 377 + (b) => b.type === "text" || b.type === "heading", 378 + ); 379 + let blocks = useBlocks(rootPage).filter( 380 + (b) => b.type === "text" || b.type === "heading", 381 + ); 382 + let firstBlock = canvasBlocks[0] || blocks[0]; 383 + 384 + let firstBlockText = useEntity(firstBlock?.value, "block/text")?.data.value; 385 + 386 + const leafletTitle = useMemo(() => { 387 + if (!firstBlockText) return "Untitled"; 388 + let doc = new Y.Doc(); 389 + const update = base64.toByteArray(firstBlockText); 390 + Y.applyUpdate(doc, update); 391 + let nodes = doc.getXmlElement("prosemirror").toArray(); 392 + return YJSFragmentToString(nodes[0]) || "Untitled"; 393 + }, [firstBlockText]); 394 + 395 + // Only handle second block logic for linear documents, not canvas 396 + let isCanvas = canvasBlocks.length > 0; 397 + let secondBlock = !isCanvas ? blocks[1] : undefined; 398 + let secondBlockTextValue = useEntity(secondBlock?.value || null, "block/text") 399 + ?.data.value; 400 + const secondBlockText = useMemo(() => { 401 + if (!secondBlockTextValue) return ""; 402 + let doc = new Y.Doc(); 403 + const update = base64.toByteArray(secondBlockTextValue); 404 + Y.applyUpdate(doc, update); 405 + let nodes = doc.getXmlElement("prosemirror").toArray(); 406 + return YJSFragmentToString(nodes[0]) || ""; 407 + }, [secondBlockTextValue]); 408 + 409 + let entitiesToDelete = useMemo(() => { 410 + let etod: string[] = []; 411 + // Only delete first block if it's a heading type 412 + if (firstBlock?.type === "heading") { 413 + etod.push(firstBlock.value); 414 + } 415 + // Delete second block if it's empty text (only for linear documents) 416 + if ( 417 + !isCanvas && 418 + secondBlockText.trim() === "" && 419 + secondBlock?.type === "text" 420 + ) { 421 + etod.push(secondBlock.value); 422 + } 423 + return etod; 424 + }, [firstBlock, secondBlockText, secondBlock, isCanvas]); 425 + 426 + return { title: leafletTitle, entitiesToDelete }; 427 + };
+4 -2
app/[leaflet_id]/icon.tsx
··· 24 24 process.env.SUPABASE_SERVICE_ROLE_KEY as string, 25 25 { cookies: {} }, 26 26 ); 27 - export default async function Icon(props: { params: { leaflet_id: string } }) { 27 + export default async function Icon(props: { 28 + params: Promise<{ leaflet_id: string }>; 29 + }) { 28 30 let res = await supabase 29 31 .from("permission_tokens") 30 32 .select("*, permission_token_rights(*)") 31 - .eq("id", props.params.leaflet_id) 33 + .eq("id", (await props.params).leaflet_id) 32 34 .single(); 33 35 let rootEntity = res.data?.root_entity; 34 36 let outlineColor, fillColor;
+3 -2
app/[leaflet_id]/opengraph-image.tsx
··· 4 4 export const revalidate = 60; 5 5 6 6 export default async function OpenGraphImage(props: { 7 - params: { leaflet_id: string }; 7 + params: Promise<{ leaflet_id: string }>; 8 8 }) { 9 - return getMicroLinkOgImage(`/${props.params.leaflet_id}`); 9 + let params = await props.params; 10 + return getMicroLinkOgImage(`/${params.leaflet_id}`); 10 11 }
+2 -5
app/[leaflet_id]/page.tsx
··· 13 13 import { supabaseServerClient } from "supabase/serverClient"; 14 14 import { get_leaflet_data } from "app/api/rpc/[command]/get_leaflet_data"; 15 15 import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; 16 + import { getPublicationMetadataFromLeafletData } from "src/utils/getPublicationMetadataFromLeafletData"; 16 17 17 18 export const preferredRegion = ["sfo1"]; 18 19 export const dynamic = "force-dynamic"; ··· 70 71 ); 71 72 let rootEntity = res.data?.root_entity; 72 73 if (!rootEntity || !res.data) return { title: "Leaflet not found" }; 73 - let publication_data = 74 - res.data?.leaflets_in_publications?.[0] || 75 - res.data?.permission_token_rights[0].entity_sets?.permission_tokens?.find( 76 - (p) => p.leaflets_in_publications.length, 77 - )?.leaflets_in_publications?.[0]; 74 + let publication_data = getPublicationMetadataFromLeafletData(res.data); 78 75 if (publication_data) { 79 76 return { 80 77 title: publication_data.title || "Untitled",
+3 -1
app/[leaflet_id]/publish/BskyPostEditorProsemirror.tsx
··· 245 245 view.updateState(newState); 246 246 setEditorState(newState); 247 247 props.editorStateRef.current = newState; 248 - props.onCharCountChange?.(newState.doc.textContent.length); 248 + props.onCharCountChange?.( 249 + newState.doc.textContent.length + newState.doc.children.length - 1, 250 + ); 249 251 }, 250 252 }, 251 253 );
+92 -47
app/[leaflet_id]/publish/PublishPost.tsx
··· 19 19 } from "./BskyPostEditorProsemirror"; 20 20 import { EditorState } from "prosemirror-state"; 21 21 import { TagSelector } from "../../../components/Tags"; 22 + import { LooseLeafSmall } from "components/Icons/LooseleafSmall"; 23 + import { PubIcon } from "components/ActionBar/Publications"; 22 24 23 25 type Props = { 24 26 title: string; ··· 26 28 root_entity: string; 27 29 profile: ProfileViewDetailed; 28 30 description: string; 29 - publication_uri: string; 31 + publication_uri?: string; 30 32 record?: PubLeafletPublication.Record; 31 33 posts_in_pub?: number; 34 + entitiesToDelete?: string[]; 32 35 }; 33 36 34 37 export function PublishPost(props: Props) { ··· 73 76 leaflet_id: props.leaflet_id, 74 77 title: props.title, 75 78 description: props.description, 79 + entitiesToDelete: props.entitiesToDelete, 76 80 }); 77 81 if (!doc) return; 78 82 79 - let post_url = `https://${props.record?.base_path}/${doc.rkey}`; 83 + // Generate post URL based on whether it's in a publication or standalone 84 + let post_url = props.record?.base_path 85 + ? `https://${props.record.base_path}/${doc.rkey}` 86 + : `https://leaflet.pub/p/${props.profile.did}/${doc.rkey}`; 87 + 80 88 let [text, facets] = editorStateRef.current 81 89 ? editorStateToFacetedText(editorStateRef.current) 82 90 : []; ··· 95 103 } 96 104 97 105 return ( 98 - <form 99 - onSubmit={(e) => { 100 - e.preventDefault(); 101 - submit(); 102 - }} 103 - > 104 - <div className="container flex flex-col gap-6 sm:p-3 p-4 max-w-screen sm:max-w-xl w-[1000px]"> 105 - <div className="flex flex-col gap-2"> 106 - <h4>Add Global Tags</h4> 107 - <div> 108 - <TagSelector /> 106 + <div className="flex flex-col gap-4 w-[640px] max-w-full sm:px-4 px-3 text-primary"> 107 + <form 108 + onSubmit={(e) => { 109 + e.preventDefault(); 110 + submit(); 111 + }} 112 + > 113 + <div className="container flex flex-col gap-2 sm:p-3 p-4"> 114 + <PublishingTo 115 + publication_uri={props.publication_uri} 116 + record={props.record} 117 + /> 118 + <hr className="border-border-light my-1" /> 119 + <ShareOptions 120 + setShareOption={setShareOption} 121 + shareOption={shareOption} 122 + charCount={charCount} 123 + setCharCount={setCharCount} 124 + editorStateRef={editorStateRef} 125 + {...props} 126 + /> 127 + <hr className="border-border-light " /> 128 + <div className="flex justify-between"> 129 + <Link 130 + className="hover:no-underline! font-bold" 131 + href={`/${params.leaflet_id}`} 132 + > 133 + Back 134 + </Link> 135 + <ButtonPrimary 136 + type="submit" 137 + className="place-self-end h-[30px]" 138 + disabled={charCount > 300} 139 + > 140 + {isLoading ? <DotLoader /> : "Publish this Post!"} 141 + </ButtonPrimary> 109 142 </div> 110 143 </div> 111 - <ShareOptions 112 - setShareOption={setShareOption} 113 - shareOption={shareOption} 114 - charCount={charCount} 115 - setCharCount={setCharCount} 116 - editorStateRef={editorStateRef} 117 - {...props} 118 - /> 119 - <hr className="border-border-light " /> 120 - <div className="flex justify-between"> 121 - <Link 122 - className="hover:no-underline! font-bold" 123 - href={`/${params.leaflet_id}`} 124 - > 125 - Back 126 - </Link> 127 - <ButtonPrimary 128 - type="submit" 129 - className="place-self-end h-[30px]" 130 - disabled={charCount > 300} 131 - > 132 - {isLoading ? <DotLoader /> : "Publish this Post!"} 133 - </ButtonPrimary> 134 - </div> 135 - </div> 136 - </form> 144 + </form> 145 + </div> 137 146 ); 138 147 }; 139 148 ··· 228 237 ); 229 238 }; 230 239 240 + const PublishingTo = (props: { 241 + publication_uri?: string; 242 + record?: PubLeafletPublication.Record; 243 + }) => { 244 + if (props.publication_uri && props.record) { 245 + return ( 246 + <div className="flex flex-col gap-1"> 247 + <h3>Publishing to</h3> 248 + <div className="flex gap-2 items-center p-2 rounded-md bg-[var(--accent-light)]"> 249 + <PubIcon record={props.record} uri={props.publication_uri} /> 250 + <div className="font-bold text-secondary">{props.record.name}</div> 251 + </div> 252 + </div> 253 + ); 254 + } 255 + 256 + return ( 257 + <div className="flex flex-col gap-1"> 258 + <h3>Publishing as</h3> 259 + <div className="flex gap-2 items-center p-2 rounded-md bg-[var(--accent-light)]"> 260 + <LooseLeafSmall className="shrink-0" /> 261 + <div className="font-bold text-secondary">Looseleaf</div> 262 + </div> 263 + </div> 264 + ); 265 + }; 266 + 231 267 const PublishPostSuccess = (props: { 232 268 post_url: string; 233 - publication_uri: string; 269 + publication_uri?: string; 234 270 record: Props["record"]; 235 271 posts_in_pub: number; 236 272 }) => { 237 - let uri = new AtUri(props.publication_uri); 273 + let uri = props.publication_uri ? new AtUri(props.publication_uri) : null; 238 274 return ( 239 275 <div className="container p-4 m-3 sm:m-4 flex flex-col gap-1 justify-center text-center w-fit h-fit mx-auto"> 240 276 <PublishIllustration posts_in_pub={props.posts_in_pub} /> 241 277 <h2 className="pt-2">Published!</h2> 242 - <Link 243 - className="hover:no-underline! font-bold place-self-center pt-2" 244 - href={`/lish/${uri.host}/${encodeURIComponent(props.record?.name || "")}/dashboard`} 245 - > 246 - <ButtonPrimary>Back to Dashboard</ButtonPrimary> 247 - </Link> 278 + {uri && props.record ? ( 279 + <Link 280 + className="hover:no-underline! font-bold place-self-center pt-2" 281 + href={`/lish/${uri.host}/${encodeURIComponent(props.record.name || "")}/dashboard`} 282 + > 283 + <ButtonPrimary>Back to Dashboard</ButtonPrimary> 284 + </Link> 285 + ) : ( 286 + <Link 287 + className="hover:no-underline! font-bold place-self-center pt-2" 288 + href="/" 289 + > 290 + <ButtonPrimary>Back to Home</ButtonPrimary> 291 + </Link> 292 + )} 248 293 <a href={props.post_url}>See published post</a> 249 294 </div> 250 295 );
+63 -9
app/[leaflet_id]/publish/page.tsx
··· 13 13 type Props = { 14 14 // this is now a token id not leaflet! Should probs rename 15 15 params: Promise<{ leaflet_id: string }>; 16 + searchParams: Promise<{ 17 + publication_uri: string; 18 + title: string; 19 + description: string; 20 + entitiesToDelete: string; 21 + }>; 16 22 }; 17 23 export default async function PublishLeafletPage(props: Props) { 18 24 let leaflet_id = (await props.params).leaflet_id; ··· 27 33 *, 28 34 documents_in_publications(count) 29 35 ), 30 - documents(*))`, 36 + documents(*)), 37 + leaflets_to_documents( 38 + *, 39 + documents(*) 40 + )`, 31 41 ) 32 42 .eq("id", leaflet_id) 33 43 .single(); 34 44 let rootEntity = data?.root_entity; 35 - if (!data || !rootEntity || !data.leaflets_in_publications[0]) 45 + 46 + // Try to find publication from leaflets_in_publications first 47 + let publication = data?.leaflets_in_publications[0]?.publications; 48 + 49 + // If not found, check if publication_uri is in searchParams 50 + if (!publication) { 51 + let pub_uri = (await props.searchParams).publication_uri; 52 + if (pub_uri) { 53 + console.log(decodeURIComponent(pub_uri)); 54 + let { data: pubData, error } = await supabaseServerClient 55 + .from("publications") 56 + .select("*, documents_in_publications(count)") 57 + .eq("uri", decodeURIComponent(pub_uri)) 58 + .single(); 59 + console.log(error); 60 + publication = pubData; 61 + } 62 + } 63 + 64 + // Check basic data requirements 65 + if (!data || !rootEntity) 36 66 return ( 37 67 <div> 38 68 missin something ··· 42 72 43 73 let identity = await getIdentityData(); 44 74 if (!identity || !identity.atp_did) return null; 45 - let pub = data.leaflets_in_publications[0]; 46 - let agent = new AtpAgent({ service: "https://public.api.bsky.app" }); 47 75 76 + // Get title and description from either source 77 + let title = 78 + data.leaflets_in_publications[0]?.title || 79 + data.leaflets_to_documents[0]?.title || 80 + decodeURIComponent((await props.searchParams).title || ""); 81 + let description = 82 + data.leaflets_in_publications[0]?.description || 83 + data.leaflets_to_documents[0]?.description || 84 + decodeURIComponent((await props.searchParams).description || ""); 85 + 86 + let agent = new AtpAgent({ service: "https://public.api.bsky.app" }); 48 87 let profile = await agent.getProfile({ actor: identity.atp_did }); 88 + 89 + // Parse entitiesToDelete from URL params 90 + let searchParams = await props.searchParams; 91 + let entitiesToDelete: string[] = []; 92 + try { 93 + if (searchParams.entitiesToDelete) { 94 + entitiesToDelete = JSON.parse( 95 + decodeURIComponent(searchParams.entitiesToDelete), 96 + ); 97 + } 98 + } catch (e) { 99 + // If parsing fails, just use empty array 100 + } 101 + 49 102 return ( 50 103 <ReplicacheProvider 51 104 rootEntity={rootEntity} ··· 57 110 leaflet_id={leaflet_id} 58 111 root_entity={rootEntity} 59 112 profile={profile.data} 60 - title={pub.title} 61 - publication_uri={pub.publication} 62 - description={pub.description} 63 - record={pub.publications?.record as PubLeafletPublication.Record} 64 - posts_in_pub={pub.publications?.documents_in_publications[0].count} 113 + title={title} 114 + description={description} 115 + publication_uri={publication?.uri} 116 + record={publication?.record as PubLeafletPublication.Record | undefined} 117 + posts_in_pub={publication?.documents_in_publications[0]?.count} 118 + entitiesToDelete={entitiesToDelete} 65 119 /> 66 120 </ReplicacheProvider> 67 121 );
+9 -8
app/[leaflet_id]/publish/publishBskyPost.ts
··· 12 12 import { createOauthClient } from "src/atproto-oauth"; 13 13 import { supabaseServerClient } from "supabase/serverClient"; 14 14 import { Json } from "supabase/database.types"; 15 + import { 16 + getMicroLinkOgImage, 17 + getWebpageImage, 18 + } from "src/utils/getMicroLinkOgImage"; 15 19 16 20 export async function publishPostToBsky(args: { 17 21 text: string; ··· 31 35 credentialSession.fetchHandler.bind(credentialSession), 32 36 ); 33 37 let newPostUrl = args.url; 34 - let preview_image = await fetch( 35 - `https://pro.microlink.io/?url=${newPostUrl}&screenshot=true&viewport.width=1400&viewport.height=733&meta=false&embed=screenshot.url&force=true`, 36 - { 37 - headers: { 38 - "x-api-key": process.env.MICROLINK_API_KEY!, 39 - }, 40 - }, 41 - ); 38 + let preview_image = await getWebpageImage(newPostUrl, { 39 + width: 1400, 40 + height: 733, 41 + noCache: true, 42 + }); 42 43 43 44 let binary = await preview_image.blob(); 44 45 let resized_preview_image = await sharp(await binary.arrayBuffer())
-30
app/about/page.tsx
··· 1 - import { LegalContent } from "app/legal/content"; 2 - import Link from "next/link"; 3 - 4 - export default function AboutPage() { 5 - return ( 6 - <div className="flex flex-col gap-2"> 7 - <div className="flex flex-col h-[80vh] mx-auto sm:px-4 px-3 sm:py-6 py-4 max-w-prose gap-4 text-lg"> 8 - <p> 9 - Leaflet.pub is a web app for instantly creating and collaborating on 10 - documents.{" "} 11 - <Link href="/" prefetch={false}> 12 - Click here 13 - </Link>{" "} 14 - to create one and get started! 15 - </p> 16 - 17 - <p> 18 - Leaflet is made by Learning Futures Inc. Previously we built{" "} 19 - <a href="https://hyperlink.academy">hyperlink.academy</a> 20 - </p> 21 - <p> 22 - You can find us on{" "} 23 - <a href="https://bsky.app/profile/leaflet.pub">Bluesky</a> or email as 24 - at <a href="mailto:contact@leaflet.pub">contact@leaflet.pub</a> 25 - </p> 26 - </div> 27 - <LegalContent /> 28 - </div> 29 - ); 30 - }
+5
app/api/atproto_images/route.ts
··· 16 16 if (!service) return new NextResponse(null, { status: 404 }); 17 17 const response = await fetch( 18 18 `${service.serviceEndpoint}/xrpc/com.atproto.sync.getBlob?did=${params.did}&cid=${params.cid}`, 19 + { 20 + headers: { 21 + "Accept-Encoding": "gzip, deflate, br, zstd", 22 + }, 23 + }, 19 24 ); 20 25 21 26 // Clone the response to modify headers
+75
app/api/bsky/hydrate/route.ts
··· 1 + import { Agent, lexToJson } from "@atproto/api"; 2 + import { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; 3 + import { NextRequest } from "next/server"; 4 + 5 + export const runtime = "nodejs"; 6 + 7 + export async function GET(req: NextRequest) { 8 + try { 9 + const searchParams = req.nextUrl.searchParams; 10 + const urisParam = searchParams.get("uris"); 11 + 12 + if (!urisParam) { 13 + return Response.json( 14 + { error: "uris parameter is required" }, 15 + { status: 400 }, 16 + ); 17 + } 18 + 19 + // Parse URIs from JSON string 20 + let uris: string[]; 21 + try { 22 + uris = JSON.parse(urisParam); 23 + } catch (e) { 24 + return Response.json( 25 + { error: "uris must be valid JSON array" }, 26 + { status: 400 }, 27 + ); 28 + } 29 + 30 + if (!Array.isArray(uris)) { 31 + return Response.json({ error: "uris must be an array" }, { status: 400 }); 32 + } 33 + 34 + if (uris.length === 0) { 35 + return Response.json([], { 36 + headers: { 37 + "Cache-Control": "public, s-maxage=600, stale-while-revalidate=3600", 38 + }, 39 + }); 40 + } 41 + 42 + // Hydrate Bluesky URIs with post data 43 + let agent = new Agent({ 44 + service: "https://public.api.bsky.app", 45 + }); 46 + 47 + // Process URIs in batches of 25 48 + let allPostRequests = []; 49 + for (let i = 0; i < uris.length; i += 25) { 50 + let batch = uris.slice(i, i + 25); 51 + let batchPosts = agent.getPosts( 52 + { 53 + uris: batch, 54 + }, 55 + { headers: {} }, 56 + ); 57 + allPostRequests.push(batchPosts); 58 + } 59 + let allPosts = (await Promise.all(allPostRequests)).flatMap( 60 + (r) => r.data.posts, 61 + ); 62 + 63 + const posts = lexToJson(allPosts) as PostView[]; 64 + 65 + return Response.json(posts, { 66 + headers: { 67 + // Cache for 1 hour on CDN, allow stale content for 24 hours while revalidating 68 + "Cache-Control": "public, s-maxage=3600, stale-while-revalidate=86400", 69 + }, 70 + }); 71 + } catch (error) { 72 + console.error("Error hydrating Bluesky posts:", error); 73 + return Response.json({ error: "Failed to hydrate posts" }, { status: 500 }); 74 + } 75 + }
+80 -17
app/api/inngest/functions/index_post_mention.ts
··· 3 3 import { AtpAgent, AtUri } from "@atproto/api"; 4 4 import { Json } from "supabase/database.types"; 5 5 import { ids } from "lexicons/api/lexicons"; 6 + import { Notification, pingIdentityToUpdateNotification } from "src/notifications"; 7 + import { v7 } from "uuid"; 8 + import { idResolver } from "app/(home-pages)/reader/idResolver"; 6 9 7 10 export const index_post_mention = inngest.createFunction( 8 11 { id: "index_post_mention" }, ··· 11 14 let url = new URL(event.data.document_link); 12 15 let path = url.pathname.split("/").filter(Boolean); 13 16 14 - let { data: pub, error } = await supabaseServerClient 15 - .from("publications") 16 - .select("*") 17 - .eq("record->>base_path", url.host) 18 - .single(); 17 + // Check if this is a standalone document URL (/p/didOrHandle/rkey/...) 18 + const isStandaloneDoc = path[0] === "p" && path.length >= 3; 19 + 20 + let documentUri: string; 21 + let authorDid: string; 22 + 23 + if (isStandaloneDoc) { 24 + // Standalone doc: /p/didOrHandle/rkey/l-quote/... 25 + const didOrHandle = decodeURIComponent(path[1]); 26 + const rkey = path[2]; 27 + 28 + // Resolve handle to DID if necessary 29 + let did = didOrHandle; 30 + if (!didOrHandle.startsWith("did:")) { 31 + const resolved = await step.run("resolve-handle", async () => { 32 + return idResolver.handle.resolve(didOrHandle); 33 + }); 34 + if (!resolved) { 35 + return { message: `Could not resolve handle: ${didOrHandle}` }; 36 + } 37 + did = resolved; 38 + } 19 39 20 - if (!pub) { 21 - return { 22 - message: `No publication found for ${url.host}/${path[0]}`, 23 - error, 24 - }; 40 + documentUri = AtUri.make(did, ids.PubLeafletDocument, rkey).toString(); 41 + authorDid = did; 42 + } else { 43 + // Publication post: look up by custom domain 44 + let { data: pub, error } = await supabaseServerClient 45 + .from("publications") 46 + .select("*") 47 + .eq("record->>base_path", url.host) 48 + .single(); 49 + 50 + if (!pub) { 51 + return { 52 + message: `No publication found for ${url.host}/${path[0]}`, 53 + error, 54 + }; 55 + } 56 + 57 + documentUri = AtUri.make( 58 + pub.identity_did, 59 + ids.PubLeafletDocument, 60 + path[0], 61 + ).toString(); 62 + authorDid = pub.identity_did; 25 63 } 26 64 27 65 let bsky_post = await step.run("get-bsky-post-data", async () => { ··· 38 76 } 39 77 40 78 await step.run("index-bsky-post", async () => { 41 - await supabaseServerClient.from("bsky_posts").insert({ 79 + await supabaseServerClient.from("bsky_posts").upsert({ 42 80 uri: bsky_post.uri, 43 81 cid: bsky_post.cid, 44 82 post_view: bsky_post as Json, 45 83 }); 46 - await supabaseServerClient.from("document_mentions_in_bsky").insert({ 84 + await supabaseServerClient.from("document_mentions_in_bsky").upsert({ 47 85 uri: bsky_post.uri, 48 - document: AtUri.make( 49 - pub.identity_did, 50 - ids.PubLeafletDocument, 51 - path[0], 52 - ).toString(), 86 + document: documentUri, 53 87 link: event.data.document_link, 54 88 }); 89 + }); 90 + 91 + await step.run("create-notification", async () => { 92 + // Only create notification if the quote is from someone other than the author 93 + if (bsky_post.author.did !== authorDid) { 94 + // Check if a notification already exists for this post and recipient 95 + const { data: existingNotification } = await supabaseServerClient 96 + .from("notifications") 97 + .select("id") 98 + .eq("recipient", authorDid) 99 + .eq("data->>type", "quote") 100 + .eq("data->>bsky_post_uri", bsky_post.uri) 101 + .eq("data->>document_uri", documentUri) 102 + .single(); 103 + 104 + if (!existingNotification) { 105 + const notification: Notification = { 106 + id: v7(), 107 + recipient: authorDid, 108 + data: { 109 + type: "quote", 110 + bsky_post_uri: bsky_post.uri, 111 + document_uri: documentUri, 112 + }, 113 + }; 114 + await supabaseServerClient.from("notifications").insert(notification); 115 + await pingIdentityToUpdateNotification(authorDid); 116 + } 117 + } 55 118 }); 56 119 }, 57 120 );
+33 -4
app/api/rpc/[command]/getFactsFromHomeLeaflets.ts
··· 4 4 import { makeRoute } from "../lib"; 5 5 import type { Env } from "./route"; 6 6 import { scanIndexLocal } from "src/replicache/utils"; 7 - import { getBlocksWithTypeLocal } from "src/hooks/queries/useBlocks"; 8 7 import * as base64 from "base64-js"; 9 8 import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment"; 10 9 import { applyUpdate, Doc } from "yjs"; ··· 35 34 let scan = scanIndexLocal(facts[token]); 36 35 let [root] = scan.eav(token, "root/page"); 37 36 let rootEntity = root?.data.value || token; 38 - let [title] = getBlocksWithTypeLocal(facts[token], rootEntity).filter( 39 - (b) => b.type === "text" || b.type === "heading", 40 - ); 37 + 38 + // Check page type to determine which blocks to look up 39 + let [pageType] = scan.eav(rootEntity, "page/type"); 40 + let isCanvas = pageType?.data.value === "canvas"; 41 + 42 + // Get blocks and sort by position 43 + let rawBlocks = isCanvas 44 + ? scan.eav(rootEntity, "canvas/block").sort((a, b) => { 45 + if (a.data.position.y === b.data.position.y) 46 + return a.data.position.x - b.data.position.x; 47 + return a.data.position.y - b.data.position.y; 48 + }) 49 + : scan.eav(rootEntity, "card/block").sort((a, b) => { 50 + if (a.data.position === b.data.position) 51 + return a.id > b.id ? 1 : -1; 52 + return a.data.position > b.data.position ? 1 : -1; 53 + }); 54 + 55 + // Map to get type and filter for text/heading 56 + let blocks = rawBlocks 57 + .map((b) => { 58 + let type = scan.eav(b.data.value, "block/type")[0]; 59 + if ( 60 + !type || 61 + (type.data.value !== "text" && type.data.value !== "heading") 62 + ) 63 + return null; 64 + return b.data; 65 + }) 66 + .filter((b): b is NonNullable<typeof b> => b !== null); 67 + 68 + let title = blocks[0]; 69 + 41 70 if (!title) titles[token] = "Untitled"; 42 71 else { 43 72 let [content] = scan.eav(title.value, "block/text");
+4 -2
app/api/rpc/[command]/get_leaflet_data.ts
··· 7 7 >; 8 8 9 9 const leaflets_in_publications_query = `leaflets_in_publications(*, publications(*), documents(*))`; 10 + const leaflets_to_documents_query = `leaflets_to_documents(*, documents(*))`; 10 11 export const get_leaflet_data = makeRoute({ 11 12 route: "get_leaflet_data", 12 13 input: z.object({ ··· 18 19 .from("permission_tokens") 19 20 .select( 20 21 `*, 21 - permission_token_rights(*, entity_sets(permission_tokens(${leaflets_in_publications_query}))), 22 + permission_token_rights(*, entity_sets(permission_tokens(${leaflets_in_publications_query}, ${leaflets_to_documents_query}))), 22 23 custom_domain_routes!custom_domain_routes_edit_permission_token_fkey(*), 23 - ${leaflets_in_publications_query}`, 24 + ${leaflets_in_publications_query}, 25 + ${leaflets_to_documents_query}`, 24 26 ) 25 27 .eq("id", token_id) 26 28 .single();
+27
app/globals.css
··· 96 96 --accent-2: 255, 255, 255; 97 97 --accent-contrast: 0, 0, 225; 98 98 --accent-1-is-contrast: "true"; 99 + --accent-light: color-mix( 100 + in oklab, 101 + rgb(var(--accent-contrast)), 102 + rgb(var(--bg-page)) 85% 103 + ); 99 104 100 105 --highlight-1: 255, 177, 177; 101 106 --highlight-2: 253, 245, 203; ··· 334 339 @apply focus-within:outline-offset-1; 335 340 336 341 @apply disabled:border-border-light; 342 + @apply disabled:hover:border-border-light; 337 343 @apply disabled:bg-border-light; 338 344 @apply disabled:text-tertiary; 339 345 } ··· 365 371 @apply pl-3; 366 372 @apply ml-2; 367 373 } 374 + 368 375 .transparent-container { 369 376 @apply border; 370 377 @apply border-border-light; ··· 392 399 rgb(var(--bg-page)) 85% 393 400 ); 394 401 @apply rounded-md; 402 + } 403 + 404 + .menuItem { 405 + @apply text-secondary; 406 + @apply hover:text-secondary; 407 + @apply data-highlighted:bg-[var(--accent-light)]; 408 + @apply data-highlighted:outline-none; 409 + @apply hover:bg-[var(--accent-light)]; 410 + text-align: left; 411 + font-weight: 800; 412 + padding: 0.25rem 0.5rem; 413 + border-radius: 0.25rem; 414 + outline: none !important; 415 + cursor: pointer; 416 + background-color: transparent; 417 + 418 + :hover { 419 + text-decoration: none !important; 420 + background-color: rgb(var(--accent-light)); 421 + } 395 422 } 396 423 397 424 .pwa-padding {
+24
app/lish/[did]/[publication]/PublicationHomeLayout.tsx
··· 1 + "use client"; 2 + 3 + import { usePreserveScroll } from "src/hooks/usePreserveScroll"; 4 + 5 + export function PublicationHomeLayout(props: { 6 + uri: string; 7 + showPageBackground: boolean; 8 + children: React.ReactNode; 9 + }) { 10 + let { ref } = usePreserveScroll<HTMLDivElement>(props.uri); 11 + return ( 12 + <div 13 + ref={props.showPageBackground ? null : ref} 14 + className={`pubWrapper flex flex-col sm:py-6 h-full ${props.showPageBackground ? "max-w-prose mx-auto sm:px-0 px-[6px] py-2" : "w-full overflow-y-scroll"}`} 15 + > 16 + <div 17 + ref={!props.showPageBackground ? null : ref} 18 + className={`pub sm:max-w-prose max-w-(--page-width-units) w-[1000px] mx-auto px-3 sm:px-4 py-5 ${props.showPageBackground ? "overflow-auto h-full bg-[rgba(var(--bg-page),var(--bg-page-alpha))] border border-border rounded-lg" : "h-fit"}`} 19 + > 20 + {props.children} 21 + </div> 22 + </div> 23 + ); 24 + }
+9 -3
app/lish/[did]/[publication]/[rkey]/BaseTextBlock.tsx
··· 36 36 ${isStrikethrough ? "line-through decoration-tertiary" : ""} 37 37 ${isHighlighted ? "highlight bg-highlight-1" : ""}`.replaceAll("\n", " "); 38 38 39 + // Split text by newlines and insert <br> tags 40 + const textParts = segment.text.split('\n'); 41 + const renderedText = textParts.flatMap((part, i) => 42 + i < textParts.length - 1 ? [part, <br key={`br-${counter}-${i}`} />] : [part] 43 + ); 44 + 39 45 if (isCode) { 40 46 children.push( 41 47 <code key={counter} className={className} id={id?.id}> 42 - {segment.text} 48 + {renderedText} 43 49 </code>, 44 50 ); 45 51 } else if (link) { ··· 50 56 className={`text-accent-contrast hover:underline ${className}`} 51 57 target="_blank" 52 58 > 53 - {segment.text} 59 + {renderedText} 54 60 </a>, 55 61 ); 56 62 } else { 57 63 children.push( 58 64 <span key={counter} className={className} id={id?.id}> 59 - {segment.text} 65 + {renderedText} 60 66 </span>, 61 67 ); 62 68 }
+21 -26
app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx
··· 21 21 import { PostHeader } from "./PostHeader/PostHeader"; 22 22 import { useDrawerOpen } from "./Interactions/InteractionDrawer"; 23 23 import { PollData } from "./fetchPollData"; 24 + import { SharedPageProps } from "./PostPages"; 24 25 25 26 export function CanvasPage({ 26 - document, 27 27 blocks, 28 - did, 29 - profile, 30 - preferences, 31 - pubRecord, 32 - prerenderedCodeBlocks, 33 - bskyPostData, 34 - pollData, 35 - document_uri, 36 - pageId, 37 - pageOptions, 38 - fullPageScroll, 39 28 pages, 40 - }: { 41 - document_uri: string; 42 - document: PostPageData; 29 + ...props 30 + }: Omit<SharedPageProps, "allPages"> & { 43 31 blocks: PubLeafletPagesCanvas.Block[]; 44 - profile: ProfileViewDetailed; 45 - pubRecord: PubLeafletPublication.Record; 46 - did: string; 47 - prerenderedCodeBlocks?: Map<string, string>; 48 - bskyPostData: AppBskyFeedDefs.PostView[]; 49 - pollData: PollData[]; 50 - preferences: { showComments?: boolean }; 51 - pageId?: string; 52 - pageOptions?: React.ReactNode; 53 - fullPageScroll: boolean; 54 32 pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 55 33 }) { 56 - let hasPageBackground = !!pubRecord.theme?.showPageBackground; 34 + const { 35 + document, 36 + did, 37 + profile, 38 + preferences, 39 + pubRecord, 40 + theme, 41 + prerenderedCodeBlocks, 42 + bskyPostData, 43 + pollData, 44 + document_uri, 45 + pageId, 46 + pageOptions, 47 + fullPageScroll, 48 + hasPageBackground, 49 + } = props; 50 + if (!document) return null; 51 + 57 52 let isSubpage = !!pageId; 58 53 let drawer = useDrawerOpen(document_uri); 59 54
+146
app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx
··· 1 + import { AtpAgent } from "@atproto/api"; 2 + import { AtUri } from "@atproto/syntax"; 3 + import { ids } from "lexicons/api/lexicons"; 4 + import { 5 + PubLeafletBlocksBskyPost, 6 + PubLeafletDocument, 7 + PubLeafletPagesLinearDocument, 8 + PubLeafletPagesCanvas, 9 + PubLeafletPublication, 10 + } from "lexicons/api"; 11 + import { QuoteHandler } from "./QuoteHandler"; 12 + import { 13 + PublicationBackgroundProvider, 14 + PublicationThemeProvider, 15 + } from "components/ThemeManager/PublicationThemeProvider"; 16 + import { getPostPageData } from "./getPostPageData"; 17 + import { PostPageContextProvider } from "./PostPageContext"; 18 + import { PostPages } from "./PostPages"; 19 + import { extractCodeBlocks } from "./extractCodeBlocks"; 20 + import { LeafletLayout } from "components/LeafletLayout"; 21 + import { fetchPollData } from "./fetchPollData"; 22 + 23 + export async function DocumentPageRenderer({ 24 + did, 25 + rkey, 26 + }: { 27 + did: string; 28 + rkey: string; 29 + }) { 30 + let agent = new AtpAgent({ 31 + service: "https://public.api.bsky.app", 32 + fetch: (...args) => 33 + fetch(args[0], { 34 + ...args[1], 35 + next: { revalidate: 3600 }, 36 + }), 37 + }); 38 + 39 + let [document, profile] = await Promise.all([ 40 + getPostPageData(AtUri.make(did, ids.PubLeafletDocument, rkey).toString()), 41 + agent.getProfile({ actor: did }), 42 + ]); 43 + 44 + if (!document?.data) 45 + return ( 46 + <div className="bg-bg-leaflet h-full p-3 text-center relative"> 47 + <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 max-w-md w-full"> 48 + <div className=" px-3 py-4 opaque-container flex flex-col gap-1 mx-2 "> 49 + <h3>Sorry, post not found!</h3> 50 + <p> 51 + This may be a glitch on our end. If the issue persists please{" "} 52 + <a href="mailto:contact@leaflet.pub">send us a note</a>. 53 + </p> 54 + </div> 55 + </div> 56 + </div> 57 + ); 58 + 59 + let record = document.data as PubLeafletDocument.Record; 60 + let bskyPosts = 61 + record.pages.flatMap((p) => { 62 + let page = p as PubLeafletPagesLinearDocument.Main; 63 + return page.blocks?.filter( 64 + (b) => b.block.$type === ids.PubLeafletBlocksBskyPost, 65 + ); 66 + }) || []; 67 + 68 + // Batch bsky posts into groups of 25 and fetch in parallel 69 + let bskyPostBatches = []; 70 + for (let i = 0; i < bskyPosts.length; i += 25) { 71 + bskyPostBatches.push(bskyPosts.slice(i, i + 25)); 72 + } 73 + 74 + let bskyPostResponses = await Promise.all( 75 + bskyPostBatches.map((batch) => 76 + agent.getPosts( 77 + { 78 + uris: batch.map((p) => { 79 + let block = p?.block as PubLeafletBlocksBskyPost.Main; 80 + return block.postRef.uri; 81 + }), 82 + }, 83 + { headers: {} }, 84 + ), 85 + ), 86 + ); 87 + 88 + let bskyPostData = 89 + bskyPostResponses.length > 0 90 + ? bskyPostResponses.flatMap((response) => response.data.posts) 91 + : []; 92 + 93 + // Extract poll blocks and fetch vote data 94 + let pollBlocks = record.pages.flatMap((p) => { 95 + let page = p as PubLeafletPagesLinearDocument.Main; 96 + return ( 97 + page.blocks?.filter((b) => b.block.$type === ids.PubLeafletBlocksPoll) || 98 + [] 99 + ); 100 + }); 101 + let pollData = await fetchPollData( 102 + pollBlocks.map((b) => (b.block as any).pollRef.uri), 103 + ); 104 + 105 + // Get theme from publication or document (for standalone docs) 106 + let pubRecord = document.documents_in_publications[0]?.publications 107 + ?.record as PubLeafletPublication.Record | undefined; 108 + let theme = pubRecord?.theme || record.theme || null; 109 + let pub_creator = 110 + document.documents_in_publications[0]?.publications?.identity_did || did; 111 + let isStandalone = !pubRecord; 112 + 113 + let firstPage = record.pages[0]; 114 + 115 + let firstPageBlocks = 116 + ( 117 + firstPage as 118 + | PubLeafletPagesLinearDocument.Main 119 + | PubLeafletPagesCanvas.Main 120 + ).blocks || []; 121 + let prerenderedCodeBlocks = await extractCodeBlocks(firstPageBlocks); 122 + 123 + return ( 124 + <PostPageContextProvider value={document}> 125 + <PublicationThemeProvider theme={theme} pub_creator={pub_creator} isStandalone={isStandalone}> 126 + <PublicationBackgroundProvider theme={theme} pub_creator={pub_creator}> 127 + <LeafletLayout> 128 + <PostPages 129 + document_uri={document.uri} 130 + preferences={pubRecord?.preferences || {}} 131 + pubRecord={pubRecord} 132 + profile={JSON.parse(JSON.stringify(profile.data))} 133 + document={document} 134 + bskyPostData={bskyPostData} 135 + did={did} 136 + prerenderedCodeBlocks={prerenderedCodeBlocks} 137 + pollData={pollData} 138 + /> 139 + </LeafletLayout> 140 + 141 + <QuoteHandler /> 142 + </PublicationBackgroundProvider> 143 + </PublicationThemeProvider> 144 + </PostPageContextProvider> 145 + ); 146 + }
+2 -1
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/CommentBox.tsx
··· 43 43 replyTo?: string; 44 44 onSubmit?: () => void; 45 45 autoFocus?: boolean; 46 + className?: string; 46 47 }) { 47 48 let mountRef = useRef<HTMLPreElement | null>(null); 48 49 let { ··· 216 217 }, []); 217 218 218 219 return ( 219 - <div className=" flex flex-col"> 220 + <div className={`flex flex-col grow ${props.className}`}> 220 221 {quote && ( 221 222 <div className="relative mt-2 mb-2"> 222 223 <QuoteContent position={quote} did="" index={-1} />
+23
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/commentAction.ts
··· 8 8 import { AtUri, lexToJson, Un$Typed } from "@atproto/api"; 9 9 import { supabaseServerClient } from "supabase/serverClient"; 10 10 import { Json } from "supabase/database.types"; 11 + import { 12 + Notification, 13 + pingIdentityToUpdateNotification, 14 + } from "src/notifications"; 15 + import { v7 } from "uuid"; 11 16 12 17 export async function publishComment(args: { 13 18 document: string; ··· 65 70 } as unknown as Json, 66 71 }) 67 72 .select(); 73 + let notifications: Notification[] = []; 74 + let recipient = args.comment.replyTo 75 + ? new AtUri(args.comment.replyTo).host 76 + : new AtUri(args.document).host; 77 + if (recipient !== credentialSession.did) { 78 + notifications.push({ 79 + id: v7(), 80 + recipient, 81 + data: { 82 + type: "comment", 83 + comment_uri: uri.toString(), 84 + parent_uri: args.comment.replyTo, 85 + }, 86 + }); 87 + // SOMEDAY: move this out the action with inngest or workflows 88 + await supabaseServerClient.from("notifications").insert(notifications); 89 + await pingIdentityToUpdateNotification(recipient); 90 + } 68 91 69 92 return { 70 93 record: data?.[0].record as Json,
+64 -43
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx
··· 162 162 163 163 let [replyBoxOpen, setReplyBoxOpen] = useState(false); 164 164 let [repliesOpen, setRepliesOpen] = useState(true); 165 + 165 166 let replies = props.comments 166 167 .filter( 167 168 (comment) => ··· 176 177 new Date(aRecord.createdAt).getTime() 177 178 ); 178 179 }); 180 + 181 + let repliesOrReplyBoxOpen = 182 + replyBoxOpen || (repliesOpen && replies.length > 0); 179 183 return ( 180 184 <> 181 185 <div className="flex gap-2 items-center"> ··· 203 207 </> 204 208 )} 205 209 </div> 206 - <div className="flex flex-col gap-2"> 207 - {replyBoxOpen && ( 208 - <CommentBox 209 - pageId={props.pageId} 210 - doc_uri={props.document} 211 - replyTo={props.comment_uri} 212 - autoFocus={true} 213 - onSubmit={() => { 214 - setReplyBoxOpen(false); 215 - }} 216 - /> 217 - )} 218 - {repliesOpen && replies.length > 0 && ( 219 - <div className="repliesWrapper flex"> 220 - <button 221 - className="repliesCollapse pr-[14px] ml-[7px] pt-0.5" 222 - onClick={() => { 223 - setReplyBoxOpen(false); 224 - setRepliesOpen(false); 225 - }} 226 - > 227 - <div className="bg-border-light w-[2px] h-full" /> 228 - </button> 229 - <div className="repliesContent flex flex-col gap-3 pt-2 w-full"> 230 - {replies.map((reply) => { 231 - return ( 232 - <Comment 233 - pageId={props.pageId} 234 - document={props.document} 235 - key={reply.uri} 236 - comment={reply} 237 - profile={ 238 - reply.bsky_profiles?.record as AppBskyActorProfile.Record 239 - } 240 - record={reply.record as PubLeafletComment.Record} 241 - comments={props.comments} 242 - /> 243 - ); 244 - })} 210 + {repliesOrReplyBoxOpen && ( 211 + <div className="flex flex-col pt-1"> 212 + {replyBoxOpen && ( 213 + <div className="repliesWrapper flex w-full"> 214 + <button 215 + className="repliesCollapse pr-[14px] ml-[7px]" 216 + onClick={() => { 217 + setReplyBoxOpen(false); 218 + setRepliesOpen(false); 219 + }} 220 + > 221 + <div className="bg-border-light w-[2px] h-full" /> 222 + </button> 223 + <CommentBox 224 + className="pt-3" 225 + pageId={props.pageId} 226 + doc_uri={props.document} 227 + replyTo={props.comment_uri} 228 + autoFocus={true} 229 + onSubmit={() => { 230 + setReplyBoxOpen(false); 231 + }} 232 + /> 233 + </div> 234 + )} 235 + {repliesOpen && replies.length > 0 && ( 236 + <div className="repliesWrapper flex"> 237 + <button 238 + className="repliesCollapse pr-[14px] ml-[7px]" 239 + onClick={() => { 240 + setReplyBoxOpen(false); 241 + setRepliesOpen(false); 242 + }} 243 + > 244 + <div className="bg-border-light w-[2px] h-full" /> 245 + </button> 246 + <div className="repliesContent flex flex-col gap-3 pt-2 w-full"> 247 + {replies.map((reply) => { 248 + return ( 249 + <Comment 250 + pageId={props.pageId} 251 + document={props.document} 252 + key={reply.uri} 253 + comment={reply} 254 + profile={ 255 + reply.bsky_profiles 256 + ?.record as AppBskyActorProfile.Record 257 + } 258 + record={reply.record as PubLeafletComment.Record} 259 + comments={props.comments} 260 + /> 261 + ); 262 + })} 263 + </div> 245 264 </div> 246 - </div> 247 - )} 248 - </div> 265 + )} 266 + </div> 267 + )} 249 268 </> 250 269 ); 251 270 }; ··· 263 282 return ( 264 283 <Popover 265 284 trigger={ 266 - <div className="italic text-sm text-tertiary hover:underline">{timeAgoText}</div> 285 + <div className="italic text-sm text-tertiary hover:underline"> 286 + {timeAgoText} 287 + </div> 267 288 } 268 289 > 269 290 <div className="text-sm text-secondary">{fullDate}</div>
+7 -6
app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx
··· 10 10 11 11 export const InteractionDrawer = (props: { 12 12 document_uri: string; 13 - quotes: { link: string; bsky_posts: { post_view: Json } | null }[]; 13 + quotesAndMentions: { uri: string; link?: string }[]; 14 14 comments: Comment[]; 15 15 did: string; 16 16 pageId?: string; ··· 23 23 (c) => (c.record as any)?.onPage === props.pageId, 24 24 ); 25 25 26 - const filteredQuotes = props.quotes.filter((q) => { 26 + const filteredQuotesAndMentions = props.quotesAndMentions.filter((q) => { 27 + if (!q.link) return !props.pageId; // Direct mentions without quote context go to main page 27 28 const url = new URL(q.link); 28 29 const quoteParam = url.pathname.split("/l-quote/")[1]; 29 - if (!quoteParam) return null; 30 + if (!quoteParam) return !props.pageId; 30 31 const quotePosition = decodeQuotePosition(quoteParam); 31 32 return quotePosition?.pageId === props.pageId; 32 33 }); ··· 40 41 className="opaque-container rounded-l-none! rounded-r-lg! h-full w-full px-3 sm:px-4 pt-2 sm:pt-3 pb-6 overflow-scroll -ml-[1px] " 41 42 > 42 43 {drawer.drawer === "quotes" ? ( 43 - <Quotes {...props} quotes={filteredQuotes} /> 44 - ) : drawer.drawer === "comments" ? ( 44 + <Quotes {...props} quotesAndMentions={filteredQuotesAndMentions} /> 45 + ) : ( 45 46 <Comments 46 47 document_uri={props.document_uri} 47 48 comments={filteredComments} 48 49 pageId={props.pageId} 49 50 /> 50 - ) : null} 51 + )} 51 52 </div> 52 53 </div> 53 54 </>
+69 -57
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
··· 14 14 import { Popover } from "components/Popover"; 15 15 import { PostPageData } from "../getPostPageData"; 16 16 import { PubLeafletComment } from "lexicons/api"; 17 + import { prefetchQuotesData } from "./Quotes"; 17 18 18 19 export type InteractionState = { 19 20 drawerOpen: undefined | boolean; ··· 113 114 114 115 let { drawerOpen, drawer, pageId } = useInteractionState(document_uri); 115 116 117 + const handleQuotePrefetch = () => { 118 + if (data?.quotesAndMentions) { 119 + prefetchQuotesData(data.quotesAndMentions); 120 + } 121 + }; 122 + 116 123 return ( 117 - <div className="flex flex-col"> 118 - <div 119 - className={`flex gap-2 items-center text-tertiary ${props.compact ? "text-sm" : "pb-2"} ${props.className}`} 124 + <div 125 + className={`flex gap-2 text-tertiary ${props.compact ? "text-sm" : "px-3 sm:px-4"} ${props.className}`} 126 + > 127 + {props.compact && <TagPopover />} 128 + <button 129 + className={`flex gap-1 items-center ${!props.compact && "px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline"}`} 130 + onClick={() => { 131 + if (!drawerOpen || drawer !== "quotes") 132 + openInteractionDrawer("quotes", document_uri, props.pageId); 133 + else setInteractionState(document_uri, { drawerOpen: false }); 134 + }} 135 + onMouseEnter={handleQuotePrefetch} 136 + onTouchStart={handleQuotePrefetch} 137 + aria-label="Post quotes" 120 138 > 121 - {props.compact && <TagPopover />} 122 - 123 - {props.quotesCount > 0 && ( 124 - <button 125 - className={`flex items-center ${!props.compact ? " gap-2 px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" : "gap-1"}`} 126 - onClick={() => { 127 - if (!drawerOpen || drawer !== "quotes") 128 - openInteractionDrawer("quotes", document_uri, props.pageId); 129 - else setInteractionState(document_uri, { drawerOpen: false }); 130 - }} 131 - aria-label="Post quotes" 132 - > 133 - <QuoteTiny aria-hidden /> {props.quotesCount}{" "} 134 - {!props.compact && ( 135 - <span 136 - aria-hidden 137 - >{`Quote${props.quotesCount === 1 ? "" : "s"}`}</span> 138 - )} 139 - </button> 140 - )} 141 - {props.showComments === false ? null : ( 142 - <button 143 - className={`flex items-center ${!props.compact ? "gap-2 px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" : "gap-1"}`} 144 - onClick={() => { 145 - if ( 146 - !drawerOpen || 147 - drawer !== "comments" || 148 - pageId !== props.pageId 149 - ) 150 - openInteractionDrawer("comments", document_uri, props.pageId); 151 - else setInteractionState(document_uri, { drawerOpen: false }); 152 - }} 153 - aria-label="Post comments" 154 - > 155 - <CommentTiny aria-hidden />{" "} 156 - {props.compact ? ( 157 - props.commentsCount 158 - ) : props.commentsCount > 0 ? ( 159 - <span aria-hidden> 160 - {`${props.commentsCount} Comment${props.commentsCount === 1 ? "" : "s"}`} 161 - </span> 162 - ) : ( 163 - "Comment" 164 - )} 165 - </button> 139 + <QuoteTiny aria-hidden /> {props.quotesCount}{" "} 140 + {!props.compact && ( 141 + <span 142 + aria-hidden 143 + >{`Quote${props.quotesCount === 1 ? "" : "s"}`}</span> 166 144 )} 167 - </div> 168 - 145 + </button> 146 + {props.showComments === false ? null : ( 147 + <button 148 + className={`flex gap-1 items-center ${!props.compact && "px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline"}`} 149 + onClick={() => { 150 + if ( 151 + !drawerOpen || 152 + drawer !== "comments" || 153 + pageId !== props.pageId 154 + ) 155 + openInteractionDrawer("comments", document_uri, props.pageId); 156 + else setInteractionState(document_uri, { drawerOpen: false }); 157 + }} 158 + aria-label="Post comments" 159 + > 160 + <CommentTiny aria-hidden />{" "} 161 + {props.compact ? ( 162 + props.commentsCount 163 + ) : props.commentsCount > 0 ? ( 164 + <span aria-hidden> 165 + {`${props.commentsCount} Comment${props.commentsCount === 1 ? "" : "s"}`} 166 + </span> 167 + ) : ( 168 + "Comment" 169 + )} 170 + </button> 171 + )} 169 172 {!props.compact && <TagList />} 170 173 </div> 171 174 ); ··· 207 210 ]; 208 211 export function getQuoteCount(document: PostPageData, pageId?: string) { 209 212 if (!document) return; 213 + return getQuoteCountFromArray(document.quotesAndMentions, pageId); 214 + } 210 215 211 - if (pageId) 212 - return document.document_mentions_in_bsky.filter((q) => 213 - q.link.includes(pageId), 214 - ).length; 215 - else 216 - return document.document_mentions_in_bsky.filter((q) => { 216 + export function getQuoteCountFromArray( 217 + quotesAndMentions: { uri: string; link?: string }[], 218 + pageId?: string, 219 + ) { 220 + if (pageId) { 221 + return quotesAndMentions.filter((q) => { 222 + if (!q.link) return false; 223 + return q.link.includes(pageId); 224 + }).length; 225 + } else { 226 + return quotesAndMentions.filter((q) => { 227 + if (!q.link) return true; // Direct mentions go to main page 217 228 const url = new URL(q.link); 218 229 const quoteParam = url.pathname.split("/l-quote/")[1]; 219 - if (!quoteParam) return null; 230 + if (!quoteParam) return true; 220 231 const quotePosition = decodeQuotePosition(quoteParam); 221 232 return !quotePosition?.pageId; 222 233 }).length; 234 + } 223 235 } 224 236 225 237 export function getCommentCount(document: PostPageData, pageId?: string) {
+91 -7
app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx
··· 5 5 import { setInteractionState } from "./Interactions"; 6 6 import { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; 7 7 import { AtUri } from "@atproto/api"; 8 - import { Json } from "supabase/database.types"; 9 8 import { PostPageContext } from "../PostPageContext"; 10 9 import { 11 10 PubLeafletBlocksText, ··· 21 20 import { ProfileViewBasic } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 22 21 import { flushSync } from "react-dom"; 23 22 import { openPage } from "../PostPages"; 23 + import useSWR, { mutate } from "swr"; 24 + import { DotLoader } from "components/utils/DotLoader"; 25 + 26 + // Helper to get SWR key for quotes 27 + export function getQuotesSWRKey(uris: string[]) { 28 + if (uris.length === 0) return null; 29 + const params = new URLSearchParams({ 30 + uris: JSON.stringify(uris), 31 + }); 32 + return `/api/bsky/hydrate?${params.toString()}`; 33 + } 34 + 35 + // Fetch posts from API route 36 + async function fetchBskyPosts(uris: string[]): Promise<PostView[]> { 37 + const params = new URLSearchParams({ 38 + uris: JSON.stringify(uris), 39 + }); 40 + 41 + const response = await fetch(`/api/bsky/hydrate?${params.toString()}`); 42 + 43 + if (!response.ok) { 44 + throw new Error("Failed to fetch Bluesky posts"); 45 + } 46 + 47 + return response.json(); 48 + } 49 + 50 + // Prefetch quotes data 51 + export function prefetchQuotesData( 52 + quotesAndMentions: { uri: string; link?: string }[], 53 + ) { 54 + const uris = quotesAndMentions.map((q) => q.uri); 55 + const key = getQuotesSWRKey(uris); 56 + if (key) { 57 + // Start fetching without blocking 58 + mutate(key, fetchBskyPosts(uris), { revalidate: false }); 59 + } 60 + } 24 61 25 62 export const Quotes = (props: { 26 - quotes: { link: string; bsky_posts: { post_view: Json } | null }[]; 63 + quotesAndMentions: { uri: string; link?: string }[]; 27 64 did: string; 28 65 }) => { 29 66 let data = useContext(PostPageContext); ··· 31 68 if (!document_uri) 32 69 throw new Error("document_uri not available in PostPageContext"); 33 70 71 + // Fetch Bluesky post data for all URIs 72 + const uris = props.quotesAndMentions.map((q) => q.uri); 73 + const key = getQuotesSWRKey(uris); 74 + const { data: bskyPosts, isLoading } = useSWR(key, () => 75 + fetchBskyPosts(uris), 76 + ); 77 + 78 + // Separate quotes with links (quoted content) from direct mentions 79 + const quotesWithLinks = props.quotesAndMentions.filter((q) => q.link); 80 + const directMentions = props.quotesAndMentions.filter((q) => !q.link); 81 + 82 + // Create a map of URIs to post views for easy lookup 83 + const postViewMap = new Map<string, PostView>(); 84 + bskyPosts?.forEach((pv) => { 85 + postViewMap.set(pv.uri, pv); 86 + }); 87 + 34 88 return ( 35 89 <div className="flex flex-col gap-2"> 36 90 <div className="w-full flex justify-between text-secondary font-bold"> ··· 44 98 <CloseTiny /> 45 99 </button> 46 100 </div> 47 - {props.quotes.length === 0 ? ( 101 + {props.quotesAndMentions.length === 0 ? ( 48 102 <div className="opaque-container flex flex-col gap-0.5 p-[6px] text-tertiary italic text-sm text-center"> 49 103 <div className="font-bold">no quotes yet!</div> 50 104 <div>highlight any part of this post to quote it</div> 51 105 </div> 106 + ) : isLoading ? ( 107 + <div className="flex items-center justify-center gap-1 text-tertiary italic text-sm mt-8"> 108 + <span>loading</span> 109 + <DotLoader /> 110 + </div> 52 111 ) : ( 53 112 <div className="quotes flex flex-col gap-8"> 54 - {props.quotes.map((q, index) => { 55 - let pv = q.bsky_posts?.post_view as unknown as PostView; 113 + {/* Quotes with links (quoted content) */} 114 + {quotesWithLinks.map((q, index) => { 115 + const pv = postViewMap.get(q.uri); 116 + if (!pv || !q.link) return null; 56 117 const url = new URL(q.link); 57 118 const quoteParam = url.pathname.split("/l-quote/")[1]; 58 119 if (!quoteParam) return null; 59 120 const quotePosition = decodeQuotePosition(quoteParam); 60 121 if (!quotePosition) return null; 61 122 return ( 62 - <div key={index} className="flex flex-col "> 123 + <div key={`quote-${index}`} className="flex flex-col "> 63 124 <QuoteContent 64 125 index={index} 65 126 did={props.did} ··· 77 138 </div> 78 139 ); 79 140 })} 141 + 142 + {/* Direct post mentions (without quoted content) */} 143 + {directMentions.length > 0 && ( 144 + <div className="flex flex-col gap-4"> 145 + <div className="text-secondary font-bold">Post Mentions</div> 146 + <div className="flex flex-col gap-8"> 147 + {directMentions.map((q, index) => { 148 + const pv = postViewMap.get(q.uri); 149 + if (!pv) return null; 150 + return ( 151 + <BskyPost 152 + key={`mention-${index}`} 153 + rkey={new AtUri(pv.uri).rkey} 154 + content={pv.record.text as string} 155 + user={pv.author.displayName || pv.author.handle} 156 + profile={pv.author} 157 + handle={pv.author.handle} 158 + /> 159 + ); 160 + })} 161 + </div> 162 + </div> 163 + )} 80 164 </div> 81 165 )} 82 166 </div> ··· 154 238 ); 155 239 }; 156 240 157 - const BskyPost = (props: { 241 + export const BskyPost = (props: { 158 242 rkey: string; 159 243 content: string; 160 244 user: string;
+38 -44
app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx
··· 23 23 import { PageWrapper } from "components/Pages/Page"; 24 24 import { decodeQuotePosition } from "./quotePosition"; 25 25 import { PollData } from "./fetchPollData"; 26 + import { SharedPageProps } from "./PostPages"; 26 27 27 28 export function LinearDocumentPage({ 28 - document, 29 29 blocks, 30 - did, 31 - profile, 32 - preferences, 33 - pubRecord, 34 - prerenderedCodeBlocks, 35 - bskyPostData, 36 - document_uri, 37 - pageId, 38 - pageOptions, 39 - pollData, 40 - fullPageScroll, 41 - }: { 42 - document_uri: string; 43 - document: PostPageData; 30 + ...props 31 + }: Omit<SharedPageProps, "allPages"> & { 44 32 blocks: PubLeafletPagesLinearDocument.Block[]; 45 - profile?: ProfileViewDetailed; 46 - pubRecord: PubLeafletPublication.Record; 47 - did: string; 48 - prerenderedCodeBlocks?: Map<string, string>; 49 - bskyPostData: AppBskyFeedDefs.PostView[]; 50 - pollData: PollData[]; 51 - preferences: { showComments?: boolean }; 52 - pageId?: string; 53 - pageOptions?: React.ReactNode; 54 - fullPageScroll: boolean; 55 33 }) { 34 + const { 35 + document, 36 + did, 37 + profile, 38 + preferences, 39 + pubRecord, 40 + theme, 41 + prerenderedCodeBlocks, 42 + bskyPostData, 43 + pollData, 44 + document_uri, 45 + pageId, 46 + pageOptions, 47 + fullPageScroll, 48 + hasPageBackground, 49 + } = props; 56 50 let { identity } = useIdentityData(); 57 51 let drawer = useDrawerOpen(document_uri); 58 52 59 - if (!document || !document.documents_in_publications[0].publications) 60 - return null; 53 + if (!document) return null; 61 54 62 - let hasPageBackground = !!pubRecord.theme?.showPageBackground; 63 55 let record = document.data as PubLeafletDocument.Record; 64 56 65 57 const isSubpage = !!pageId; ··· 114 106 <EditTiny /> Edit Post 115 107 </a> 116 108 ) : ( 117 - <SubscribeWithBluesky 118 - isPost 119 - base_url={getPublicationURL( 120 - document.documents_in_publications[0].publications, 121 - )} 122 - pub_uri={ 123 - document.documents_in_publications[0].publications.uri 124 - } 125 - subscribers={ 126 - document.documents_in_publications[0].publications 127 - .publication_subscriptions 128 - } 129 - pubName={ 130 - document.documents_in_publications[0].publications.name 131 - } 132 - /> 109 + document.documents_in_publications[0]?.publications && ( 110 + <SubscribeWithBluesky 111 + isPost 112 + base_url={getPublicationURL( 113 + document.documents_in_publications[0].publications, 114 + )} 115 + pub_uri={ 116 + document.documents_in_publications[0].publications.uri 117 + } 118 + subscribers={ 119 + document.documents_in_publications[0].publications 120 + .publication_subscriptions 121 + } 122 + pubName={ 123 + document.documents_in_publications[0].publications.name 124 + } 125 + /> 126 + ) 133 127 )} 134 128 </div> 135 129 </>
+21 -2
app/lish/[did]/[publication]/[rkey]/PostContent.tsx
··· 16 16 PubLeafletBlocksIframe, 17 17 PubLeafletBlocksPage, 18 18 PubLeafletBlocksPoll, 19 + PubLeafletBlocksButton, 19 20 } from "lexicons/api"; 20 21 21 22 import { blobRefToSrc } from "src/utils/blobRefToSrc"; ··· 32 33 import { PublishedPageLinkBlock } from "./PublishedPageBlock"; 33 34 import { PublishedPollBlock } from "./PublishedPollBlock"; 34 35 import { PollData } from "./fetchPollData"; 36 + import { ButtonPrimary } from "components/Buttons"; 35 37 36 38 export function PostContent({ 37 39 blocks, ··· 128 130 : b.alignment === 129 131 "lex:pub.leaflet.pages.linearDocument#textAlignJustify" 130 132 ? "text-justify justify-start" 131 - : ""; 132 - if (!alignment && PubLeafletBlocksImage.isMain(b.block)) 133 + : b.alignment === "lex:pub.leaflet.pages.linearDocument#textAlignLeft" 134 + ? "text-left justify-start" 135 + : undefined; 136 + if ( 137 + !alignment && 138 + (PubLeafletBlocksImage.isMain(b.block) || 139 + PubLeafletBlocksButton.isMain(b.block)) 140 + ) 133 141 alignment = "text-center justify-center"; 134 142 135 143 let className = ` ··· 192 200 className={className} 193 201 pollData={pollVoteData} 194 202 /> 203 + ); 204 + } 205 + case PubLeafletBlocksButton.isMain(b.block): { 206 + return ( 207 + <div className={`flex ${alignment} ${className}`} {...blockProps}> 208 + <a href={b.block.url} target="_blank" rel="noopener noreferrer"> 209 + <ButtonPrimary role="link" type="submit"> 210 + {b.block.text} 211 + </ButtonPrimary> 212 + </a> 213 + </div> 195 214 ); 196 215 } 197 216 case PubLeafletBlocksUnorderedList.isMain(b.block): {
+36 -56
app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx
··· 5 5 PubLeafletPublication, 6 6 } from "lexicons/api"; 7 7 import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 8 - import { Interactions } from "../Interactions/Interactions"; 8 + import { 9 + Interactions, 10 + getQuoteCount, 11 + getCommentCount, 12 + } from "../Interactions/Interactions"; 9 13 import { PostPageData } from "../getPostPageData"; 10 14 import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 11 15 import { useIdentityData } from "components/IdentityProvider"; 12 16 import { EditTiny } from "components/Icons/EditTiny"; 13 17 import { SpeedyLink } from "components/SpeedyLink"; 14 - import { decodeQuotePosition } from "../quotePosition"; 15 - import { Separator } from "components/Layout"; 16 18 import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 17 19 18 20 export function PostHeader(props: { ··· 25 27 26 28 let record = document?.data as PubLeafletDocument.Record; 27 29 let profile = props.profile; 28 - let pub = props.data?.documents_in_publications[0].publications; 29 - let pubRecord = pub?.record as PubLeafletPublication.Record; 30 + let pub = props.data?.documents_in_publications[0]?.publications; 30 31 31 32 const formattedDate = useLocalizedDate( 32 33 record.publishedAt || new Date().toISOString(), ··· 37 38 }, 38 39 ); 39 40 40 - if (!document?.data || !document.documents_in_publications[0].publications) 41 - return; 41 + if (!document?.data) return; 42 42 return ( 43 43 <div 44 44 className="max-w-prose w-full mx-auto px-3 sm:px-4 sm:pt-3 pt-2" ··· 46 46 > 47 47 <div className="pubHeader flex flex-col pb-5"> 48 48 <div className="flex justify-between w-full"> 49 - <SpeedyLink 50 - className="font-bold hover:no-underline text-accent-contrast" 51 - href={ 52 - document && 53 - getPublicationURL( 54 - document.documents_in_publications[0].publications, 55 - ) 56 - } 57 - > 58 - {pub?.name} 59 - </SpeedyLink> 49 + {pub && ( 50 + <SpeedyLink 51 + className="font-bold hover:no-underline text-accent-contrast" 52 + href={document && getPublicationURL(pub)} 53 + > 54 + {pub?.name} 55 + </SpeedyLink> 56 + )} 60 57 {identity && 61 - identity.atp_did === 62 - document.documents_in_publications[0]?.publications 63 - .identity_did && 58 + pub && 59 + identity.atp_did === pub.identity_did && 64 60 document.leaflets_in_publications[0] && ( 65 61 <a 66 62 className=" rounded-full flex place-items-center" ··· 74 70 {record.description ? ( 75 71 <p className="italic text-secondary">{record.description}</p> 76 72 ) : null} 77 - <div className="flex flex-row gap-0 sm:gap-2 pt-3 justify-between"> 78 - <div className="text-sm text-tertiary flex gap-2 items-center "> 79 - {profile ? ( 80 - <> 81 - <a 82 - className="text-tertiary text-left" 83 - href={`https://bsky.app/profile/${profile.handle}`} 84 - > 85 - {props.profile.avatar && ( 86 - <img 87 - className="rounded-full w-4 h-4" 88 - src={props.profile.avatar} 89 - alt={props.profile.displayName} 90 - /> 91 - )} 92 - </a> 93 - </> 94 - ) : null} 95 - {record.publishedAt ? <p>{formattedDate}</p> : null} 96 - </div> 97 - 73 + <div className="text-sm text-tertiary pt-3 flex gap-1 flex-wrap"> 74 + {profile ? ( 75 + <> 76 + <a 77 + className="text-tertiary" 78 + href={`https://bsky.app/profile/${profile.handle}`} 79 + > 80 + by {profile.displayName || profile.handle} 81 + </a> 82 + </> 83 + ) : null} 84 + {record.publishedAt ? ( 85 + <> 86 + |<p>{formattedDate}</p> 87 + </> 88 + ) : null} 89 + |{" "} 98 90 <Interactions 99 91 showComments={props.preferences.showComments} 100 92 compact 101 - quotesCount={ 102 - document.document_mentions_in_bsky.filter((q) => { 103 - const url = new URL(q.link); 104 - const quoteParam = url.pathname.split("/l-quote/")[1]; 105 - if (!quoteParam) return null; 106 - const quotePosition = decodeQuotePosition(quoteParam); 107 - return !quotePosition?.pageId; 108 - }).length 109 - } 110 - commentsCount={ 111 - document.comments_on_documents.filter( 112 - (c) => !(c.record as PubLeafletComment.Record)?.onPage, 113 - ).length 114 - } 93 + quotesCount={getQuoteCount(document) || 0} 94 + commentsCount={getCommentCount(document) || 0} 115 95 /> 116 96 </div> 117 97 </div>
+113 -77
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 99 99 }; 100 100 }); 101 101 102 + // Shared props type for both page components 103 + export type SharedPageProps = { 104 + document: PostPageData; 105 + did: string; 106 + profile: ProfileViewDetailed; 107 + preferences: { showComments?: boolean }; 108 + pubRecord?: PubLeafletPublication.Record; 109 + theme?: PubLeafletPublication.Theme | null; 110 + prerenderedCodeBlocks?: Map<string, string>; 111 + bskyPostData: AppBskyFeedDefs.PostView[]; 112 + pollData: PollData[]; 113 + document_uri: string; 114 + fullPageScroll: boolean; 115 + hasPageBackground: boolean; 116 + pageId?: string; 117 + pageOptions?: React.ReactNode; 118 + allPages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 119 + }; 120 + 121 + // Component that renders either Canvas or Linear page based on page type 122 + function PageRenderer({ 123 + page, 124 + ...sharedProps 125 + }: { 126 + page: PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main; 127 + } & SharedPageProps) { 128 + const isCanvas = PubLeafletPagesCanvas.isMain(page); 129 + 130 + if (isCanvas) { 131 + return ( 132 + <CanvasPage 133 + {...sharedProps} 134 + blocks={(page as PubLeafletPagesCanvas.Main).blocks || []} 135 + pages={sharedProps.allPages} 136 + /> 137 + ); 138 + } 139 + 140 + return ( 141 + <LinearDocumentPage 142 + {...sharedProps} 143 + blocks={(page as PubLeafletPagesLinearDocument.Main).blocks || []} 144 + /> 145 + ); 146 + } 147 + 102 148 export function PostPages({ 103 149 document, 104 - blocks, 105 150 did, 106 151 profile, 107 152 preferences, ··· 113 158 }: { 114 159 document_uri: string; 115 160 document: PostPageData; 116 - blocks: PubLeafletPagesLinearDocument.Block[]; 117 161 profile: ProfileViewDetailed; 118 - pubRecord: PubLeafletPublication.Record; 162 + pubRecord?: PubLeafletPublication.Record; 119 163 did: string; 120 164 prerenderedCodeBlocks?: Map<string, string>; 121 165 bskyPostData: AppBskyFeedDefs.PostView[]; ··· 124 168 }) { 125 169 let drawer = useDrawerOpen(document_uri); 126 170 useInitializeOpenPages(); 127 - let pages = useOpenPages(); 128 - if (!document || !document.documents_in_publications[0].publications) 129 - return null; 171 + let openPageIds = useOpenPages(); 172 + if (!document) return null; 130 173 131 - let hasPageBackground = !!pubRecord.theme?.showPageBackground; 132 174 let record = document.data as PubLeafletDocument.Record; 175 + let theme = pubRecord?.theme || record.theme || null; 176 + // For publication posts, respect the publication's showPageBackground setting 177 + // For standalone documents, default to showing page background 178 + let isInPublication = !!pubRecord; 179 + let hasPageBackground = isInPublication ? !!theme?.showPageBackground : true; 180 + let quotesAndMentions = document.quotesAndMentions; 133 181 134 - let fullPageScroll = !hasPageBackground && !drawer && pages.length === 0; 182 + let firstPage = record.pages[0] as 183 + | PubLeafletPagesLinearDocument.Main 184 + | PubLeafletPagesCanvas.Main; 185 + 186 + // Canvas pages don't support fullPageScroll well due to fixed 1272px width 187 + let firstPageIsCanvas = PubLeafletPagesCanvas.isMain(firstPage); 188 + 189 + // Shared props used for all pages 190 + const sharedProps: SharedPageProps = { 191 + document, 192 + did, 193 + profile, 194 + preferences, 195 + pubRecord, 196 + theme, 197 + prerenderedCodeBlocks, 198 + bskyPostData, 199 + pollData, 200 + document_uri, 201 + hasPageBackground, 202 + allPages: record.pages as ( 203 + | PubLeafletPagesLinearDocument.Main 204 + | PubLeafletPagesCanvas.Main 205 + )[], 206 + fullPageScroll: 207 + !hasPageBackground && 208 + !drawer && 209 + openPageIds.length === 0 && 210 + !firstPageIsCanvas, 211 + }; 212 + 135 213 return ( 136 214 <> 137 - {!fullPageScroll && <BookendSpacer />} 138 - <LinearDocumentPage 139 - document={document} 140 - blocks={blocks} 141 - did={did} 142 - profile={profile} 143 - fullPageScroll={fullPageScroll} 144 - pollData={pollData} 145 - preferences={preferences} 146 - pubRecord={pubRecord} 147 - prerenderedCodeBlocks={prerenderedCodeBlocks} 148 - bskyPostData={bskyPostData} 149 - document_uri={document_uri} 150 - /> 215 + {!sharedProps.fullPageScroll && <BookendSpacer />} 216 + 217 + <PageRenderer page={firstPage} {...sharedProps} /> 151 218 152 219 {drawer && !drawer.pageId && ( 153 220 <InteractionDrawer 154 221 document_uri={document.uri} 155 222 comments={ 156 - pubRecord.preferences?.showComments === false 223 + pubRecord?.preferences?.showComments === false 157 224 ? [] 158 225 : document.comments_on_documents 159 226 } 160 - quotes={document.document_mentions_in_bsky} 227 + quotesAndMentions={quotesAndMentions} 161 228 did={did} 162 229 /> 163 230 )} 164 231 165 - {pages.map((p) => { 232 + {openPageIds.map((pageId) => { 166 233 let page = record.pages.find( 167 - (page) => 234 + (p) => 168 235 ( 169 - page as 236 + p as 170 237 | PubLeafletPagesLinearDocument.Main 171 238 | PubLeafletPagesCanvas.Main 172 - ).id === p, 239 + ).id === pageId, 173 240 ) as 174 241 | PubLeafletPagesLinearDocument.Main 175 242 | PubLeafletPagesCanvas.Main 176 243 | undefined; 177 - if (!page) return null; 178 244 179 - const isCanvas = PubLeafletPagesCanvas.isMain(page); 245 + if (!page) return null; 180 246 181 247 return ( 182 - <Fragment key={p}> 248 + <Fragment key={pageId}> 183 249 <SandwichSpacer /> 184 - {isCanvas ? ( 185 - <CanvasPage 186 - fullPageScroll={false} 187 - document={document} 188 - blocks={(page as PubLeafletPagesCanvas.Main).blocks} 189 - did={did} 190 - preferences={preferences} 191 - profile={profile} 192 - pubRecord={pubRecord} 193 - prerenderedCodeBlocks={prerenderedCodeBlocks} 194 - pollData={pollData} 195 - bskyPostData={bskyPostData} 196 - document_uri={document_uri} 197 - pageId={page.id} 198 - pages={record.pages as PubLeafletPagesLinearDocument.Main[]} 199 - pageOptions={ 200 - <PageOptions 201 - onClick={() => closePage(page?.id!)} 202 - hasPageBackground={hasPageBackground} 203 - /> 204 - } 205 - /> 206 - ) : ( 207 - <LinearDocumentPage 208 - fullPageScroll={false} 209 - document={document} 210 - blocks={(page as PubLeafletPagesLinearDocument.Main).blocks} 211 - did={did} 212 - preferences={preferences} 213 - pubRecord={pubRecord} 214 - pollData={pollData} 215 - prerenderedCodeBlocks={prerenderedCodeBlocks} 216 - bskyPostData={bskyPostData} 217 - document_uri={document_uri} 218 - pageId={page.id} 219 - pageOptions={ 220 - <PageOptions 221 - onClick={() => closePage(page?.id!)} 222 - hasPageBackground={hasPageBackground} 223 - /> 224 - } 225 - /> 226 - )} 250 + <PageRenderer 251 + page={page} 252 + {...sharedProps} 253 + fullPageScroll={false} 254 + pageId={page.id} 255 + pageOptions={ 256 + <PageOptions 257 + onClick={() => closePage(page.id!)} 258 + hasPageBackground={hasPageBackground} 259 + /> 260 + } 261 + /> 227 262 {drawer && drawer.pageId === page.id && ( 228 263 <InteractionDrawer 229 264 pageId={page.id} 230 265 document_uri={document.uri} 231 266 comments={ 232 - pubRecord.preferences?.showComments === false 267 + pubRecord?.preferences?.showComments === false 233 268 ? [] 234 269 : document.comments_on_documents 235 270 } 236 - quotes={document.document_mentions_in_bsky} 271 + quotesAndMentions={quotesAndMentions} 237 272 did={did} 238 273 /> 239 274 )} 240 275 </Fragment> 241 276 ); 242 277 })} 243 - {!fullPageScroll && <BookendSpacer />} 278 + 279 + {!sharedProps.fullPageScroll && <BookendSpacer />} 244 280 </> 245 281 ); 246 282 }
+20 -15
app/lish/[did]/[publication]/[rkey]/PublishBskyPostBlock.tsx
··· 7 7 import { focusBlock } from "src/utils/focusBlock"; 8 8 import { AppBskyFeedDefs, AppBskyFeedPost, RichText } from "@atproto/api"; 9 9 import { Separator } from "components/Layout"; 10 - import { useInitialPageLoad } from "components/InitialPageLoadProvider"; 10 + import { useHasPageLoaded } from "components/InitialPageLoadProvider"; 11 11 import { BlueskyTiny } from "components/Icons/BlueskyTiny"; 12 12 import { CommentTiny } from "components/Icons/CommentTiny"; 13 13 import { useLocalizedDate } from "src/hooks/useLocalizedDate"; ··· 60 60 {post.author && record && ( 61 61 <> 62 62 <div className="bskyAuthor w-full flex items-center gap-2"> 63 - <img 64 - src={post.author?.avatar} 65 - alt={`${post.author?.displayName}'s avatar`} 66 - className="shink-0 w-8 h-8 rounded-full border border-border-light" 67 - /> 63 + {post.author.avatar && ( 64 + <img 65 + src={post.author?.avatar} 66 + alt={`${post.author?.displayName}'s avatar`} 67 + className="shink-0 w-8 h-8 rounded-full border border-border-light" 68 + /> 69 + )} 68 70 <div className="grow flex flex-col gap-0.5 leading-tight"> 69 71 <div className=" font-bold text-secondary"> 70 72 {post.author?.displayName} ··· 121 123 }; 122 124 123 125 const ClientDate = (props: { date?: string }) => { 124 - let pageLoaded = useInitialPageLoad(); 125 - const formattedDate = useLocalizedDate(props.date || new Date().toISOString(), { 126 - month: "short", 127 - day: "numeric", 128 - year: "numeric", 129 - hour: "numeric", 130 - minute: "numeric", 131 - hour12: true, 132 - }); 126 + let pageLoaded = useHasPageLoaded(); 127 + const formattedDate = useLocalizedDate( 128 + props.date || new Date().toISOString(), 129 + { 130 + month: "short", 131 + day: "numeric", 132 + year: "numeric", 133 + hour: "numeric", 134 + minute: "numeric", 135 + hour12: true, 136 + }, 137 + ); 133 138 134 139 if (!pageLoaded) return null; 135 140
+4 -5
app/lish/[did]/[publication]/[rkey]/PublishedPageBlock.tsx
··· 106 106 <div className="grow"> 107 107 {title && ( 108 108 <div 109 - className={`pageBlockOne outline-none resize-none align-top flex gap-2 ${title.$type === "pub.leaflet.blocks.header" ? "font-bold text-base" : ""}`} 109 + className={`pageBlockOne outline-none resize-none align-top gap-2 ${title.$type === "pub.leaflet.blocks.header" ? "font-bold text-base" : ""}`} 110 110 > 111 111 <TextBlock 112 112 facets={title.facets} ··· 118 118 )} 119 119 {description && ( 120 120 <div 121 - className={`pageBlockLineTwo outline-none resize-none align-top flex gap-2 ${description.$type === "pub.leaflet.blocks.header" ? "font-bold" : ""}`} 121 + className={`pageBlockLineTwo outline-none resize-none align-top gap-2 ${description.$type === "pub.leaflet.blocks.header" ? "font-bold" : ""}`} 122 122 > 123 123 <TextBlock 124 124 facets={description.facets} ··· 151 151 let previewRef = useRef<HTMLDivElement | null>(null); 152 152 let { rootEntity } = useReplicache(); 153 153 let data = useContext(PostPageContext); 154 - let theme = data?.documents_in_publications[0]?.publications 155 - ?.record as PubLeafletPublication.Record; 154 + let theme = data?.theme; 156 155 let pageWidth = `var(--page-width-unitless)`; 157 - let cardBorderHidden = !theme.theme?.showPageBackground; 156 + let cardBorderHidden = !theme?.showPageBackground; 158 157 return ( 159 158 <div 160 159 ref={previewRef}
+109 -41
app/lish/[did]/[publication]/[rkey]/PublishedPollBlock.tsx
··· 1 1 "use client"; 2 2 3 - import { PubLeafletBlocksPoll, PubLeafletPollDefinition, PubLeafletPollVote } from "lexicons/api"; 3 + import { 4 + PubLeafletBlocksPoll, 5 + PubLeafletPollDefinition, 6 + PubLeafletPollVote, 7 + } from "lexicons/api"; 4 8 import { useState, useEffect } from "react"; 5 9 import { ButtonPrimary, ButtonSecondary } from "components/Buttons"; 6 10 import { useIdentityData } from "components/IdentityProvider"; ··· 10 14 import { Popover } from "components/Popover"; 11 15 import LoginForm from "app/login/LoginForm"; 12 16 import { BlueskyTiny } from "components/Icons/BlueskyTiny"; 17 + import { getVoterIdentities, VoterIdentity } from "./getVoterIdentities"; 18 + import { Json } from "supabase/database.types"; 19 + import { InfoSmall } from "components/Icons/InfoSmall"; 13 20 14 21 // Helper function to extract the first option from a vote record 15 22 const getVoteOption = (voteRecord: any): string | null => { ··· 101 108 setShowResults={setShowResults} 102 109 optimisticVote={optimisticVote} 103 110 /> 104 - {isCreator && !hasVoted && ( 111 + {!hasVoted && ( 105 112 <div className="flex justify-start"> 106 113 <button 107 114 className="w-fit flex gap-2 items-center justify-start text-sm text-accent-contrast" ··· 124 131 disabled={!identity?.atp_did} 125 132 /> 126 133 ))} 127 - <div className="flex justify-between items-center"> 128 - <div className="flex justify-end gap-2"> 129 - {isCreator && ( 130 - <button 131 - className="w-fit flex gap-2 items-center justify-start text-sm text-accent-contrast" 132 - onClick={() => setShowResults(!showResults)} 134 + <div className="flex flex-col-reverse sm:flex-row sm:justify-between gap-2 items-center pt-2"> 135 + <div className="text-sm text-tertiary">All votes are public</div> 136 + <div className="flex sm:gap-3 sm:flex-row flex-col-reverse sm:justify-end justify-center gap-1 items-center"> 137 + <button 138 + className="w-fit font-bold text-accent-contrast" 139 + onClick={() => setShowResults(!showResults)} 140 + > 141 + See Results 142 + </button> 143 + {identity?.atp_did ? ( 144 + <ButtonPrimary 145 + className="place-self-end" 146 + onClick={handleVote} 147 + disabled={!selectedOption || isVoting} 133 148 > 134 - See Results 135 - </button> 149 + {isVoting ? "Voting..." : "Vote!"} 150 + </ButtonPrimary> 151 + ) : ( 152 + <Popover 153 + asChild 154 + trigger={ 155 + <ButtonPrimary className="place-self-center"> 156 + <BlueskyTiny /> Login to vote 157 + </ButtonPrimary> 158 + } 159 + > 160 + {isClient && ( 161 + <LoginForm 162 + text="Log in to vote on this poll!" 163 + noEmail 164 + redirectRoute={window?.location.href + "?refreshAuth"} 165 + /> 166 + )} 167 + </Popover> 136 168 )} 137 169 </div> 138 - {identity?.atp_did ? ( 139 - <ButtonPrimary 140 - className="place-self-end" 141 - onClick={handleVote} 142 - disabled={!selectedOption || isVoting} 143 - > 144 - {isVoting ? "Voting..." : "Vote!"} 145 - </ButtonPrimary> 146 - ) : ( 147 - <Popover 148 - asChild 149 - trigger={ 150 - <ButtonPrimary className="place-self-center"> 151 - <BlueskyTiny /> Login to vote 152 - </ButtonPrimary> 153 - } 154 - > 155 - {isClient && ( 156 - <LoginForm 157 - text="Log in to vote on this poll!" 158 - noEmail 159 - redirectRoute={window?.location.href + "?refreshAuth"} 160 - /> 161 - )} 162 - </Popover> 163 - )} 164 170 </div> 165 171 </> 166 172 )} ··· 221 227 return ( 222 228 <> 223 229 {pollRecord.options.map((option, index) => { 224 - const votes = allVotes.filter( 230 + const voteRecords = allVotes.filter( 225 231 (v) => getVoteOption(v.record) === index.toString(), 226 - ).length; 227 - const isWinner = totalVotes > 0 && votes === highestVotes; 232 + ); 233 + const isWinner = totalVotes > 0 && voteRecords.length === highestVotes; 228 234 229 235 return ( 230 236 <PollResult 231 237 key={index} 232 238 option={option} 233 - votes={votes} 239 + votes={voteRecords.length} 240 + voteRecords={voteRecords} 234 241 totalVotes={totalVotes} 235 242 winner={isWinner} 236 243 /> ··· 240 247 ); 241 248 }; 242 249 250 + const VoterListPopover = (props: { 251 + votes: number; 252 + voteRecords: { voter_did: string; record: Json }[]; 253 + }) => { 254 + const [voterIdentities, setVoterIdentities] = useState<VoterIdentity[]>([]); 255 + const [isLoading, setIsLoading] = useState(false); 256 + const [hasFetched, setHasFetched] = useState(false); 257 + 258 + const handleOpenChange = async () => { 259 + if (!hasFetched && props.voteRecords.length > 0) { 260 + setIsLoading(true); 261 + setHasFetched(true); 262 + try { 263 + const dids = props.voteRecords.map((v) => v.voter_did); 264 + const identities = await getVoterIdentities(dids); 265 + setVoterIdentities(identities); 266 + } catch (error) { 267 + console.error("Failed to fetch voter identities:", error); 268 + } finally { 269 + setIsLoading(false); 270 + } 271 + } 272 + }; 273 + 274 + return ( 275 + <Popover 276 + trigger={ 277 + <button 278 + className="hover:underline cursor-pointer" 279 + disabled={props.votes === 0} 280 + > 281 + {props.votes} 282 + </button> 283 + } 284 + onOpenChange={handleOpenChange} 285 + className="w-64 max-h-80" 286 + > 287 + {isLoading ? ( 288 + <div className="flex justify-center py-4"> 289 + <div className="text-sm text-secondary">Loading...</div> 290 + </div> 291 + ) : ( 292 + <div className="flex flex-col gap-1 text-sm py-0.5"> 293 + {voterIdentities.map((voter) => ( 294 + <a 295 + key={voter.did} 296 + href={`https://bsky.app/profile/${voter.handle || voter.did}`} 297 + target="_blank" 298 + rel="noopener noreferrer" 299 + className="" 300 + > 301 + @{voter.handle || voter.did} 302 + </a> 303 + ))} 304 + </div> 305 + )} 306 + </Popover> 307 + ); 308 + }; 309 + 243 310 const PollResult = (props: { 244 311 option: PubLeafletPollDefinition.Option; 245 312 votes: number; 313 + voteRecords: { voter_did: string; record: Json }[]; 246 314 totalVotes: number; 247 315 winner: boolean; 248 316 }) => { ··· 258 326 className="pollResultContent text-accent-contrast relative flex gap-2 justify-between z-10" 259 327 > 260 328 <div className="grow max-w-full truncate">{props.option.text}</div> 261 - <div>{props.votes}</div> 329 + <VoterListPopover votes={props.votes} voteRecords={props.voteRecords} /> 262 330 </div> 263 331 <div className="pollResultBG absolute bg-bg-page w-full top-0 bottom-0 left-0 right-0 flex flex-row z-0"> 264 332 <div
+8
app/lish/[did]/[publication]/[rkey]/QuoteHandler.tsx
··· 83 83 selectionTop += quoteRect.height + 8; 84 84 } 85 85 86 + // Ensure tooltip stays within viewport bounds (330px wide + 8px padding) 87 + const TOOLTIP_WIDTH = 338; 88 + const viewportWidth = window.innerWidth; 89 + const maxLeft = viewportWidth - TOOLTIP_WIDTH; 90 + 91 + // Clamp selectionLeft to stay within bounds 92 + selectionLeft = Math.max(8, Math.min(selectionLeft, maxLeft)); 93 + 86 94 let startIndex = findDataIndex(range.startContainer); 87 95 let endIndex = findDataIndex(range.endContainer); 88 96 if (!startIndex || !endIndex) return;
+3 -2
app/lish/[did]/[publication]/[rkey]/extractCodeBlocks.ts
··· 1 1 import { 2 2 PubLeafletDocument, 3 3 PubLeafletPagesLinearDocument, 4 + PubLeafletPagesCanvas, 4 5 PubLeafletBlocksCode, 5 6 } from "lexicons/api"; 6 7 import { codeToHtml, bundledLanguagesInfo, bundledThemesInfo } from "shiki"; 7 8 8 9 export async function extractCodeBlocks( 9 - blocks: PubLeafletPagesLinearDocument.Block[], 10 + blocks: PubLeafletPagesLinearDocument.Block[] | PubLeafletPagesCanvas.Block[], 10 11 ): Promise<Map<string, string>> { 11 12 const codeBlocks = new Map<string, string>(); 12 13 13 - // Process all pages in the document 14 + // Process all blocks (works for both linear and canvas) 14 15 for (let i = 0; i < blocks.length; i++) { 15 16 const block = blocks[i]; 16 17 const currentIndex = [i];
+85 -3
app/lish/[did]/[publication]/[rkey]/getPostPageData.ts
··· 1 1 import { supabaseServerClient } from "supabase/serverClient"; 2 + import { AtUri } from "@atproto/syntax"; 3 + import { PubLeafletDocument, PubLeafletPublication } from "lexicons/api"; 2 4 3 - export type PostPageData = Awaited<ReturnType<typeof getPostPageData>>; 4 5 export async function getPostPageData(uri: string) { 5 6 let { data: document } = await supabaseServerClient 6 7 .from("documents") ··· 10 11 uri, 11 12 comments_on_documents(*, bsky_profiles(*)), 12 13 documents_in_publications(publications(*, publication_subscriptions(*))), 13 - document_mentions_in_bsky(*, bsky_posts(*)), 14 + document_mentions_in_bsky(*), 14 15 leaflets_in_publications(*) 15 16 `, 16 17 ) 17 18 .eq("uri", uri) 18 19 .single(); 19 - return document; 20 + 21 + if (!document) return null; 22 + 23 + // Fetch constellation backlinks for mentions 24 + const pubRecord = document.documents_in_publications[0]?.publications 25 + ?.record as PubLeafletPublication.Record; 26 + let aturi = new AtUri(uri); 27 + const postUrl = pubRecord 28 + ? `https://${pubRecord?.base_path}/${aturi.rkey}` 29 + : `https://leaflet.pub/p/${aturi.host}/${aturi.rkey}`; 30 + const constellationBacklinks = await getConstellationBacklinks(postUrl); 31 + 32 + // Deduplicate constellation backlinks (same post could appear in both links and embeds) 33 + const uniqueBacklinks = Array.from( 34 + new Map(constellationBacklinks.map((b) => [b.uri, b])).values(), 35 + ); 36 + 37 + // Combine database mentions (already deduplicated by DB constraint) and constellation backlinks 38 + const quotesAndMentions: { uri: string; link?: string }[] = [ 39 + // Database mentions (quotes with link to quoted content) 40 + ...document.document_mentions_in_bsky.map((m) => ({ 41 + uri: m.uri, 42 + link: m.link, 43 + })), 44 + // Constellation backlinks (direct post mentions without quote context) 45 + ...uniqueBacklinks, 46 + ]; 47 + 48 + let theme = 49 + ( 50 + document?.documents_in_publications[0]?.publications 51 + ?.record as PubLeafletPublication.Record 52 + )?.theme || (document?.data as PubLeafletDocument.Record)?.theme; 53 + 54 + return { 55 + ...document, 56 + quotesAndMentions, 57 + theme, 58 + }; 20 59 } 60 + 61 + export type PostPageData = Awaited<ReturnType<typeof getPostPageData>>; 62 + 63 + const headers = { 64 + "Content-type": "application/json", 65 + "user-agent": "leaflet.pub", 66 + }; 67 + 68 + // Fetch constellation backlinks without hydrating with Bluesky post data 69 + export async function getConstellationBacklinks( 70 + url: string, 71 + ): Promise<{ uri: string }[]> { 72 + try { 73 + let baseURL = `https://constellation.microcosm.blue/xrpc/blue.microcosm.links.getBacklinks?subject=${encodeURIComponent(url)}`; 74 + let externalEmbeds = new URL( 75 + `${baseURL}&source=${encodeURIComponent("app.bsky.feed.post:embed.external.uri")}`, 76 + ); 77 + let linkFacets = new URL( 78 + `${baseURL}&source=${encodeURIComponent("app.bsky.feed.post:facets[].features[app.bsky.richtext.facet#link].uri")}`, 79 + ); 80 + 81 + let [links, embeds] = (await Promise.all([ 82 + fetch(linkFacets, { headers, next: { revalidate: 3600 } }).then((req) => 83 + req.json(), 84 + ), 85 + fetch(externalEmbeds, { headers, next: { revalidate: 3600 } }).then( 86 + (req) => req.json(), 87 + ), 88 + ])) as ConstellationResponse[]; 89 + 90 + let uris = [...links.records, ...embeds.records].map((i) => 91 + AtUri.make(i.did, i.collection, i.rkey).toString(), 92 + ); 93 + 94 + return uris.map((uri) => ({ uri })); 95 + } catch (e) { 96 + return []; 97 + } 98 + } 99 + 100 + type ConstellationResponse = { 101 + records: { did: string; collection: string; rkey: string }[]; 102 + };
+35
app/lish/[did]/[publication]/[rkey]/getVoterIdentities.ts
··· 1 + "use server"; 2 + 3 + import { idResolver } from "app/(home-pages)/reader/idResolver"; 4 + 5 + export type VoterIdentity = { 6 + did: string; 7 + handle: string | null; 8 + }; 9 + 10 + export async function getVoterIdentities( 11 + dids: string[], 12 + ): Promise<VoterIdentity[]> { 13 + const identities = await Promise.all( 14 + dids.map(async (did) => { 15 + try { 16 + const resolved = await idResolver.did.resolve(did); 17 + const handle = resolved?.alsoKnownAs?.[0] 18 + ? resolved.alsoKnownAs[0].slice(5) // Remove "at://" prefix 19 + : null; 20 + return { 21 + did, 22 + handle, 23 + }; 24 + } catch (error) { 25 + console.error(`Failed to resolve DID ${did}:`, error); 26 + return { 27 + did, 28 + handle: null, 29 + }; 30 + } 31 + }), 32 + ); 33 + 34 + return identities; 35 + }
+4 -3
app/lish/[did]/[publication]/[rkey]/l-quote/[quote]/opengraph-image.ts
··· 5 5 export const revalidate = 60; 6 6 7 7 export default async function OpenGraphImage(props: { 8 - params: { publication: string; did: string; rkey: string; quote: string }; 8 + params: Promise<{ publication: string; did: string; rkey: string; quote: string }>; 9 9 }) { 10 - let quotePosition = decodeQuotePosition(props.params.quote); 10 + let params = await props.params; 11 + let quotePosition = decodeQuotePosition(params.quote); 11 12 return getMicroLinkOgImage( 12 - `/lish/${decodeURIComponent(props.params.did)}/${decodeURIComponent(props.params.publication)}/${props.params.rkey}/l-quote/${props.params.quote}#${quotePosition?.pageId ? `${quotePosition.pageId}~` : ""}${quotePosition?.start.block.join(".")}_${quotePosition?.start.offset}`, 13 + `/lish/${decodeURIComponent(params.did)}/${decodeURIComponent(params.publication)}/${params.rkey}/l-quote/${params.quote}#${quotePosition?.pageId ? `${quotePosition.pageId}~` : ""}${quotePosition?.start.block.join(".")}_${quotePosition?.start.offset}`, 13 14 { 14 15 width: 620, 15 16 height: 324,
+3 -2
app/lish/[did]/[publication]/[rkey]/opengraph-image.ts
··· 4 4 export const revalidate = 60; 5 5 6 6 export default async function OpenGraphImage(props: { 7 - params: { publication: string; did: string; rkey: string }; 7 + params: Promise<{ publication: string; did: string; rkey: string }>; 8 8 }) { 9 + let params = await props.params; 9 10 return getMicroLinkOgImage( 10 - `/lish/${decodeURIComponent(props.params.did)}/${decodeURIComponent(props.params.publication)}/${props.params.rkey}/`, 11 + `/lish/${decodeURIComponent(params.did)}/${decodeURIComponent(params.publication)}/${params.rkey}/`, 11 12 ); 12 13 }
+12 -142
app/lish/[did]/[publication]/[rkey]/page.tsx
··· 1 1 import { supabaseServerClient } from "supabase/serverClient"; 2 2 import { AtUri } from "@atproto/syntax"; 3 3 import { ids } from "lexicons/api/lexicons"; 4 - import { 5 - PubLeafletBlocksBskyPost, 6 - PubLeafletDocument, 7 - PubLeafletPagesLinearDocument, 8 - PubLeafletPublication, 9 - } from "lexicons/api"; 4 + import { PubLeafletDocument } from "lexicons/api"; 10 5 import { Metadata } from "next"; 11 - import { AtpAgent } from "@atproto/api"; 12 - import { QuoteHandler } from "./QuoteHandler"; 13 - import { InteractionDrawer } from "./Interactions/InteractionDrawer"; 14 - import { 15 - PublicationBackgroundProvider, 16 - PublicationThemeProvider, 17 - } from "components/ThemeManager/PublicationThemeProvider"; 18 - import { getPostPageData } from "./getPostPageData"; 19 - import { PostPageContextProvider } from "./PostPageContext"; 20 - import { PostPages } from "./PostPages"; 21 - import { extractCodeBlocks } from "./extractCodeBlocks"; 22 - import { LeafletLayout } from "components/LeafletLayout"; 23 - import { fetchPollData } from "./fetchPollData"; 6 + import { DocumentPageRenderer } from "./DocumentPageRenderer"; 24 7 25 8 export async function generateMetadata(props: { 26 9 params: Promise<{ publication: string; did: string; rkey: string }>; ··· 41 24 let docRecord = document.data as PubLeafletDocument.Record; 42 25 43 26 return { 27 + icons: { 28 + other: { 29 + rel: "alternate", 30 + url: document.uri, 31 + }, 32 + }, 44 33 title: 45 34 docRecord.title + 46 35 " - " + ··· 51 40 export default async function Post(props: { 52 41 params: Promise<{ publication: string; did: string; rkey: string }>; 53 42 }) { 54 - let did = decodeURIComponent((await props.params).did); 43 + let params = await props.params; 44 + let did = decodeURIComponent(params.did); 45 + 55 46 if (!did) 56 47 return ( 57 48 <div className="p-4 text-lg text-center flex flex-col gap-4"> ··· 62 53 </p> 63 54 </div> 64 55 ); 65 - let agent = new AtpAgent({ 66 - service: "https://public.api.bsky.app", 67 - fetch: (...args) => 68 - fetch(args[0], { 69 - ...args[1], 70 - cache: "no-store", 71 - next: { revalidate: 3600 }, 72 - }), 73 - }); 74 - let [document, profile] = await Promise.all([ 75 - getPostPageData( 76 - AtUri.make( 77 - did, 78 - ids.PubLeafletDocument, 79 - (await props.params).rkey, 80 - ).toString(), 81 - ), 82 - agent.getProfile({ actor: did }), 83 - ]); 84 - if (!document?.data || !document.documents_in_publications[0].publications) 85 - return ( 86 - <div className="bg-bg-leaflet h-full p-3 text-center relative"> 87 - <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 max-w-md w-full"> 88 - <div className=" px-3 py-4 opaque-container flex flex-col gap-1 mx-2 "> 89 - <h3>Sorry, post not found!</h3> 90 - <p> 91 - This may be a glitch on our end. If the issue persists please{" "} 92 - <a href="mailto:contact@leaflet.pub">send us a note</a>. 93 - </p> 94 - </div> 95 - </div> 96 - </div> 97 - ); 98 - let record = document.data as PubLeafletDocument.Record; 99 - let bskyPosts = record.pages.flatMap((p) => { 100 - let page = p as PubLeafletPagesLinearDocument.Main; 101 - return page.blocks?.filter( 102 - (b) => b.block.$type === ids.PubLeafletBlocksBskyPost, 103 - ); 104 - }); 105 - let bskyPostData = 106 - bskyPosts.length > 0 107 - ? await agent.getPosts( 108 - { 109 - uris: bskyPosts 110 - .map((p) => { 111 - let block = p?.block as PubLeafletBlocksBskyPost.Main; 112 - return block.postRef.uri; 113 - }) 114 - .slice(0, 24), 115 - }, 116 - { headers: {} }, 117 - ) 118 - : { data: { posts: [] } }; 119 56 120 - // Extract poll blocks and fetch vote data 121 - let pollBlocks = record.pages.flatMap((p) => { 122 - let page = p as PubLeafletPagesLinearDocument.Main; 123 - return page.blocks?.filter( 124 - (b) => b.block.$type === ids.PubLeafletBlocksPoll, 125 - ) || []; 126 - }); 127 - let pollData = await fetchPollData(pollBlocks.map(b => (b.block as any).pollRef.uri)); 128 - 129 - let firstPage = record.pages[0]; 130 - let blocks: PubLeafletPagesLinearDocument.Block[] = []; 131 - if (PubLeafletPagesLinearDocument.isMain(firstPage)) { 132 - blocks = firstPage.blocks || []; 133 - } 134 - 135 - let pubRecord = document.documents_in_publications[0]?.publications 136 - .record as PubLeafletPublication.Record; 137 - 138 - let prerenderedCodeBlocks = await extractCodeBlocks(blocks); 139 - 140 - return ( 141 - <PostPageContextProvider value={document}> 142 - <PublicationThemeProvider 143 - record={pubRecord} 144 - pub_creator={ 145 - document.documents_in_publications[0].publications.identity_did 146 - } 147 - > 148 - <PublicationBackgroundProvider 149 - record={pubRecord} 150 - pub_creator={ 151 - document.documents_in_publications[0].publications.identity_did 152 - } 153 - > 154 - {/* 155 - TODO: SCROLL PAGE TO FIT DRAWER 156 - If the drawer fits without scrolling, dont scroll 157 - If both drawer and page fit if you scrolled it, scroll it all into the center 158 - If the drawer and pafe doesn't all fit, scroll to drawer 159 - 160 - TODO: SROLL BAR 161 - If there is no drawer && there is no page bg, scroll the entire page 162 - If there is either a drawer open OR a page background, scroll just the post content 163 - 164 - TODO: HIGHLIGHTING BORKED 165 - on chrome, if you scroll backward, things stop working 166 - seems like if you use an older browser, sel direction is not a thing yet 167 - */} 168 - <LeafletLayout> 169 - <PostPages 170 - document_uri={document.uri} 171 - preferences={pubRecord.preferences || {}} 172 - pubRecord={pubRecord} 173 - profile={JSON.parse(JSON.stringify(profile.data))} 174 - document={document} 175 - bskyPostData={bskyPostData.data.posts} 176 - did={did} 177 - blocks={blocks} 178 - prerenderedCodeBlocks={prerenderedCodeBlocks} 179 - pollData={pollData} 180 - /> 181 - </LeafletLayout> 182 - 183 - <QuoteHandler /> 184 - </PublicationBackgroundProvider> 185 - </PublicationThemeProvider> 186 - </PostPageContextProvider> 187 - ); 57 + return <DocumentPageRenderer did={did} rkey={params.rkey} />; 188 58 }
+1 -48
app/lish/[did]/[publication]/dashboard/Actions.tsx
··· 1 1 "use client"; 2 2 3 - import { Media } from "components/Media"; 4 3 import { NewDraftActionButton } from "./NewDraftButton"; 4 + import { PublicationSettingsButton } from "./PublicationSettings"; 5 5 import { ActionButton } from "components/ActionBar/ActionButton"; 6 - import { useRouter } from "next/navigation"; 7 - import { Popover } from "components/Popover"; 8 - import { SettingsSmall } from "components/Icons/SettingsSmall"; 9 6 import { ShareSmall } from "components/Icons/ShareSmall"; 10 7 import { Menu } from "components/Layout"; 11 8 import { MenuItem } from "components/Layout"; 12 - import { HomeSmall } from "components/Icons/HomeSmall"; 13 - import { EditPubForm } from "app/lish/createPub/UpdatePubForm"; 14 9 import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 15 10 import { usePublicationData } from "./PublicationSWRProvider"; 16 11 import { useSmoker } from "components/Toast"; 17 - import { PaintSmall } from "components/Icons/PaintSmall"; 18 - import { PubThemeSetter } from "components/ThemeManager/PubThemeSetter"; 19 12 import { useIsMobile } from "src/hooks/isMobile"; 20 13 import { SpeedyLink } from "components/SpeedyLink"; 21 14 ··· 24 17 <> 25 18 <NewDraftActionButton publication={props.publication} /> 26 19 <PublicationShareButton /> 27 - <PublicationThemeButton /> 28 20 <PublicationSettingsButton publication={props.publication} /> 29 21 </> 30 22 ); ··· 85 77 </Menu> 86 78 ); 87 79 } 88 - 89 - function PublicationSettingsButton(props: { publication: string }) { 90 - let isMobile = useIsMobile(); 91 - return ( 92 - <Popover 93 - asChild 94 - side={isMobile ? "top" : "right"} 95 - align={isMobile ? "center" : "start"} 96 - className="max-w-xs" 97 - trigger={ 98 - <ActionButton 99 - id="pub-settings-button" 100 - icon=<SettingsSmall /> 101 - label="Settings" 102 - /> 103 - } 104 - > 105 - <EditPubForm /> 106 - </Popover> 107 - ); 108 - } 109 - 110 - function PublicationThemeButton() { 111 - let isMobile = useIsMobile(); 112 - 113 - return ( 114 - <Popover 115 - asChild 116 - className="max-w-xs pb-0 bg-white!" 117 - side={isMobile ? "top" : "right"} 118 - align={isMobile ? "center" : "start"} 119 - trigger={ 120 - <ActionButton id="pub-theme-button" icon=<PaintSmall /> label="Theme" /> 121 - } 122 - > 123 - <PubThemeSetter /> 124 - </Popover> 125 - ); 126 - }
+3 -1
app/lish/[did]/[publication]/dashboard/DraftList.tsx
··· 26 26 cardBorderHidden={!props.showPageBackground} 27 27 leaflets={leaflets_in_publications 28 28 .filter((l) => !l.documents) 29 + .filter((l) => !l.archived) 29 30 .map((l) => { 30 31 return { 32 + archived: l.archived, 33 + added_at: "", 31 34 token: { 32 35 ...l.permission_tokens!, 33 36 leaflets_in_publications: [ ··· 39 42 }, 40 43 ], 41 44 }, 42 - added_at: "", 43 45 }; 44 46 })} 45 47 initialFacts={pub_data.leaflet_data.facts || {}}
+22 -2
app/lish/[did]/[publication]/dashboard/PublicationSWRProvider.tsx
··· 2 2 3 3 import type { GetPublicationDataReturnType } from "app/api/rpc/[command]/get_publication_data"; 4 4 import { callRPC } from "app/api/rpc/client"; 5 - import { createContext, useContext } from "react"; 6 - import useSWR, { SWRConfig } from "swr"; 5 + import { createContext, useContext, useEffect } from "react"; 6 + import useSWR, { SWRConfig, KeyedMutator, mutate } from "swr"; 7 + import { produce, Draft } from "immer"; 8 + 9 + export type PublicationData = GetPublicationDataReturnType["result"]; 7 10 8 11 const PublicationContext = createContext({ name: "", did: "" }); 9 12 export function PublicationSWRDataProvider(props: { ··· 13 16 children: React.ReactNode; 14 17 }) { 15 18 let key = `publication-data-${props.publication_did}-${props.publication_rkey}`; 19 + useEffect(() => { 20 + console.log("UPDATING"); 21 + mutate(key, props.publication_data); 22 + }, [props.publication_data]); 16 23 return ( 17 24 <PublicationContext 18 25 value={{ name: props.publication_rkey, did: props.publication_did }} ··· 41 48 ); 42 49 return { data, mutate }; 43 50 } 51 + 52 + export function mutatePublicationData( 53 + mutate: KeyedMutator<PublicationData>, 54 + recipe: (draft: Draft<NonNullable<PublicationData>>) => void, 55 + ) { 56 + mutate( 57 + (data) => { 58 + if (!data) return data; 59 + return produce(data, recipe); 60 + }, 61 + { revalidate: false }, 62 + ); 63 + }
+132
app/lish/[did]/[publication]/dashboard/PublicationSettings.tsx
··· 1 + "use client"; 2 + 3 + import { ActionButton } from "components/ActionBar/ActionButton"; 4 + import { Popover } from "components/Popover"; 5 + import { SettingsSmall } from "components/Icons/SettingsSmall"; 6 + import { EditPubForm } from "app/lish/createPub/UpdatePubForm"; 7 + import { PubThemeSetter } from "components/ThemeManager/PubThemeSetter"; 8 + import { useIsMobile } from "src/hooks/isMobile"; 9 + import { useState } from "react"; 10 + import { GoBackSmall } from "components/Icons/GoBackSmall"; 11 + import { theme } from "tailwind.config"; 12 + import { ButtonPrimary } from "components/Buttons"; 13 + import { DotLoader } from "components/utils/DotLoader"; 14 + import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; 15 + 16 + export function PublicationSettingsButton(props: { publication: string }) { 17 + let isMobile = useIsMobile(); 18 + let [state, setState] = useState<"menu" | "general" | "theme">("menu"); 19 + let [loading, setLoading] = useState(false); 20 + 21 + return ( 22 + <Popover 23 + asChild 24 + onOpenChange={() => setState("menu")} 25 + side={isMobile ? "top" : "right"} 26 + align={isMobile ? "center" : "start"} 27 + className={`max-w-xs w-[1000px] ${state === "theme" && "bg-white!"}`} 28 + arrowFill={theme.colors["border-light"]} 29 + trigger={ 30 + <ActionButton 31 + id="pub-settings-button" 32 + icon=<SettingsSmall /> 33 + label="Settings" 34 + /> 35 + } 36 + > 37 + {state === "general" ? ( 38 + <EditPubForm 39 + backToMenuAction={() => setState("menu")} 40 + loading={loading} 41 + setLoadingAction={setLoading} 42 + /> 43 + ) : state === "theme" ? ( 44 + <PubThemeSetter 45 + backToMenu={() => setState("menu")} 46 + loading={loading} 47 + setLoading={setLoading} 48 + /> 49 + ) : ( 50 + <PubSettingsMenu 51 + state={state} 52 + setState={setState} 53 + loading={loading} 54 + setLoading={setLoading} 55 + /> 56 + )} 57 + </Popover> 58 + ); 59 + } 60 + 61 + const PubSettingsMenu = (props: { 62 + state: "menu" | "general" | "theme"; 63 + setState: (s: typeof props.state) => void; 64 + loading: boolean; 65 + setLoading: (l: boolean) => void; 66 + }) => { 67 + let menuItemClassName = 68 + "menuItem -mx-[8px] text-left flex items-center justify-between hover:no-underline!"; 69 + 70 + return ( 71 + <div className="flex flex-col gap-0.5"> 72 + <PubSettingsHeader 73 + loading={props.loading} 74 + setLoadingAction={props.setLoading} 75 + state={"menu"} 76 + /> 77 + <button 78 + className={menuItemClassName} 79 + type="button" 80 + onClick={() => { 81 + props.setState("general"); 82 + }} 83 + > 84 + Publication Settings 85 + <ArrowRightTiny /> 86 + </button> 87 + <button 88 + className={menuItemClassName} 89 + type="button" 90 + onClick={() => props.setState("theme")} 91 + > 92 + Publication Theme 93 + <ArrowRightTiny /> 94 + </button> 95 + </div> 96 + ); 97 + }; 98 + 99 + export const PubSettingsHeader = (props: { 100 + state: "menu" | "general" | "theme"; 101 + backToMenuAction?: () => void; 102 + loading: boolean; 103 + setLoadingAction: (l: boolean) => void; 104 + }) => { 105 + return ( 106 + <div className="flex justify-between font-bold text-secondary bg-border-light -mx-3 -mt-2 px-3 py-2 mb-1"> 107 + {props.state === "menu" 108 + ? "Settings" 109 + : props.state === "general" 110 + ? "General" 111 + : props.state === "theme" 112 + ? "Publication Theme" 113 + : ""} 114 + {props.state !== "menu" && ( 115 + <div className="flex gap-2"> 116 + <button 117 + type="button" 118 + onClick={() => { 119 + props.backToMenuAction && props.backToMenuAction(); 120 + }} 121 + > 122 + <GoBackSmall className="text-accent-contrast" /> 123 + </button> 124 + 125 + <ButtonPrimary compact type="submit"> 126 + {props.loading ? <DotLoader /> : "Update"} 127 + </ButtonPrimary> 128 + </div> 129 + )} 130 + </div> 131 + ); 132 + };
+111 -103
app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx
··· 13 13 import { MoreOptionsVerticalTiny } from "components/Icons/MoreOptionsVerticalTiny"; 14 14 import { DeleteSmall } from "components/Icons/DeleteSmall"; 15 15 import { ShareSmall } from "components/Icons/ShareSmall"; 16 - import { ShareButton } from "components/ShareOptions"; 16 + import { ShareButton } from "app/[leaflet_id]/actions/ShareOptions"; 17 17 import { SpeedyLink } from "components/SpeedyLink"; 18 18 import { QuoteTiny } from "components/Icons/QuoteTiny"; 19 19 import { CommentTiny } from "components/Icons/CommentTiny"; 20 20 import { InteractionPreview } from "components/InteractionsPreview"; 21 21 import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 22 + import { LeafletOptions } from "app/(home-pages)/home/LeafletList/LeafletOptions"; 23 + import { StaticLeafletDataContext } from "components/PageSWRDataProvider"; 22 24 23 25 export function PublishedPostsList(props: { 24 26 searchValue: string; ··· 60 62 let quotes = doc.documents.document_mentions_in_bsky[0]?.count || 0; 61 63 let comments = doc.documents.comments_on_documents[0]?.count || 0; 62 64 65 + let postLink = data?.publication 66 + ? `${getPublicationURL(data?.publication)}/${new AtUri(doc.documents.uri).rkey}` 67 + : ""; 68 + 63 69 return ( 64 70 <Fragment key={doc.documents?.uri}> 65 71 <div className="flex gap-2 w-full "> ··· 82 88 </h3> 83 89 </a> 84 90 <div className="flex justify-start align-top flex-row gap-1"> 85 - {leaflet && ( 86 - <SpeedyLink 87 - className="pt-[6px]" 88 - href={`/${leaflet.leaflet}`} 89 - > 90 - <EditTiny /> 91 - </SpeedyLink> 91 + {leaflet && leaflet.permission_tokens && ( 92 + <> 93 + <SpeedyLink 94 + className="pt-[6px]" 95 + href={`/${leaflet.leaflet}`} 96 + > 97 + <EditTiny /> 98 + </SpeedyLink> 99 + 100 + <StaticLeafletDataContext 101 + value={{ 102 + ...leaflet.permission_tokens, 103 + leaflets_in_publications: [ 104 + { 105 + ...leaflet, 106 + publications: publication, 107 + documents: doc.documents 108 + ? { 109 + uri: doc.documents.uri, 110 + indexed_at: doc.documents.indexed_at, 111 + data: doc.documents.data, 112 + } 113 + : null, 114 + }, 115 + ], 116 + leaflets_to_documents: [], 117 + blocked_by_admin: null, 118 + custom_domain_routes: [], 119 + }} 120 + > 121 + <LeafletOptions loggedIn={true} /> 122 + </StaticLeafletDataContext> 123 + </> 92 124 )} 93 - <Options document_uri={doc.documents.uri} /> 94 125 </div> 95 126 </div> 96 127 ··· 123 154 ); 124 155 } 125 156 126 - let Options = (props: { document_uri: string }) => { 127 - return ( 128 - <Menu 129 - align="end" 130 - alignOffset={20} 131 - asChild 132 - trigger={ 133 - <button className="text-secondary rounded-md selected-outline border-transparent! hover:border-border! h-min"> 134 - <MoreOptionsVerticalTiny /> 135 - </button> 136 - } 137 - > 138 - <> 139 - <OptionsMenu document_uri={props.document_uri} /> 140 - </> 141 - </Menu> 142 - ); 143 - }; 157 + // function OptionsMenu(props: { document_uri: string }) { 158 + // let { mutate, data } = usePublicationData(); 159 + // let [state, setState] = useState<"normal" | "confirm">("normal"); 144 160 145 - function OptionsMenu(props: { document_uri: string }) { 146 - let { mutate, data } = usePublicationData(); 147 - let [state, setState] = useState<"normal" | "confirm">("normal"); 148 - 149 - let postLink = data?.publication 150 - ? `${getPublicationURL(data?.publication)}/${new AtUri(props.document_uri).rkey}` 151 - : null; 152 - 153 - if (state === "normal") { 154 - return ( 155 - <> 156 - <ShareButton 157 - className="justify-end" 158 - text={ 159 - <div className="flex gap-2"> 160 - Share Post Link 161 - <ShareSmall /> 162 - </div> 163 - } 164 - subtext="" 165 - smokerText="Post link copied!" 166 - id="get-post-link" 167 - fullLink={postLink?.includes("https") ? postLink : undefined} 168 - link={postLink} 169 - /> 161 + // if (state === "normal") { 162 + // return ( 163 + // <> 164 + // <ShareButton 165 + // className="justify-end" 166 + // text={ 167 + // <div className="flex gap-2"> 168 + // Share Post Link 169 + // <ShareSmall /> 170 + // </div> 171 + // } 172 + // subtext="" 173 + // smokerText="Post link copied!" 174 + // id="get-post-link" 175 + // fullLink={postLink?.includes("https") ? postLink : undefined} 176 + // link={postLink} 177 + // /> 170 178 171 - <hr className="border-border-light" /> 172 - <MenuItem 173 - className="justify-end" 174 - onSelect={async (e) => { 175 - e.preventDefault(); 176 - setState("confirm"); 177 - return; 178 - }} 179 - > 180 - Delete Post 181 - <DeleteSmall /> 182 - </MenuItem> 183 - </> 184 - ); 185 - } 186 - if (state === "confirm") { 187 - return ( 188 - <div className="flex flex-col items-center font-bold text-secondary px-2 py-1"> 189 - Are you sure? 190 - <div className="text-sm text-tertiary font-normal"> 191 - This action cannot be undone! 192 - </div> 193 - <ButtonPrimary 194 - className="mt-2" 195 - onClick={async () => { 196 - await mutate((data) => { 197 - if (!data) return data; 198 - return { 199 - ...data, 200 - publication: { 201 - ...data.publication!, 202 - leaflets_in_publications: 203 - data.publication?.leaflets_in_publications.filter( 204 - (l) => l.doc !== props.document_uri, 205 - ) || [], 206 - documents_in_publications: 207 - data.publication?.documents_in_publications.filter( 208 - (d) => d.documents?.uri !== props.document_uri, 209 - ) || [], 210 - }, 211 - }; 212 - }, false); 213 - await deletePost(props.document_uri); 214 - }} 215 - > 216 - Delete 217 - </ButtonPrimary> 218 - </div> 219 - ); 220 - } 221 - } 179 + // <hr className="border-border-light" /> 180 + // <MenuItem 181 + // className="justify-end" 182 + // onSelect={async (e) => { 183 + // e.preventDefault(); 184 + // setState("confirm"); 185 + // return; 186 + // }} 187 + // > 188 + // Delete Post 189 + // <DeleteSmall /> 190 + // </MenuItem> 191 + // </> 192 + // ); 193 + // } 194 + // if (state === "confirm") { 195 + // return ( 196 + // <div className="flex flex-col items-center font-bold text-secondary px-2 py-1"> 197 + // Are you sure? 198 + // <div className="text-sm text-tertiary font-normal"> 199 + // This action cannot be undone! 200 + // </div> 201 + // <ButtonPrimary 202 + // className="mt-2" 203 + // onClick={async () => { 204 + // await mutate((data) => { 205 + // if (!data) return data; 206 + // return { 207 + // ...data, 208 + // publication: { 209 + // ...data.publication!, 210 + // leaflets_in_publications: 211 + // data.publication?.leaflets_in_publications.filter( 212 + // (l) => l.doc !== props.document_uri, 213 + // ) || [], 214 + // documents_in_publications: 215 + // data.publication?.documents_in_publications.filter( 216 + // (d) => d.documents?.uri !== props.document_uri, 217 + // ) || [], 218 + // }, 219 + // }; 220 + // }, false); 221 + // await deletePost(props.document_uri); 222 + // }} 223 + // > 224 + // Delete 225 + // </ButtonPrimary> 226 + // </div> 227 + // ); 228 + // } 229 + //} 222 230 223 231 function PublishedDate(props: { dateString: string }) { 224 232 const formattedDate = useLocalizedDate(props.dateString, {
+23
app/lish/[did]/[publication]/dashboard/deletePost.ts
··· 30 30 .delete() 31 31 .eq("doc", document_uri), 32 32 ]); 33 + 34 + return revalidatePath("/lish/[did]/[publication]/dashboard", "layout"); 35 + } 36 + 37 + export async function unpublishPost(document_uri: string) { 38 + let identity = await getIdentityData(); 39 + if (!identity || !identity.atp_did) throw new Error("No Identity"); 40 + 41 + const oauthClient = await createOauthClient(); 42 + let credentialSession = await oauthClient.restore(identity.atp_did); 43 + let agent = new AtpBaseClient( 44 + credentialSession.fetchHandler.bind(credentialSession), 45 + ); 46 + let uri = new AtUri(document_uri); 47 + if (uri.host !== identity.atp_did) return; 48 + 49 + await Promise.all([ 50 + agent.pub.leaflet.document.delete({ 51 + repo: credentialSession.did, 52 + rkey: uri.rkey, 53 + }), 54 + supabaseServerClient.from("documents").delete().eq("uri", document_uri), 55 + ]); 33 56 return revalidatePath("/lish/[did]/[publication]/dashboard", "layout"); 34 57 }
+3 -4
app/lish/[did]/[publication]/icon.ts
··· 14 14 }; 15 15 16 16 export const contentType = "image/png"; 17 - export default async function Icon({ 18 - params, 19 - }: { 20 - params: { did: string; publication: string }; 17 + export default async function Icon(props: { 18 + params: Promise<{ did: string; publication: string }>; 21 19 }) { 20 + const params = await props.params; 22 21 try { 23 22 let did = decodeURIComponent(params.did); 24 23 let uri;
+6
app/lish/[did]/[publication]/layout.tsx
··· 46 46 return { 47 47 title: pubRecord?.name || "Untitled Publication", 48 48 description: pubRecord?.description || "", 49 + icons: { 50 + other: { 51 + rel: "alternate", 52 + url: publication.uri, 53 + }, 54 + }, 49 55 alternates: pubRecord?.base_path 50 56 ? { 51 57 types: {
+3 -2
app/lish/[did]/[publication]/opengraph-image.ts
··· 4 4 export const revalidate = 60; 5 5 6 6 export default async function OpenGraphImage(props: { 7 - params: { publication: string; did: string }; 7 + params: Promise<{ publication: string; did: string }>; 8 8 }) { 9 + let params = await props.params; 9 10 return getMicroLinkOgImage( 10 - `/lish/${encodeURIComponent(props.params.did)}/${encodeURIComponent(props.params.publication)}/`, 11 + `/lish/${encodeURIComponent(params.did)}/${encodeURIComponent(params.publication)}/`, 11 12 ); 12 13 }
+116 -110
app/lish/[did]/[publication]/page.tsx
··· 16 16 import { CommentTiny } from "components/Icons/CommentTiny"; 17 17 import { InteractionPreview } from "components/InteractionsPreview"; 18 18 import { LocalizedDate } from "./LocalizedDate"; 19 + import { PublicationHomeLayout } from "./PublicationHomeLayout"; 19 20 20 21 export default async function Publication(props: { 21 22 params: Promise<{ publication: string; did: string }>; ··· 60 61 try { 61 62 return ( 62 63 <PublicationThemeProvider 63 - record={record} 64 + theme={record?.theme} 64 65 pub_creator={publication.identity_did} 65 66 > 66 67 <PublicationBackgroundProvider 67 - record={record} 68 + theme={record?.theme} 68 69 pub_creator={publication.identity_did} 69 70 > 70 - <div 71 - className={`pubWrapper flex flex-col sm:py-6 h-full ${showPageBackground ? "max-w-prose mx-auto sm:px-0 px-[6px] py-2" : "w-full overflow-y-scroll"}`} 71 + <PublicationHomeLayout 72 + uri={publication.uri} 73 + showPageBackground={!!showPageBackground} 72 74 > 73 - <div 74 - className={`pub sm:max-w-prose max-w-(--page-width-units) w-[1000px] mx-auto px-3 sm:px-4 py-5 ${showPageBackground ? "overflow-auto h-full bg-[rgba(var(--bg-page),var(--bg-page-alpha))] border border-border rounded-lg" : "h-fit"}`} 75 - > 76 - <div className="pubHeader flex flex-col pb-8 w-full text-center justify-center "> 77 - {record?.icon && ( 78 - <div 79 - className="shrink-0 w-10 h-10 rounded-full mx-auto" 80 - style={{ 81 - backgroundImage: `url(/api/atproto_images?did=${did}&cid=${(record.icon.ref as unknown as { $link: string })["$link"]})`, 82 - backgroundRepeat: "no-repeat", 83 - backgroundPosition: "center", 84 - backgroundSize: "cover", 85 - }} 86 - /> 87 - )} 88 - <h2 className="text-accent-contrast sm:text-xl text-[22px] pt-1 "> 89 - {publication.name} 90 - </h2> 91 - <p className="sm:text-lg text-secondary"> 92 - {record?.description}{" "} 75 + <div className="pubHeader flex flex-col pb-8 w-full text-center justify-center "> 76 + {record?.icon && ( 77 + <div 78 + className="shrink-0 w-10 h-10 rounded-full mx-auto" 79 + style={{ 80 + backgroundImage: `url(/api/atproto_images?did=${did}&cid=${(record.icon.ref as unknown as { $link: string })["$link"]})`, 81 + backgroundRepeat: "no-repeat", 82 + backgroundPosition: "center", 83 + backgroundSize: "cover", 84 + }} 85 + /> 86 + )} 87 + <h2 className="text-accent-contrast sm:text-xl text-[22px] pt-1 "> 88 + {publication.name} 89 + </h2> 90 + <p className="sm:text-lg text-secondary"> 91 + {record?.description}{" "} 92 + </p> 93 + {profile && ( 94 + <p className="italic text-tertiary sm:text-base text-sm"> 95 + <strong className="">by {profile.displayName}</strong>{" "} 96 + <a 97 + className="text-tertiary" 98 + href={`https://bsky.app/profile/${profile.handle}`} 99 + > 100 + @{profile.handle} 101 + </a> 93 102 </p> 94 - {profile && ( 95 - <p className="italic text-tertiary sm:text-base text-sm"> 96 - <strong className="">by {profile.displayName}</strong>{" "} 97 - <a 98 - className="text-tertiary" 99 - href={`https://bsky.app/profile/${profile.handle}`} 100 - > 101 - @{profile.handle} 102 - </a> 103 - </p> 104 - )} 105 - <div className="sm:pt-4 pt-4"> 106 - <SubscribeWithBluesky 107 - base_url={getPublicationURL(publication)} 108 - pubName={publication.name} 109 - pub_uri={publication.uri} 110 - subscribers={publication.publication_subscriptions} 111 - /> 112 - </div> 103 + )} 104 + <div className="sm:pt-4 pt-4"> 105 + <SubscribeWithBluesky 106 + base_url={getPublicationURL(publication)} 107 + pubName={publication.name} 108 + pub_uri={publication.uri} 109 + subscribers={publication.publication_subscriptions} 110 + /> 113 111 </div> 114 - <div className="publicationPostList w-full flex flex-col gap-4"> 115 - {publication.documents_in_publications 116 - .filter((d) => !!d?.documents) 117 - .sort((a, b) => { 118 - let aRecord = a.documents 119 - ?.data! as PubLeafletDocument.Record; 120 - let bRecord = b.documents 121 - ?.data! as PubLeafletDocument.Record; 122 - const aDate = aRecord.publishedAt 123 - ? new Date(aRecord.publishedAt) 124 - : new Date(0); 125 - const bDate = bRecord.publishedAt 126 - ? new Date(bRecord.publishedAt) 127 - : new Date(0); 128 - return bDate.getTime() - aDate.getTime(); // Sort by most recent first 129 - }) 130 - .map((doc) => { 131 - if (!doc.documents) return null; 132 - let uri = new AtUri(doc.documents.uri); 133 - let doc_record = doc.documents 134 - .data as PubLeafletDocument.Record; 135 - let quotes = 136 - doc.documents.document_mentions_in_bsky[0].count || 0; 137 - let comments = 138 - record?.preferences?.showComments === false 139 - ? 0 140 - : doc.documents.comments_on_documents[0].count || 0; 141 - 142 - return ( 143 - <React.Fragment key={doc.documents?.uri}> 144 - <div className="flex w-full grow flex-col "> 145 - <SpeedyLink 146 - href={`${getPublicationURL(publication)}/${uri.rkey}`} 147 - className="publishedPost hover:no-underline! flex flex-col" 148 - > 149 - <h3 className="text-primary">{doc_record.title}</h3> 150 - <p className="italic text-secondary"> 151 - {doc_record.description} 152 - </p> 153 - </SpeedyLink> 112 + </div> 113 + <div className="publicationPostList w-full flex flex-col gap-4"> 114 + {publication.documents_in_publications 115 + .filter((d) => !!d?.documents) 116 + .sort((a, b) => { 117 + let aRecord = a.documents?.data! as PubLeafletDocument.Record; 118 + let bRecord = b.documents?.data! as PubLeafletDocument.Record; 119 + const aDate = aRecord.publishedAt 120 + ? new Date(aRecord.publishedAt) 121 + : new Date(0); 122 + const bDate = bRecord.publishedAt 123 + ? new Date(bRecord.publishedAt) 124 + : new Date(0); 125 + return bDate.getTime() - aDate.getTime(); // Sort by most recent first 126 + }) 127 + .map((doc) => { 128 + if (!doc.documents) return null; 129 + let uri = new AtUri(doc.documents.uri); 130 + let doc_record = doc.documents 131 + .data as PubLeafletDocument.Record; 132 + let quotes = 133 + doc.documents.document_mentions_in_bsky[0].count || 0; 134 + let comments = 135 + record?.preferences?.showComments === false 136 + ? 0 137 + : doc.documents.comments_on_documents[0].count || 0; 154 138 155 - <div className="text-sm text-tertiary flex gap-3 items-center justify-start pt-2"> 156 - <p className="text-sm text-tertiary "> 157 - {doc_record.publishedAt && ( 158 - <LocalizedDate 159 - dateString={doc_record.publishedAt} 160 - options={{ 161 - year: "numeric", 162 - month: "long", 163 - day: "2-digit", 164 - }} 165 - /> 166 - )}{" "} 167 - </p> 139 + return ( 140 + <React.Fragment key={doc.documents?.uri}> 141 + <div className="flex w-full grow flex-col "> 142 + <SpeedyLink 143 + href={`${getPublicationURL(publication)}/${uri.rkey}`} 144 + className="publishedPost hover:no-underline! flex flex-col" 145 + > 146 + <h3 className="text-primary">{doc_record.title}</h3> 147 + <p className="italic text-secondary"> 148 + {doc_record.description} 149 + </p> 150 + </SpeedyLink> 168 151 169 - <InteractionPreview 170 - quotesCount={quotes} 171 - commentsCount={comments} 172 - tagsCount={6} 173 - showComments={record?.preferences?.showComments} 174 - postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} 175 - /> 176 - </div> 152 + <div className="text-sm text-tertiary flex gap-1 flex-wrap pt-2"> 153 + <p className="text-sm text-tertiary "> 154 + {doc_record.publishedAt && ( 155 + <LocalizedDate 156 + dateString={doc_record.publishedAt} 157 + options={{ 158 + year: "numeric", 159 + month: "long", 160 + day: "2-digit", 161 + }} 162 + /> 163 + )}{" "} 164 + </p> 165 + {comments > 0 || quotes > 0 ? "| " : ""} 166 + {quotes > 0 && ( 167 + <SpeedyLink 168 + href={`${getPublicationURL(publication)}/${uri.rkey}?interactionDrawer=quotes`} 169 + className="flex flex-row gap-0 text-sm text-tertiary items-center flex-wrap" 170 + > 171 + <QuoteTiny /> {quotes} 172 + </SpeedyLink> 173 + )} 174 + {comments > 0 && 175 + record?.preferences?.showComments !== false && ( 176 + <SpeedyLink 177 + href={`${getPublicationURL(publication)}/${uri.rkey}?interactionDrawer=comments`} 178 + className="flex flex-row gap-0 text-sm text-tertiary items-center flex-wrap" 179 + > 180 + <CommentTiny /> {comments} 181 + </SpeedyLink> 182 + )} 177 183 </div> 178 - <hr className="last:hidden border-border-light" /> 179 - </React.Fragment> 180 - ); 181 - })} 182 - </div> 184 + </div> 185 + <hr className="last:hidden border-border-light" /> 186 + </React.Fragment> 187 + ); 188 + })} 183 189 </div> 184 - </div> 190 + </PublicationHomeLayout> 185 191 </PublicationBackgroundProvider> 186 192 </PublicationThemeProvider> 187 193 );
+6 -7
app/lish/createPub/CreatePubForm.tsx
··· 127 127 onChange={(e) => setShowInDiscover(e.target.checked)} 128 128 > 129 129 <div className=" pt-0.5 flex flex-col text-sm text-tertiary "> 130 - <p className="font-bold italic"> 131 - Show In{" "} 130 + <p className="font-bold italic">Show In Discover</p> 131 + <p className="text-sm text-tertiary font-normal"> 132 + Your posts will appear on our{" "} 132 133 <a href="/discover" target="_blank"> 133 134 Discover 134 - </a> 135 - </p> 136 - <p className="text-sm text-tertiary font-normal"> 137 - You'll be able to change this later! 135 + </a>{" "} 136 + page. You can change this at any time! 138 137 </p> 139 138 </div> 140 139 </Checkbox> 141 140 <hr className="border-border-light" /> 142 141 143 - <div className="flex w-full justify-center"> 142 + <div className="flex w-full justify-end"> 144 143 <ButtonPrimary 145 144 type="submit" 146 145 disabled={
+109 -98
app/lish/createPub/UpdatePubForm.tsx
··· 20 20 import Link from "next/link"; 21 21 import { Checkbox } from "components/Checkbox"; 22 22 import type { GetDomainConfigResponseBody } from "@vercel/sdk/esm/models/getdomainconfigop"; 23 + import { PubSettingsHeader } from "../[did]/[publication]/dashboard/PublicationSettings"; 23 24 24 - export const EditPubForm = () => { 25 + export const EditPubForm = (props: { 26 + backToMenuAction: () => void; 27 + loading: boolean; 28 + setLoadingAction: (l: boolean) => void; 29 + }) => { 25 30 let { data } = usePublicationData(); 26 31 let { publication: pubData } = data || {}; 27 32 let record = pubData?.record as PubLeafletPublication.Record; ··· 57 62 58 63 return ( 59 64 <form 60 - className="flex flex-col gap-3 w-[1000px] max-w-full py-1" 61 65 onSubmit={async (e) => { 62 66 if (!pubData) return; 63 67 e.preventDefault(); 64 - setFormState("loading"); 68 + props.setLoadingAction(true); 65 69 let data = await updatePublication({ 66 70 uri: pubData.uri, 67 71 name: nameValue, ··· 73 77 }, 74 78 }); 75 79 toast({ type: "success", content: "Updated!" }); 76 - setFormState("normal"); 80 + props.setLoadingAction(false); 77 81 mutate("publication-data"); 78 82 }} 79 83 > 80 - <div className="flex items-center justify-between gap-2 "> 81 - <p className="pl-0.5 pb-0.5 text-tertiary italic text-sm font-bold"> 82 - Logo <span className="font-normal">(optional)</span> 83 - </p> 84 - <div 85 - className={`w-8 h-8 rounded-full flex items-center justify-center cursor-pointer ${iconPreview ? "border border-border-light hover:outline-border" : "border border-dotted border-accent-contrast hover:outline-accent-contrast"} selected-outline`} 86 - onClick={() => fileInputRef.current?.click()} 87 - > 88 - {iconPreview ? ( 89 - <img 90 - src={iconPreview} 91 - alt="Logo preview" 92 - className="w-full h-full rounded-full object-cover" 93 - /> 94 - ) : ( 95 - <AddTiny className="text-accent-1" /> 96 - )} 84 + <PubSettingsHeader 85 + loading={props.loading} 86 + setLoadingAction={props.setLoadingAction} 87 + backToMenuAction={props.backToMenuAction} 88 + state={"theme"} 89 + /> 90 + <div className="flex flex-col gap-3 w-[1000px] max-w-full pb-2"> 91 + <div className="flex items-center justify-between gap-2 "> 92 + <p className="pl-0.5 pb-0.5 text-tertiary italic text-sm font-bold"> 93 + Logo <span className="font-normal">(optional)</span> 94 + </p> 95 + <div 96 + className={`w-8 h-8 rounded-full flex items-center justify-center cursor-pointer ${iconPreview ? "border border-border-light hover:outline-border" : "border border-dotted border-accent-contrast hover:outline-accent-contrast"} selected-outline`} 97 + onClick={() => fileInputRef.current?.click()} 98 + > 99 + {iconPreview ? ( 100 + <img 101 + src={iconPreview} 102 + alt="Logo preview" 103 + className="w-full h-full rounded-full object-cover" 104 + /> 105 + ) : ( 106 + <AddTiny className="text-accent-1" /> 107 + )} 108 + </div> 109 + <input 110 + type="file" 111 + accept="image/*" 112 + className="hidden" 113 + ref={fileInputRef} 114 + onChange={(e) => { 115 + const file = e.target.files?.[0]; 116 + if (file) { 117 + setIconFile(file); 118 + const reader = new FileReader(); 119 + reader.onload = (e) => { 120 + setIconPreview(e.target?.result as string); 121 + }; 122 + reader.readAsDataURL(file); 123 + } 124 + }} 125 + /> 97 126 </div> 98 - <input 99 - type="file" 100 - accept="image/*" 101 - className="hidden" 102 - ref={fileInputRef} 103 - onChange={(e) => { 104 - const file = e.target.files?.[0]; 105 - if (file) { 106 - setIconFile(file); 107 - const reader = new FileReader(); 108 - reader.onload = (e) => { 109 - setIconPreview(e.target?.result as string); 110 - }; 111 - reader.readAsDataURL(file); 112 - } 113 - }} 114 - /> 115 - </div> 116 127 117 - <label> 118 - <p className="pl-0.5 pb-0.5 text-tertiary italic text-sm font-bold"> 119 - Publication Name 120 - </p> 121 - <Input 122 - className="input-with-border w-full text-primary" 123 - type="text" 124 - id="pubName" 125 - value={nameValue} 126 - onChange={(e) => { 127 - setNameValue(e.currentTarget.value); 128 - }} 129 - /> 130 - </label> 131 - <label> 132 - <p className="text-tertiary italic text-sm font-bold pl-0.5 pb-0.5"> 133 - Description <span className="font-normal">(optional)</span> 134 - </p> 135 - <Input 136 - textarea 137 - className="input-with-border w-full text-primary" 138 - rows={3} 139 - id="pubDescription" 140 - value={descriptionValue} 141 - onChange={(e) => { 142 - setDescriptionValue(e.currentTarget.value); 143 - }} 144 - /> 145 - </label> 146 - 147 - <CustomDomainForm /> 148 - <hr className="border-border-light" /> 149 - 150 - <Checkbox 151 - checked={showInDiscover} 152 - onChange={(e) => setShowInDiscover(e.target.checked)} 153 - > 154 - <div className=" pt-0.5 flex flex-col text-sm italic text-tertiary "> 155 - <p className="font-bold"> 156 - Show In{" "} 157 - <a href="/discover" target="_blank"> 158 - Discover 159 - </a> 128 + <label> 129 + <p className="pl-0.5 pb-0.5 text-tertiary italic text-sm font-bold"> 130 + Publication Name 160 131 </p> 161 - <p className="text-xs text-tertiary font-normal"> 162 - This publication will appear on our public Discover page 132 + <Input 133 + className="input-with-border w-full text-primary" 134 + type="text" 135 + id="pubName" 136 + value={nameValue} 137 + onChange={(e) => { 138 + setNameValue(e.currentTarget.value); 139 + }} 140 + /> 141 + </label> 142 + <label> 143 + <p className="text-tertiary italic text-sm font-bold pl-0.5 pb-0.5"> 144 + Description <span className="font-normal">(optional)</span> 163 145 </p> 164 - </div> 165 - </Checkbox> 146 + <Input 147 + textarea 148 + className="input-with-border w-full text-primary" 149 + rows={3} 150 + id="pubDescription" 151 + value={descriptionValue} 152 + onChange={(e) => { 153 + setDescriptionValue(e.currentTarget.value); 154 + }} 155 + /> 156 + </label> 157 + 158 + <CustomDomainForm /> 159 + <hr className="border-border-light" /> 166 160 167 - <Checkbox 168 - checked={showComments} 169 - onChange={(e) => setShowComments(e.target.checked)} 170 - > 171 - <div className=" pt-0.5 flex flex-col text-sm italic text-tertiary "> 172 - <p className="font-bold">Show comments on posts</p> 173 - </div> 174 - </Checkbox> 175 - <hr className="border-border-light" /> 161 + <Checkbox 162 + checked={showInDiscover} 163 + onChange={(e) => setShowInDiscover(e.target.checked)} 164 + > 165 + <div className=" pt-0.5 flex flex-col text-sm italic text-tertiary "> 166 + <p className="font-bold"> 167 + Show In{" "} 168 + <a href="/discover" target="_blank"> 169 + Discover 170 + </a> 171 + </p> 172 + <p className="text-xs text-tertiary font-normal"> 173 + Your posts will appear on our{" "} 174 + <a href="/discover" target="_blank"> 175 + Discover 176 + </a>{" "} 177 + page. You can change this at any time! 178 + </p> 179 + </div> 180 + </Checkbox> 176 181 177 - <ButtonPrimary className="place-self-end" type="submit"> 178 - {formState === "loading" ? <DotLoader /> : "Update!"} 179 - </ButtonPrimary> 182 + <Checkbox 183 + checked={showComments} 184 + onChange={(e) => setShowComments(e.target.checked)} 185 + > 186 + <div className=" pt-0.5 flex flex-col text-sm italic text-tertiary "> 187 + <p className="font-bold">Show comments on posts</p> 188 + </div> 189 + </Checkbox> 190 + </div> 180 191 </form> 181 192 ); 182 193 };
+1
app/lish/createPub/createPublication.ts
··· 101 101 await supabaseServerClient 102 102 .from("custom_domains") 103 103 .insert({ domain, confirmed: true, identity: null }); 104 + 104 105 await supabaseServerClient 105 106 .from("publication_domains") 106 107 .insert({ domain, publication: result.uri, identity: identity.atp_did });
+3 -11
app/lish/createPub/getPublicationURL.ts
··· 3 3 import { isProductionDomain } from "src/utils/isProductionDeployment"; 4 4 import { Json } from "supabase/database.types"; 5 5 6 - export function getPublicationURL(pub: { 7 - uri: string; 8 - name: string; 9 - record: Json; 10 - }) { 6 + export function getPublicationURL(pub: { uri: string; record: Json }) { 11 7 let record = pub.record as PubLeafletPublication.Record; 12 8 if (isProductionDomain() && record?.base_path) 13 9 return `https://${record.base_path}`; 14 10 else return getBasePublicationURL(pub); 15 11 } 16 12 17 - export function getBasePublicationURL(pub: { 18 - uri: string; 19 - name: string; 20 - record: Json; 21 - }) { 13 + export function getBasePublicationURL(pub: { uri: string; record: Json }) { 22 14 let record = pub.record as PubLeafletPublication.Record; 23 15 let aturi = new AtUri(pub.uri); 24 - return `/lish/${aturi.host}/${encodeURIComponent(aturi.rkey || record?.name || pub.name)}`; 16 + return `/lish/${aturi.host}/${encodeURIComponent(aturi.rkey || record?.name)}`; 25 17 }
+1 -1
app/lish/createPub/page.tsx
··· 26 26 <div className="createPubContent h-full flex items-center max-w-sm w-full mx-auto"> 27 27 <div className="createPubFormWrapper h-fit w-full flex flex-col gap-4"> 28 28 <h2 className="text-center">Create Your Publication!</h2> 29 - <div className="container w-full p-3"> 29 + <div className="opaque-container w-full sm:py-4 p-3"> 30 30 <CreatePubForm /> 31 31 </div> 32 32 </div>
-7
app/lish/createPub/updatePublication.ts
··· 10 10 import { supabaseServerClient } from "supabase/serverClient"; 11 11 import { Json } from "supabase/database.types"; 12 12 import { AtUri } from "@atproto/syntax"; 13 - import { redirect } from "next/navigation"; 14 13 import { $Typed } from "@atproto/api"; 15 - import { ids } from "lexicons/api/lexicons"; 16 14 17 15 export async function updatePublication({ 18 16 uri, ··· 87 85 .eq("uri", uri) 88 86 .select() 89 87 .single(); 90 - if (name !== existingPub.name) 91 - return redirect( 92 - `/lish/${aturi.host}/${encodeURIComponent(name)}/dashboard`, 93 - ); 94 - 95 88 return { success: true, publication }; 96 89 } 97 90
+21
app/lish/subscribeToPublication.ts
··· 12 12 import { encodeActionToSearchParam } from "app/api/oauth/[route]/afterSignInActions"; 13 13 import { Json } from "supabase/database.types"; 14 14 import { IdResolver } from "@atproto/identity"; 15 + import { 16 + Notification, 17 + pingIdentityToUpdateNotification, 18 + } from "src/notifications"; 19 + import { v7 } from "uuid"; 15 20 16 21 let leafletFeedURI = 17 22 "at://did:plc:btxrwcaeyodrap5mnjw2fvmz/app.bsky.feed.generator/subscribedPublications"; ··· 46 51 publication, 47 52 identity: credentialSession.did!, 48 53 }); 54 + 55 + // Create notification for the publication owner 56 + let publicationOwner = new AtUri(publication).host; 57 + if (publicationOwner !== credentialSession.did) { 58 + let notification: Notification = { 59 + id: v7(), 60 + recipient: publicationOwner, 61 + data: { 62 + type: "subscribe", 63 + subscription_uri: record.uri, 64 + }, 65 + }; 66 + await supabaseServerClient.from("notifications").insert(notification); 67 + await pingIdentityToUpdateNotification(publicationOwner); 68 + } 69 + 49 70 let bsky = new BskyAgent(credentialSession); 50 71 let [prefs, profile, resolveDid] = await Promise.all([ 51 72 bsky.app.bsky.actor.getPreferences(),
+14 -11
app/login/LoginForm.tsx
··· 13 13 import { useSmoker, useToaster } from "components/Toast"; 14 14 import React, { useState } from "react"; 15 15 import { mutate } from "swr"; 16 + import { BlueskyTiny } from "components/Icons/BlueskyTiny"; 16 17 17 18 export default function LoginForm(props: { 18 19 noEmail?: boolean; ··· 167 168 export function BlueskyLogin(props: { 168 169 redirectRoute?: string; 169 170 action?: ActionAfterSignIn; 171 + compact?: boolean; 170 172 }) { 171 173 const [signingWithHandle, setSigningWithHandle] = useState(false); 172 174 const [handle, setHandle] = useState(""); ··· 186 188 /> 187 189 )} 188 190 {signingWithHandle ? ( 189 - <div className="w-full flex flex-col gap-2"> 191 + <div className="w-full flex gap-1"> 190 192 <Input 191 193 type="text" 192 194 name="handle" ··· 197 199 onChange={(e) => setHandle(e.target.value)} 198 200 required 199 201 /> 200 - <ButtonPrimary type="submit" fullWidth className="py-2"> 201 - <BlueskySmall /> 202 - Sign In 203 - </ButtonPrimary> 202 + <ButtonPrimary type="submit">Sign In</ButtonPrimary> 204 203 </div> 205 204 ) : ( 206 - <div className="flex flex-col"> 207 - <ButtonPrimary fullWidth className="py-2"> 208 - <BlueskySmall /> 209 - Log In/Sign Up with Bluesky 205 + <div className="flex flex-col justify-center"> 206 + <ButtonPrimary 207 + fullWidth={!props.compact} 208 + compact={props.compact} 209 + className={`${props.compact ? "mx-auto text-sm" : "py-2"}`} 210 + > 211 + {props.compact ? <BlueskyTiny /> : <BlueskySmall />} 212 + {props.compact ? "Link" : "Log In/Sign Up with"} Bluesky 210 213 </ButtonPrimary> 211 214 <button 212 215 type="button" 213 - className="text-sm text-accent-contrast place-self-center mt-[6px]" 216 + className={`${props.compact ? "text-xs mt-0.5" : "text-sm mt-[6px]"} text-accent-contrast place-self-center`} 214 217 onClick={() => setSigningWithHandle(true)} 215 218 > 216 - or use an ATProto handle 219 + use an ATProto handle 217 220 </button> 218 221 </div> 219 222 )}
+20
app/p/[didOrHandle]/[rkey]/l-quote/[quote]/opengraph-image.ts
··· 1 + import { getMicroLinkOgImage } from "src/utils/getMicroLinkOgImage"; 2 + import { decodeQuotePosition } from "app/lish/[did]/[publication]/[rkey]/quotePosition"; 3 + 4 + export const runtime = "edge"; 5 + export const revalidate = 60; 6 + 7 + export default async function OpenGraphImage(props: { 8 + params: Promise<{ didOrHandle: string; rkey: string; quote: string }>; 9 + }) { 10 + let params = await props.params; 11 + let quotePosition = decodeQuotePosition(params.quote); 12 + return getMicroLinkOgImage( 13 + `/p/${decodeURIComponent(params.didOrHandle)}/${params.rkey}/l-quote/${params.quote}#${quotePosition?.pageId ? `${quotePosition.pageId}~` : ""}${quotePosition?.start.block.join(".")}_${quotePosition?.start.offset}`, 14 + { 15 + width: 620, 16 + height: 324, 17 + deviceScaleFactor: 2, 18 + }, 19 + ); 20 + }
+8
app/p/[didOrHandle]/[rkey]/l-quote/[quote]/page.tsx
··· 1 + import PostPage from "app/p/[didOrHandle]/[rkey]/page"; 2 + 3 + export { generateMetadata } from "app/p/[didOrHandle]/[rkey]/page"; 4 + export default async function Post(props: { 5 + params: Promise<{ didOrHandle: string; rkey: string }>; 6 + }) { 7 + return <PostPage {...props} />; 8 + }
+13
app/p/[didOrHandle]/[rkey]/opengraph-image.ts
··· 1 + import { getMicroLinkOgImage } from "src/utils/getMicroLinkOgImage"; 2 + 3 + export const runtime = "edge"; 4 + export const revalidate = 60; 5 + 6 + export default async function OpenGraphImage(props: { 7 + params: Promise<{ rkey: string; didOrHandle: string }>; 8 + }) { 9 + let params = await props.params; 10 + return getMicroLinkOgImage( 11 + `/p/${params.didOrHandle}/${params.rkey}/`, 12 + ); 13 + }
+90
app/p/[didOrHandle]/[rkey]/page.tsx
··· 1 + import { supabaseServerClient } from "supabase/serverClient"; 2 + import { AtUri } from "@atproto/syntax"; 3 + import { ids } from "lexicons/api/lexicons"; 4 + import { PubLeafletDocument } from "lexicons/api"; 5 + import { Metadata } from "next"; 6 + import { idResolver } from "app/(home-pages)/reader/idResolver"; 7 + import { DocumentPageRenderer } from "app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer"; 8 + 9 + export async function generateMetadata(props: { 10 + params: Promise<{ didOrHandle: string; rkey: string }>; 11 + }): Promise<Metadata> { 12 + let params = await props.params; 13 + let didOrHandle = decodeURIComponent(params.didOrHandle); 14 + 15 + // Resolve handle to DID if necessary 16 + let did = didOrHandle; 17 + if (!didOrHandle.startsWith("did:")) { 18 + try { 19 + let resolved = await idResolver.handle.resolve(didOrHandle); 20 + if (resolved) did = resolved; 21 + } catch (e) { 22 + return { title: "404" }; 23 + } 24 + } 25 + 26 + let { data: document } = await supabaseServerClient 27 + .from("documents") 28 + .select("*, documents_in_publications(publications(*))") 29 + .eq("uri", AtUri.make(did, ids.PubLeafletDocument, params.rkey)) 30 + .single(); 31 + 32 + if (!document) return { title: "404" }; 33 + 34 + let docRecord = document.data as PubLeafletDocument.Record; 35 + 36 + // For documents in publications, include publication name 37 + let publicationName = document.documents_in_publications[0]?.publications?.name; 38 + 39 + return { 40 + icons: { 41 + other: { 42 + rel: "alternate", 43 + url: document.uri, 44 + }, 45 + }, 46 + title: publicationName 47 + ? `${docRecord.title} - ${publicationName}` 48 + : docRecord.title, 49 + description: docRecord?.description || "", 50 + }; 51 + } 52 + 53 + export default async function StandaloneDocumentPage(props: { 54 + params: Promise<{ didOrHandle: string; rkey: string }>; 55 + }) { 56 + let params = await props.params; 57 + let didOrHandle = decodeURIComponent(params.didOrHandle); 58 + 59 + // Resolve handle to DID if necessary 60 + let did = didOrHandle; 61 + if (!didOrHandle.startsWith("did:")) { 62 + try { 63 + let resolved = await idResolver.handle.resolve(didOrHandle); 64 + if (!resolved) { 65 + return ( 66 + <div className="p-4 text-lg text-center flex flex-col gap-4"> 67 + <p>Sorry, can&apos;t resolve handle.</p> 68 + <p> 69 + This may be a glitch on our end. If the issue persists please{" "} 70 + <a href="mailto:contact@leaflet.pub">send us a note</a>. 71 + </p> 72 + </div> 73 + ); 74 + } 75 + did = resolved; 76 + } catch (e) { 77 + return ( 78 + <div className="p-4 text-lg text-center flex flex-col gap-4"> 79 + <p>Sorry, can&apos;t resolve handle.</p> 80 + <p> 81 + This may be a glitch on our end. If the issue persists please{" "} 82 + <a href="mailto:contact@leaflet.pub">send us a note</a>. 83 + </p> 84 + </div> 85 + ); 86 + } 87 + } 88 + 89 + return <DocumentPageRenderer did={did} rkey={params.rkey} />; 90 + }
-159
app/templates/TemplateList.tsx
··· 1 - "use client"; 2 - 3 - import { ButtonPrimary } from "components/Buttons"; 4 - import Image from "next/image"; 5 - import Link from "next/link"; 6 - import { createNewLeafletFromTemplate } from "actions/createNewLeafletFromTemplate"; 7 - import { AddTiny } from "components/Icons/AddTiny"; 8 - 9 - export function LeafletTemplate(props: { 10 - title: string; 11 - description?: string; 12 - image: string; 13 - alt: string; 14 - templateID: string; // readonly id for the leaflet that will be duplicated 15 - }) { 16 - return ( 17 - <div className="flex flex-col gap-4"> 18 - <div className="flex flex-col gap-2"> 19 - <div className="max-w-[274px] h-[154px] relative"> 20 - <Image 21 - className="absolute top-0 left-0 rounded-md w-full h-full object-cover" 22 - src={props.image} 23 - alt={props.alt} 24 - width={274} 25 - height={154} 26 - /> 27 - </div> 28 - </div> 29 - <div className={`flex flex-col ${props.description ? "gap-4" : "gap-2"}`}> 30 - <div className="gap-0"> 31 - <h3 className="font-bold text-center text-secondary"> 32 - {props.title} 33 - </h3> 34 - {props.description && ( 35 - <div className="text-tertiary text-sm font-normal text-center"> 36 - {props.description} 37 - </div> 38 - )} 39 - </div> 40 - <div className="flex sm:flex-row flex-col gap-2 justify-center items-center bottom-4"> 41 - <Link 42 - href={`https://leaflet.pub/` + props.templateID} 43 - target="_blank" 44 - className="no-underline hover:no-underline" 45 - > 46 - <ButtonPrimary className="bg-primary hover:outline-hidden! hover:scale-105 hover:rotate-3 transition-all"> 47 - Preview 48 - </ButtonPrimary> 49 - </Link> 50 - <ButtonPrimary 51 - className=" hover:outline-hidden! hover:scale-105 hover:-rotate-2 transition-all" 52 - onClick={async () => { 53 - let id = await createNewLeafletFromTemplate( 54 - props.templateID, 55 - false, 56 - ); 57 - window.open(`/${id}`, "_blank"); 58 - }} 59 - > 60 - Create 61 - <AddTiny /> 62 - </ButtonPrimary> 63 - </div> 64 - </div> 65 - </div> 66 - ); 67 - } 68 - 69 - export function TemplateList(props: { 70 - name: string; 71 - description?: string; 72 - children: React.ReactNode; 73 - }) { 74 - return ( 75 - <div className="templateLeafletGrid flex flex-col gap-6"> 76 - <div className="flex flex-col gap-0 text-center"> 77 - <h3 className="text-[24px]">{props.name}</h3> 78 - <p className="text-secondary">{props.description}</p> 79 - </div> 80 - <div className="grid auto-rows-max md:grid-cols-4 sm:grid-cols-3 grid-cols-2 gap-y-8 gap-x-6 sm:gap-6 grow pb-8"> 81 - {props.children} 82 - </div> 83 - </div> 84 - ); 85 - } 86 - 87 - export function TemplateListThemes() { 88 - return ( 89 - <> 90 - <TemplateList 91 - name="Themes" 92 - description="A small sampling of Leaflet's infinite theme possibilities!" 93 - > 94 - <LeafletTemplate 95 - title="Foliage" 96 - image="/templates/template-foliage-548x308.jpg" 97 - alt="preview image of Foliage theme, with lots of green and leafy bg" 98 - templateID="e4323c1d-15c1-407d-afaf-e5d772a35f0e" 99 - /> 100 - <LeafletTemplate 101 - title="Lunar" 102 - image="/templates/template-lunar-548x308.jpg" 103 - alt="preview image of Lunar theme, with dark grey, red, and moon bg" 104 - templateID="219d14ab-096c-4b48-83ee-36446e335c3e" 105 - /> 106 - <LeafletTemplate 107 - title="Paper" 108 - image="/templates/template-paper-548x308.jpg" 109 - alt="preview image of Paper theme, with red, gold, green and marbled paper bg" 110 - templateID="9b28ceea-0220-42ac-87e6-3976d156f653" 111 - /> 112 - <LeafletTemplate 113 - title="Oceanic" 114 - image="/templates/template-oceanic-548x308.jpg" 115 - alt="preview image of Oceanic theme, with dark and light blue and ocean bg" 116 - templateID="a65a56d7-713d-437e-9c42-f18bdc6fe2a7" 117 - /> 118 - </TemplateList> 119 - </> 120 - ); 121 - } 122 - 123 - export function TemplateListExamples() { 124 - return ( 125 - <TemplateList 126 - name="Examples" 127 - description="Creative documents you can make and share with Leaflet" 128 - > 129 - <LeafletTemplate 130 - title="Reading List" 131 - description="Make a list for your own reading, or share recs with friends!" 132 - image="/templates/template-reading-548x308.jpg" 133 - alt="preview image of Reading List template, with a few sections and example books as sub-pages" 134 - templateID="a5655b68-fe7a-4494-bda6-c9847523b2f6" 135 - /> 136 - <LeafletTemplate 137 - title="Travel Plan" 138 - description="Organize a trip — notes, logistics, itinerary, even a shared scrapbook" 139 - image="/templates/template-travel-548x308.jpg" 140 - alt="preview image of a Travel Plan template, with pages for itinerary, logistics, research, and a travel diary canvas" 141 - templateID="4d6f1392-dfd3-4015-925d-df55b7da5566" 142 - /> 143 - <LeafletTemplate 144 - title="Gift Guide" 145 - description="Share your favorite things — products, restaurants, movies…" 146 - image="/templates/template-gift-548x308.jpg" 147 - alt="preview image for a Gift Guide template, with three blank canvases for different categories" 148 - templateID="de73df29-35d9-4a43-a441-7ce45ad3b498" 149 - /> 150 - <LeafletTemplate 151 - title="Event Page" 152 - description="Host an event — from a single meetup, to a whole conference!" 153 - image="/templates/template-event-548x308.jpg" 154 - alt="preview image for an Event Page template, with an event info section and linked pages / canvases for more info" 155 - templateID="23d8a4ec-b2f6-438a-933d-726d2188974d" 156 - /> 157 - </TemplateList> 158 - ); 159 - }
-108
app/templates/icon.tsx
··· 1 - // NOTE: duplicated from home/icon.tsx 2 - // we could make it different so it's clear it's not your personal colors? 3 - 4 - import { ImageResponse } from "next/og"; 5 - import type { Fact } from "src/replicache"; 6 - import type { Attribute } from "src/replicache/attributes"; 7 - import { Database } from "../../supabase/database.types"; 8 - import { createServerClient } from "@supabase/ssr"; 9 - import { parseHSBToRGB } from "src/utils/parseHSB"; 10 - import { cookies } from "next/headers"; 11 - 12 - // Route segment config 13 - export const revalidate = 0; 14 - export const preferredRegion = ["sfo1"]; 15 - export const dynamic = "force-dynamic"; 16 - export const fetchCache = "force-no-store"; 17 - 18 - // Image metadata 19 - export const size = { 20 - width: 32, 21 - height: 32, 22 - }; 23 - export const contentType = "image/png"; 24 - 25 - // Image generation 26 - let supabase = createServerClient<Database>( 27 - process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, 28 - process.env.SUPABASE_SERVICE_ROLE_KEY as string, 29 - { cookies: {} }, 30 - ); 31 - export default async function Icon() { 32 - let cookieStore = await cookies(); 33 - let identity = cookieStore.get("identity"); 34 - let rootEntity: string | null = null; 35 - if (identity) { 36 - let res = await supabase 37 - .from("identities") 38 - .select( 39 - `*, 40 - permission_tokens!identities_home_page_fkey(*, permission_token_rights(*)), 41 - permission_token_on_homepage( 42 - *, permission_tokens(*, permission_token_rights(*)) 43 - ) 44 - `, 45 - ) 46 - .eq("id", identity?.value) 47 - .single(); 48 - rootEntity = res.data?.permission_tokens?.root_entity || null; 49 - } 50 - let outlineColor, fillColor; 51 - if (rootEntity) { 52 - let { data } = await supabase.rpc("get_facts", { 53 - root: rootEntity, 54 - }); 55 - let initialFacts = (data as unknown as Fact<Attribute>[]) || []; 56 - let themePageBG = initialFacts.find( 57 - (f) => f.attribute === "theme/card-background", 58 - ) as Fact<"theme/card-background"> | undefined; 59 - 60 - let themePrimary = initialFacts.find( 61 - (f) => f.attribute === "theme/primary", 62 - ) as Fact<"theme/primary"> | undefined; 63 - 64 - outlineColor = parseHSBToRGB(`hsba(${themePageBG?.data.value})`); 65 - 66 - fillColor = parseHSBToRGB(`hsba(${themePrimary?.data.value})`); 67 - } 68 - 69 - return new ImageResponse( 70 - ( 71 - // ImageResponse JSX element 72 - <div style={{ display: "flex" }}> 73 - <svg 74 - width="32" 75 - height="32" 76 - viewBox="0 0 32 32" 77 - fill="none" 78 - xmlns="http://www.w3.org/2000/svg" 79 - > 80 - {/* outline */} 81 - <path 82 - fillRule="evenodd" 83 - clipRule="evenodd" 84 - d="M3.09628 21.8809C2.1044 23.5376 1.19806 25.3395 0.412496 27.2953C-0.200813 28.8223 0.539843 30.5573 2.06678 31.1706C3.59372 31.7839 5.32873 31.0433 5.94204 29.5163C6.09732 29.1297 6.24696 28.7489 6.39151 28.3811L6.39286 28.3777C6.94334 26.9769 7.41811 25.7783 7.99246 24.6987C8.63933 24.6636 9.37895 24.6582 10.2129 24.6535L10.3177 24.653C11.8387 24.6446 13.6711 24.6345 15.2513 24.3147C16.8324 23.9947 18.789 23.2382 19.654 21.2118C19.8881 20.6633 20.1256 19.8536 19.9176 19.0311C19.98 19.0311 20.044 19.031 20.1096 19.031C20.1447 19.031 20.1805 19.0311 20.2169 19.0311C21.0513 19.0316 22.2255 19.0324 23.2752 18.7469C24.5 18.4137 25.7878 17.6248 26.3528 15.9629C26.557 15.3624 26.5948 14.7318 26.4186 14.1358C26.4726 14.1262 26.528 14.1165 26.5848 14.1065C26.6121 14.1018 26.6398 14.0969 26.6679 14.092C27.3851 13.9667 28.3451 13.7989 29.1653 13.4921C29.963 13.1936 31.274 12.5268 31.6667 10.9987C31.8906 10.1277 31.8672 9.20568 31.3642 8.37294C31.1551 8.02669 30.889 7.75407 30.653 7.55302C30.8728 7.27791 31.1524 6.89517 31.345 6.47292C31.6791 5.74032 31.8513 4.66394 31.1679 3.61078C30.3923 2.4155 29.0623 2.2067 28.4044 2.1526C27.7203 2.09635 26.9849 2.15644 26.4564 2.2042C26.3846 2.02839 26.2858 1.84351 26.1492 1.66106C25.4155 0.681263 24.2775 0.598914 23.6369 0.61614C22.3428 0.650943 21.3306 1.22518 20.5989 1.82076C20.2149 2.13334 19.8688 2.48545 19.5698 2.81786C18.977 2.20421 18.1625 1.90193 17.3552 1.77751C15.7877 1.53594 14.5082 2.58853 13.6056 3.74374C12.4805 5.18375 11.7295 6.8566 10.7361 8.38059C10.3814 8.14984 9.83685 7.89945 9.16529 7.93065C8.05881 7.98204 7.26987 8.73225 6.79424 9.24551C5.96656 10.1387 5.46273 11.5208 5.10424 12.7289C4.71615 14.0368 4.38077 15.5845 4.06569 17.1171C3.87054 18.0664 3.82742 18.5183 4.01638 20.2489C3.43705 21.1826 3.54993 21.0505 3.09628 21.8809Z" 85 - fill={outlineColor ? outlineColor : "#FFFFFF"} 86 - /> 87 - 88 - {/* fill */} 89 - <path 90 - fillRule="evenodd" 91 - clipRule="evenodd" 92 - d="M9.86889 10.2435C10.1927 10.528 10.5723 10.8615 11.3911 10.5766C11.9265 10.3903 12.6184 9.17682 13.3904 7.82283C14.5188 5.84367 15.8184 3.56431 17.0505 3.7542C18.5368 3.98325 18.4453 4.80602 18.3749 5.43886C18.3255 5.88274 18.2866 6.23317 18.8098 6.21972C19.3427 6.20601 19.8613 5.57971 20.4632 4.8529C21.2945 3.84896 22.2847 2.65325 23.6906 2.61544C24.6819 2.58879 24.6663 3.01595 24.6504 3.44913C24.6403 3.72602 24.63 4.00537 24.8826 4.17024C25.1314 4.33266 25.7571 4.2759 26.4763 4.21065C27.6294 4.10605 29.023 3.97963 29.4902 4.6995C29.9008 5.33235 29.3776 5.96135 28.8762 6.56423C28.4514 7.07488 28.0422 7.56679 28.2293 8.02646C28.3819 8.40149 28.6952 8.61278 29.0024 8.81991C29.5047 9.15866 29.9905 9.48627 29.7297 10.5009C29.4539 11.5737 27.7949 11.8642 26.2398 12.1366C24.937 12.3647 23.7072 12.5801 23.4247 13.2319C23.2475 13.6407 23.5414 13.8311 23.8707 14.0444C24.2642 14.2992 24.7082 14.5869 24.4592 15.3191C23.8772 17.031 21.9336 17.031 20.1095 17.0311C18.5438 17.0311 17.0661 17.0311 16.6131 18.1137C16.3515 18.7387 16.7474 18.849 17.1818 18.9701C17.7135 19.1183 18.3029 19.2826 17.8145 20.4267C16.8799 22.6161 13.3934 22.6357 10.2017 22.6536C9.03136 22.6602 7.90071 22.6665 6.95003 22.7795C6.84152 22.7924 6.74527 22.8547 6.6884 22.948C5.81361 24.3834 5.19318 25.9622 4.53139 27.6462C4.38601 28.0162 4.23862 28.3912 4.08611 28.7709C3.88449 29.2729 3.31413 29.5163 2.81217 29.3147C2.31021 29.1131 2.06673 28.5427 2.26834 28.0408C3.01927 26.1712 3.88558 24.452 4.83285 22.8739C6.37878 20.027 9.42621 16.5342 12.6488 13.9103C15.5162 11.523 18.2544 9.73614 21.4413 8.38026C21.8402 8.21054 21.7218 7.74402 21.3053 7.86437C18.4789 8.68119 15.9802 10.3013 13.3904 11.9341C10.5735 13.71 8.21288 16.1115 6.76027 17.8575C6.50414 18.1653 5.94404 17.9122 6.02468 17.5199C6.65556 14.4512 7.30668 11.6349 8.26116 10.605C9.16734 9.62708 9.47742 9.8995 9.86889 10.2435Z" 93 - fill={fillColor ? fillColor : "#272727"} 94 - /> 95 - </svg> 96 - </div> 97 - ), 98 - // ImageResponse options 99 - { 100 - // For convenience, we can re-use the exported icons size metadata 101 - // config to also set the ImageResponse's width and height. 102 - ...size, 103 - headers: { 104 - "Cache-Control": "no-cache", 105 - }, 106 - }, 107 - ); 108 - }
-29
app/templates/page.tsx
··· 1 - import Link from "next/link"; 2 - import { TemplateListExamples, TemplateListThemes } from "./TemplateList"; 3 - import { ActionButton } from "components/ActionBar/ActionButton"; 4 - import { HomeSmall } from "components/Icons/HomeSmall"; 5 - 6 - export const metadata = { 7 - title: "Leaflet Templates", 8 - description: "example themes and documents you can use!", 9 - }; 10 - 11 - export default function Templates() { 12 - return ( 13 - <div className="flex h-full bg-bg-leaflet"> 14 - <div className="home relative max-w-(--breakpoint-lg) w-full h-full mx-auto flex sm:flex-row flex-col-reverse px-4 sm:px-6 "> 15 - <div className="homeOptions z-10 shrink-0 sm:static absolute bottom-0 place-self-end sm:place-self-start flex sm:flex-col flex-row-reverse gap-2 sm:w-fit w-full items-center pb-2 pt-1 sm:pt-7"> 16 - {/* NOT using <HomeButton /> b/c it does a permission check we don't need */} 17 - <Link href="/home"> 18 - <ActionButton icon={<HomeSmall />} label="Go Home" /> 19 - </Link> 20 - </div> 21 - <div className="flex flex-col gap-10 py-6 pt-3 sm:pt-6 sm:pb-12 sm:pl-6 grow w-full h-full overflow-y-scroll no-scrollbar"> 22 - <h1 className="text-center">Template Library</h1> 23 - <TemplateListThemes /> 24 - <TemplateListExamples /> 25 - </div> 26 - </div> 27 - </div> 28 - ); 29 - }
+20 -17
appview/index.ts
··· 104 104 data: record.value as Json, 105 105 }); 106 106 if (docResult.error) console.log(docResult.error); 107 - let publicationURI = new AtUri(record.value.publication); 107 + if (record.value.publication) { 108 + let publicationURI = new AtUri(record.value.publication); 109 + 110 + if (publicationURI.host !== evt.uri.host) { 111 + console.log("Unauthorized to create post!"); 112 + return; 113 + } 114 + let docInPublicationResult = await supabase 115 + .from("documents_in_publications") 116 + .upsert({ 117 + publication: record.value.publication, 118 + document: evt.uri.toString(), 119 + }); 120 + await supabase 121 + .from("documents_in_publications") 122 + .delete() 123 + .neq("publication", record.value.publication) 124 + .eq("document", evt.uri.toString()); 108 125 109 - if (publicationURI.host !== evt.uri.host) { 110 - console.log("Unauthorized to create post!"); 111 - return; 126 + if (docInPublicationResult.error) 127 + console.log(docInPublicationResult.error); 112 128 } 113 - let docInPublicationResult = await supabase 114 - .from("documents_in_publications") 115 - .upsert({ 116 - publication: record.value.publication, 117 - document: evt.uri.toString(), 118 - }); 119 - await supabase 120 - .from("documents_in_publications") 121 - .delete() 122 - .neq("publication", record.value.publication) 123 - .eq("document", evt.uri.toString()); 124 - if (docInPublicationResult.error) 125 - console.log(docInPublicationResult.error); 126 129 } 127 130 if (evt.event === "delete") { 128 131 await supabase.from("documents").delete().eq("uri", evt.uri.toString());
+28 -10
components/ActionBar/ActionButton.tsx
··· 8 8 type ButtonProps = Omit<JSX.IntrinsicElements["button"], "content">; 9 9 10 10 export const ActionButton = ( 11 - props: ButtonProps & { 11 + _props: ButtonProps & { 12 12 id?: string; 13 13 icon: React.ReactNode; 14 14 label: React.ReactNode; ··· 17 17 nav?: boolean; 18 18 className?: string; 19 19 subtext?: string; 20 + labelOnMobile?: boolean; 21 + z?: boolean; 20 22 }, 21 23 ) => { 22 - let { id, icon, label, primary, secondary, nav, ...buttonProps } = props; 24 + let { 25 + id, 26 + icon, 27 + label, 28 + primary, 29 + secondary, 30 + nav, 31 + labelOnMobile, 32 + subtext, 33 + className, 34 + ...buttonProps 35 + } = _props; 23 36 let sidebar = useContext(SidebarContext); 24 37 let inOpenPopover = useContext(PopoverOpenContext); 25 38 useEffect(() => { ··· 30 43 }; 31 44 } 32 45 }, [sidebar, inOpenPopover]); 46 + 47 + let showLabelOnMobile = 48 + labelOnMobile !== false && (primary || secondary || nav); 49 + 33 50 return ( 34 51 <button 35 52 {...buttonProps} ··· 38 55 rounded-md border 39 56 flex gap-2 items-start sm:justify-start justify-center 40 57 p-1 sm:mx-0 58 + ${showLabelOnMobile && !secondary ? "w-full" : "sm:w-full w-max"} 41 59 ${ 42 60 primary 43 - ? "w-full bg-accent-1 border-accent-1 text-accent-2 transparent-outline sm:hover:outline-accent-contrast focus:outline-accent-1 outline-offset-1 mx-1 first:ml-0" 61 + ? "bg-accent-1 border-accent-1 text-accent-2 transparent-outline sm:hover:outline-accent-contrast focus:outline-accent-1 outline-offset-1 mx-1 first:ml-0" 44 62 : secondary 45 - ? "sm:w-full w-max bg-bg-page border-accent-contrast text-accent-contrast transparent-outline focus:outline-accent-contrast sm:hover:outline-accent-contrast outline-offset-1 mx-1 first:ml-0" 63 + ? " bg-bg-page border-accent-contrast text-accent-contrast transparent-outline focus:outline-accent-contrast sm:hover:outline-accent-contrast outline-offset-1 mx-1 first:ml-0" 46 64 : nav 47 - ? "w-full border-transparent text-secondary sm:hover:border-border justify-start!" 48 - : "sm:w-full border-transparent text-accent-contrast sm:hover:border-accent-contrast" 65 + ? "border-transparent text-secondary sm:hover:border-border justify-start!" 66 + : "border-transparent text-accent-contrast sm:hover:border-accent-contrast" 49 67 } 50 - ${props.className} 68 + ${className} 51 69 `} 52 70 > 53 71 <div className="shrink-0">{icon}</div> 54 72 <div 55 - className={`flex flex-col pr-1 leading-snug max-w-full min-w-0 ${sidebar.open ? "block" : primary || secondary || nav ? "sm:hidden block" : "hidden"}`} 73 + className={`flex flex-col pr-1 leading-snug max-w-full min-w-0 ${sidebar.open ? "block" : showLabelOnMobile ? "sm:hidden block" : "hidden"}`} 56 74 > 57 75 <div className="truncate text-left pt-[1px]">{label}</div> 58 - {props.subtext && ( 76 + {subtext && ( 59 77 <div className="text-xs text-tertiary font-normal text-left"> 60 - {props.subtext} 78 + {subtext} 61 79 </div> 62 80 )} 63 81 </div>
+81 -48
components/ActionBar/Navigation.tsx
··· 11 11 ReaderReadSmall, 12 12 ReaderUnreadSmall, 13 13 } from "components/Icons/ReaderSmall"; 14 + import { 15 + NotificationsReadSmall, 16 + NotificationsUnreadSmall, 17 + } from "components/Icons/NotificationSmall"; 18 + import { SpeedyLink } from "components/SpeedyLink"; 19 + import { Separator } from "components/Layout"; 14 20 15 - export type navPages = "home" | "reader" | "pub" | "discover" | "tag"; 21 + export type navPages = 22 + | "home" 23 + | "reader" 24 + | "pub" 25 + | "discover" 26 + | "notifications" 27 + | "looseleafs" 28 + | "tag"; 16 29 17 30 export const DesktopNavigation = (props: { 18 31 currentPage: navPages; 19 32 publication?: string; 20 33 }) => { 34 + let { identity } = useIdentityData(); 21 35 return ( 22 - <div className="flex flex-col gap-4"> 36 + <div className="flex flex-col gap-3"> 23 37 <Sidebar alwaysOpen> 24 38 <NavigationOptions 25 39 currentPage={props.currentPage} 26 40 publication={props.publication} 27 41 /> 28 42 </Sidebar> 29 - {/*<Sidebar alwaysOpen> 30 - <ActionButton 31 - icon={ 32 - unreadNotifications ? ( 33 - <NotificationsUnreadSmall /> 34 - ) : ( 35 - <NotificationsReadSmall /> 36 - ) 37 - } 38 - label="Notifications" 39 - /> 40 - </Sidebar>*/} 43 + {identity?.atp_did && ( 44 + <Sidebar alwaysOpen> 45 + <NotificationButton current={props.currentPage === "notifications"} /> 46 + </Sidebar> 47 + )} 41 48 </div> 42 49 ); 43 50 }; ··· 47 54 publication?: string; 48 55 }) => { 49 56 let { identity } = useIdentityData(); 50 - let thisPublication = identity?.publications?.find( 51 - (pub) => pub.uri === props.publication, 52 - ); 57 + 53 58 return ( 54 - <Popover 55 - onOpenAutoFocus={(e) => e.preventDefault()} 56 - asChild 57 - className="px-2! !max-w-[256px]" 58 - trigger={ 59 - <div className="shrink-0 p-1 pr-2 text-accent-contrast h-full flex gap-2 font-bold items-center"> 60 - <MenuSmall /> 61 - <div className="truncate max-w-[72px]"> 62 - {props.currentPage === "home" ? ( 63 - <>Home</> 64 - ) : props.currentPage === "reader" ? ( 65 - <>Reader</> 66 - ) : props.currentPage === "discover" ? ( 67 - <>Discover</> 68 - ) : props.currentPage === "pub" ? ( 69 - thisPublication && <>{thisPublication.name}</> 70 - ) : null} 59 + <div className="flex gap-1 "> 60 + <Popover 61 + onOpenAutoFocus={(e) => e.preventDefault()} 62 + asChild 63 + className="px-2! !max-w-[256px]" 64 + trigger={ 65 + <div className="shrink-0 p-1 text-accent-contrast h-full flex gap-2 font-bold items-center"> 66 + <MenuSmall /> 71 67 </div> 72 - </div> 73 - } 74 - > 75 - <NavigationOptions 76 - currentPage={props.currentPage} 77 - publication={props.publication} 78 - /> 79 - </Popover> 68 + } 69 + > 70 + <NavigationOptions 71 + currentPage={props.currentPage} 72 + publication={props.publication} 73 + isMobile 74 + /> 75 + </Popover> 76 + {identity?.atp_did && ( 77 + <> 78 + <Separator /> 79 + <NotificationButton /> 80 + </> 81 + )} 82 + </div> 80 83 ); 81 84 }; 82 85 83 86 const NavigationOptions = (props: { 84 87 currentPage: navPages; 85 88 publication?: string; 89 + isMobile?: boolean; 86 90 }) => { 87 91 let { identity } = useIdentityData(); 88 92 let thisPublication = identity?.publications?.find( ··· 93 97 <HomeButton current={props.currentPage === "home"} /> 94 98 <ReaderButton 95 99 current={props.currentPage === "reader"} 96 - subs={identity?.publication_subscriptions?.length !== 0} 100 + subs={ 101 + identity?.publication_subscriptions?.length !== 0 && 102 + identity?.publication_subscriptions?.length !== undefined 103 + } 97 104 /> 98 105 <DiscoverButton current={props.currentPage === "discover"} /> 99 106 100 107 <hr className="border-border-light my-1" /> 101 - <PublicationButtons currentPubUri={thisPublication?.uri} /> 108 + <PublicationButtons 109 + currentPage={props.currentPage} 110 + currentPubUri={thisPublication?.uri} 111 + /> 102 112 </> 103 113 ); 104 114 }; 105 115 106 116 const HomeButton = (props: { current?: boolean }) => { 107 117 return ( 108 - <Link href={"/home"} className="hover:!no-underline"> 118 + <SpeedyLink href={"/home"} className="hover:!no-underline"> 109 119 <ActionButton 110 120 nav 111 121 icon={<HomeSmall />} 112 122 label="Home" 113 123 className={props.current ? "bg-bg-page! border-border-light!" : ""} 114 124 /> 115 - </Link> 125 + </SpeedyLink> 116 126 ); 117 127 }; 118 128 119 129 const ReaderButton = (props: { current?: boolean; subs: boolean }) => { 120 130 if (!props.subs) return; 121 131 return ( 122 - <Link href={"/reader"} className="hover:no-underline!"> 132 + <SpeedyLink href={"/reader"} className="hover:no-underline!"> 123 133 <ActionButton 124 134 nav 125 135 icon={<ReaderUnreadSmall />} 126 136 label="Reader" 127 137 className={props.current ? "bg-bg-page! border-border-light!" : ""} 128 138 /> 129 - </Link> 139 + </SpeedyLink> 130 140 ); 131 141 }; 132 142 ··· 143 153 </Link> 144 154 ); 145 155 }; 156 + 157 + export function NotificationButton(props: { current?: boolean }) { 158 + let { identity } = useIdentityData(); 159 + let unreads = identity?.notifications[0]?.count; 160 + 161 + return ( 162 + <SpeedyLink href={"/notifications"} className="hover:no-underline!"> 163 + <ActionButton 164 + nav 165 + labelOnMobile={false} 166 + icon={ 167 + unreads ? ( 168 + <NotificationsUnreadSmall className="text-accent-contrast" /> 169 + ) : ( 170 + <NotificationsReadSmall /> 171 + ) 172 + } 173 + label="Notifications" 174 + className={`${props.current ? "bg-bg-page! border-border-light!" : ""} ${unreads ? "text-accent-contrast!" : ""}`} 175 + /> 176 + </SpeedyLink> 177 + ); 178 + }
+115 -25
components/ActionBar/Publications.tsx
··· 10 10 import { ActionButton } from "./ActionButton"; 11 11 import { SpeedyLink } from "components/SpeedyLink"; 12 12 import { PublishSmall } from "components/Icons/PublishSmall"; 13 + import { Popover } from "components/Popover"; 14 + import { BlueskyLogin } from "app/login/LoginForm"; 15 + import { ButtonSecondary } from "components/Buttons"; 16 + import { useIsMobile } from "src/hooks/isMobile"; 17 + import { useState } from "react"; 18 + import { LooseLeafSmall } from "components/Icons/LooseleafSmall"; 19 + import { navPages } from "./Navigation"; 13 20 14 21 export const PublicationButtons = (props: { 22 + currentPage: navPages; 15 23 currentPubUri: string | undefined; 16 24 }) => { 17 25 let { identity } = useIdentityData(); 26 + let looseleaves = identity?.permission_token_on_homepage.find( 27 + (f) => f.permission_tokens.leaflets_to_documents, 28 + ); 18 29 19 30 // don't show pub list button if not logged in or no pub list 20 31 // we show a "start a pub" banner instead 21 - if (!identity || !identity.atp_did) return <PubListEmpty />; 32 + if (!identity || !identity.atp_did || identity.publications.length === 0) 33 + return <PubListEmpty />; 34 + 22 35 return ( 23 36 <div className="pubListWrapper w-full flex flex-col gap-1 sm:bg-transparent sm:border-0"> 24 - {identity.publications?.map((d) => { 25 - // console.log("thisURI : " + d.uri); 26 - // console.log("currentURI : " + props.currentPubUri); 37 + {looseleaves && ( 38 + <> 39 + <SpeedyLink 40 + href={`/looseleafs`} 41 + className="flex gap-2 items-start text-secondary font-bold hover:no-underline! hover:text-accent-contrast w-full" 42 + > 43 + {/*TODO How should i get if this is the current page or not? 44 + theres not "pub" to check the uri for. Do i need to add it as an option to NavPages? thats kinda annoying*/} 45 + <ActionButton 46 + label="Looseleafs" 47 + icon={<LooseLeafSmall />} 48 + nav 49 + className={ 50 + props.currentPage === "looseleafs" 51 + ? "bg-bg-page! border-border!" 52 + : "" 53 + } 54 + /> 55 + </SpeedyLink> 56 + <hr className="border-border-light border-dashed mx-1" /> 57 + </> 58 + )} 27 59 60 + {identity.publications?.map((d) => { 28 61 return ( 29 62 <PublicationOption 30 63 {...d} 31 64 key={d.uri} 32 65 record={d.record} 33 - asActionButton 34 66 current={d.uri === props.currentPubUri} 35 67 /> 36 68 ); ··· 49 81 uri: string; 50 82 name: string; 51 83 record: Json; 52 - asActionButton?: boolean; 53 84 current?: boolean; 54 85 }) => { 55 86 let record = props.record as PubLeafletPublication.Record | null; ··· 60 91 href={`${getBasePublicationURL(props)}/dashboard`} 61 92 className="flex gap-2 items-start text-secondary font-bold hover:no-underline! hover:text-accent-contrast w-full" 62 93 > 63 - {props.asActionButton ? ( 64 - <ActionButton 65 - label={record.name} 66 - icon={<PubIcon record={record} uri={props.uri} />} 67 - nav 68 - className={props.current ? "bg-bg-page! border-border!" : ""} 69 - /> 70 - ) : ( 71 - <> 72 - <PubIcon record={record} uri={props.uri} /> 73 - <div className="truncate">{record.name}</div> 74 - </> 75 - )} 94 + <ActionButton 95 + label={record.name} 96 + icon={<PubIcon record={record} uri={props.uri} />} 97 + nav 98 + className={props.current ? "bg-bg-page! border-border!" : ""} 99 + /> 76 100 </SpeedyLink> 77 101 ); 78 102 }; 79 103 80 104 const PubListEmpty = () => { 81 - return ( 82 - <SpeedyLink href={`lish/createPub`} className=" hover:no-underline!"> 105 + let isMobile = useIsMobile(); 106 + 107 + let [state, setState] = useState<"default" | "info">("default"); 108 + if (isMobile && state == "default") 109 + return ( 83 110 <ActionButton 84 111 label="Publish" 85 112 icon={<PublishSmall />} 86 113 nav 87 - subtext="Blog on ATProto!" 114 + subtext="Start a blog on ATProto!" 115 + onClick={() => { 116 + setState("info"); 117 + }} 88 118 /> 89 - </SpeedyLink> 119 + ); 120 + 121 + if (isMobile && state === "info") return <PubListEmptyContent />; 122 + else 123 + return ( 124 + <Popover 125 + side="right" 126 + align="start" 127 + className="p-1! max-w-56" 128 + asChild 129 + trigger={ 130 + <ActionButton 131 + label="Publish" 132 + icon={<PublishSmall />} 133 + nav 134 + subtext="Start a blog on ATProto!" 135 + /> 136 + } 137 + > 138 + <PubListEmptyContent /> 139 + </Popover> 140 + ); 141 + }; 142 + 143 + export const PubListEmptyContent = (props: { compact?: boolean }) => { 144 + let { identity } = useIdentityData(); 145 + 146 + return ( 147 + <div 148 + className={`bg-[var(--accent-light)] w-full rounded-md flex flex-col text-center justify-center p-2 pb-4 text-sm`} 149 + > 150 + <div className="mx-auto pt-2 scale-90"> 151 + <PubListEmptyIllo /> 152 + </div> 153 + <div className="pt-1 font-bold">Publish on AT Proto</div> 154 + {identity && identity.atp_did ? ( 155 + // has ATProto account and no pubs 156 + <> 157 + <div className="pb-2 text-secondary text-xs"> 158 + Start a new publication <br /> 159 + on AT Proto 160 + </div> 161 + <SpeedyLink href={`lish/createPub`} className=" hover:no-underline!"> 162 + <ButtonSecondary className="text-sm mx-auto" compact> 163 + Start a Publication! 164 + </ButtonSecondary> 165 + </SpeedyLink> 166 + </> 167 + ) : ( 168 + // no ATProto account and no pubs 169 + <> 170 + <div className="pb-2 text-secondary text-xs"> 171 + Link a Bluesky account to start <br /> a new publication on AT Proto 172 + </div> 173 + 174 + <BlueskyLogin compact /> 175 + </> 176 + )} 177 + </div> 90 178 ); 91 179 }; 92 180 ··· 102 190 let iconSizeClassName = `${props.small ? "w-4 h-4" : props.large ? "w-12 h-12" : "w-6 h-6"} rounded-full`; 103 191 104 192 return props.record.icon ? ( 105 - <div className={`${iconSizeClassName} ${props.className} relative overflow-hidden`}> 193 + <div 194 + className={`${iconSizeClassName} ${props.className} relative overflow-hidden`} 195 + > 106 196 <img 107 197 src={`/api/atproto_images?did=${new AtUri(props.uri).host}&cid=${(props.record.icon?.ref as unknown as { $link: string })["$link"]}`} 108 198 alt={`${props.record.name} icon`} ··· 116 206 <div 117 207 className={`${props.small ? "text-xs" : props.large ? "text-2xl" : "text-sm"} font-bold absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-accent-2`} 118 208 > 119 - {props.record?.name.slice(0, 1)} 209 + {props.record?.name.slice(0, 1).toUpperCase()} 120 210 </div> 121 211 </div> 122 212 );
+28
components/Avatar.tsx
··· 1 + import { AccountTiny } from "./Icons/AccountTiny"; 2 + 3 + export const Avatar = (props: { 4 + src: string | undefined; 5 + displayName: string | undefined; 6 + tiny?: boolean; 7 + }) => { 8 + if (props.src) 9 + return ( 10 + <img 11 + className={`${props.tiny ? "w-4 h-4" : "w-5 h-5"} rounded-full shrink-0 border border-border-light`} 12 + src={props.src} 13 + alt={ 14 + props.displayName 15 + ? `${props.displayName}'s avatar` 16 + : "someone's avatar" 17 + } 18 + /> 19 + ); 20 + else 21 + return ( 22 + <div 23 + className={`bg-[var(--accent-light)] flex rounded-full shrink-0 border border-border-light place-items-center justify-center text-accent-1 ${props.tiny ? "w-4 h-4" : "w-5 h-5"}`} 24 + > 25 + <AccountTiny className={props.tiny ? "scale-80" : "scale-90"} /> 26 + </div> 27 + ); 28 + };
+43 -6
components/Blocks/BaseTextareaBlock.tsx
··· 5 5 import { BlockProps } from "./Block"; 6 6 import { getCoordinatesInTextarea } from "src/utils/getCoordinatesInTextarea"; 7 7 import { focusBlock } from "src/utils/focusBlock"; 8 + import { generateKeyBetween } from "fractional-indexing"; 9 + import { v7 } from "uuid"; 10 + import { elementId } from "src/utils/elementId"; 11 + import { Replicache } from "replicache"; 12 + import { ReplicacheMutators } from "src/replicache"; 8 13 9 - export function BaseTextareaBlock( 10 - props: AutosizeTextareaProps & { 11 - block: Pick<BlockProps, "previousBlock" | "nextBlock">; 12 - }, 13 - ) { 14 - let { block, ...passDownProps } = props; 14 + type BaseTextareaBlockProps = AutosizeTextareaProps & { 15 + block: Pick< 16 + BlockProps, 17 + "previousBlock" | "nextBlock" | "parent" | "position" | "nextPosition" 18 + >; 19 + rep?: Replicache<ReplicacheMutators> | null; 20 + permissionSet?: string; 21 + }; 22 + 23 + export function BaseTextareaBlock(props: BaseTextareaBlockProps) { 24 + let { block, rep, permissionSet, ...passDownProps } = props; 15 25 return ( 16 26 <AsyncValueAutosizeTextarea 17 27 {...passDownProps} 18 28 noWrap 19 29 onKeyDown={(e) => { 30 + // Shift-Enter or Ctrl-Enter: create new text block below and focus it 31 + if ( 32 + (e.shiftKey || e.ctrlKey || e.metaKey) && 33 + e.key === "Enter" && 34 + rep && 35 + permissionSet 36 + ) { 37 + e.preventDefault(); 38 + let newEntityID = v7(); 39 + rep.mutate.addBlock({ 40 + parent: block.parent, 41 + type: "text", 42 + factID: v7(), 43 + permission_set: permissionSet, 44 + position: generateKeyBetween( 45 + block.position, 46 + block.nextPosition || null, 47 + ), 48 + newEntityID, 49 + }); 50 + 51 + setTimeout(() => { 52 + document.getElementById(elementId.block(newEntityID).text)?.focus(); 53 + }, 10); 54 + return true; 55 + } 56 + 20 57 if (e.key === "ArrowUp") { 21 58 let selection = e.currentTarget.selectionStart; 22 59
+1 -1
components/Blocks/Block.tsx
··· 430 430 className={`listMarker group/list-marker p-2 ${children.length > 0 ? "cursor-pointer" : "cursor-default"}`} 431 431 > 432 432 <div 433 - className={`h-[5px] w-[5px] rounded-full bg-secondary shrink-0 right-0 outline outline-1 outline-offset-1 433 + className={`h-[5px] w-[5px] rounded-full bg-secondary shrink-0 right-0 outline outline-offset-1 434 434 ${ 435 435 folded 436 436 ? "outline-secondary"
+29 -6
components/Blocks/BlockCommandBar.tsx
··· 6 6 import { NestedCardThemeProvider } from "components/ThemeManager/ThemeProvider"; 7 7 import { UndoManager } from "src/undoManager"; 8 8 import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 9 + import { setEditorState, useEditorStates } from "src/state/useEditorState"; 9 10 10 11 type Props = { 11 12 parent: string; ··· 32 33 let entity_set = useEntitySetContext(); 33 34 let { data: pub } = useLeafletPublicationData(); 34 35 36 + // This clears '/' AND anything typed after it 37 + const clearCommandSearchText = () => { 38 + if (!props.entityID) return; 39 + const entityID = props.entityID; 40 + 41 + const existingState = useEditorStates.getState().editorStates[entityID]; 42 + if (!existingState) return; 43 + 44 + const tr = existingState.editor.tr; 45 + tr.deleteRange(1, tr.doc.content.size - 1); 46 + setEditorState(entityID, { editor: existingState.editor.apply(tr) }); 47 + }; 48 + 35 49 let commandResults = blockCommands.filter((command) => { 36 - const matchesSearch = command.name 50 + const lowerSearchValue = searchValue.toLocaleLowerCase(); 51 + const matchesName = command.name 37 52 .toLocaleLowerCase() 38 - .includes(searchValue.toLocaleLowerCase()); 53 + .includes(lowerSearchValue); 54 + const matchesAlternate = command.alternateNames?.some((altName) => 55 + altName.toLocaleLowerCase().includes(lowerSearchValue) 56 + ) ?? false; 57 + const matchesSearch = matchesName || matchesAlternate; 39 58 const isVisible = !pub || !command.hiddenInPublication; 40 59 return matchesSearch && isVisible; 41 60 }); ··· 98 117 undoManager.endGroup(); 99 118 return; 100 119 } 101 - 102 - // radix menu component handles esc 103 - if (e.key === "Escape") return; 104 120 }; 105 121 window.addEventListener("keydown", listener); 106 122 ··· 108 124 }, [highlighted, setHighlighted, commandResults, rep, entity_set.set, props]); 109 125 110 126 return ( 111 - <Popover.Root open> 127 + <Popover.Root 128 + open 129 + onOpenChange={(open) => { 130 + if (!open) { 131 + clearCommandSearchText(); 132 + } 133 + }} 134 + > 112 135 <Popover.Trigger className="absolute left-0"></Popover.Trigger> 113 136 <Popover.Portal> 114 137 <Popover.Content
+14 -2
components/Blocks/BlockCommands.tsx
··· 32 32 import { BlockMathSmall } from "components/Icons/BlockMathSmall"; 33 33 import { BlockCodeSmall } from "components/Icons/BlockCodeSmall"; 34 34 import { QuoteSmall } from "components/Icons/QuoteSmall"; 35 + import { LAST_USED_CODE_LANGUAGE_KEY } from "src/utils/codeLanguageStorage"; 35 36 36 37 type Props = { 37 38 parent: string; ··· 102 103 name: string; 103 104 icon: React.ReactNode; 104 105 type: string; 106 + alternateNames?: string[]; 105 107 hiddenInPublication?: boolean; 106 108 onSelect: ( 107 109 rep: Replicache<ReplicacheMutators>, ··· 125 127 name: "Title", 126 128 icon: <Header1Small />, 127 129 type: "text", 130 + alternateNames: ["h1"], 128 131 onSelect: async (rep, props, um) => { 129 132 await setHeaderCommand(1, rep, props); 130 133 }, ··· 133 136 name: "Header", 134 137 icon: <Header2Small />, 135 138 type: "text", 139 + alternateNames: ["h2"], 136 140 onSelect: async (rep, props, um) => { 137 141 await setHeaderCommand(2, rep, props); 138 142 }, ··· 141 145 name: "Subheader", 142 146 icon: <Header3Small />, 143 147 type: "text", 148 + alternateNames: ["h3"], 144 149 onSelect: async (rep, props, um) => { 145 150 await setHeaderCommand(3, rep, props); 146 151 }, ··· 204 209 name: "Button", 205 210 icon: <BlockButtonSmall />, 206 211 type: "block", 207 - hiddenInPublication: true, 208 212 onSelect: async (rep, props, um) => { 209 213 props.entityID && clearCommandSearchText(props.entityID); 210 214 await createBlockWithType(rep, props, "button"); ··· 307 311 type: "block", 308 312 hiddenInPublication: false, 309 313 onSelect: async (rep, props) => { 310 - createBlockWithType(rep, props, "code"); 314 + let entity = await createBlockWithType(rep, props, "code"); 315 + let lastLang = localStorage.getItem(LAST_USED_CODE_LANGUAGE_KEY); 316 + if (lastLang) { 317 + await rep.mutate.assertFact({ 318 + entity, 319 + attribute: "block/code-language", 320 + data: { type: "string", value: lastLang }, 321 + }); 322 + } 311 323 }, 312 324 }, 313 325
+7 -5
components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx
··· 125 125 className={`flex flex-col gap-1 relative w-full overflow-hidden sm:p-3 p-2 text-xs block-border`} 126 126 > 127 127 <div className="bskyAuthor w-full flex items-center gap-1"> 128 - <img 129 - src={record.author?.avatar} 130 - alt={`${record.author?.displayName}'s avatar`} 131 - className="shink-0 w-6 h-6 rounded-full border border-border-light" 132 - /> 128 + {record.author.avatar && ( 129 + <img 130 + src={record.author?.avatar} 131 + alt={`${record.author?.displayName}'s avatar`} 132 + className="shink-0 w-6 h-6 rounded-full border border-border-light" 133 + /> 134 + )} 133 135 <div className=" font-bold text-secondary"> 134 136 {record.author?.displayName} 135 137 </div>
-1
components/Blocks/BlueskyPostBlock/index.tsx
··· 10 10 import { BlueskyPostEmpty } from "./BlueskyEmpty"; 11 11 import { BlueskyRichText } from "./BlueskyRichText"; 12 12 import { Separator } from "components/Layout"; 13 - import { useInitialPageLoad } from "components/InitialPageLoadProvider"; 14 13 import { BlueskyTiny } from "components/Icons/BlueskyTiny"; 15 14 import { CommentTiny } from "components/Icons/CommentTiny"; 16 15 import { useLocalizedDate } from "src/hooks/useLocalizedDate";
+6 -1
components/Blocks/CodeBlock.tsx
··· 13 13 import { useEntitySetContext } from "components/EntitySetProvider"; 14 14 import { flushSync } from "react-dom"; 15 15 import { elementId } from "src/utils/elementId"; 16 + import { LAST_USED_CODE_LANGUAGE_KEY } from "src/utils/codeLanguageStorage"; 16 17 17 18 export function CodeBlock(props: BlockProps) { 18 19 let { rep, rootEntity } = useReplicache(); ··· 25 26 let focusedBlock = useUIState( 26 27 (s) => s.focusedEntity?.entityID === props.entityID, 27 28 ); 28 - let { permissions } = useEntitySetContext(); 29 + let entity_set = useEntitySetContext(); 30 + let { permissions } = entity_set; 29 31 const [html, setHTML] = useState<string | null>(null); 30 32 31 33 useLayoutEffect(() => { ··· 100 102 }} 101 103 value={lang} 102 104 onChange={async (e) => { 105 + localStorage.setItem(LAST_USED_CODE_LANGUAGE_KEY, e.target.value); 103 106 await rep?.mutate.assertFact({ 104 107 attribute: "block/code-language", 105 108 entity: props.entityID, ··· 123 126 data-entityid={props.entityID} 124 127 id={elementId.block(props.entityID).input} 125 128 block={props} 129 + rep={rep} 130 + permissionSet={entity_set.set} 126 131 spellCheck={false} 127 132 autoCapitalize="none" 128 133 autoCorrect="off"
+2 -2
components/Blocks/DateTimeBlock.tsx
··· 8 8 import { setHours, setMinutes } from "date-fns"; 9 9 import { Separator } from "react-aria-components"; 10 10 import { Checkbox } from "components/Checkbox"; 11 - import { useInitialPageLoad } from "components/InitialPageLoadProvider"; 11 + import { useHasPageLoaded } from "components/InitialPageLoadProvider"; 12 12 import { useSpring, animated } from "@react-spring/web"; 13 13 import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; 14 14 import { BlockCalendarSmall } from "components/Icons/BlockCalendarSmall"; 15 15 16 16 export function DateTimeBlock(props: BlockProps) { 17 17 const [isClient, setIsClient] = useState(false); 18 - let initialPageLoad = useInitialPageLoad(); 18 + let initialPageLoad = useHasPageLoaded(); 19 19 20 20 useEffect(() => { 21 21 setIsClient(true);
+65 -16
components/Blocks/EmbedBlock.tsx
··· 10 10 import { Input } from "components/Input"; 11 11 import { isUrl } from "src/utils/isURL"; 12 12 import { elementId } from "src/utils/elementId"; 13 - import { deleteBlock } from "./DeleteBlock"; 14 13 import { focusBlock } from "src/utils/focusBlock"; 15 14 import { useDrag } from "src/hooks/useDrag"; 16 15 import { BlockEmbedSmall } from "components/Icons/BlockEmbedSmall"; 17 16 import { CheckTiny } from "components/Icons/CheckTiny"; 17 + import { DotLoader } from "components/utils/DotLoader"; 18 + import { 19 + LinkPreviewBody, 20 + LinkPreviewMetadataResult, 21 + } from "app/api/link_previews/route"; 18 22 19 23 export const EmbedBlock = (props: BlockProps & { preview?: boolean }) => { 20 24 let { permissions } = useEntitySetContext(); ··· 132 136 133 137 let entity_set = useEntitySetContext(); 134 138 let [linkValue, setLinkValue] = useState(""); 139 + let [loading, setLoading] = useState(false); 135 140 let { rep } = useReplicache(); 136 141 let submit = async () => { 137 142 let entity = props.entityID; ··· 149 154 } 150 155 let link = linkValue; 151 156 if (!linkValue.startsWith("http")) link = `https://${linkValue}`; 152 - // these mutations = simpler subset of addLinkBlock 153 157 if (!rep) return; 154 - await rep.mutate.assertFact({ 155 - entity: entity, 156 - attribute: "block/type", 157 - data: { type: "block-type-union", value: "embed" }, 158 - }); 159 - await rep?.mutate.assertFact({ 160 - entity: entity, 161 - attribute: "embed/url", 162 - data: { 163 - type: "string", 164 - value: link, 165 - }, 166 - }); 158 + 159 + // Try to get embed URL from iframely, fallback to direct URL 160 + setLoading(true); 161 + try { 162 + let res = await fetch("/api/link_previews", { 163 + headers: { "Content-Type": "application/json" }, 164 + method: "POST", 165 + body: JSON.stringify({ url: link, type: "meta" } as LinkPreviewBody), 166 + }); 167 + 168 + let embedUrl = link; 169 + let embedHeight = 360; 170 + 171 + if (res.status === 200) { 172 + let data = await (res.json() as LinkPreviewMetadataResult); 173 + if (data.success && data.data.links?.player?.[0]) { 174 + let embed = data.data.links.player[0]; 175 + embedUrl = embed.href; 176 + embedHeight = embed.media?.height || 300; 177 + } 178 + } 179 + 180 + await rep.mutate.assertFact([ 181 + { 182 + entity: entity, 183 + attribute: "embed/url", 184 + data: { 185 + type: "string", 186 + value: embedUrl, 187 + }, 188 + }, 189 + { 190 + entity: entity, 191 + attribute: "embed/height", 192 + data: { 193 + type: "number", 194 + value: embedHeight, 195 + }, 196 + }, 197 + ]); 198 + } catch { 199 + // On any error, fallback to using the URL directly 200 + await rep.mutate.assertFact([ 201 + { 202 + entity: entity, 203 + attribute: "embed/url", 204 + data: { 205 + type: "string", 206 + value: link, 207 + }, 208 + }, 209 + ]); 210 + } finally { 211 + setLoading(false); 212 + } 167 213 }; 168 214 let smoker = useSmoker(); 169 215 ··· 171 217 <form 172 218 onSubmit={(e) => { 173 219 e.preventDefault(); 220 + if (loading) return; 174 221 let rect = document 175 222 .getElementById("embed-block-submit") 176 223 ?.getBoundingClientRect(); ··· 212 259 <button 213 260 type="submit" 214 261 id="embed-block-submit" 262 + disabled={loading} 215 263 className={`p-1 ${isSelected && !isLocked ? "text-accent-contrast" : "text-border"}`} 216 264 onMouseDown={(e) => { 217 265 e.preventDefault(); 266 + if (loading) return; 218 267 if (!linkValue || linkValue === "") { 219 268 smoker({ 220 269 error: true, ··· 234 283 submit(); 235 284 }} 236 285 > 237 - <CheckTiny /> 286 + {loading ? <DotLoader /> : <CheckTiny />} 238 287 </button> 239 288 </div> 240 289 </form>
-2
components/Blocks/PublicationPollBlock.tsx
··· 31 31 32 32 const docRecord = publicationData.documents 33 33 .data as PubLeafletDocument.Record; 34 - console.log(docRecord); 35 34 36 35 // Search through all pages and blocks to find if this poll entity has been published 37 36 for (const page of docRecord.pages || []) { ··· 40 39 for (const blockWrapper of linearPage.blocks || []) { 41 40 if (blockWrapper.block?.$type === ids.PubLeafletBlocksPoll) { 42 41 const pollBlock = blockWrapper.block as PubLeafletBlocksPoll.Main; 43 - console.log(pollBlock); 44 42 // Check if this poll's rkey matches our entity ID 45 43 const rkey = pollBlock.pollRef.uri.split("/").pop(); 46 44 if (rkey === props.entityID) {
+2 -2
components/Blocks/RSVPBlock/SendUpdate.tsx
··· 9 9 import { sendUpdateToRSVPS } from "actions/sendUpdateToRSVPS"; 10 10 import { useReplicache } from "src/replicache"; 11 11 import { Checkbox } from "components/Checkbox"; 12 - import { usePublishLink } from "components/ShareOptions"; 12 + import { useReadOnlyShareLink } from "app/[leaflet_id]/actions/ShareOptions"; 13 13 14 14 export function SendUpdateButton(props: { entityID: string }) { 15 - let publishLink = usePublishLink(); 15 + let publishLink = useReadOnlyShareLink(); 16 16 let { permissions } = useEntitySetContext(); 17 17 let { permission_token } = useReplicache(); 18 18 let [input, setInput] = useState("");
+9 -1
components/Blocks/TextBlock/RenderYJSFragment.tsx
··· 27 27 return ( 28 28 <BlockWrapper wrapper={wrapper} attrs={attrs}> 29 29 {children.length === 0 ? ( 30 - <div /> 30 + <br /> 31 31 ) : ( 32 32 node.toArray().map((node, index) => { 33 33 if (node.constructor === XmlText) { ··· 58 58 })} 59 59 </Fragment> 60 60 ); 61 + } 62 + 63 + if (node.constructor === XmlElement && node.nodeName === "hard_break") { 64 + return <br key={index} />; 61 65 } 62 66 63 67 return null; ··· 144 148 node: XmlElement | XmlText | XmlHook, 145 149 ): string { 146 150 if (node.constructor === XmlElement) { 151 + // Handle hard_break nodes specially 152 + if (node.nodeName === "hard_break") { 153 + return "\n"; 154 + } 147 155 return node 148 156 .toArray() 149 157 .map((f) => YJSFragmentToString(f))
+20 -198
components/Blocks/TextBlock/index.tsx
··· 1 - import { useRef, useEffect, useState, useLayoutEffect } from "react"; 1 + import { useRef, useEffect, useState } from "react"; 2 2 import { elementId } from "src/utils/elementId"; 3 - import { baseKeymap } from "prosemirror-commands"; 4 - import { keymap } from "prosemirror-keymap"; 5 - import * as Y from "yjs"; 6 - import * as base64 from "base64-js"; 7 - import { useReplicache, useEntity, ReplicacheMutators } from "src/replicache"; 3 + import { useReplicache, useEntity } from "src/replicache"; 8 4 import { isVisible } from "src/utils/isVisible"; 9 - 10 5 import { EditorState, TextSelection } from "prosemirror-state"; 11 - import { EditorView } from "prosemirror-view"; 12 - 13 - import { ySyncPlugin } from "y-prosemirror"; 14 - import { Replicache } from "replicache"; 15 6 import { RenderYJSFragment } from "./RenderYJSFragment"; 16 - import { useInitialPageLoad } from "components/InitialPageLoadProvider"; 7 + import { useHasPageLoaded } from "components/InitialPageLoadProvider"; 17 8 import { BlockProps } from "../Block"; 18 9 import { focusBlock } from "src/utils/focusBlock"; 19 - import { TextBlockKeymap } from "./keymap"; 20 - import { multiBlockSchema, schema } from "./schema"; 21 10 import { useUIState } from "src/useUIState"; 22 11 import { addBlueskyPostBlock, addLinkBlock } from "src/utils/addLinkBlock"; 23 12 import { BlockCommandBar } from "components/Blocks/BlockCommandBar"; 24 13 import { useEditorStates } from "src/state/useEditorState"; 25 14 import { useEntitySetContext } from "components/EntitySetProvider"; 26 - import { useHandlePaste } from "./useHandlePaste"; 27 - import { highlightSelectionPlugin } from "./plugins"; 28 - import { inputrules } from "./inputRules"; 29 - import { autolink } from "./autolink-plugin"; 30 15 import { TooltipButton } from "components/Buttons"; 31 16 import { blockCommands } from "../BlockCommands"; 32 17 import { betterIsUrl } from "src/utils/isURL"; ··· 37 22 import { isIOS } from "src/utils/isDevice"; 38 23 import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 39 24 import { DotLoader } from "components/utils/DotLoader"; 25 + import { useMountProsemirror } from "./mountProsemirror"; 40 26 41 27 const HeadingStyle = { 42 28 1: "text-xl font-bold", ··· 51 37 }, 52 38 ) { 53 39 let isLocked = useEntity(props.entityID, "block/is-locked"); 54 - let initialized = useInitialPageLoad(); 40 + let initialized = useHasPageLoaded(); 55 41 let first = props.previousBlock === null; 56 42 let permission = useEntitySetContext().permissions.write; 57 43 ··· 177 163 } 178 164 179 165 export function BaseTextBlock(props: BlockProps & { className?: string }) { 180 - let mountRef = useRef<HTMLPreElement | null>(null); 181 - let actionTimeout = useRef<number | null>(null); 182 - let repRef = useRef<null | Replicache<ReplicacheMutators>>(null); 183 166 let headingLevel = useEntity(props.entityID, "block/heading-level"); 184 - let entity_set = useEntitySetContext(); 185 167 let alignment = 186 168 useEntity(props.entityID, "block/text-alignment")?.data.value || "left"; 187 - let propsRef = useRef({ ...props, entity_set, alignment }); 188 - useEffect(() => { 189 - propsRef.current = { ...props, entity_set, alignment }; 190 - }, [props, entity_set, alignment]); 169 + 191 170 let rep = useReplicache(); 192 - useEffect(() => { 193 - repRef.current = rep.rep; 194 - }, [rep?.rep]); 195 171 196 172 let selected = useUIState( 197 173 (s) => !!s.selectedBlocks.find((b) => b.value === props.entityID), ··· 204 180 justify: "text-justify", 205 181 }[alignment]; 206 182 207 - let value = useYJSValue(props.entityID); 208 - 209 183 let editorState = useEditorStates( 210 184 (s) => s.editorStates[props.entityID], 211 185 )?.editor; 212 - let handlePaste = useHandlePaste(props.entityID, propsRef); 213 - useLayoutEffect(() => { 214 - if (!mountRef.current) return; 215 - let km = TextBlockKeymap(propsRef, repRef, rep.undoManager); 216 - let editor = EditorState.create({ 217 - schema: schema, 218 - plugins: [ 219 - ySyncPlugin(value), 220 - keymap(km), 221 - inputrules(propsRef, repRef), 222 - keymap(baseKeymap), 223 - highlightSelectionPlugin, 224 - autolink({ 225 - type: schema.marks.link, 226 - shouldAutoLink: () => true, 227 - defaultProtocol: "https", 228 - }), 229 - ], 230 - }); 231 186 232 - let unsubscribe = useEditorStates.subscribe((s) => { 233 - let editorState = s.editorStates[props.entityID]; 234 - if (editorState?.initial) return; 235 - if (editorState?.editor) 236 - editorState.view?.updateState(editorState.editor); 237 - }); 238 - let view = new EditorView( 239 - { mount: mountRef.current }, 240 - { 241 - state: editor, 242 - handlePaste, 243 - handleClickOn: (view, _pos, node, _nodePos, _event, direct) => { 244 - if (!direct) return; 245 - if (node.nodeSize - 2 <= _pos) return; 246 - let mark = 247 - node 248 - .nodeAt(_pos - 1) 249 - ?.marks.find((f) => f.type === schema.marks.link) || 250 - node 251 - .nodeAt(Math.max(_pos - 2, 0)) 252 - ?.marks.find((f) => f.type === schema.marks.link); 253 - if (mark) { 254 - window.open(mark.attrs.href, "_blank"); 255 - } 256 - }, 257 - dispatchTransaction(tr) { 258 - useEditorStates.setState((s) => { 259 - let oldEditorState = this.state; 260 - let newState = this.state.apply(tr); 261 - let addToHistory = tr.getMeta("addToHistory"); 262 - let isBulkOp = tr.getMeta("bulkOp"); 263 - let docHasChanges = tr.steps.length !== 0 || tr.docChanged; 264 - if (addToHistory !== false && docHasChanges) { 265 - if (actionTimeout.current) { 266 - window.clearTimeout(actionTimeout.current); 267 - } else { 268 - if (!isBulkOp) rep.undoManager.startGroup(); 269 - } 270 - 271 - if (!isBulkOp) 272 - actionTimeout.current = window.setTimeout(() => { 273 - rep.undoManager.endGroup(); 274 - actionTimeout.current = null; 275 - }, 200); 276 - rep.undoManager.add({ 277 - redo: () => { 278 - useEditorStates.setState((oldState) => { 279 - let view = oldState.editorStates[props.entityID]?.view; 280 - if (!view?.hasFocus() && !isBulkOp) view?.focus(); 281 - return { 282 - editorStates: { 283 - ...oldState.editorStates, 284 - [props.entityID]: { 285 - ...oldState.editorStates[props.entityID]!, 286 - editor: newState, 287 - }, 288 - }, 289 - }; 290 - }); 291 - }, 292 - undo: () => { 293 - useEditorStates.setState((oldState) => { 294 - let view = oldState.editorStates[props.entityID]?.view; 295 - if (!view?.hasFocus() && !isBulkOp) view?.focus(); 296 - return { 297 - editorStates: { 298 - ...oldState.editorStates, 299 - [props.entityID]: { 300 - ...oldState.editorStates[props.entityID]!, 301 - editor: oldEditorState, 302 - }, 303 - }, 304 - }; 305 - }); 306 - }, 307 - }); 308 - } 309 - 310 - return { 311 - editorStates: { 312 - ...s.editorStates, 313 - [props.entityID]: { 314 - editor: newState, 315 - view: this as unknown as EditorView, 316 - initial: false, 317 - keymap: km, 318 - }, 319 - }, 320 - }; 321 - }); 322 - }, 323 - }, 324 - ); 325 - return () => { 326 - unsubscribe(); 327 - view.destroy(); 328 - useEditorStates.setState((s) => ({ 329 - ...s, 330 - editorStates: { 331 - ...s.editorStates, 332 - [props.entityID]: undefined, 333 - }, 334 - })); 335 - }; 336 - }, [props.entityID, props.parent, value, handlePaste, rep]); 187 + let { mountRef, actionTimeout } = useMountProsemirror({ 188 + props, 189 + }); 337 190 338 191 return ( 339 192 <> ··· 586 439 ); 587 440 }; 588 441 589 - function useYJSValue(entityID: string) { 590 - const [ydoc] = useState(new Y.Doc()); 591 - const docStateFromReplicache = useEntity(entityID, "block/text"); 592 - let rep = useReplicache(); 593 - const [yText] = useState(ydoc.getXmlFragment("prosemirror")); 594 - 595 - if (docStateFromReplicache) { 596 - const update = base64.toByteArray(docStateFromReplicache.data.value); 597 - Y.applyUpdate(ydoc, update); 598 - } 599 - 600 - useEffect(() => { 601 - if (!rep.rep) return; 602 - let timeout = null as null | number; 603 - const updateReplicache = async () => { 604 - const update = Y.encodeStateAsUpdate(ydoc); 605 - await rep.rep?.mutate.assertFact({ 606 - //These undos are handled above in the Prosemirror context 607 - ignoreUndo: true, 608 - entity: entityID, 609 - attribute: "block/text", 610 - data: { 611 - value: base64.fromByteArray(update), 612 - type: "text", 613 - }, 614 - }); 615 - }; 616 - const f = async (events: Y.YEvent<any>[], transaction: Y.Transaction) => { 617 - if (!transaction.origin) return; 618 - if (timeout) clearTimeout(timeout); 619 - timeout = window.setTimeout(async () => { 620 - updateReplicache(); 621 - }, 300); 622 - }; 623 - 624 - yText.observeDeep(f); 625 - return () => { 626 - yText.unobserveDeep(f); 627 - }; 628 - }, [yText, entityID, rep, ydoc]); 629 - return yText; 630 - } 442 + const useMentionState = () => { 443 + const [editorState, setEditorState] = useState<EditorState | null>(null); 444 + const [mentionState, setMentionState] = useState<{ 445 + active: boolean; 446 + range: { from: number; to: number } | null; 447 + selectedMention: { handle: string; did: string } | null; 448 + }>({ active: false, range: null, selectedMention: null }); 449 + const mentionStateRef = useRef(mentionState); 450 + mentionStateRef.current = mentionState; 451 + return { mentionStateRef }; 452 + };
+12 -3
components/Blocks/TextBlock/inputRules.ts
··· 11 11 import { schema } from "./schema"; 12 12 import { useUIState } from "src/useUIState"; 13 13 import { flushSync } from "react-dom"; 14 + import { LAST_USED_CODE_LANGUAGE_KEY } from "src/utils/codeLanguageStorage"; 14 15 export const inputrules = ( 15 16 propsRef: MutableRefObject<BlockProps & { entity_set: { set: string } }>, 16 17 repRef: MutableRefObject<Replicache<ReplicacheMutators> | null>, ··· 108 109 109 110 // Code Block 110 111 new InputRule(/^```\s$/, (state, match) => { 111 - flushSync(() => 112 + flushSync(() => { 112 113 repRef.current?.mutate.assertFact({ 113 114 entity: propsRef.current.entityID, 114 115 attribute: "block/type", 115 116 data: { type: "block-type-union", value: "code" }, 116 - }), 117 - ); 117 + }); 118 + let lastLang = localStorage.getItem(LAST_USED_CODE_LANGUAGE_KEY); 119 + if (lastLang) { 120 + repRef.current?.mutate.assertFact({ 121 + entity: propsRef.current.entityID, 122 + attribute: "block/code-language", 123 + data: { type: "string", value: lastLang }, 124 + }); 125 + } 126 + }); 118 127 setTimeout(() => { 119 128 focusBlock({ ...propsRef.current, type: "code" }, { type: "start" }); 120 129 }, 20);
+5 -5
components/Blocks/TextBlock/keymap.ts
··· 145 145 ); 146 146 }, 147 147 "Shift-Enter": (state, dispatch, view) => { 148 - if (multiLine) { 149 - return baseKeymap.Enter(state, dispatch, view); 148 + // Insert a hard break 149 + let hardBreak = schema.nodes.hard_break.create(); 150 + if (dispatch) { 151 + dispatch(state.tr.replaceSelectionWith(hardBreak).scrollIntoView()); 150 152 } 151 - return um.withUndoGroup(() => 152 - enter(propsRef, repRef)(state, dispatch, view), 153 - ); 153 + return true; 154 154 }, 155 155 "Ctrl-Enter": CtrlEnter(propsRef, repRef), 156 156 "Meta-Enter": CtrlEnter(propsRef, repRef),
+203
components/Blocks/TextBlock/mountProsemirror.ts
··· 1 + import { useLayoutEffect, useRef, useEffect, useState } from "react"; 2 + import { EditorState } from "prosemirror-state"; 3 + import { EditorView } from "prosemirror-view"; 4 + import { baseKeymap } from "prosemirror-commands"; 5 + import { keymap } from "prosemirror-keymap"; 6 + import { ySyncPlugin } from "y-prosemirror"; 7 + import * as Y from "yjs"; 8 + import * as base64 from "base64-js"; 9 + import { Replicache } from "replicache"; 10 + import { produce } from "immer"; 11 + 12 + import { schema } from "./schema"; 13 + import { TextBlockKeymap } from "./keymap"; 14 + import { inputrules } from "./inputRules"; 15 + import { highlightSelectionPlugin } from "./plugins"; 16 + import { autolink } from "./autolink-plugin"; 17 + import { useEditorStates } from "src/state/useEditorState"; 18 + import { 19 + useEntity, 20 + useReplicache, 21 + type ReplicacheMutators, 22 + } from "src/replicache"; 23 + import { useHandlePaste } from "./useHandlePaste"; 24 + import { BlockProps } from "../Block"; 25 + import { useEntitySetContext } from "components/EntitySetProvider"; 26 + 27 + export function useMountProsemirror({ props }: { props: BlockProps }) { 28 + let { entityID, parent } = props; 29 + let rep = useReplicache(); 30 + let mountRef = useRef<HTMLPreElement | null>(null); 31 + const repRef = useRef<Replicache<ReplicacheMutators> | null>(null); 32 + let value = useYJSValue(entityID); 33 + let entity_set = useEntitySetContext(); 34 + let alignment = 35 + useEntity(entityID, "block/text-alignment")?.data.value || "left"; 36 + let propsRef = useRef({ ...props, entity_set, alignment }); 37 + let handlePaste = useHandlePaste(entityID, propsRef); 38 + 39 + const actionTimeout = useRef<number | null>(null); 40 + 41 + propsRef.current = { ...props, entity_set, alignment }; 42 + repRef.current = rep.rep; 43 + 44 + useLayoutEffect(() => { 45 + if (!mountRef.current) return; 46 + 47 + const km = TextBlockKeymap(propsRef, repRef, rep.undoManager); 48 + const editor = EditorState.create({ 49 + schema: schema, 50 + plugins: [ 51 + ySyncPlugin(value), 52 + keymap(km), 53 + inputrules(propsRef, repRef), 54 + keymap(baseKeymap), 55 + highlightSelectionPlugin, 56 + autolink({ 57 + type: schema.marks.link, 58 + shouldAutoLink: () => true, 59 + defaultProtocol: "https", 60 + }), 61 + ], 62 + }); 63 + 64 + const view = new EditorView( 65 + { mount: mountRef.current }, 66 + { 67 + state: editor, 68 + handlePaste, 69 + handleClickOn: (_view, _pos, node, _nodePos, _event, direct) => { 70 + if (!direct) return; 71 + if (node.nodeSize - 2 <= _pos) return; 72 + let mark = 73 + node 74 + .nodeAt(_pos - 1) 75 + ?.marks.find((f) => f.type === schema.marks.link) || 76 + node 77 + .nodeAt(Math.max(_pos - 2, 0)) 78 + ?.marks.find((f) => f.type === schema.marks.link); 79 + if (mark) { 80 + window.open(mark.attrs.href, "_blank"); 81 + } 82 + }, 83 + dispatchTransaction, 84 + }, 85 + ); 86 + 87 + const unsubscribe = useEditorStates.subscribe((s) => { 88 + let editorState = s.editorStates[entityID]; 89 + if (editorState?.initial) return; 90 + if (editorState?.editor) 91 + editorState.view?.updateState(editorState.editor); 92 + }); 93 + 94 + let editorState = useEditorStates.getState().editorStates[entityID]; 95 + if (editorState?.editor && !editorState.initial) 96 + editorState.view?.updateState(editorState.editor); 97 + 98 + return () => { 99 + unsubscribe(); 100 + view.destroy(); 101 + useEditorStates.setState((s) => ({ 102 + ...s, 103 + editorStates: { 104 + ...s.editorStates, 105 + [entityID]: undefined, 106 + }, 107 + })); 108 + }; 109 + 110 + function dispatchTransaction(this: EditorView, tr: any) { 111 + useEditorStates.setState((s) => { 112 + let oldEditorState = this.state; 113 + let newState = this.state.apply(tr); 114 + let addToHistory = tr.getMeta("addToHistory"); 115 + let isBulkOp = tr.getMeta("bulkOp"); 116 + let docHasChanges = tr.steps.length !== 0 || tr.docChanged; 117 + 118 + // Handle undo/redo history with timeout-based grouping 119 + if (addToHistory !== false && docHasChanges) { 120 + if (actionTimeout.current) window.clearTimeout(actionTimeout.current); 121 + else if (!isBulkOp) rep.undoManager.startGroup(); 122 + 123 + if (!isBulkOp) { 124 + actionTimeout.current = window.setTimeout(() => { 125 + rep.undoManager.endGroup(); 126 + actionTimeout.current = null; 127 + }, 200); 128 + } 129 + 130 + let setState = (s: EditorState) => () => 131 + useEditorStates.setState( 132 + produce((draft) => { 133 + let view = draft.editorStates[entityID]?.view; 134 + if (!view?.hasFocus() && !isBulkOp) view?.focus(); 135 + draft.editorStates[entityID]!.editor = s; 136 + }), 137 + ); 138 + 139 + rep.undoManager.add({ 140 + redo: setState(newState), 141 + undo: setState(oldEditorState), 142 + }); 143 + } 144 + 145 + return { 146 + editorStates: { 147 + ...s.editorStates, 148 + [entityID]: { 149 + editor: newState, 150 + view: this as unknown as EditorView, 151 + initial: false, 152 + keymap: km, 153 + }, 154 + }, 155 + }; 156 + }); 157 + } 158 + }, [entityID, parent, value, handlePaste, rep]); 159 + return { mountRef, actionTimeout }; 160 + } 161 + 162 + function useYJSValue(entityID: string) { 163 + const [ydoc] = useState(new Y.Doc()); 164 + const docStateFromReplicache = useEntity(entityID, "block/text"); 165 + let rep = useReplicache(); 166 + const [yText] = useState(ydoc.getXmlFragment("prosemirror")); 167 + 168 + if (docStateFromReplicache) { 169 + const update = base64.toByteArray(docStateFromReplicache.data.value); 170 + Y.applyUpdate(ydoc, update); 171 + } 172 + 173 + useEffect(() => { 174 + if (!rep.rep) return; 175 + let timeout = null as null | number; 176 + const updateReplicache = async () => { 177 + const update = Y.encodeStateAsUpdate(ydoc); 178 + await rep.rep?.mutate.assertFact({ 179 + //These undos are handled above in the Prosemirror context 180 + ignoreUndo: true, 181 + entity: entityID, 182 + attribute: "block/text", 183 + data: { 184 + value: base64.fromByteArray(update), 185 + type: "text", 186 + }, 187 + }); 188 + }; 189 + const f = async (events: Y.YEvent<any>[], transaction: Y.Transaction) => { 190 + if (!transaction.origin) return; 191 + if (timeout) clearTimeout(timeout); 192 + timeout = window.setTimeout(async () => { 193 + updateReplicache(); 194 + }, 300); 195 + }; 196 + 197 + yText.observeDeep(f); 198 + return () => { 199 + yText.unobserveDeep(f); 200 + }; 201 + }, [yText, entityID, rep, ydoc]); 202 + return yText; 203 + }
+7
components/Blocks/TextBlock/schema.ts
··· 115 115 text: { 116 116 group: "inline", 117 117 }, 118 + hard_break: { 119 + group: "inline", 120 + inline: true, 121 + selectable: false, 122 + parseDOM: [{ tag: "br" }], 123 + toDOM: () => ["br"] as const, 124 + }, 118 125 }, 119 126 }; 120 127 export const schema = new Schema(baseSchema);
+35 -21
components/Buttons.tsx
··· 10 10 import { PopoverArrow } from "./Icons/PopoverArrow"; 11 11 12 12 type ButtonProps = Omit<JSX.IntrinsicElements["button"], "content">; 13 + 13 14 export const ButtonPrimary = forwardRef< 14 15 HTMLButtonElement, 15 16 ButtonProps & { ··· 35 36 m-0 h-max 36 37 ${fullWidth ? "w-full" : fullWidthOnMobile ? "w-full sm:w-max" : "w-max"} 37 38 ${compact ? "py-0 px-1" : "px-2 py-0.5 "} 38 - bg-accent-1 outline-transparent border border-accent-1 39 - rounded-md text-base font-bold text-accent-2 39 + bg-accent-1 disabled:bg-border-light 40 + border border-accent-1 rounded-md disabled:border-border-light 41 + outline outline-transparent outline-offset-1 focus:outline-accent-1 hover:outline-accent-1 42 + text-base font-bold text-accent-2 disabled:text-border disabled:hover:text-border 40 43 flex gap-2 items-center justify-center shrink-0 41 - transparent-outline focus:outline-accent-1 hover:outline-accent-1 outline-offset-1 42 - disabled:bg-border-light disabled:border-border-light disabled:text-border disabled:hover:text-border 43 44 ${className} 44 45 `} 45 46 > ··· 70 71 <button 71 72 {...buttonProps} 72 73 ref={ref} 73 - className={`m-0 h-max 74 + className={` 75 + m-0 h-max 74 76 ${fullWidth ? "w-full" : fullWidthOnMobile ? "w-full sm:w-max" : "w-max"} 75 - ${props.compact ? "py-0 px-1" : "px-2 py-0.5 "} 76 - bg-bg-page outline-transparent 77 - rounded-md text-base font-bold text-accent-contrast 78 - flex gap-2 items-center justify-center shrink-0 79 - transparent-outline focus:outline-accent-contrast hover:outline-accent-contrast outline-offset-1 80 - border border-accent-contrast 81 - disabled:bg-border-light disabled:text-border disabled:hover:text-border 82 - ${props.className} 83 - `} 77 + ${compact ? "py-0 px-1" : "px-2 py-0.5 "} 78 + bg-bg-page disabled:bg-border-light 79 + border border-accent-contrast rounded-md 80 + outline outline-transparent focus:outline-accent-contrast hover:outline-accent-contrast outline-offset-1 81 + text-base font-bold text-accent-contrast disabled:text-border disabled:hover:text-border 82 + flex gap-2 items-center justify-center shrink-0 83 + ${props.className} 84 + `} 84 85 > 85 86 {props.children} 86 87 </button> ··· 92 93 HTMLButtonElement, 93 94 { 94 95 fullWidth?: boolean; 96 + fullWidthOnMobile?: boolean; 95 97 children: React.ReactNode; 96 98 compact?: boolean; 97 99 } & ButtonProps 98 100 >((props, ref) => { 99 - let { fullWidth, children, compact, ...buttonProps } = props; 101 + let { 102 + className, 103 + fullWidth, 104 + fullWidthOnMobile, 105 + compact, 106 + children, 107 + ...buttonProps 108 + } = props; 100 109 return ( 101 110 <button 102 111 {...buttonProps} 103 112 ref={ref} 104 - className={`m-0 h-max ${fullWidth ? "w-full" : "w-max"} ${compact ? "px-0" : "px-1"} 105 - bg-transparent text-base font-bold text-accent-contrast 106 - flex gap-2 items-center justify-center shrink-0 107 - hover:underline disabled:text-border 108 - ${props.className} 109 - `} 113 + className={` 114 + m-0 h-max 115 + ${fullWidth ? "w-full" : fullWidthOnMobile ? "w-full sm:w-max" : "w-max"} 116 + ${compact ? "py-0 px-1" : "px-2 py-0.5 "} 117 + bg-transparent hover:bg-[var(--accent-light)] 118 + border border-transparent rounded-md hover:border-[var(--accent-light)] 119 + outline outline-transparent focus:outline-[var(--accent-light)] hover:outline-[var(--accent-light)] outline-offset-1 120 + text-base font-bold text-accent-contrast disabled:text-border 121 + flex gap-2 items-center justify-center shrink-0 122 + ${props.className} 123 + `} 110 124 > 111 125 {children} 112 126 </button>
+6 -6
components/HelpPopover.tsx app/[leaflet_id]/actions/HelpButton.tsx
··· 1 1 "use client"; 2 - import { ShortcutKey } from "./Layout"; 3 - import { Media } from "./Media"; 4 - import { Popover } from "./Popover"; 2 + import { ShortcutKey } from "../../../components/Layout"; 3 + import { Media } from "../../../components/Media"; 4 + import { Popover } from "../../../components/Popover"; 5 5 import { metaKey } from "src/utils/metaKey"; 6 - import { useEntitySetContext } from "./EntitySetProvider"; 6 + import { useEntitySetContext } from "../../../components/EntitySetProvider"; 7 7 import { useState } from "react"; 8 8 import { ActionButton } from "components/ActionBar/ActionButton"; 9 - import { HelpSmall } from "./Icons/HelpSmall"; 9 + import { HelpSmall } from "../../../components/Icons/HelpSmall"; 10 10 import { isMac } from "src/utils/isDevice"; 11 11 import { useIsMobile } from "src/hooks/isMobile"; 12 12 13 - export const HelpPopover = (props: { noShortcuts?: boolean }) => { 13 + export const HelpButton = (props: { noShortcuts?: boolean }) => { 14 14 let entity_set = useEntitySetContext(); 15 15 let isMobile = useIsMobile(); 16 16
+17 -19
components/HomeButton.tsx app/[leaflet_id]/actions/HomeButton.tsx
··· 1 1 "use client"; 2 2 import Link from "next/link"; 3 - import { useEntitySetContext } from "./EntitySetProvider"; 3 + import { useEntitySetContext } from "../../../components/EntitySetProvider"; 4 4 import { ActionButton } from "components/ActionBar/ActionButton"; 5 - import { useParams, useSearchParams } from "next/navigation"; 6 - import { useIdentityData } from "./IdentityProvider"; 5 + import { useSearchParams } from "next/navigation"; 6 + import { useIdentityData } from "../../../components/IdentityProvider"; 7 7 import { useReplicache } from "src/replicache"; 8 8 import { addLeafletToHome } from "actions/addLeafletToHome"; 9 - import { useSmoker } from "./Toast"; 10 - import { AddToHomeSmall } from "./Icons/AddToHomeSmall"; 11 - import { HomeSmall } from "./Icons/HomeSmall"; 12 - import { permission } from "process"; 9 + import { useSmoker } from "../../../components/Toast"; 10 + import { AddToHomeSmall } from "../../../components/Icons/AddToHomeSmall"; 11 + import { HomeSmall } from "../../../components/Icons/HomeSmall"; 12 + import { produce } from "immer"; 13 13 14 14 export function HomeButton() { 15 15 let { permissions } = useEntitySetContext(); ··· 47 47 await addLeafletToHome(permission_token.id); 48 48 mutate((identity) => { 49 49 if (!identity) return; 50 - return { 51 - ...identity, 52 - permission_token_on_homepage: [ 53 - ...identity.permission_token_on_homepage, 54 - { 55 - created_at: new Date().toISOString(), 56 - permission_tokens: { 57 - ...permission_token, 58 - leaflets_in_publications: [], 59 - }, 50 + return produce<typeof identity>((draft) => { 51 + draft.permission_token_on_homepage.push({ 52 + created_at: new Date().toISOString(), 53 + archived: null, 54 + permission_tokens: { 55 + ...permission_token, 56 + leaflets_to_documents: [], 57 + leaflets_in_publications: [], 60 58 }, 61 - ], 62 - }; 59 + }); 60 + })(identity); 63 61 }); 64 62 smoker({ 65 63 position: {
+19
components/Icons/AccountTiny.tsx
··· 1 + import { Props } from "./Props"; 2 + 3 + export const AccountTiny = (props: Props) => { 4 + return ( 5 + <svg 6 + width="16" 7 + height="16" 8 + viewBox="0 0 16 16" 9 + fill="none" 10 + xmlns="http://www.w3.org/2000/svg" 11 + {...props} 12 + > 13 + <path 14 + d="M11.9995 11.6042C12.2359 11.3531 12.6319 11.3406 12.8833 11.5768C13.1345 11.8133 13.1469 12.2102 12.9106 12.4616C10.9942 14.4996 8.48343 14.9669 5.82467 14.4899C5.48536 14.4287 5.25917 14.1047 5.31979 13.7653C5.38075 13.4255 5.7066 13.1985 6.04635 13.2594C8.41545 13.6844 10.4511 13.2509 11.9995 11.6042ZM7.40377 1.64517C7.57942 1.34822 7.96315 1.25 8.26022 1.42544C8.55725 1.60111 8.65554 1.98479 8.47995 2.28189L4.62155 8.80923C4.68119 8.84969 4.74613 8.89372 4.81686 8.93716C5.20557 9.17585 5.72696 9.42535 6.30123 9.51724C7.59938 9.72475 8.32429 9.55762 8.60495 9.41959C8.91451 9.26714 9.28927 9.39433 9.44186 9.70376C9.59429 10.0133 9.46707 10.3881 9.15768 10.5407C8.55667 10.8366 7.53939 10.9811 6.10397 10.7516C5.31168 10.6249 4.63266 10.2913 4.16256 10.0026C3.92499 9.85669 3.73326 9.71756 3.60006 9.61392C3.53354 9.56215 3.48092 9.51848 3.44381 9.48697C3.42534 9.47127 3.41058 9.45834 3.39987 9.44888C3.39453 9.44418 3.38953 9.44016 3.3862 9.43716C3.38469 9.43579 3.38337 9.43423 3.38229 9.43326L3.38034 9.43228V9.4313H3.37936C3.16132 9.23186 3.11298 8.90647 3.26315 8.65201L7.40377 1.64517ZM12.4995 2.25259C13.2777 2.19942 13.9584 2.87497 14.019 3.76138C14.0795 4.64775 13.4974 5.40938 12.7192 5.46255C11.941 5.51572 11.2612 4.84018 11.2006 3.95376C11.1401 3.06754 11.7215 2.306 12.4995 2.25259ZM2.08444 2.98501C2.35274 2.19505 3.03678 1.71257 3.61178 1.90787C4.18673 2.1032 4.43574 2.90212 4.16745 3.69205C3.89911 4.48193 3.21507 4.9635 2.6401 4.76822C2.06529 4.57291 1.81644 3.77476 2.08444 2.98501Z" 15 + fill="currentColor" 16 + /> 17 + </svg> 18 + ); 19 + };
+21
components/Icons/ArchiveSmall.tsx
··· 1 + import { Props } from "./Props"; 2 + 3 + export const ArchiveSmall = (props: Props) => { 4 + return ( 5 + <svg 6 + width="24" 7 + height="24" 8 + viewBox="0 0 24 24" 9 + fill="none" 10 + xmlns="http://www.w3.org/2000/svg" 11 + {...props} 12 + > 13 + <path 14 + fillRule="evenodd" 15 + clipRule="evenodd" 16 + d="M14.3935 2.33729C14.4781 2.30741 14.5682 2.29611 14.6576 2.30415C14.7774 2.31514 14.897 2.32836 15.0165 2.34211C15.2401 2.36784 15.5571 2.40755 15.9337 2.46375C16.6844 2.57577 17.6834 2.755 18.6552 3.02334C20.043 3.40654 21.1623 4.08204 21.9307 4.65549C22.3161 4.94319 22.6172 5.20811 22.8237 5.40315C22.9788 5.5496 23.0813 5.6572 23.1271 5.70673C23.3287 5.92633 23.375 6.26081 23.1986 6.51162C23.0315 6.74906 22.723 6.84022 22.4537 6.73167C22.0456 6.56715 21.4938 6.48314 21.0486 6.65428C20.807 6.74717 20.531 6.94113 20.3218 7.3713L20.6009 7.19094C20.7969 7.06426 21.0472 7.05737 21.2499 7.17306C21.4527 7.28875 21.574 7.50775 21.5646 7.74096L21.2277 16.1284C21.2197 16.3285 21.1162 16.5127 20.9494 16.6237L11.9336 22.6232C11.7666 22.7343 11.5564 22.7585 11.3685 22.6883L2.23473 19.2743C2.00112 19.187 1.84179 18.9692 1.82933 18.7201L1.40252 10.1857C1.39041 9.94356 1.5194 9.71628 1.73347 9.60253L2.89319 8.98631C3.19801 8.82434 3.57642 8.94015 3.73838 9.24497C3.8855 9.52184 3.80344 9.85944 3.55872 10.0404L4.46834 10.3669C4.529 10.1684 4.63256 9.64884 4.57793 9.06783C4.51992 8.45086 4.29459 7.8533 3.74994 7.45779C3.09256 6.98978 2.55044 6.51789 2.315 6.27264C2.07596 6.02363 2.08403 5.62799 2.33304 5.38894C2.58204 5.14989 2.97769 5.15797 3.21674 5.40697C3.38499 5.58224 3.87255 6.01278 4.49863 6.45635C5.12762 6.90198 5.83958 7.31975 6.4589 7.5144C7.00579 7.68628 7.7553 7.62969 8.5369 7.43649C9.3015 7.24751 10.0054 6.95105 10.4074 6.74228C10.5756 6.65494 10.7743 6.64864 10.9477 6.72514C12.2233 7.28795 12.9191 8.50607 13.2891 9.66169C13.5067 10.3415 13.6259 11.0415 13.6803 11.6632L15.3414 10.5898C15.3412 10.5032 15.3407 10.4155 15.3403 10.3268C15.3336 9.034 15.3259 7.52674 16.0328 6.1972C15.7338 6.16682 15.3912 6.12949 15.0302 6.08539C13.9285 5.95083 12.5649 5.74352 11.7833 5.45362C11.0189 5.17008 10.3102 4.75223 9.80152 4.41446C9.6696 4.32685 9.54977 4.24371 9.4444 4.16843C9.26969 4.41598 9.11811 4.6909 8.99766 4.9675C8.79907 5.42358 8.71173 5.82238 8.71173 6.05267C8.71173 6.39784 8.43191 6.67767 8.08673 6.67767C7.74155 6.67767 7.46173 6.39784 7.46173 6.05267C7.46173 5.58769 7.61509 5.01162 7.8516 4.46846C8.09203 3.91632 8.44552 3.33542 8.89963 2.8725C9.12701 2.64071 9.4943 2.62192 9.74446 2.82883L9.74577 2.8299C9.80956 2.88191 9.87475 2.93223 9.94039 2.98188C10.0714 3.08094 10.2612 3.21923 10.493 3.37315C10.9612 3.68404 11.5799 4.04492 12.218 4.28164C12.8391 4.512 14.0548 4.70696 15.1817 4.84461C15.7313 4.91174 16.2384 4.96292 16.6084 4.99732C16.8076 5.01584 17.007 5.03362 17.2065 5.04896C17.4444 5.06698 17.6512 5.21883 17.7397 5.44036C17.8282 5.66191 17.7828 5.9145 17.6228 6.09143C16.7171 7.09276 16.6045 8.33681 16.5923 9.78143L18.8039 8.35222C18.7998 8.30706 18.8006 8.26075 18.8068 8.21391C19.0047 6.71062 19.6821 5.84043 20.6001 5.48753C20.6783 5.45746 20.7569 5.4317 20.8356 5.40989C20.1821 4.96625 19.3286 4.50604 18.3225 4.22826C17.4178 3.97844 16.4732 3.80809 15.7493 3.70006C15.3886 3.64625 15.0857 3.60832 14.8736 3.58392C14.8084 3.57642 14.7519 3.57021 14.705 3.56521C14.6894 3.57354 14.6728 3.58282 14.6556 3.59303C14.5489 3.65657 14.4711 3.72644 14.4347 3.7856C14.2538 4.07957 13.8688 4.17123 13.5749 3.99032C13.2809 3.80941 13.1892 3.42445 13.3701 3.13047C13.5575 2.82606 13.8293 2.63024 14.0162 2.51897C14.1352 2.44809 14.2601 2.38531 14.3906 2.33829L14.3921 2.33776L14.3935 2.33729ZM12.4675 12.447C12.4635 11.7846 12.3687 10.8866 12.0986 10.0428C11.8096 9.1402 11.353 8.39584 10.6886 7.99621C10.209 8.21933 9.54785 8.47423 8.83684 8.64998C7.98278 8.86108 6.96103 8.98249 6.08412 8.70689C5.98146 8.67463 5.87826 8.63824 5.77495 8.59834C5.79615 8.71819 5.81166 8.83611 5.82244 8.95081C5.89602 9.73333 5.75996 10.4455 5.64541 10.7895L11.68 12.9559L12.4675 12.447ZM4.77065 13.1487C4.60756 13.0891 4.43494 13.2099 4.43494 13.3835V14.9513C4.43494 15.1613 4.5662 15.3489 4.76351 15.421L8.55169 16.8036C8.71479 16.8631 8.88741 16.7423 8.88741 16.5687V15.001C8.88741 14.7909 8.75614 14.6033 8.55884 14.5313L4.77065 13.1487ZM2.69778 11.0594L11.1256 14.085L11.0552 17.5412C11.0482 17.8863 11.3222 18.1718 11.6673 18.1788C12.0124 18.1859 12.2979 17.9118 12.3049 17.5667L12.3778 13.9933L20.2673 8.89485L19.9915 15.7596L12.2366 20.9201L12.2469 20.4127C12.254 20.0676 11.9799 19.7821 11.6348 19.7751C11.2897 19.768 11.0042 20.0421 10.9972 20.3872L10.9804 21.2088L3.05725 18.2473L2.69778 11.0594Z" 17 + fill="currentColor" 18 + /> 19 + </svg> 20 + ); 21 + };
+1
components/Icons/GoBackSmall.tsx
··· 8 8 viewBox="0 0 24 24" 9 9 fill="none" 10 10 xmlns="http://www.w3.org/2000/svg" 11 + {...props} 11 12 > 12 13 <path 13 14 d="M12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2ZM12.6826 5.96582C12.2921 5.57556 11.659 5.57557 11.2686 5.96582L5.94141 11.293C5.55114 11.6834 5.55114 12.3166 5.94141 12.707L11.2686 18.0332L11.3438 18.1025C11.7365 18.4229 12.3165 18.3993 12.6826 18.0332C13.0484 17.6671 13.0712 17.088 12.751 16.6953L12.6826 16.6191L9.06348 13H17.9473L18.0498 12.9951C18.5538 12.9438 18.9471 12.5175 18.9473 12C18.9472 11.4824 18.5538 11.0563 18.0498 11.0049L17.9473 11H9.06152L12.6826 7.37988C13.0729 6.98941 13.0729 6.35629 12.6826 5.96582Z"
+19
components/Icons/LooseleafSmall.tsx
··· 1 + import { Props } from "./Props"; 2 + 3 + export const LooseLeafSmall = (props: Props) => { 4 + return ( 5 + <svg 6 + width="24" 7 + height="24" 8 + viewBox="0 0 24 24" 9 + fill="none" 10 + xmlns="http://www.w3.org/2000/svg" 11 + {...props} 12 + > 13 + <path 14 + d="M16.5339 4.65788L21.9958 5.24186C22.4035 5.28543 22.7014 5.6481 22.6638 6.05632C22.5159 7.65303 22.3525 9.87767 22.0925 11.9186C21.9621 12.9418 21.805 13.9374 21.6091 14.8034C21.4166 15.6542 21.1733 16.442 20.8454 17.0104C20.1989 18.131 19.0036 18.9569 17.9958 19.4782C17.4793 19.7453 16.9792 19.9495 16.569 20.0827C16.3649 20.1489 16.1724 20.2013 16.0046 20.234C15.8969 20.255 15.7254 20.2816 15.5495 20.2682C15.5466 20.2681 15.5423 20.2684 15.5378 20.2682C15.527 20.2678 15.5112 20.267 15.4919 20.2663C15.4526 20.2647 15.3959 20.2623 15.3239 20.2584C15.1788 20.2506 14.9699 20.2366 14.7116 20.2145C14.1954 20.1703 13.4757 20.0909 12.6598 19.9489C11.0477 19.6681 8.97633 19.1301 7.36198 18.0807C6.70824 17.6557 5.95381 17.064 5.21842 16.4469C5.09798 16.5214 4.97261 16.591 4.81803 16.6706C4.28341 16.9455 3.71779 17.0389 3.17935 16.9137C2.64094 16.7885 2.20091 16.4608 1.89126 16.0231C1.28226 15.1618 1.16463 13.8852 1.5729 12.5514L1.60708 12.4606C1.7005 12.255 1.88295 12.1001 2.10513 12.0436C2.35906 11.9792 2.62917 12.0524 2.81607 12.236L2.82486 12.2448C2.8309 12.2507 2.84033 12.2596 2.8522 12.2712C2.87664 12.295 2.91343 12.3309 2.9606 12.3766C3.05513 12.4682 3.19281 12.6016 3.3649 12.7653C3.70953 13.0931 4.19153 13.5443 4.73795 14.0378C5.84211 15.0349 7.17372 16.1691 8.17937 16.8229C9.53761 17.7059 11.3696 18.2017 12.9177 18.4713C13.6815 18.6043 14.3565 18.679 14.8395 18.7204C15.0804 18.741 15.2731 18.7533 15.404 18.7604C15.4691 18.7639 15.5195 18.7659 15.5524 18.7672C15.5684 18.7679 15.5809 18.7689 15.5886 18.7692H15.5983L15.6374 18.7731C15.6457 18.7724 15.671 18.7704 15.7175 18.7614C15.8087 18.7436 15.9399 18.7095 16.1052 18.6559C16.4345 18.549 16.8594 18.3773 17.3063 18.1461C18.2257 17.6706 19.1147 17.0089 19.5466 16.2604C19.7578 15.8941 19.9618 15.2874 20.1462 14.4723C20.3271 13.6723 20.4767 12.7294 20.6042 11.7292C20.8232 10.0102 20.9711 8.17469 21.1042 6.65397L16.3747 6.14909C15.963 6.10498 15.6648 5.73562 15.7087 5.3239C15.7528 4.91222 16.1222 4.61399 16.5339 4.65788ZM12.0593 13.1315L12.2038 13.1647L12.3776 13.235C12.7592 13.4197 12.9689 13.7541 13.0837 14.0573C13.2089 14.3885 13.2545 14.7654 13.2858 15.0573C13.3144 15.3233 13.3319 15.5214 13.361 15.6774C13.4345 15.6215 13.5233 15.5493 13.6413 15.4479C13.7924 15.318 14.0034 15.1374 14.2429 15.0114C14.4965 14.878 14.8338 14.7772 15.2175 14.8747C15.5354 14.9556 15.7394 15.1539 15.8679 15.3229C15.9757 15.4648 16.0814 15.6631 16.1247 15.736C16.1889 15.8438 16.2218 15.8788 16.239 15.8922C16.2438 15.896 16.2462 15.8979 16.2497 15.8991C16.2541 15.9005 16.2717 15.9049 16.3093 15.9049C16.6541 15.9051 16.934 16.1851 16.9343 16.5299C16.9343 16.875 16.6543 17.1548 16.3093 17.1549C15.9766 17.1549 15.6957 17.0542 15.4694 16.8776C15.2617 16.7153 15.1322 16.5129 15.0505 16.3756C14.9547 16.2147 14.9262 16.1561 14.8815 16.0944C14.8684 16.0989 14.849 16.1051 14.8249 16.1178C14.7289 16.1684 14.6182 16.2555 14.4557 16.3952C14.3175 16.514 14.1171 16.6946 13.9069 16.821C13.6882 16.9524 13.3571 17.0902 12.9684 16.9938C12.4305 16.8602 12.2473 16.3736 12.1764 16.1051C12.1001 15.8159 12.0709 15.4542 12.0427 15.1911C12.0102 14.8884 11.9751 14.662 11.9138 14.4997C11.9011 14.4662 11.8884 14.4403 11.8776 14.4206C11.7899 14.4801 11.6771 14.5721 11.5329 14.7047C11.3855 14.8404 11.181 15.0386 11.0016 15.196C10.8175 15.3575 10.5936 15.5364 10.3512 15.6569C10.19 15.737 9.99118 15.7919 9.77214 15.7594C9.55026 15.7264 9.38367 15.6153 9.27019 15.5045C9.08085 15.3197 8.96362 15.0503 8.91081 14.9391C8.8766 14.8671 8.85074 14.814 8.82585 14.7692C8.541 14.777 8.27798 14.5891 8.20378 14.3014C8.11797 13.9674 8.31907 13.6269 8.653 13.5407L8.79558 13.5124C8.93966 13.4936 9.0875 13.5034 9.23308 13.5485C9.42396 13.6076 9.569 13.7155 9.67449 13.8239C9.85113 14.0055 9.96389 14.244 10.027 14.3776C10.0723 14.3417 10.124 14.3034 10.1774 14.2565C10.3474 14.1073 10.4942 13.9615 10.6862 13.7848C10.8571 13.6276 11.0614 13.4475 11.2731 13.32C11.4428 13.2178 11.7294 13.081 12.0593 13.1315ZM2.84537 14.3366C2.88081 14.6965 2.98677 14.9742 3.11588 15.1569C3.24114 15.334 3.38295 15.4211 3.5192 15.4528C3.63372 15.4794 3.79473 15.4775 4.00553 15.3932C3.9133 15.3109 3.82072 15.2311 3.73209 15.151C3.40947 14.8597 3.10909 14.5828 2.84537 14.3366ZM8.73601 3.86003C9.14672 3.91292 9.43715 4.28918 9.38445 4.69987C9.25964 5.66903 9.14642 7.35598 8.87077 9.02018C8.59001 10.7151 8.11848 12.5766 7.20085 14.1003C6.98712 14.4551 6.52539 14.5698 6.17057 14.3561C5.81623 14.1423 5.70216 13.6814 5.91569 13.3268C6.68703 12.0463 7.121 10.4066 7.39128 8.77506C7.66663 7.11265 7.74965 5.64618 7.89616 4.50847C7.94916 4.09794 8.32546 3.80744 8.73601 3.86003ZM11.7614 8.36784C12.1238 8.21561 12.4973 8.25977 12.8054 8.46452C13.0762 8.64474 13.2601 8.92332 13.3884 9.18912C13.5214 9.46512 13.6241 9.79028 13.7009 10.1354C13.7561 10.3842 13.7827 10.6162 13.8034 10.8044C13.8257 11.0069 13.8398 11.1363 13.864 11.2438C13.8806 11.3174 13.8959 11.3474 13.9011 11.3561C13.9095 11.3609 13.9289 11.3695 13.9655 11.3786C14.0484 11.3991 14.0814 11.3929 14.0895 11.3913C14.1027 11.3885 14.1323 11.3804 14.2028 11.3366C14.3137 11.2677 14.6514 11.0042 15.0563 10.8288L15.1364 10.7985C15.3223 10.7392 15.4987 10.7526 15.6335 10.7838C15.7837 10.8188 15.918 10.883 16.0231 10.9421C16.2276 11.057 16.4458 11.2251 16.613 11.3503C16.8019 11.4917 16.9527 11.5999 17.0827 11.6676C17.1539 11.7047 17.1908 11.7142 17.2009 11.7165L17.2849 11.7047C17.5751 11.6944 17.8425 11.8891 17.9138 12.1823C17.995 12.5174 17.7897 12.8554 17.4548 12.9372C17.0733 13.0299 16.7253 12.8909 16.5046 12.776C16.2705 12.6541 16.042 12.4845 15.864 12.3512C15.6704 12.2064 15.5344 12.1038 15.4216 12.0387C15.2178 12.1436 15.1125 12.2426 14.862 12.3981C14.7283 12.4811 14.5564 12.5716 14.3415 12.6159C14.1216 12.6611 13.8975 12.6501 13.6647 12.5924C13.3819 12.5222 13.1344 12.3858 12.9479 12.1657C12.7701 11.9555 12.689 11.7172 12.6442 11.5182C12.601 11.3259 12.58 11.112 12.5612 10.9411C12.5408 10.7561 12.5194 10.5827 12.4802 10.4059C12.4169 10.1215 12.3411 9.89526 12.2624 9.73209C12.2296 9.66404 12.1981 9.61255 12.1716 9.57487C12.1263 9.61576 12.0615 9.68493 11.9802 9.7985C11.8864 9.92952 11.7821 10.0922 11.6589 10.2838C11.5393 10.4698 11.4043 10.6782 11.2634 10.8786C11.123 11.0782 10.9664 11.2843 10.7975 11.4635C10.633 11.6381 10.4285 11.8185 10.1862 11.9342C9.87476 12.0828 9.50095 11.9507 9.35222 11.6393C9.20377 11.3279 9.33594 10.9551 9.64714 10.8063C9.69148 10.7851 9.77329 10.7282 9.88835 10.6061C9.99931 10.4883 10.1167 10.3365 10.2409 10.1598C10.3647 9.98378 10.4855 9.79617 10.6071 9.60709C10.7249 9.42397 10.8479 9.23258 10.9636 9.07096C11.1814 8.76677 11.4424 8.50191 11.7614 8.36784ZM12.4304 2.81218C13.631 2.81246 14.6042 3.78628 14.6042 4.98698C14.6041 5.39899 14.4869 5.78271 14.2878 6.111L15.0007 6.9069C15.2772 7.21532 15.2515 7.689 14.9431 7.96549C14.6347 8.24164 14.1609 8.21606 13.8845 7.90788L13.1139 7.0485C12.8988 7.11984 12.6695 7.16075 12.4304 7.16081C11.2296 7.16081 10.2558 6.18766 10.2555 4.98698C10.2555 3.7861 11.2295 2.81218 12.4304 2.81218ZM12.4304 4.31218C12.0579 4.31218 11.7555 4.61453 11.7555 4.98698C11.7558 5.35924 12.058 5.66081 12.4304 5.66081C12.8024 5.66053 13.104 5.35907 13.1042 4.98698C13.1042 4.6147 12.8026 4.31246 12.4304 4.31218Z" 15 + fill="currentColor" 16 + /> 17 + </svg> 18 + ); 19 + };
+21
components/Icons/MentionTiny.tsx
··· 1 + import { Props } from "./Props"; 2 + 3 + export const MentionTiny = (props: Props) => { 4 + return ( 5 + <svg 6 + width="16" 7 + height="16" 8 + viewBox="0 0 16 16" 9 + fill="none" 10 + xmlns="http://www.w3.org/2000/svg" 11 + {...props} 12 + > 13 + <path 14 + fillRule="evenodd" 15 + clipRule="evenodd" 16 + d="M8.7548 1.48131C8.37164 1.12732 7.78076 1.12733 7.39761 1.48131L6.31216 2.48412C6.11579 2.66553 5.85496 2.76077 5.58789 2.74856L4.02324 2.67702C3.42714 2.64976 2.93999 3.14727 2.97979 3.74267L3.04008 4.64469C3.06279 4.98446 2.91104 5.31244 2.63737 5.51507L1.84867 6.09903C1.30874 6.4988 1.30874 7.30663 1.84867 7.7064L2.63737 8.29036C2.91104 8.49299 3.06279 8.82097 3.04008 9.16074L2.97979 10.0628C2.93999 10.6582 3.42714 11.1557 4.02324 11.1284L5.58789 11.0569C5.85496 11.0447 6.11579 11.1399 6.31216 11.3213L7.39761 12.3241C7.78076 12.6781 8.37165 12.6781 8.7548 12.3241L9.84025 11.3213C9.8673 11.2963 9.89557 11.273 9.92492 11.2513C10.379 11.5423 10.9394 11.8764 11.3808 12.0072C12.1456 12.2339 12.9198 12.3728 13.6513 12.2853C14.4861 12.1855 14.9021 12.0899 15.3797 11.8006C14.3597 11.4989 13.8748 11.0143 13.4688 10.4865C13.2705 10.2287 13.1568 9.97205 13.0619 9.71255L12.8396 9.00919C12.8217 8.74234 13.0797 8.57625 13.3239 8.41908C13.3906 8.37613 13.4563 8.33385 13.515 8.29036L14.3037 7.7064C14.8437 7.30663 14.8437 6.4988 14.3037 6.09903L13.515 5.51507C13.2414 5.31244 13.0896 4.98447 13.1123 4.6447L13.1726 3.74267C13.2124 3.14727 12.7253 2.64976 12.1292 2.67702L10.5645 2.74856C10.2975 2.76077 10.0366 2.66553 9.84025 2.48412L8.7548 1.48131ZM0.724555 8.54935C0.893092 8.33061 1.20705 8.2899 1.42579 8.45844L1.95221 8.86403C2.17095 9.03256 2.21166 9.34652 2.04312 9.56526C1.87458 9.78401 1.56063 9.82471 1.34188 9.65618L0.815467 9.25059C0.596721 9.08206 0.556018 8.7681 0.724555 8.54935ZM2.22796 11.7918C2.40295 11.5782 2.71798 11.5469 2.9316 11.7219C3.05506 11.823 3.21912 11.8981 3.3887 11.9473C3.55945 11.9968 3.70151 12.0103 3.75507 12.0103C4.04231 12.0103 4.26845 11.9927 4.45796 11.9729C4.51258 11.9672 4.56967 11.9606 4.62708 11.954C4.75105 11.9396 4.8765 11.9251 4.98174 11.9192C5.15574 11.9095 5.34286 11.9165 5.54475 11.9825C5.74373 12.0475 5.92168 12.158 6.10009 12.3035C6.28301 12.4526 6.47827 12.6379 6.65933 12.8097C6.70174 12.8499 6.74338 12.8894 6.7839 12.9276C7.01119 13.1415 7.20968 13.3186 7.38302 13.4332C7.50037 13.5108 7.73051 13.5859 8.01215 13.6062C8.29295 13.6264 8.53643 13.5857 8.67785 13.5166C8.81401 13.4501 8.97286 13.3418 9.18171 13.1869C9.235 13.1474 9.2917 13.1048 9.35123 13.06L9.3513 13.0599L9.35133 13.0599C9.51197 12.9391 9.69328 12.8027 9.88425 12.6704C10.0346 12.5664 10.2298 12.5526 10.3932 12.6347C11.1162 12.9977 11.6692 13.1581 12.1996 13.2235C12.7423 13.2903 13.2802 13.261 14.0061 13.217C14.2817 13.2003 14.5187 13.4102 14.5354 13.6858C14.5521 13.9615 14.3422 14.1984 14.0666 14.2151C13.3566 14.2582 12.7265 14.2959 12.0773 14.216C11.492 14.1438 10.9056 13.9787 10.2184 13.6606C10.133 13.7233 10.0503 13.7855 9.96791 13.8475L9.96771 13.8477C9.90441 13.8953 9.84129 13.9428 9.77726 13.9902C9.56766 14.1456 9.34378 14.3043 9.11669 14.4152C8.76159 14.5886 8.32363 14.6312 7.94034 14.6036C7.55787 14.5761 7.14126 14.4722 6.83145 14.2673C6.57763 14.0995 6.32221 13.8663 6.09857 13.6558C6.05019 13.6103 6.00322 13.5657 5.9575 13.5224L5.95731 13.5222L5.9573 13.5222C5.77867 13.3528 5.61903 13.2015 5.46819 13.0785C5.34858 12.981 5.27842 12.9475 5.23423 12.933C5.19296 12.9196 5.14185 12.9118 5.03761 12.9176C4.96094 12.9219 4.88552 12.9308 4.78482 12.9425L4.78461 12.9426L4.78437 12.9426C4.72196 12.9499 4.64983 12.9583 4.56169 12.9675C4.34798 12.9898 4.08593 13.0103 3.75507 13.0103C3.59643 13.0103 3.36037 12.9803 3.11004 12.9077C2.85853 12.8347 2.55853 12.7089 2.29792 12.4955C2.0843 12.3205 2.05298 12.0054 2.22796 11.7918ZM7.36287 5.60901L7.868 7.84218H8.29336L8.79849 5.60901V3.81006H7.36287V5.60901ZM8.89597 9.99561V8.4182H7.25653V9.99561H8.89597Z" 17 + fill="currentColor" 18 + /> 19 + </svg> 20 + ); 21 + };
+17
components/Icons/NotificationSmall.tsx
··· 26 26 viewBox="0 0 24 24" 27 27 fill="none" 28 28 xmlns="http://www.w3.org/2000/svg" 29 + > 30 + <path 31 + d="M12.3779 0.890636C13.5297 0.868361 14.2312 1.35069 14.6104 1.8047C15.1942 2.50387 15.2636 3.34086 15.2129 3.95314C17.7074 4.96061 18.8531 7.45818 19.375 10.3975C19.5903 11.1929 20.0262 11.5635 20.585 11.9336C21.1502 12.3079 22.0847 12.7839 22.5879 13.7998C23.4577 15.556 22.8886 17.8555 20.9297 19.083C20.1439 19.5754 19.2029 20.1471 17.8496 20.5869C17.1962 20.7993 16.454 20.9768 15.5928 21.1055C15.2068 22.4811 13.9287 23.4821 12.4238 23.4824C10.9225 23.4824 9.64464 22.4867 9.25489 21.1162C8.37384 20.9871 7.61998 20.8046 6.95899 20.5869C5.62158 20.1464 4.69688 19.5723 3.91602 19.083C1.95717 17.8555 1.38802 15.556 2.25782 13.7998C2.76329 12.7794 3.60199 12.3493 4.18653 12.0068C4.7551 11.6737 5.1753 11.386 5.45606 10.7432C5.62517 9.31217 5.93987 8.01645 6.4668 6.92482C7.1312 5.54855 8.13407 4.49633 9.56251 3.92482C9.53157 3.34709 9.6391 2.63284 10.1133 1.98927C10.1972 1.87543 10.4043 1.594 10.7822 1.34669C11.1653 1.09611 11.6872 0.904101 12.3779 0.890636ZM14.1709 21.2608C13.6203 21.3007 13.0279 21.3242 12.3887 21.3242C11.7757 21.3242 11.2072 21.3024 10.6777 21.2656C11.0335 21.8421 11.6776 22.2324 12.4238 22.2324C13.1718 22.2321 13.816 21.8396 14.1709 21.2608ZM12.4004 2.38966C11.9872 2.39776 11.7419 2.50852 11.5996 2.60157C11.4528 2.6977 11.3746 2.801 11.3193 2.87599C11.088 3.19 11.031 3.56921 11.0664 3.92677C11.084 4.10311 11.1233 4.258 11.1631 4.37013C11.1875 4.43883 11.205 4.47361 11.21 4.48341C11.452 4.78119 11.4299 5.22068 11.1484 5.49415C10.8507 5.78325 10.3748 5.77716 10.0869 5.48048C10.0533 5.44582 10.0231 5.40711 9.99415 5.3672C9.0215 5.79157 8.31886 6.53162 7.81641 7.57228C7.21929 8.80941 6.91013 10.4656 6.82129 12.4746L6.81934 12.5137L6.81446 12.5518C6.73876 13.0607 6.67109 13.5103 6.53418 13.9121C6.38567 14.3476 6.16406 14.7061 5.82032 15.0899C5.54351 15.3988 5.06973 15.4268 4.76172 15.1514C4.45392 14.8758 4.42871 14.4019 4.70508 14.0928C4.93763 13.8332 5.04272 13.6453 5.11524 13.4326C5.14365 13.3492 5.16552 13.2588 5.18848 13.1553C5.10586 13.2062 5.02441 13.2544 4.94532 13.3008C4.28651 13.6868 3.87545 13.9129 3.60157 14.4658C3.08548 15.5082 3.38433 16.9793 4.71192 17.8115C5.4776 18.2913 6.27423 18.7818 7.42872 19.1621C8.58507 19.543 10.1358 19.8242 12.3887 19.8242C14.6416 19.8242 16.2108 19.5429 17.3857 19.1611C18.5582 18.7801 19.3721 18.2882 20.1328 17.8115C21.4611 16.9793 21.7595 15.5084 21.2432 14.4658C20.9668 13.9081 20.515 13.6867 19.7568 13.1846C19.7553 13.1835 19.7535 13.1827 19.752 13.1817C19.799 13.3591 19.8588 13.5202 19.9287 13.6514C20.021 13.8244 20.1034 13.8927 20.1533 13.917C20.5249 14.0981 20.6783 14.5465 20.4961 14.919C20.3135 15.2913 19.8639 15.4467 19.4922 15.2656C19.0607 15.0553 18.7821 14.6963 18.6035 14.3613C18.4238 14.0242 18.3154 13.6559 18.2471 13.3379C18.1778 13.0155 18.1437 12.7147 18.127 12.4971C18.1185 12.3873 18.1145 12.2956 18.1123 12.2305C18.1115 12.2065 18.1107 12.1856 18.1104 12.169C18.0569 11.6585 17.9885 11.1724 17.9082 10.7109C17.9002 10.6794 17.8913 10.6476 17.8838 10.6152L17.8906 10.6133C17.4166 7.97573 16.4732 6.17239 14.791 5.40821C14.5832 5.64607 14.2423 5.73912 13.9365 5.61036C13.5557 5.44988 13.3777 5.01056 13.5391 4.62892C13.5394 4.62821 13.5397 4.62699 13.54 4.62599C13.5425 4.61977 13.5479 4.6087 13.5537 4.59278C13.5658 4.55999 13.5837 4.50758 13.6035 4.44142C13.6438 4.30713 13.6903 4.12034 13.7139 3.91212C13.7631 3.47644 13.7038 3.06402 13.457 2.76857C13.3434 2.63264 13.0616 2.37678 12.4004 2.38966ZM10.1055 16.625C11.6872 16.8411 12.8931 16.8585 13.8174 16.7539C14.2287 16.7076 14.5997 17.0028 14.6465 17.4141C14.693 17.8256 14.3969 18.1976 13.9854 18.2442C12.9038 18.3665 11.5684 18.3389 9.90235 18.1113C9.49223 18.0551 9.20488 17.6768 9.26075 17.2666C9.3168 16.8563 9.6952 16.5691 10.1055 16.625ZM16.3887 16.3047C16.7403 16.086 17.203 16.1935 17.4219 16.5449C17.6406 16.8967 17.5324 17.3594 17.1807 17.5781C16.9689 17.7097 16.6577 17.8424 16.4033 17.9131C16.0045 18.0237 15.5914 17.7904 15.4805 17.3916C15.3696 16.9926 15.6031 16.5788 16.002 16.4678C16.1344 16.431 16.3112 16.3527 16.3887 16.3047Z" 32 + fill="currentColor" 33 + /> 34 + </svg> 35 + ); 36 + }; 37 + 38 + export const ReaderUnread = (props: Props) => { 39 + return ( 40 + <svg 41 + width="24" 42 + height="24" 43 + viewBox="0 0 24 24" 44 + fill="none" 45 + xmlns="http://www.w3.org/2000/svg" 29 46 {...props} 30 47 > 31 48 <path
+20
components/Icons/ReplyTiny.tsx
··· 1 + import { Props } from "./Props"; 2 + 3 + export const ReplyTiny = (props: Props) => { 4 + return ( 5 + <svg 6 + width="16" 7 + height="16" 8 + viewBox="0 0 16 16" 9 + fill="none" 10 + xmlns="http://www.w3.org/2000/svg" 11 + > 12 + <path 13 + fillRule="evenodd" 14 + clipRule="evenodd" 15 + d="M10.7767 3.01749C11.6289 3.39627 12.1593 3.79765 12.4801 4.2201C12.7868 4.62405 12.9578 5.12048 12.9578 5.81175C12.9578 6.45434 12.7165 7.17288 12.2111 7.72195C11.7245 8.25058 10.9456 8.67427 9.75117 8.67427L4.45638 8.67427L6.97173 6.15892C7.36226 5.7684 7.36226 5.13523 6.97173 4.74471C6.58121 4.35418 5.94804 4.35418 5.55752 4.74471L1.33513 8.9671C0.944605 9.35762 0.944605 9.99079 1.33513 10.3813L5.55752 14.6037C5.94804 14.9942 6.58121 14.9942 6.97173 14.6037C7.36226 14.2132 7.36226 13.58 6.97173 13.1895L4.45652 10.6743L9.75117 10.6743C11.4697 10.6743 12.7941 10.0416 13.6826 9.07646C14.5522 8.13173 14.9578 6.91901 14.9578 5.81175C14.9578 4.75316 14.6829 3.81405 14.073 3.01069C13.4771 2.22581 12.62 1.64809 11.589 1.18986C11.0843 0.965558 10.4933 1.19285 10.269 1.69754C10.0447 2.20222 10.272 2.79318 10.7767 3.01749Z" 16 + fill="currentColor" 17 + /> 18 + </svg> 19 + ); 20 + };
-21
components/Icons/TemplateRemoveSmall.tsx
··· 1 - import { Props } from "./Props"; 2 - 3 - export const TemplateRemoveSmall = (props: Props) => { 4 - return ( 5 - <svg 6 - width="24" 7 - height="24" 8 - viewBox="0 0 24 24" 9 - fill="none" 10 - xmlns="http://www.w3.org/2000/svg" 11 - {...props} 12 - > 13 - <path 14 - fillRule="evenodd" 15 - clipRule="evenodd" 16 - d="M21.6598 1.22969C22.0503 0.839167 22.6835 0.839167 23.074 1.22969C23.4646 1.62021 23.4646 2.25338 23.074 2.6439L21.9991 3.71887C22 3.72121 22.001 3.72355 22.002 3.7259L21.0348 4.69374C21.0347 4.69033 21.0345 4.68693 21.0344 4.68353L17.2882 8.42972L17.2977 8.43313L16.3813 9.35011L16.3714 9.34656L15.5955 10.1224L15.6058 10.1261L14.6894 11.0431L14.6787 11.0393L14.3959 11.3221L14.4067 11.326L13.4903 12.2429L13.479 12.2389L12.8919 12.8261L12.9034 12.8302L10.2156 15.5198L10.2028 15.5152L9.35969 16.3583C9.36255 16.3614 9.36541 16.3645 9.36826 16.3676L7.20585 18.5314C7.19871 18.5321 7.19159 18.5328 7.18448 18.5335L6.26611 19.4519C6.27069 19.4539 6.27528 19.4559 6.27989 19.4579L5.40679 20.3316C5.40244 20.3291 5.39809 20.3267 5.39376 20.3242L2.54817 23.1698C2.15765 23.5603 1.52448 23.5603 1.13396 23.1698C0.743434 22.7793 0.743433 22.1461 1.13396 21.7556L4.57518 18.3144C4.5862 18.296 4.59778 18.2779 4.6099 18.2599C4.72342 18.0917 4.86961 17.964 5.02393 17.8656L6.39488 16.4947C6.25376 16.4822 6.10989 16.4734 5.96441 16.4685C5.20904 16.4433 4.461 16.5264 3.88183 16.7201C3.2818 16.9207 2.99485 17.1912 2.91069 17.4452C2.80892 17.7525 2.47737 17.919 2.17013 17.8173C1.8629 17.7155 1.69634 17.3839 1.79811 17.0767C2.05627 16.2973 2.78206 15.852 3.51019 15.6085C4.2592 15.3581 5.15477 15.2689 6.00346 15.2972C6.48903 15.3133 6.97583 15.3686 7.42782 15.4617L8.11942 14.7701L7.89431 14.6896C7.7838 14.6501 7.69213 14.5705 7.63742 14.4667L5.91365 11.1952C5.86162 11.0964 5.84836 10.9944 5.86434 10.9002L5.85245 10.9196L5.11563 9.4308C4.96523 9.11293 5.04515 8.78343 5.24544 8.56361L5.25054 8.55806C5.25749 8.55058 5.26457 8.54323 5.2718 8.53601L6.43022 7.3457C6.6445 7.11834 6.97346 7.03892 7.26837 7.14439L9.80363 8.05107L12.9624 7.10485C13.1067 7.02062 13.2859 6.99834 13.4555 7.05901L14.4322 7.40831C14.7942 6.69891 14.93 5.89897 15.0777 5.02873L15.0777 5.02872L15.0958 4.9222C15.2586 3.96572 15.4529 2.86736 16.1798 2.04515C17.0056 1.11114 18.7307 0.837125 20.2663 1.83615C20.4285 1.94168 20.5821 2.05061 20.7266 2.16294L21.6598 1.22969ZM19.8899 2.99965C19.8075 2.93935 19.72 2.87895 19.6271 2.81856C18.4897 2.07854 17.4326 2.39759 17.0579 2.82147C16.5869 3.3541 16.4234 4.10723 16.2512 5.11887L16.2231 5.28522L16.2231 5.28523C16.1304 5.83581 16.0274 6.44661 15.8342 7.05527L19.8899 2.99965ZM14.288 8.60148L13.2682 8.23675L11.6654 8.71688L13.5122 9.37736L14.288 8.60148ZM12.5953 10.2942L9.59692 9.22187L9.58424 9.21734L7.10654 8.33124L6.82935 8.61605L12.3125 10.577L12.5953 10.2942ZM11.3957 11.4938L6.56005 9.76447L6.04788 10.6006C6.16458 10.5123 6.32269 10.4767 6.48628 10.5352L10.8085 12.081L11.3957 11.4938ZM17.0099 12.2569L16.2294 11.9778L15.313 12.8948L16.8798 13.4551L18.7426 16.9905L18.0747 17.8398L19.1912 18.2615C19.6607 18.4294 20.1033 18.1358 20.2179 17.728L20.7391 16.3648C20.824 16.1511 20.8112 15.9108 20.7039 15.7071L19.124 12.7086L18.8949 11.321L18.8931 11.3104L18.8904 11.2969C18.8874 11.234 18.8742 11.1705 18.8497 11.1087L18.3522 9.8537L16.5121 11.6949L16.5482 11.7078L16.5582 11.7115L17.1419 11.9202L17.0099 12.2569ZM12.0382 16.1716L14.7261 13.482L16.0553 13.9574C16.1658 13.9969 16.2575 14.0764 16.3122 14.1803L18.0359 17.4518C18.2352 17.83 17.8658 18.2557 17.4633 18.1118L12.0382 16.1716ZM8.44038 19.7717L7.26492 20.9479C7.80247 21.0274 8.35468 21.0252 8.82243 20.8811C9.24804 20.7499 9.52382 20.5096 9.73008 20.285C9.79978 20.2091 9.87046 20.1246 9.92979 20.0536L9.92981 20.0536L9.92999 20.0534L9.9306 20.0527C9.95072 20.0286 9.96953 20.0061 9.98653 19.9861C10.0618 19.8973 10.1248 19.8281 10.1905 19.7694C10.307 19.6651 10.4472 19.579 10.6908 19.5395C10.9182 19.5027 11.2529 19.5041 11.7567 19.6004C11.6943 19.6815 11.6359 19.764 11.5823 19.8476C11.3276 20.2439 11.1352 20.7322 11.2038 21.2293C11.3097 21.9955 11.8139 22.4463 12.3522 22.6544C12.8626 22.8518 13.4377 22.8513 13.8631 22.731C14.7279 22.4863 15.6213 21.724 15.4107 20.664C15.3105 20.1591 14.9656 19.7211 14.4516 19.3701C14.3677 19.3128 14.2783 19.2571 14.1833 19.203C14.5987 19.0436 14.9889 19.0051 15.2828 19.1025C15.59 19.2042 15.9215 19.0377 16.0233 18.7304C16.1251 18.4232 15.9585 18.0916 15.6513 17.9899C14.6724 17.6656 13.5751 18.0821 12.7766 18.6397C12.6141 18.5938 12.4436 18.5504 12.265 18.5097C11.5394 18.3444 10.9698 18.307 10.5035 18.3825C10.018 18.4612 9.67586 18.657 9.40877 18.8961C9.28262 19.009 9.17853 19.1268 9.09296 19.2277C9.06342 19.2625 9.03731 19.2937 9.0131 19.3227L9.01295 19.3228C8.9605 19.3856 8.91697 19.4377 8.86686 19.4922C8.73917 19.6313 8.63185 19.7134 8.47726 19.761C8.46519 19.7648 8.45289 19.7683 8.44038 19.7717ZM12.5683 20.4811C12.3863 20.7644 12.3505 20.965 12.3648 21.0689C12.4003 21.3259 12.5445 21.4722 12.7749 21.5613C13.0331 21.6611 13.3469 21.659 13.544 21.6032C14.1554 21.4302 14.2952 21.0637 14.2612 20.8923C14.2391 20.7814 14.1422 20.578 13.7907 20.338C13.6005 20.2082 13.347 20.076 13.0173 19.9508C12.8341 20.1242 12.681 20.3057 12.5683 20.4811Z" 17 - fill="currentColor" 18 - /> 19 - </svg> 20 - ); 21 - };
-25
components/Icons/TemplateSmall.tsx
··· 1 - import { Props } from "./Props"; 2 - 3 - export const TemplateSmall = (props: Props & { fill?: string }) => { 4 - return ( 5 - <svg 6 - width="24" 7 - height="24" 8 - viewBox="0 0 24 24" 9 - fill="none" 10 - xmlns="http://www.w3.org/2000/svg" 11 - {...props} 12 - > 13 - <path 14 - d="M14.1876 3.5073C14.3657 2.68428 14.8409 1.80449 15.1974 1.39941L15.2085 1.38682C15.5258 1.02605 16.1664 0.297788 17.7348 0.0551971C19.7272 -0.252968 22.338 1.22339 23.1781 3.53026C23.9464 5.63998 22.4863 7.65134 21.1778 8.49107C20.443 8.96256 19.8776 9.29865 19.5389 9.6655C19.6381 9.88024 19.8755 10.4623 19.9945 10.8588C20.1304 11.312 20.1356 11.8263 20.2444 12.3342C20.6412 13.1008 21.4615 14.6122 21.6483 14.9894C21.9441 15.5868 22.0637 16.0554 21.901 16.59C21.7793 16.99 21.3809 18.0037 21.2098 18.4064C21.1134 18.6333 20.6741 19.1794 20.165 19.3516C19.5207 19.5694 19.2 19.533 18.2867 19.1682C17.9231 19.3768 17.3068 19.3194 17.0874 19.2128C16.9902 19.5392 16.6234 19.8695 16.4353 20.0055C16.5008 20.1749 16.6684 20.619 16.5759 21.4191C16.4257 22.7176 14.6119 24.4819 12.2763 23.8544C10.5744 23.3971 10.2099 22.1002 10.0744 21.5462C8.16651 22.8209 5.74592 21.9772 4.43632 21.1133C3.44653 20.4603 3.16063 19.4467 3.2199 18.7888C2.57837 19.147 1.33433 19.2159 0.756062 17.9729C0.320217 17.036 0.838862 15.6535 2.49397 14.7706C3.56898 14.1971 5.01017 14.061 6.14456 14.136C5.47545 12.9417 4.17774 10.4051 3.97777 9.74456C3.72779 8.91889 3.94746 8.3129 4.30348 7.88113C4.6595 7.44936 5.21244 6.90396 5.75026 6.38129C6.28808 5.85862 7.06074 5.85862 7.7349 6.07072C8.27424 6.2404 9.36352 6.65146 9.84074 6.83578C10.5069 6.63086 11.9689 6.18102 12.4877 6.02101C13.0065 5.861 13.184 5.78543 13.7188 5.90996C13.8302 5.37643 14.0045 4.35336 14.1876 3.5073Z" 15 - fill={props.fill || "transparent"} 16 - /> 17 - <path 18 - fillRule="evenodd" 19 - clipRule="evenodd" 20 - d="M19.6271 2.81856C18.4896 2.07854 17.4326 2.39759 17.0578 2.82147C16.5869 3.3541 16.4234 4.10723 16.2512 5.11887L16.2231 5.28522L16.2231 5.28523C16.0919 6.06363 15.9405 6.96241 15.5423 7.80533L17.4557 8.48962C18.0778 7.71969 18.7304 7.28473 19.2974 6.92363L19.3687 6.87829C20.0258 6.46022 20.473 6.17579 20.7913 5.5972C21.0667 5.09643 21.0978 4.64884 20.9415 4.23092C20.7767 3.79045 20.3738 3.3044 19.6271 2.81856ZM15.0777 5.02873C14.9299 5.89897 14.7941 6.69891 14.4321 7.4083L13.4555 7.05901C13.2858 6.99834 13.1067 7.02061 12.9624 7.10485L9.80359 8.05107L7.26833 7.14438C6.97342 7.03892 6.64447 7.11834 6.43018 7.3457L5.27176 8.53601C5.26453 8.54323 5.25745 8.55058 5.2505 8.55806L5.2454 8.56361C5.04511 8.78343 4.9652 9.11292 5.1156 9.43079L5.85241 10.9196L5.8643 10.9002C5.84832 10.9944 5.86158 11.0964 5.91361 11.1952L7.63738 14.4667C7.6921 14.5705 7.78376 14.6501 7.89428 14.6896L17.4633 18.1118C17.8658 18.2557 18.2352 17.83 18.0359 17.4518L16.3121 14.1803C16.2574 14.0764 16.1657 13.9969 16.0552 13.9574L6.48624 10.5352C6.32266 10.4767 6.16454 10.5123 6.04784 10.6006L6.56002 9.76447L16.8798 13.4551L18.7426 16.9905L18.0747 17.8398L19.1912 18.2615C19.6606 18.4294 20.1033 18.1358 20.2179 17.728L20.7391 16.3648C20.8239 16.1511 20.8112 15.9108 20.7039 15.7071L19.124 12.7086L18.8949 11.321C18.8935 11.3129 18.892 11.3049 18.8904 11.2969C18.8874 11.234 18.8741 11.1705 18.8496 11.1087L18.1936 9.45372C18.7455 8.68856 19.3357 8.28878 19.927 7.9122C19.9681 7.88603 20.0096 7.85977 20.0514 7.83331C20.6663 7.44436 21.3511 7.01112 21.8182 6.16211C22.2345 5.40522 22.3314 4.60167 22.0392 3.82037C21.7555 3.06161 21.1334 2.40034 20.2662 1.83615C18.7307 0.837123 17.0056 1.11114 16.1798 2.04515C15.4528 2.86736 15.2586 3.96572 15.0958 4.92219L15.0777 5.02872L15.0777 5.02873ZM13.2681 8.23675L11.6653 8.71688L16.3567 10.3947L16.6254 9.4374L13.2681 8.23675ZM16.5481 11.7078L16.5582 11.7114L17.1419 11.9202L17.0098 12.2569L6.82932 8.61605L7.1065 8.33124L9.5842 9.21734L9.59688 9.22187L16.5481 11.7078ZM12.5683 20.4811C12.3863 20.7644 12.3505 20.965 12.3648 21.0689C12.4003 21.3259 12.5444 21.4722 12.7748 21.5613C13.0331 21.6611 13.3469 21.659 13.544 21.6032C14.1553 21.4302 14.2952 21.0637 14.2611 20.8923C14.2391 20.7814 14.1421 20.578 13.7906 20.338C13.6004 20.2082 13.3469 20.076 13.0173 19.9508C12.834 20.1242 12.681 20.3057 12.5683 20.4811ZM11.7567 19.6004C11.6942 19.6815 11.6359 19.764 11.5822 19.8476C11.3276 20.2439 11.1351 20.7322 11.2038 21.2293C11.3096 21.9955 11.8139 22.4463 12.3521 22.6544C12.8626 22.8518 13.4377 22.8513 13.863 22.731C14.7279 22.4863 15.6213 21.724 15.4107 20.664C15.3104 20.1591 14.9656 19.7211 14.4515 19.3701C14.3677 19.3128 14.2783 19.2571 14.1833 19.203C14.5987 19.0436 14.9889 19.0051 15.2827 19.1025C15.59 19.2042 15.9215 19.0377 16.0233 18.7304C16.125 18.4232 15.9585 18.0916 15.6513 17.9899C14.6724 17.6656 13.5751 18.0821 12.7766 18.6397C12.6141 18.5938 12.4436 18.5504 12.265 18.5097C11.5393 18.3444 10.9698 18.307 10.5034 18.3825C10.018 18.4612 9.67582 18.657 9.40873 18.8961C9.28258 19.009 9.17849 19.1268 9.09292 19.2277C9.06338 19.2625 9.03727 19.2937 9.01306 19.3227L9.01291 19.3228C8.96046 19.3856 8.91693 19.4377 8.86682 19.4922C8.73913 19.6313 8.63181 19.7134 8.47722 19.761C8.03942 19.896 7.30137 19.8237 6.60705 19.5851C6.27195 19.4699 5.98787 19.3293 5.79222 19.1916C5.64379 19.0871 5.59428 19.019 5.58047 19L5.58045 19C5.57827 18.997 5.57698 18.9952 5.57634 18.9947C5.57144 18.9579 5.57397 18.938 5.57539 18.9305C5.57674 18.9233 5.57829 18.9201 5.58128 18.9156C5.59031 18.9023 5.63142 18.8546 5.76375 18.7965C6.04383 18.6735 6.48291 18.6061 7.03421 18.5487C7.12534 18.5392 7.22003 18.5299 7.31675 18.5205L7.31734 18.5205L7.31774 18.5204C7.75337 18.478 8.22986 18.4315 8.60602 18.3399C8.83695 18.2837 9.10046 18.1956 9.31444 18.0333C9.55604 17.8501 9.73703 17.5659 9.72457 17.1949C9.71117 16.7955 9.50249 16.4807 9.2559 16.2553C9.01235 16.0327 8.69774 15.863 8.36729 15.7333C7.70363 15.4729 6.85166 15.3254 6.00343 15.2972C5.15473 15.2689 4.25916 15.3581 3.51015 15.6085C2.78202 15.852 2.05623 16.2973 1.79807 17.0767C1.6963 17.3839 1.86287 17.7155 2.1701 17.8173C2.47733 17.919 2.80889 17.7525 2.91065 17.4452C2.99481 17.1912 3.28176 16.9207 3.8818 16.7201C4.46096 16.5264 5.209 16.4433 5.96437 16.4685C6.7202 16.4937 7.43275 16.6256 7.93908 16.8243C8.19363 16.9243 8.36538 17.0292 8.46519 17.1204C8.4773 17.1315 8.4878 17.1419 8.49689 17.1515C8.45501 17.1668 8.39992 17.1838 8.3287 17.2012C8.04154 17.2711 7.67478 17.3072 7.24492 17.3496L7.24413 17.3497L7.24246 17.3498C7.13635 17.3603 7.02639 17.3711 6.91284 17.3829C6.38763 17.4376 5.76632 17.5153 5.29238 17.7234C5.0477 17.8309 4.78839 17.9954 4.60986 18.2599C4.42009 18.541 4.36482 18.8707 4.42432 19.213C4.49899 19.6426 4.83826 19.9534 5.11763 20.15C5.42736 20.368 5.81812 20.5533 6.22607 20.6935C7.01783 20.9656 8.03865 21.1226 8.82239 20.8811C9.248 20.7499 9.52379 20.5096 9.73004 20.285C9.79974 20.2091 9.87042 20.1246 9.92975 20.0536L9.92977 20.0536L9.92995 20.0534C9.9503 20.0291 9.96932 20.0063 9.98649 19.9861C10.0618 19.8973 10.1248 19.8281 10.1905 19.7694C10.3069 19.6651 10.4472 19.579 10.6908 19.5395C10.9181 19.5027 11.2529 19.5041 11.7567 19.6004Z" 21 - fill="currentColor" 22 - /> 23 - </svg> 24 - ); 25 - };
+19
components/Icons/UnpublishSmall.tsx
··· 1 + import { Props } from "./Props"; 2 + 3 + export const UnpublishSmall = (props: Props) => { 4 + return ( 5 + <svg 6 + width="24" 7 + height="24" 8 + viewBox="0 0 24 24" 9 + fill="none" 10 + xmlns="http://www.w3.org/2000/svg" 11 + {...props} 12 + > 13 + <path 14 + d="M15.5207 11.5526C15.9624 11.2211 16.5896 11.3101 16.9211 11.7518L18.9162 14.411L21.5754 12.4158C22.017 12.0845 22.6433 12.1735 22.9748 12.6151C23.306 13.0568 23.2172 13.684 22.7756 14.0155L20.1164 16.0106L22.1115 18.6698C22.4425 19.1114 22.3537 19.7378 21.9123 20.0692C21.4707 20.4006 20.8434 20.3114 20.5119 19.87L18.5168 17.2108L15.8576 19.2059C15.416 19.537 14.7897 19.4479 14.4582 19.0067C14.1267 18.565 14.2158 17.9378 14.6574 17.6063L17.3166 15.6112L15.3215 12.952C14.9902 12.5103 15.0792 11.8841 15.5207 11.5526ZM12.2062 4.29378C13.7932 3.59008 15.5128 3.49569 16.9767 4.29769C19.1391 5.48261 19.9471 8.15954 19.5314 10.8885C19.4793 11.2296 19.1606 11.4638 18.8195 11.4119C18.4786 11.3598 18.2444 11.042 18.2961 10.701C18.669 8.25384 17.8985 6.22855 16.3761 5.39436C15.5192 4.92484 14.4833 4.85746 13.4006 5.1805C13.3522 5.21491 13.3004 5.24633 13.2414 5.26644C13.0411 5.33451 12.8498 5.39707 12.6662 5.45686C12.6176 5.47894 12.5684 5.50065 12.5197 5.52425C11.1279 6.19898 9.77207 7.47892 8.81657 9.22249C7.86108 10.9662 7.51225 12.7985 7.69254 14.3348C7.87314 15.8723 8.57043 17.0593 9.65739 17.6551C10.3281 18.0226 11.1012 18.1431 11.9211 18.0272C12.2625 17.9791 12.5786 18.2161 12.6271 18.5575C12.6754 18.8992 12.4375 19.216 12.0959 19.2645C11.0448 19.4131 9.99397 19.2653 9.0568 18.7518C7.96346 18.1527 7.21589 17.1633 6.79801 15.9862C6.74111 15.914 6.69783 15.829 6.67692 15.7332C6.5875 15.3237 6.4571 14.8734 6.30387 14.4188C6.00205 14.7748 5.69607 15.0308 5.37419 15.1834C5.04355 15.3401 4.70719 15.3838 4.38102 15.327C4.06576 15.272 3.79527 15.129 3.57145 14.9696C2.96057 14.5342 2.36597 14.0627 1.89274 13.5487C1.4209 13.036 1.0333 12.4423 0.8986 11.7596C0.842171 11.4736 0.768809 11.1336 0.89274 10.5985C0.997303 10.1475 1.23987 9.57405 1.69059 8.73226L1.60758 8.66585C1.60246 8.66173 1.59696 8.65743 1.59196 8.65315C1.16612 8.2884 1.07023 7.69032 1.08708 7.21468C1.1054 6.69843 1.25893 6.12189 1.54411 5.6014C1.81576 5.10576 2.17253 4.65997 2.58903 4.35433C3.00424 4.04981 3.53772 3.84664 4.10661 3.97737C4.12165 3.98084 4.13775 3.98453 4.15251 3.98909L5.22575 4.3221C5.62556 4.21028 6.05447 4.1958 6.48747 4.32015L6.54801 4.34065L6.54997 4.34163C6.55156 4.34227 6.55431 4.34319 6.55778 4.34456C6.56529 4.34752 6.57742 4.35226 6.59294 4.35823C6.62402 4.3702 6.67024 4.3877 6.72868 4.40901C6.84618 4.45186 7.01173 4.50951 7.20133 4.56819C7.59399 4.6897 8.04168 4.79978 8.382 4.81624C9.99154 4.89405 10.8568 4.72942 12.2062 4.29378ZM12.5441 6.13655C13.7669 5.47408 15.1231 5.29219 16.256 5.91292C17.1747 6.41641 17.7296 7.33256 17.9572 8.39729C18.0148 8.66723 17.8433 8.93322 17.5734 8.99104C17.3035 9.04869 17.0375 8.8771 16.9797 8.60726C16.7956 7.74535 16.3745 7.11819 15.7756 6.78987C15.0408 6.38732 14.0621 6.45197 13.0216 7.01546C12.7704 7.15159 12.5186 7.31527 12.2716 7.50472C13.0464 8.19627 13.6187 8.92334 13.9347 9.64632C14.2881 10.4549 14.3328 11.2901 13.9328 12.0203C13.5333 12.7492 12.7922 13.1542 11.9211 13.2918C11.1394 13.4153 10.2177 13.3313 9.2277 13.0614C9.20118 13.3705 9.19947 13.6697 9.21989 13.9539C9.30483 15.1342 9.77626 15.9936 10.5109 16.3963C10.8983 16.6086 11.346 16.6898 11.8351 16.6405C12.1098 16.6128 12.3552 16.8131 12.383 17.0877C12.4107 17.3624 12.2103 17.6077 11.9357 17.6356C11.2725 17.7026 10.6177 17.5951 10.0304 17.2733C8.89778 16.6525 8.32161 15.4121 8.22184 14.0252C8.12182 12.6321 8.49018 11.0188 9.32243 9.49983C10.1548 7.98089 11.316 6.80199 12.5441 6.13655ZM2.67204 9.54866C2.32412 10.2204 2.17134 10.6184 2.11051 10.8807C2.04887 11.1469 2.07605 11.2695 2.12516 11.5184C2.19851 11.8898 2.4242 12.2809 2.81169 12.702C3.1981 13.1217 3.71082 13.5349 4.29606 13.952C4.42383 14.043 4.52152 14.0826 4.59489 14.0955C4.65746 14.1064 4.73234 14.1036 4.83805 14.0535C5.04286 13.9565 5.35376 13.6844 5.76383 13.035C5.42543 12.2826 5.08809 11.7185 4.84391 11.4735C4.57886 11.2075 4.20518 10.9304 3.87907 10.7108C3.71974 10.6035 3.57875 10.514 3.4777 10.452C3.42724 10.421 3.3866 10.3967 3.35954 10.3807C3.34614 10.3728 3.33581 10.366 3.32926 10.3621L3.32047 10.3582C3.29879 10.3457 3.278 10.3312 3.25797 10.3162C2.98299 10.1101 2.79521 9.83996 2.67204 9.54866ZM11.5216 8.17561C11.0336 8.67806 10.5807 9.28455 10.1994 9.9803C9.81804 10.6763 9.54956 11.3844 9.38883 12.0662C10.3261 12.3341 11.1364 12.4037 11.7648 12.3045C12.4323 12.1991 12.8487 11.9177 13.0558 11.5399C13.2683 11.1518 13.2832 10.6541 13.0177 10.0467C12.7657 9.47024 12.2702 8.82723 11.5216 8.17561ZM9.63883 6.07112C9.45477 6.07962 9.26355 6.08427 9.06266 6.08382C9.01613 6.11598 8.96536 6.1545 8.91032 6.20003C8.71163 6.36444 8.4977 6.58912 8.28434 6.84651C7.85781 7.36118 7.46925 7.96403 7.24626 8.37093C6.99703 8.82575 6.71681 9.39869 6.51969 9.97542C6.34987 10.4725 6.25688 10.9316 6.26969 11.3055C6.3691 11.4655 6.46736 11.6376 6.56266 11.8182C6.76355 10.7536 7.14751 9.66653 7.71989 8.6219C8.25537 7.64475 8.9105 6.78559 9.63883 6.07112ZM6.12516 5.51741C5.92665 5.46415 5.72213 5.47396 5.50895 5.54378C5.15736 5.78936 4.57147 6.28659 4.28727 6.81136C3.94853 7.43736 3.7629 8.31657 3.71598 8.67561C3.71568 8.67793 3.71436 8.68015 3.71403 8.68245C3.72929 8.72056 3.74152 8.76064 3.74919 8.80257C3.79805 9.07007 3.89591 9.222 3.99626 9.30354L3.99723 9.3055C4.02922 9.32447 4.07496 9.35213 4.13102 9.38655C4.24364 9.45571 4.40052 9.5546 4.57731 9.67366C4.82014 9.83722 5.11483 10.0498 5.39079 10.283C5.44136 10.068 5.50384 9.85578 5.5734 9.65218C5.79598 9.00089 6.10514 8.37255 6.3693 7.89046C6.61869 7.4354 7.0422 6.77704 7.51481 6.20686C7.57748 6.13127 7.64175 6.05648 7.70719 5.98323C7.39142 5.92263 7.08276 5.84103 6.83219 5.76351C6.61847 5.69737 6.43222 5.63106 6.29997 5.58284C6.23424 5.55887 6.1809 5.53953 6.14372 5.52522C6.13705 5.52265 6.1308 5.51963 6.12516 5.51741ZM3.81559 5.19319C3.71663 5.17448 3.55572 5.19609 3.32926 5.36214C3.09558 5.53353 2.84889 5.82236 2.64079 6.20198C2.4462 6.55708 2.34736 6.94361 2.3361 7.25862C2.3235 7.61435 2.42004 7.7163 2.40446 7.70296L2.81657 8.03304C2.92255 7.54286 3.11192 6.88062 3.40739 6.33479C3.61396 5.95324 3.91707 5.60514 4.21794 5.31722L3.81559 5.19319Z" 15 + fill="currentColor" 16 + /> 17 + </svg> 18 + ); 19 + };
+32 -2
components/IdentityProvider.tsx
··· 1 1 "use client"; 2 2 import { getIdentityData } from "actions/getIdentityData"; 3 - import { createContext, useContext } from "react"; 3 + import { createContext, useContext, useEffect } from "react"; 4 4 import useSWR, { KeyedMutator, mutate } from "swr"; 5 5 import { DashboardState } from "./PageLayouts/DashboardLayout"; 6 + import { supabaseBrowserClient } from "supabase/browserClient"; 7 + import { produce, Draft } from "immer"; 6 8 7 9 export type InterfaceState = { 8 10 dashboards: { [id: string]: DashboardState | undefined }; 9 11 }; 10 - type Identity = Awaited<ReturnType<typeof getIdentityData>>; 12 + export type Identity = Awaited<ReturnType<typeof getIdentityData>>; 11 13 let IdentityContext = createContext({ 12 14 identity: null as Identity, 13 15 mutate: (() => {}) as KeyedMutator<Identity>, 14 16 }); 15 17 export const useIdentityData = () => useContext(IdentityContext); 18 + 19 + export function mutateIdentityData( 20 + mutate: KeyedMutator<Identity>, 21 + recipe: (draft: Draft<NonNullable<Identity>>) => void, 22 + ) { 23 + mutate( 24 + (data) => { 25 + if (!data) return data; 26 + return produce(data, recipe); 27 + }, 28 + { revalidate: false }, 29 + ); 30 + } 16 31 export function IdentityContextProvider(props: { 17 32 children: React.ReactNode; 18 33 initialValue: Identity; ··· 20 35 let { data: identity, mutate } = useSWR("identity", () => getIdentityData(), { 21 36 fallbackData: props.initialValue, 22 37 }); 38 + useEffect(() => { 39 + mutate(props.initialValue); 40 + }, [props.initialValue]); 41 + useEffect(() => { 42 + if (!identity?.atp_did) return; 43 + let supabase = supabaseBrowserClient(); 44 + let channel = supabase.channel(`identity.atp_did:${identity.atp_did}`); 45 + channel.on("broadcast", { event: "notification" }, () => { 46 + mutate(); 47 + }); 48 + channel.subscribe(); 49 + return () => { 50 + channel.unsubscribe(); 51 + }; 52 + }, [identity?.atp_did]); 23 53 return ( 24 54 <IdentityContext.Provider value={{ identity, mutate }}> 25 55 {props.children}
+2 -2
components/InitialPageLoadProvider.tsx
··· 2 2 import { useEffect } from "react"; 3 3 import { create } from "zustand"; 4 4 5 - export const useInitialPageLoad = create(() => false); 5 + export const useHasPageLoaded = create(() => false); 6 6 export function InitialPageLoad(props: { children: React.ReactNode }) { 7 7 useEffect(() => { 8 8 setTimeout(() => { 9 - useInitialPageLoad.setState(() => true); 9 + useHasPageLoaded.setState(() => true); 10 10 }, 80); 11 11 }, []); 12 12 return <>{props.children}</>;
+10 -2
components/Input.tsx
··· 102 102 JSX.IntrinsicElements["textarea"], 103 103 ) => { 104 104 let { label, textarea, ...inputProps } = props; 105 - let style = `appearance-none w-full font-normal not-italic bg-transparent text-base text-primary focus:outline-0 ${props.className} outline-hidden resize-none`; 105 + let style = ` 106 + appearance-none resize-none w-full 107 + bg-transparent 108 + outline-hidden focus:outline-0 109 + font-normal not-italic text-base text-primary disabled:text-tertiary 110 + disabled:cursor-not-allowed 111 + ${props.className}`; 106 112 return ( 107 - <label className=" input-with-border flex flex-col gap-px text-sm text-tertiary font-bold italic leading-tight py-1! px-[6px]!"> 113 + <label 114 + className={`input-with-border flex flex-col gap-px text-sm text-tertiary font-bold italic leading-tight py-1! px-[6px]! ${props.disabled && "bg-border-light! cursor-not-allowed! hover:border-border!"}`} 115 + > 108 116 {props.label} 109 117 {textarea ? ( 110 118 <textarea {...inputProps} className={style} />
+5 -11
components/Layout.tsx
··· 1 + "use client"; 1 2 import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; 2 3 import { theme } from "tailwind.config"; 3 4 import { NestedCardThemeProvider } from "./ThemeManager/ThemeProvider"; ··· 6 7 import { useState } from "react"; 7 8 8 9 export const Separator = (props: { classname?: string }) => { 9 - return ( 10 - <div className={`min-h-full border-r border-border ${props.classname}`} /> 11 - ); 10 + return <div className={`h-full border-r border-border ${props.classname}`} />; 12 11 }; 13 12 14 13 export const Menu = (props: { ··· 45 44 alignOffset={props.alignOffset ? props.alignOffset : undefined} 46 45 sideOffset={4} 47 46 collisionPadding={16} 48 - className={`dropdownMenu z-20 bg-bg-page flex flex-col py-1 gap-0.5 border border-border rounded-md shadow-md ${props.className}`} 47 + className={`dropdownMenu z-20 bg-bg-page flex flex-col p-1 gap-0.5 border border-border rounded-md shadow-md ${props.className}`} 49 48 > 50 49 {props.children} 51 50 <DropdownMenu.Arrow ··· 86 85 props.onSelect(event); 87 86 }} 88 87 className={` 89 - MenuItem 90 - font-bold z-10 py-1 px-3 91 - text-left text-secondary 88 + menuItem 89 + z-10 py-1! px-2! 92 90 flex gap-2 93 - data-highlighted:bg-border-light data-highlighted:text-secondary 94 - hover:bg-border-light hover:text-secondary 95 - outline-hidden 96 - cursor-pointer 97 91 ${props.className} 98 92 `} 99 93 >
+2
components/LoginButton.tsx
··· 29 29 return ( 30 30 <Popover 31 31 asChild 32 + align="start" 33 + side="right" 32 34 trigger={ 33 35 <ActionButton secondary icon={<AccountSmall />} label="Sign In" /> 34 36 }
+31 -29
components/PageLayouts/DashboardLayout.tsx
··· 8 8 DesktopNavigation, 9 9 MobileNavigation, 10 10 navPages, 11 + NotificationButton, 11 12 } from "components/ActionBar/Navigation"; 12 13 import { create } from "zustand"; 13 14 import { Popover } from "components/Popover"; ··· 32 33 drafts: boolean; 33 34 published: boolean; 34 35 docs: boolean; 35 - templates: boolean; 36 + archived: boolean; 36 37 }; 37 38 }; 38 39 ··· 44 45 const defaultDashboardState: DashboardState = { 45 46 display: undefined, 46 47 sort: undefined, 47 - filter: { drafts: false, published: false, docs: false, templates: false }, 48 + filter: { 49 + drafts: false, 50 + published: false, 51 + docs: false, 52 + archived: false, 53 + }, 48 54 }; 49 55 50 56 export const useDashboardStore = create<DashboardStore>((set, get) => ({ 51 57 dashboards: {}, 52 58 setDashboard: (id: string, partial: Partial<DashboardState>) => { 53 - console.log(partial); 54 59 set((state) => ({ 55 60 dashboards: { 56 61 ...state.dashboards, ··· 139 144 const tabParam = searchParams.get("tab"); 140 145 141 146 // Initialize tab from search param if valid, otherwise use default 142 - const initialTab = tabParam && props.tabs[tabParam] ? tabParam : props.defaultTab; 147 + const initialTab = 148 + tabParam && props.tabs[tabParam] ? tabParam : props.defaultTab; 143 149 let [tab, setTab] = useState<keyof T>(initialTab); 144 150 145 151 // Custom setter that updates both state and URL ··· 165 171 className={`dashboard pwa-padding relative max-w-(--breakpoint-lg) w-full h-full mx-auto flex sm:flex-row flex-col sm:items-stretch sm:px-6`} 166 172 > 167 173 <MediaContents mobile={false}> 168 - <div className="flex flex-col gap-4 my-6"> 174 + <div className="flex flex-col gap-3 my-6"> 169 175 <DesktopNavigation 170 176 currentPage={props.currentPage} 171 177 publication={props.publication} ··· 254 260 hasBackgroundImage: boolean; 255 261 defaultDisplay: Exclude<DashboardState["display"], undefined>; 256 262 hasPubs: boolean; 257 - hasTemplates: boolean; 263 + hasArchived: boolean; 258 264 }) => { 259 265 let { display, sort } = useDashboardState(); 260 - console.log({ display, props }); 261 266 display = display || props.defaultDisplay; 262 267 let setState = useSetDashboardState(); 263 268 264 269 let { identity } = useIdentityData(); 265 - console.log(props); 266 270 267 271 return ( 268 272 <div className="dashboardControls w-full flex gap-4"> ··· 277 281 <DisplayToggle setState={setState} display={display} /> 278 282 <Separator classname="h-4 min-h-4!" /> 279 283 280 - {props.hasPubs || props.hasTemplates ? ( 284 + {props.hasPubs ? ( 281 285 <> 282 - {props.hasPubs} 283 - {props.hasTemplates} 284 286 <FilterOptions 285 287 hasPubs={props.hasPubs} 286 - hasTemplates={props.hasTemplates} 288 + hasArchived={props.hasArchived} 287 289 /> 288 290 <Separator classname="h-4 min-h-4!" />{" "} 289 291 </> ··· 301 303 defaultDisplay: Exclude<DashboardState["display"], undefined>; 302 304 }) => { 303 305 let { display, sort } = useDashboardState(); 304 - console.log({ display, props }); 305 306 display = display || props.defaultDisplay; 306 307 let setState = useSetDashboardState(); 307 308 return ( ··· 371 372 ); 372 373 } 373 374 374 - const FilterOptions = (props: { hasPubs: boolean; hasTemplates: boolean }) => { 375 + const FilterOptions = (props: { 376 + hasPubs: boolean; 377 + hasArchived: boolean; 378 + }) => { 375 379 let { filter } = useDashboardState(); 376 380 let setState = useSetDashboardState(); 377 381 let filterCount = Object.values(filter).filter(Boolean).length; ··· 408 412 </> 409 413 )} 410 414 411 - {props.hasTemplates && ( 412 - <> 413 - <Checkbox 414 - small 415 - checked={filter.templates} 416 - onChange={(e) => 417 - setState({ 418 - filter: { ...filter, templates: !!e.target.checked }, 419 - }) 420 - } 421 - > 422 - Templates 423 - </Checkbox> 424 - </> 415 + {props.hasArchived && ( 416 + <Checkbox 417 + small 418 + checked={filter.archived} 419 + onChange={(e) => 420 + setState({ 421 + filter: { ...filter, archived: !!e.target.checked }, 422 + }) 423 + } 424 + > 425 + Archived 426 + </Checkbox> 425 427 )} 426 428 <Checkbox 427 429 small ··· 443 445 docs: false, 444 446 published: false, 445 447 drafts: false, 446 - templates: false, 448 + archived: false, 447 449 }, 448 450 }); 449 451 }}
+52 -6
components/PageSWRDataProvider.tsx
··· 7 7 import { getPollData } from "actions/pollActions"; 8 8 import type { GetLeafletDataReturnType } from "app/api/rpc/[command]/get_leaflet_data"; 9 9 import { createContext, useContext } from "react"; 10 + import { getPublicationMetadataFromLeafletData } from "src/utils/getPublicationMetadataFromLeafletData"; 11 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 12 + import { AtUri } from "@atproto/syntax"; 10 13 11 14 export const StaticLeafletDataContext = createContext< 12 15 null | GetLeafletDataReturnType["result"]["data"] ··· 66 69 }; 67 70 export function useLeafletPublicationData() { 68 71 let { data, mutate } = useLeafletData(); 72 + 73 + // First check for leaflets in publications 74 + let pubData = getPublicationMetadataFromLeafletData(data); 75 + 69 76 return { 70 - data: 71 - data?.leaflets_in_publications?.[0] || 72 - data?.permission_token_rights[0].entity_sets?.permission_tokens?.find( 73 - (p) => p.leaflets_in_publications.length, 74 - )?.leaflets_in_publications?.[0] || 75 - null, 77 + data: pubData || null, 76 78 mutate, 77 79 }; 78 80 } ··· 80 82 let { data, mutate } = useLeafletData(); 81 83 return { data: data?.custom_domain_routes, mutate: mutate }; 82 84 } 85 + 86 + export function useLeafletPublicationStatus() { 87 + const data = useContext(StaticLeafletDataContext); 88 + if (!data) return null; 89 + 90 + const publishedInPublication = data.leaflets_in_publications?.find( 91 + (l) => l.doc, 92 + ); 93 + const publishedStandalone = data.leaflets_to_documents?.find( 94 + (l) => !!l.documents, 95 + ); 96 + 97 + const documentUri = 98 + publishedInPublication?.documents?.uri ?? publishedStandalone?.document; 99 + 100 + // Compute the full post URL for sharing 101 + let postShareLink: string | undefined; 102 + if (publishedInPublication?.publications && publishedInPublication.documents) { 103 + // Published in a publication - use publication URL + document rkey 104 + const docUri = new AtUri(publishedInPublication.documents.uri); 105 + postShareLink = `${getPublicationURL(publishedInPublication.publications)}/${docUri.rkey}`; 106 + } else if (publishedStandalone?.document) { 107 + // Standalone published post - use /p/{did}/{rkey} format 108 + const docUri = new AtUri(publishedStandalone.document); 109 + postShareLink = `/p/${docUri.host}/${docUri.rkey}`; 110 + } 111 + 112 + return { 113 + token: data, 114 + leafletId: data.root_entity, 115 + shareLink: data.id, 116 + // Draft state - in a publication but not yet published 117 + draftInPublication: 118 + data.leaflets_in_publications?.[0]?.publication ?? undefined, 119 + // Published state 120 + isPublished: !!(publishedInPublication || publishedStandalone), 121 + publishedAt: 122 + publishedInPublication?.documents?.indexed_at ?? 123 + publishedStandalone?.documents?.indexed_at, 124 + documentUri, 125 + // Full URL for sharing published posts 126 + postShareLink, 127 + }; 128 + }
+4 -1
components/Pages/Page.tsx
··· 16 16 import { PageOptions } from "./PageOptions"; 17 17 import { CardThemeProvider } from "components/ThemeManager/ThemeProvider"; 18 18 import { useDrawerOpen } from "app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer"; 19 + import { usePreserveScroll } from "src/hooks/usePreserveScroll"; 19 20 20 21 export function Page(props: { 21 22 entityID: string; ··· 60 61 /> 61 62 } 62 63 > 63 - {props.first && ( 64 + {props.first && pageType === "doc" && ( 64 65 <> 65 66 <PublicationMetadata /> 66 67 </> ··· 83 84 pageType: "canvas" | "doc"; 84 85 drawerOpen: boolean | undefined; 85 86 }) => { 87 + let { ref } = usePreserveScroll<HTMLDivElement>(props.id); 86 88 return ( 87 89 // this div wraps the contents AND the page options. 88 90 // it needs to be its own div because this container does NOT scroll, and therefore doesn't clip the absolutely positioned pageOptions ··· 95 97 it needs to be a separate div so that the user can scroll from anywhere on the page if there isn't a card border 96 98 */} 97 99 <div 100 + ref={ref} 98 101 onClick={props.onClickAction} 99 102 id={props.id} 100 103 className={`
+7 -6
components/Pages/PageShareMenu.tsx
··· 1 1 import { useLeafletDomains } from "components/PageSWRDataProvider"; 2 - import { ShareButton, usePublishLink } from "components/ShareOptions"; 2 + import { 3 + ShareButton, 4 + useReadOnlyShareLink, 5 + } from "app/[leaflet_id]/actions/ShareOptions"; 3 6 import { useEffect, useState } from "react"; 4 7 5 8 export const PageShareMenu = (props: { entityID: string }) => { 6 - let publishLink = usePublishLink(); 9 + let publishLink = useReadOnlyShareLink(); 7 10 let { data: domains } = useLeafletDomains(); 8 11 let [collabLink, setCollabLink] = useState<null | string>(null); 9 12 useEffect(() => { ··· 14 17 <div> 15 18 <ShareButton 16 19 text="Share Edit Link" 17 - subtext="" 18 - helptext="recipients can edit the full Leaflet" 20 + subtext="Recipients can edit the full Leaflet" 19 21 smokerText="Collab link copied!" 20 22 id="get-page-collab-link" 21 23 link={`${collabLink}?page=${props.entityID}`} 22 24 /> 23 25 <ShareButton 24 26 text="Share View Link" 25 - subtext="" 26 - helptext="recipients can view the full Leaflet" 27 + subtext="Recipients can view the full Leaflet" 27 28 smokerText="Publish link copied!" 28 29 id="get-page-publish-link" 29 30 fullLink={
+53 -47
components/Pages/PublicationMetadata.tsx
··· 18 18 import { TagTiny } from "components/Icons/TagTiny"; 19 19 import { Popover } from "components/Popover"; 20 20 import { TagSelector } from "components/Tags"; 21 + import { useIdentityData } from "components/IdentityProvider"; 21 22 export const PublicationMetadata = () => { 22 23 let { rep } = useReplicache(); 23 24 let { data: pub } = useLeafletPublicationData(); 25 + let { identity } = useIdentityData(); 24 26 let title = useSubscribe(rep, (tx) => tx.get<string>("publication_title")); 25 27 let description = useSubscribe(rep, (tx) => 26 28 tx.get<string>("publication_description"), ··· 29 31 let pubRecord = pub?.publications?.record as PubLeafletPublication.Record; 30 32 let publishedAt = record?.publishedAt; 31 33 32 - if (!pub || !pub.publications) return null; 34 + if (!pub) return null; 33 35 34 36 if (typeof title !== "string") { 35 37 title = pub?.title || ""; ··· 40 42 let tags = true; 41 43 42 44 return ( 43 - <div className={`flex flex-col px-3 sm:px-4 pb-5 pt-3`}> 44 - <div className="w-full flex gap-3 justify-between items-center"> 45 - <Link 46 - href={`${getBasePublicationURL(pub.publications)}/dashboard`} 47 - className="leafletMetadata text-accent-contrast font-bold hover:no-underline truncate" 48 - > 49 - {pub.publications?.name} 50 - </Link> 51 - 52 - <div className="flex gap-2 items-center shrink-0"> 53 - {pub.doc && ( 54 - <Link 55 - target="_blank" 56 - className="text-sm shink-0" 57 - href={`${getPublicationURL(pub.publications)}/${new AtUri(pub.doc).rkey}`} 58 - > 59 - View Post 60 - </Link> 61 - )} 62 - <div className="font-bold text-tertiary px-1 text-sm flex place-items-center bg-border rounded-md "> 63 - Editor 64 - </div> 45 + <div className={`flex flex-col px-3 sm:px-4 pb-5 sm:pt-3 pt-2`}> 46 + <div className="flex gap-2"> 47 + {pub.publications && ( 48 + <Link 49 + href={ 50 + identity?.atp_did === pub.publications?.identity_did 51 + ? `${getBasePublicationURL(pub.publications)}/dashboard` 52 + : getPublicationURL(pub.publications) 53 + } 54 + className="leafletMetadata text-accent-contrast font-bold hover:no-underline" 55 + > 56 + {pub.publications?.name} 57 + </Link> 58 + )} 59 + <div className="font-bold text-tertiary px-1 text-sm flex place-items-center bg-border-light rounded-md "> 60 + Editor 65 61 </div> 66 62 </div> 67 63 <TextField ··· 86 82 }); 87 83 }} 88 84 /> 89 - <div className="flex justify-between gap-3 sm:gap-2 pt-3 items-center"> 90 - <div className="flex flex-row gap-2 items-center"> 91 - <div className="w-4 h-4 rounded-full bg-test" /> 92 - 93 - {pub.doc ? ( 85 + {pub.doc ? ( 86 + <div className="flex flex-row items-center justify-between gap-2 pt-3"> 87 + <div className="flex gap-2 items-center"> 94 88 <p className="text-sm text-tertiary"> 95 - {publishedAt && timeAgo(publishedAt)} 89 + Published {publishedAt && timeAgo(publishedAt)} 96 90 </p> 97 - ) : ( 98 - <p className="text-sm text-tertiary pt-2">Draft</p> 99 - )} 100 - </div> 101 - <div className="flex gap-2 text-border items-center"> 102 - {tags && ( 103 - <> 104 - <AddTags /> 105 - <Separator classname="h-4" /> 106 - </> 107 - )} 108 - <div className="flex gap-1 items-center"> 109 - <QuoteTiny />— 91 + <Separator classname="h-4" /> 92 + <Link 93 + target="_blank" 94 + className="text-sm" 95 + href={ 96 + pub.publications 97 + ? `${getPublicationURL(pub.publications)}/${new AtUri(pub.doc).rkey}` 98 + : `/p/${new AtUri(pub.doc).host}/${new AtUri(pub.doc).rkey}` 99 + } 100 + > 101 + View Post 102 + </Link> 110 103 </div> 111 - {pubRecord.preferences?.showComments && ( 104 + <div className="flex gap-2 text-border items-center"> 105 + {tags && ( 106 + <> 107 + <AddTags /> 108 + <Separator classname="h-4" /> 109 + </> 110 + )} 112 111 <div className="flex gap-1 items-center"> 113 - <CommentTiny />— 112 + <QuoteTiny />— 114 113 </div> 115 - )} 114 + {pubRecord.preferences?.showComments && ( 115 + <div className="flex gap-1 items-center"> 116 + <CommentTiny />— 117 + </div> 118 + )} 119 + </div> 116 120 </div> 117 - </div> 121 + ) : ( 122 + <p className="text-sm text-tertiary pt-2">Draft</p> 123 + )} 118 124 </div> 119 125 ); 120 126 }; ··· 195 201 let record = pub?.documents?.data as PubLeafletDocument.Record | null; 196 202 let publishedAt = record?.publishedAt; 197 203 198 - if (!pub || !pub.publications) return null; 204 + if (!pub) return null; 199 205 200 206 return ( 201 207 <div className={`flex flex-col px-3 sm:px-4 pb-5 sm:pt-3 pt-2`}>
+6 -3
components/Popover.tsx
··· 2 2 import * as RadixPopover from "@radix-ui/react-popover"; 3 3 import { theme } from "tailwind.config"; 4 4 import { NestedCardThemeProvider } from "./ThemeManager/ThemeProvider"; 5 - import { createContext, useState } from "react"; 5 + import { createContext, useEffect, useState } from "react"; 6 6 import { PopoverArrow } from "./Icons/PopoverArrow"; 7 7 8 8 export const PopoverOpenContext = createContext(false); ··· 24 24 noArrow?: boolean; 25 25 }) => { 26 26 let [open, setOpen] = useState(props.open || false); 27 + useEffect(() => { 28 + if (props.open !== undefined) setOpen(props.open); 29 + }, [props.open]); 27 30 return ( 28 31 <RadixPopover.Root 29 32 open={props.open} 30 33 onOpenChange={(o) => { 31 34 setOpen(o); 32 - props.onOpenChange?.(open); 35 + props.onOpenChange?.(o); 33 36 }} 34 37 > 35 38 <PopoverOpenContext value={open}> ··· 45 48 max-w-(--radix-popover-content-available-width) 46 49 max-h-(--radix-popover-content-available-height) 47 50 border border-border rounded-md shadow-md 48 - overflow-y-scroll no-scrollbar 51 + overflow-y-scroll 49 52 ${props.className} 50 53 `} 51 54 side={props.side}
+5 -1
components/Providers/RequestHeadersProvider.tsx
··· 21 21 }) => { 22 22 return ( 23 23 <RequestHeadersContext.Provider 24 - value={{ country: props.country, language: props.language, timezone: props.timezone }} 24 + value={{ 25 + country: props.country, 26 + language: props.language, 27 + timezone: props.timezone, 28 + }} 25 29 > 26 30 {props.children} 27 31 </RequestHeadersContext.Provider>
+2 -2
components/ShareOptions/DomainOptions.tsx app/[leaflet_id]/actions/ShareOptions/DomainOptions.tsx
··· 8 8 import { addDomain } from "actions/domains/addDomain"; 9 9 import { callRPC } from "app/api/rpc/client"; 10 10 import { useLeafletDomains } from "components/PageSWRDataProvider"; 11 - import { usePublishLink } from "."; 11 + import { useReadOnlyShareLink } from "."; 12 12 import { addDomainPath } from "actions/domains/addDomainPath"; 13 13 import { useReplicache } from "src/replicache"; 14 14 import { deleteDomain } from "actions/domains/deleteDomain"; ··· 74 74 75 75 let toaster = useToaster(); 76 76 let smoker = useSmoker(); 77 - let publishLink = usePublishLink(); 77 + let publishLink = useReadOnlyShareLink(); 78 78 79 79 return ( 80 80 <div className="px-3 py-1 flex flex-col gap-3 max-w-full w-[600px]">
components/ShareOptions/getShareLink.ts app/[leaflet_id]/actions/ShareOptions/getShareLink.ts
+16 -43
components/ShareOptions/index.tsx app/[leaflet_id]/actions/ShareOptions/index.tsx
··· 6 6 import { Menu, MenuItem } from "components/Layout"; 7 7 import { ActionButton } from "components/ActionBar/ActionButton"; 8 8 import useSWR from "swr"; 9 - import { useTemplateState } from "app/(home-pages)/home/Actions/CreateNewButton"; 10 9 import LoginForm from "app/login/LoginForm"; 11 10 import { CustomDomainMenu } from "./DomainOptions"; 12 11 import { useIdentityData } from "components/IdentityProvider"; ··· 22 21 23 22 export type ShareMenuStates = "default" | "login" | "domain"; 24 23 25 - export let usePublishLink = () => { 24 + export let useReadOnlyShareLink = () => { 26 25 let { permission_token, rootEntity } = useReplicache(); 27 26 let entity_set = useEntitySetContext(); 28 27 let { data: publishLink } = useSWR( ··· 61 60 trigger={ 62 61 <ActionButton 63 62 icon=<ShareSmall /> 64 - primary={!!!pub} 65 - secondary={!!pub} 63 + secondary 66 64 label={`Share ${pub ? "Draft" : ""}`} 67 65 /> 68 66 } ··· 94 92 95 93 let record = pub?.documents?.data as PubLeafletDocument.Record | null; 96 94 97 - let postLink = 98 - pub?.publications && pub.documents 99 - ? `${getPublicationURL(pub.publications)}/${new AtUri(pub?.documents.uri).rkey}` 100 - : null; 101 - let publishLink = usePublishLink(); 95 + let docURI = pub?.documents ? new AtUri(pub?.documents.uri) : null; 96 + let postLink = !docURI 97 + ? null 98 + : pub?.publications 99 + ? `${getPublicationURL(pub.publications)}/${docURI.rkey}` 100 + : `p/${docURI.host}/${docURI.rkey}`; 101 + let publishLink = useReadOnlyShareLink(); 102 102 let [collabLink, setCollabLink] = useState<null | string>(null); 103 103 useEffect(() => { 104 104 // strip leading '/' character from pathname ··· 106 106 }, []); 107 107 let { data: domains } = useLeafletDomains(); 108 108 109 - let isTemplate = useTemplateState( 110 - (s) => !!s.templates.find((t) => t.id === permission_token.id), 111 - ); 112 - 113 109 return ( 114 110 <> 115 - {isTemplate && ( 116 - <> 117 - <ShareButton 118 - text="Share Template" 119 - subtext="Let others make new Leaflets as copies of this template" 120 - smokerText="Template link copied!" 121 - id="get-template-link" 122 - link={`template/${publishLink}` || ""} 123 - /> 124 - <hr className="border-border my-1" /> 125 - </> 126 - )} 127 - 128 111 <ShareButton 129 112 text={`Share ${postLink ? "Draft" : ""} Edit Link`} 130 113 subtext="" ··· 182 165 183 166 export const ShareButton = (props: { 184 167 text: React.ReactNode; 185 - subtext: React.ReactNode; 186 - helptext?: string; 168 + subtext?: React.ReactNode; 187 169 smokerText: string; 188 170 id: string; 189 171 link: null | string; ··· 214 196 } 215 197 }} 216 198 > 217 - <div className={`group/${props.id} ${props.className}`}> 218 - <div className={`group-hover/${props.id}:text-accent-contrast`}> 219 - {props.text} 220 - </div> 221 - <div 222 - className={`text-sm font-normal text-tertiary group-hover/${props.id}:text-accent-contrast`} 223 - > 224 - {props.subtext} 225 - </div> 226 - {/* optional help text */} 227 - {props.helptext && ( 228 - <div 229 - className={`text-sm italic font-normal text-tertiary group-hover/${props.id}:text-accent-contrast`} 230 - > 231 - {props.helptext} 199 + <div className={`group/${props.id} ${props.className} leading-snug`}> 200 + {props.text} 201 + 202 + {props.subtext && ( 203 + <div className={`text-sm font-normal text-tertiary`}> 204 + {props.subtext} 232 205 </div> 233 206 )} 234 207 </div>
+105 -145
components/ThemeManager/PubThemeSetter.tsx
··· 10 10 import { useLocalPubTheme } from "./PublicationThemeProvider"; 11 11 import { BaseThemeProvider } from "./ThemeProvider"; 12 12 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 13 - import { ButtonSecondary } from "components/Buttons"; 14 13 import { updatePublicationTheme } from "app/lish/createPub/updatePublication"; 15 - import { DotLoader } from "components/utils/DotLoader"; 16 14 import { PagePickers } from "./PubPickers/PubTextPickers"; 17 15 import { BackgroundPicker } from "./PubPickers/PubBackgroundPickers"; 18 16 import { PubAccentPickers } from "./PubPickers/PubAcccentPickers"; 19 17 import { Separator } from "components/Layout"; 18 + import { PubSettingsHeader } from "app/lish/[did]/[publication]/dashboard/PublicationSettings"; 19 + import { ColorToRGB, ColorToRGBA } from "./colorToLexicons"; 20 20 21 21 export type ImageState = { 22 22 src: string; 23 23 file?: File; 24 24 repeat: number | null; 25 25 }; 26 - export const PubThemeSetter = () => { 27 - let [loading, setLoading] = useState(false); 26 + export const PubThemeSetter = (props: { 27 + backToMenu: () => void; 28 + loading: boolean; 29 + setLoading: (l: boolean) => void; 30 + }) => { 28 31 let [sample, setSample] = useState<"pub" | "post">("pub"); 29 32 let [openPicker, setOpenPicker] = useState<pickers>("null"); 30 33 let { data, mutate } = usePublicationData(); ··· 37 40 theme: localPubTheme, 38 41 setTheme, 39 42 changes, 40 - } = useLocalPubTheme(record, showPageBackground); 43 + } = useLocalPubTheme(record?.theme, showPageBackground); 41 44 let [image, setImage] = useState<ImageState | null>( 42 45 PubLeafletThemeBackgroundImage.isMain(record?.theme?.backgroundImage) 43 46 ? { ··· 58 61 return ( 59 62 <BaseThemeProvider local {...localPubTheme}> 60 63 <form 61 - className="bg-accent-1 -mx-3 -mt-2 px-3 py-1 mb-1 flex justify-between items-center" 62 64 onSubmit={async (e) => { 63 65 e.preventDefault(); 64 66 if (!pub) return; 65 - setLoading(true); 67 + props.setLoading(true); 66 68 let result = await updatePublicationTheme({ 67 69 uri: pub.uri, 68 70 theme: { ··· 86 88 }; 87 89 return pub; 88 90 }, false); 89 - setLoading(false); 91 + props.setLoading(false); 90 92 }} 91 93 > 92 - <h4 className="text-accent-2">Publication Theme</h4> 93 - <ButtonSecondary compact> 94 - {loading ? <DotLoader /> : "Update"} 95 - </ButtonSecondary> 94 + <PubSettingsHeader 95 + loading={props.loading} 96 + setLoadingAction={props.setLoading} 97 + backToMenuAction={props.backToMenu} 98 + state={"theme"} 99 + /> 96 100 </form> 97 101 98 - <div> 99 - <div className="themeSetterContent flex flex-col w-full overflow-y-scroll no-scrollbar"> 100 - <div className="themeBGLeaflet flex"> 101 - <div 102 - className={`bgPicker flex flex-col gap-0 -mb-[6px] z-10 w-full `} 103 - > 104 - <div className="bgPickerBody w-full flex flex-col gap-2 p-2 mt-1 border border-[#CCCCCC] rounded-md text-[#595959] bg-white"> 105 - <BackgroundPicker 106 - bgImage={image} 107 - setBgImage={setImage} 108 - backgroundColor={localPubTheme.bgLeaflet} 109 - pageBackground={localPubTheme.bgPage} 110 - setPageBackground={(color) => { 111 - setTheme((t) => ({ ...t, bgPage: color })); 112 - }} 113 - setBackgroundColor={(color) => { 114 - setTheme((t) => ({ ...t, bgLeaflet: color })); 115 - }} 116 - openPicker={openPicker} 117 - setOpenPicker={setOpenPicker} 118 - hasPageBackground={!!showPageBackground} 119 - setHasPageBackground={setShowPageBackground} 120 - /> 121 - </div> 122 - 123 - <SectionArrow 124 - fill="white" 125 - stroke="#CCCCCC" 126 - className="ml-2 -mt-px" 127 - /> 128 - </div> 129 - </div> 130 - 102 + <div className="themeSetterContent flex flex-col w-full overflow-y-scroll -mb-2 "> 103 + <div className="themeBGLeaflet flex"> 131 104 <div 132 - style={{ 133 - backgroundImage: pubBGImage ? `url(${pubBGImage})` : undefined, 134 - backgroundRepeat: leafletBGRepeat ? "repeat" : "no-repeat", 135 - backgroundPosition: "center", 136 - backgroundSize: !leafletBGRepeat 137 - ? "cover" 138 - : `calc(${leafletBGRepeat}px / 2 )`, 139 - }} 140 - className={` relative bg-bg-leaflet px-3 py-4 flex flex-col rounded-md border border-border `} 105 + className={`bgPicker flex flex-col gap-0 -mb-[6px] z-10 w-full `} 141 106 > 142 - <div className={`flex flex-col gap-3 z-10`}> 143 - <PagePickers 107 + <div className="bgPickerBody w-full flex flex-col gap-2 p-2 mt-1 border border-[#CCCCCC] rounded-md text-[#595959] bg-white"> 108 + <BackgroundPicker 109 + bgImage={image} 110 + setBgImage={setImage} 111 + backgroundColor={localPubTheme.bgLeaflet} 144 112 pageBackground={localPubTheme.bgPage} 145 - primary={localPubTheme.primary} 146 113 setPageBackground={(color) => { 147 114 setTheme((t) => ({ ...t, bgPage: color })); 148 115 }} 149 - setPrimary={(color) => { 150 - setTheme((t) => ({ ...t, primary: color })); 151 - }} 152 - openPicker={openPicker} 153 - setOpenPicker={(pickers) => setOpenPicker(pickers)} 154 - hasPageBackground={showPageBackground} 155 - /> 156 - <PubAccentPickers 157 - accent1={localPubTheme.accent1} 158 - setAccent1={(color) => { 159 - setTheme((t) => ({ ...t, accent1: color })); 160 - }} 161 - accent2={localPubTheme.accent2} 162 - setAccent2={(color) => { 163 - setTheme((t) => ({ ...t, accent2: color })); 116 + setBackgroundColor={(color) => { 117 + setTheme((t) => ({ ...t, bgLeaflet: color })); 164 118 }} 165 119 openPicker={openPicker} 166 - setOpenPicker={(pickers) => setOpenPicker(pickers)} 120 + setOpenPicker={setOpenPicker} 121 + hasPageBackground={!!showPageBackground} 122 + setHasPageBackground={setShowPageBackground} 167 123 /> 168 124 </div> 125 + 126 + <SectionArrow 127 + fill="white" 128 + stroke="#CCCCCC" 129 + className="ml-2 -mt-[1px]" 130 + /> 169 131 </div> 170 - <div className="flex flex-col mt-4 "> 171 - <div className="flex gap-2 items-center text-sm text-[#8C8C8C]"> 172 - <div className="text-sm">Preview</div> 173 - <Separator classname="h-4!" />{" "} 174 - <button 175 - className={`${sample === "pub" ? "font-bold text-[#595959]" : ""}`} 176 - onClick={() => setSample("pub")} 177 - > 178 - Pub 179 - </button> 180 - <button 181 - className={`${sample === "post" ? "font-bold text-[#595959]" : ""}`} 182 - onClick={() => setSample("post")} 183 - > 184 - Post 185 - </button> 186 - </div> 187 - {sample === "pub" ? ( 188 - <SamplePub 189 - pubBGImage={pubBGImage} 190 - pubBGRepeat={leafletBGRepeat} 191 - showPageBackground={showPageBackground} 192 - /> 193 - ) : ( 194 - <SamplePost 195 - pubBGImage={pubBGImage} 196 - pubBGRepeat={leafletBGRepeat} 197 - showPageBackground={showPageBackground} 198 - /> 199 - )} 132 + </div> 133 + 134 + <div 135 + style={{ 136 + backgroundImage: pubBGImage ? `url(${pubBGImage})` : undefined, 137 + backgroundRepeat: leafletBGRepeat ? "repeat" : "no-repeat", 138 + backgroundPosition: "center", 139 + backgroundSize: !leafletBGRepeat 140 + ? "cover" 141 + : `calc(${leafletBGRepeat}px / 2 )`, 142 + }} 143 + className={` relative bg-bg-leaflet px-3 py-4 flex flex-col rounded-md border border-border `} 144 + > 145 + <div className={`flex flex-col gap-3 z-10`}> 146 + <PagePickers 147 + pageBackground={localPubTheme.bgPage} 148 + primary={localPubTheme.primary} 149 + setPageBackground={(color) => { 150 + setTheme((t) => ({ ...t, bgPage: color })); 151 + }} 152 + setPrimary={(color) => { 153 + setTheme((t) => ({ ...t, primary: color })); 154 + }} 155 + openPicker={openPicker} 156 + setOpenPicker={(pickers) => setOpenPicker(pickers)} 157 + hasPageBackground={showPageBackground} 158 + /> 159 + <PubAccentPickers 160 + accent1={localPubTheme.accent1} 161 + setAccent1={(color) => { 162 + setTheme((t) => ({ ...t, accent1: color })); 163 + }} 164 + accent2={localPubTheme.accent2} 165 + setAccent2={(color) => { 166 + setTheme((t) => ({ ...t, accent2: color })); 167 + }} 168 + openPicker={openPicker} 169 + setOpenPicker={(pickers) => setOpenPicker(pickers)} 170 + /> 171 + </div> 172 + </div> 173 + <div className="flex flex-col mt-4 "> 174 + <div className="flex gap-2 items-center text-sm text-[#8C8C8C]"> 175 + <div className="text-sm">Preview</div> 176 + <Separator classname="h-4!" />{" "} 177 + <button 178 + className={`${sample === "pub" ? "font-bold text-[#595959]" : ""}`} 179 + onClick={() => setSample("pub")} 180 + > 181 + Pub 182 + </button> 183 + <button 184 + className={`${sample === "post" ? "font-bold text-[#595959]" : ""}`} 185 + onClick={() => setSample("post")} 186 + > 187 + Post 188 + </button> 200 189 </div> 190 + {sample === "pub" ? ( 191 + <SamplePub 192 + pubBGImage={pubBGImage} 193 + pubBGRepeat={leafletBGRepeat} 194 + showPageBackground={showPageBackground} 195 + /> 196 + ) : ( 197 + <SamplePost 198 + pubBGImage={pubBGImage} 199 + pubBGRepeat={leafletBGRepeat} 200 + showPageBackground={showPageBackground} 201 + /> 202 + )} 201 203 </div> 202 204 </div> 203 205 </BaseThemeProvider> ··· 342 344 </div> 343 345 ); 344 346 }; 345 - 346 - export function ColorToRGBA(color: Color) { 347 - if (!color) 348 - return { 349 - $type: "pub.leaflet.theme.color#rgba" as const, 350 - r: 0, 351 - g: 0, 352 - b: 0, 353 - a: 1, 354 - }; 355 - let c = color.toFormat("rgba"); 356 - const r = c.getChannelValue("red"); 357 - const g = c.getChannelValue("green"); 358 - const b = c.getChannelValue("blue"); 359 - const a = c.getChannelValue("alpha"); 360 - return { 361 - $type: "pub.leaflet.theme.color#rgba" as const, 362 - r: Math.round(r), 363 - g: Math.round(g), 364 - b: Math.round(b), 365 - a: Math.round(a * 100), 366 - }; 367 - } 368 - function ColorToRGB(color: Color) { 369 - if (!color) 370 - return { 371 - $type: "pub.leaflet.theme.color#rgb" as const, 372 - r: 0, 373 - g: 0, 374 - b: 0, 375 - }; 376 - let c = color.toFormat("rgb"); 377 - const r = c.getChannelValue("red"); 378 - const g = c.getChannelValue("green"); 379 - const b = c.getChannelValue("blue"); 380 - return { 381 - $type: "pub.leaflet.theme.color#rgb" as const, 382 - r: Math.round(r), 383 - g: Math.round(g), 384 - b: Math.round(b), 385 - }; 386 - }
+35 -25
components/ThemeManager/PublicationThemeProvider.tsx
··· 16 16 accentText: "#FFFFFF", 17 17 accentBackground: "#0000FF", 18 18 }; 19 + 20 + // Default page background for standalone leaflets (matches editor default) 21 + const StandalonePageBackground = "#FFFFFF"; 19 22 function parseThemeColor( 20 23 c: PubLeafletThemeColor.Rgb | PubLeafletThemeColor.Rgba, 21 24 ) { ··· 26 29 } 27 30 28 31 let useColor = ( 29 - record: PubLeafletPublication.Record | null | undefined, 32 + theme: PubLeafletPublication.Record["theme"] | null | undefined, 30 33 c: keyof typeof PubThemeDefaults, 31 34 ) => { 32 35 return useMemo(() => { 33 - let v = record?.theme?.[c]; 36 + let v = theme?.[c]; 34 37 if (isColor(v)) { 35 38 return parseThemeColor(v); 36 39 } else return parseColor(PubThemeDefaults[c]); 37 - }, [record?.theme?.[c]]); 40 + }, [theme?.[c]]); 38 41 }; 39 42 let isColor = ( 40 43 c: any, ··· 53 56 return ( 54 57 <PublicationThemeProvider 55 58 pub_creator={pub?.identity_did || ""} 56 - record={pub?.record as PubLeafletPublication.Record} 59 + theme={(pub?.record as PubLeafletPublication.Record)?.theme} 57 60 > 58 61 <PublicationBackgroundProvider 59 - record={pub?.record as PubLeafletPublication.Record} 62 + theme={(pub?.record as PubLeafletPublication.Record)?.theme} 60 63 pub_creator={pub?.identity_did || ""} 61 64 > 62 65 {props.children} ··· 66 69 } 67 70 68 71 export function PublicationBackgroundProvider(props: { 69 - record?: PubLeafletPublication.Record | null; 72 + theme?: PubLeafletPublication.Record["theme"] | null; 70 73 pub_creator: string; 71 74 className?: string; 72 75 children: React.ReactNode; 73 76 }) { 74 - let backgroundImage = props.record?.theme?.backgroundImage?.image?.ref 75 - ? blobRefToSrc( 76 - props.record?.theme?.backgroundImage?.image?.ref, 77 - props.pub_creator, 78 - ) 77 + let backgroundImage = props.theme?.backgroundImage?.image?.ref 78 + ? blobRefToSrc(props.theme?.backgroundImage?.image?.ref, props.pub_creator) 79 79 : null; 80 80 81 - let backgroundImageRepeat = props.record?.theme?.backgroundImage?.repeat; 82 - let backgroundImageSize = props.record?.theme?.backgroundImage?.width || 500; 81 + let backgroundImageRepeat = props.theme?.backgroundImage?.repeat; 82 + let backgroundImageSize = props.theme?.backgroundImage?.width || 500; 83 83 return ( 84 84 <div 85 85 className="PubBackgroundWrapper w-full bg-bg-leaflet text-primary h-full flex flex-col bg-cover bg-center bg-no-repeat items-stretch" ··· 96 96 export function PublicationThemeProvider(props: { 97 97 local?: boolean; 98 98 children: React.ReactNode; 99 - record?: PubLeafletPublication.Record | null; 99 + theme?: PubLeafletPublication.Record["theme"] | null; 100 100 pub_creator: string; 101 + isStandalone?: boolean; 101 102 }) { 102 - let colors = usePubTheme(props.record); 103 + let colors = usePubTheme(props.theme, props.isStandalone); 103 104 return ( 104 105 <BaseThemeProvider local={props.local} {...colors}> 105 106 {props.children} ··· 107 108 ); 108 109 } 109 110 110 - export const usePubTheme = (record?: PubLeafletPublication.Record | null) => { 111 - let bgLeaflet = useColor(record, "backgroundColor"); 112 - let bgPage = useColor(record, "pageBackground"); 113 - bgPage = record?.theme?.pageBackground ? bgPage : bgLeaflet; 114 - let showPageBackground = record?.theme?.showPageBackground; 111 + export const usePubTheme = ( 112 + theme?: PubLeafletPublication.Record["theme"] | null, 113 + isStandalone?: boolean, 114 + ) => { 115 + let bgLeaflet = useColor(theme, "backgroundColor"); 116 + let bgPage = useColor(theme, "pageBackground"); 117 + // For standalone documents, use the editor default page background (#FFFFFF) 118 + // For publications without explicit pageBackground, use bgLeaflet 119 + if (isStandalone && !theme?.pageBackground) { 120 + bgPage = parseColor(StandalonePageBackground); 121 + } else if (theme && !theme.pageBackground) { 122 + bgPage = bgLeaflet; 123 + } 124 + let showPageBackground = theme?.showPageBackground; 115 125 116 - let primary = useColor(record, "primary"); 126 + let primary = useColor(theme, "primary"); 117 127 118 - let accent1 = useColor(record, "accentBackground"); 119 - let accent2 = useColor(record, "accentText"); 128 + let accent1 = useColor(theme, "accentBackground"); 129 + let accent2 = useColor(theme, "accentText"); 120 130 121 131 let highlight1 = useEntity(null, "theme/highlight-1")?.data.value; 122 132 let highlight2 = useColorAttribute(null, "theme/highlight-2"); ··· 136 146 }; 137 147 138 148 export const useLocalPubTheme = ( 139 - record: PubLeafletPublication.Record | undefined, 149 + theme: PubLeafletPublication.Record["theme"] | undefined, 140 150 showPageBackground?: boolean, 141 151 ) => { 142 - const pubTheme = usePubTheme(record); 152 + const pubTheme = usePubTheme(theme); 143 153 const [localOverrides, setTheme] = useState<Partial<typeof pubTheme>>({}); 144 154 145 155 const mergedTheme = useMemo(() => {
+4 -2
components/ThemeManager/ThemeProvider.tsx
··· 73 73 return ( 74 74 <PublicationThemeProvider 75 75 {...props} 76 - record={pub.publications?.record as PubLeafletPublication.Record} 76 + theme={(pub.publications?.record as PubLeafletPublication.Record)?.theme} 77 77 pub_creator={pub.publications?.identity_did} 78 78 /> 79 79 ); ··· 339 339 return ( 340 340 <PublicationBackgroundProvider 341 341 pub_creator={pub?.publications.identity_did || ""} 342 - record={pub?.publications.record as PubLeafletPublication.Record} 342 + theme={ 343 + (pub.publications?.record as PubLeafletPublication.Record)?.theme 344 + } 343 345 > 344 346 {props.children} 345 347 </PublicationBackgroundProvider>
+101 -79
components/ThemeManager/ThemeSetter.tsx
··· 70 70 }, [rep, props.entityID]); 71 71 72 72 if (!permission) return null; 73 - if (pub) return null; 73 + if (pub?.publications) return null; 74 74 75 75 return ( 76 76 <> ··· 82 82 align={isMobile ? "center" : "start"} 83 83 trigger={<ActionButton icon={<PaintSmall />} label="Theme" />} 84 84 > 85 - <div className="themeSetterContent flex flex-col w-full overflow-y-scroll no-scrollbar"> 86 - <div className="themeBGLeaflet flex"> 87 - <div 88 - className={`bgPicker flex flex-col gap-0 -mb-[6px] z-10 w-full `} 89 - > 90 - <div className="bgPickerBody w-full flex flex-col gap-2 p-2 mt-1 border border-[#CCCCCC] rounded-md"> 91 - <LeafletBGPicker 92 - entityID={props.entityID} 93 - thisPicker={"leaflet"} 94 - openPicker={openPicker} 95 - setOpenPicker={setOpenPicker} 96 - closePicker={() => setOpenPicker("null")} 97 - setValue={set("theme/page-background")} 98 - /> 99 - <PageBackgroundPicker 100 - entityID={props.entityID} 101 - setValue={set("theme/card-background")} 102 - openPicker={openPicker} 103 - setOpenPicker={setOpenPicker} 104 - home={props.home} 105 - /> 106 - <hr className=" border-[#CCCCCC]" /> 107 - <PageBorderHider 108 - entityID={props.entityID} 109 - openPicker={openPicker} 110 - setOpenPicker={setOpenPicker} 111 - /> 112 - </div> 85 + <ThemeSetterContent {...props} /> 86 + </Popover> 87 + </> 88 + ); 89 + }; 113 90 114 - <SectionArrow 115 - fill="white" 116 - stroke="#CCCCCC" 117 - className="ml-2 -mt-px" 118 - /> 119 - </div> 120 - </div> 91 + export const ThemeSetterContent = (props: { 92 + entityID: string; 93 + home?: boolean; 94 + }) => { 95 + let { rep } = useReplicache(); 96 + let { data: pub } = useLeafletPublicationData(); 121 97 122 - <div 123 - onClick={(e) => { 124 - e.currentTarget === e.target && setOpenPicker("leaflet"); 125 - }} 126 - style={{ 127 - backgroundImage: leafletBGImage 128 - ? `url(${leafletBGImage.data.src})` 129 - : undefined, 130 - backgroundRepeat: leafletBGRepeat ? "repeat" : "no-repeat", 131 - backgroundPosition: "center", 132 - backgroundSize: !leafletBGRepeat 133 - ? "cover" 134 - : `calc(${leafletBGRepeat.data.value}px / 2 )`, 135 - }} 136 - className={`bg-bg-leaflet px-3 pt-4 pb-0 mb-2 flex flex-col gap-4 rounded-md border border-border`} 137 - > 138 - <PageThemePickers 98 + // I need to get these variables from replicache and then write them to the DB. I also need to parse them into a state that can be used here. 99 + let permission = useEntitySetContext().permissions.write; 100 + let leafletBGImage = useEntity(props.entityID, "theme/background-image"); 101 + let leafletBGRepeat = useEntity( 102 + props.entityID, 103 + "theme/background-image-repeat", 104 + ); 105 + 106 + let [openPicker, setOpenPicker] = useState<pickers>( 107 + props.home === true ? "leaflet" : "null", 108 + ); 109 + let set = useMemo(() => { 110 + return setColorAttribute(rep, props.entityID); 111 + }, [rep, props.entityID]); 112 + 113 + if (!permission) return null; 114 + if (pub?.publications) return null; 115 + return ( 116 + <div className="themeSetterContent flex flex-col w-full overflow-y-scroll no-scrollbar"> 117 + <div className="themeBGLeaflet flex"> 118 + <div className={`bgPicker flex flex-col gap-0 -mb-[6px] z-10 w-full `}> 119 + <div className="bgPickerBody w-full flex flex-col gap-2 p-2 mt-1 border border-[#CCCCCC] rounded-md"> 120 + <LeafletBGPicker 139 121 entityID={props.entityID} 122 + thisPicker={"leaflet"} 140 123 openPicker={openPicker} 141 - setOpenPicker={(pickers) => setOpenPicker(pickers)} 124 + setOpenPicker={setOpenPicker} 125 + closePicker={() => setOpenPicker("null")} 126 + setValue={set("theme/page-background")} 142 127 /> 143 - <div className="flex flex-col -gap-[6px]"> 144 - <div className={`flex flex-col z-10 -mb-[6px] `}> 145 - <AccentPickers 146 - entityID={props.entityID} 147 - openPicker={openPicker} 148 - setOpenPicker={(pickers) => setOpenPicker(pickers)} 149 - /> 150 - <SectionArrow 151 - fill={theme.colors["accent-2"]} 152 - stroke={theme.colors["accent-1"]} 153 - className="ml-2" 154 - /> 155 - </div> 156 - 157 - <SampleButton 158 - entityID={props.entityID} 159 - setOpenPicker={setOpenPicker} 160 - /> 161 - </div> 162 - 163 - <SamplePage 128 + <PageBackgroundPicker 129 + entityID={props.entityID} 130 + setValue={set("theme/card-background")} 131 + openPicker={openPicker} 164 132 setOpenPicker={setOpenPicker} 165 133 home={props.home} 134 + /> 135 + <hr className=" border-[#CCCCCC]" /> 136 + <PageBorderHider 166 137 entityID={props.entityID} 138 + openPicker={openPicker} 139 + setOpenPicker={setOpenPicker} 167 140 /> 168 141 </div> 169 - {!props.home && <WatermarkSetter entityID={props.entityID} />} 142 + 143 + <SectionArrow fill="white" stroke="#CCCCCC" className="ml-2 -mt-px" /> 170 144 </div> 171 - </Popover> 172 - </> 145 + </div> 146 + 147 + <div 148 + onClick={(e) => { 149 + e.currentTarget === e.target && setOpenPicker("leaflet"); 150 + }} 151 + style={{ 152 + backgroundImage: leafletBGImage 153 + ? `url(${leafletBGImage.data.src})` 154 + : undefined, 155 + backgroundRepeat: leafletBGRepeat ? "repeat" : "no-repeat", 156 + backgroundPosition: "center", 157 + backgroundSize: !leafletBGRepeat 158 + ? "cover" 159 + : `calc(${leafletBGRepeat.data.value}px / 2 )`, 160 + }} 161 + className={`bg-bg-leaflet px-3 pt-4 pb-0 mb-2 flex flex-col gap-4 rounded-md border border-border`} 162 + > 163 + <PageThemePickers 164 + entityID={props.entityID} 165 + openPicker={openPicker} 166 + setOpenPicker={(pickers) => setOpenPicker(pickers)} 167 + /> 168 + <div className="flex flex-col -gap-[6px]"> 169 + <div className={`flex flex-col z-10 -mb-[6px] `}> 170 + <AccentPickers 171 + entityID={props.entityID} 172 + openPicker={openPicker} 173 + setOpenPicker={(pickers) => setOpenPicker(pickers)} 174 + /> 175 + <SectionArrow 176 + fill={theme.colors["accent-2"]} 177 + stroke={theme.colors["accent-1"]} 178 + className="ml-2" 179 + /> 180 + </div> 181 + 182 + <SampleButton 183 + entityID={props.entityID} 184 + setOpenPicker={setOpenPicker} 185 + /> 186 + </div> 187 + 188 + <SamplePage 189 + setOpenPicker={setOpenPicker} 190 + home={props.home} 191 + entityID={props.entityID} 192 + /> 193 + </div> 194 + {!props.home && <WatermarkSetter entityID={props.entityID} />} 195 + </div> 173 196 ); 174 197 }; 175 - 176 198 function WatermarkSetter(props: { entityID: string }) { 177 199 let { rep } = useReplicache(); 178 200 let checked = useEntity(props.entityID, "theme/page-leaflet-watermark");
+44
components/ThemeManager/colorToLexicons.ts
··· 1 + import { Color } from "react-aria-components"; 2 + 3 + export function ColorToRGBA(color: Color) { 4 + if (!color) 5 + return { 6 + $type: "pub.leaflet.theme.color#rgba" as const, 7 + r: 0, 8 + g: 0, 9 + b: 0, 10 + a: 1, 11 + }; 12 + let c = color.toFormat("rgba"); 13 + const r = c.getChannelValue("red"); 14 + const g = c.getChannelValue("green"); 15 + const b = c.getChannelValue("blue"); 16 + const a = c.getChannelValue("alpha"); 17 + return { 18 + $type: "pub.leaflet.theme.color#rgba" as const, 19 + r: Math.round(r), 20 + g: Math.round(g), 21 + b: Math.round(b), 22 + a: Math.round(a * 100), 23 + }; 24 + } 25 + 26 + export function ColorToRGB(color: Color) { 27 + if (!color) 28 + return { 29 + $type: "pub.leaflet.theme.color#rgb" as const, 30 + r: 0, 31 + g: 0, 32 + b: 0, 33 + }; 34 + let c = color.toFormat("rgb"); 35 + const r = c.getChannelValue("red"); 36 + const g = c.getChannelValue("green"); 37 + const b = c.getChannelValue("blue"); 38 + return { 39 + $type: "pub.leaflet.theme.color#rgb" as const, 40 + r: Math.round(r), 41 + g: Math.round(g), 42 + b: Math.round(b), 43 + }; 44 + }
+2 -6
components/Toast.tsx
··· 95 95 from: { top: -40 }, 96 96 enter: { top: 8 }, 97 97 leave: { top: -40 }, 98 - config: { 99 - mass: 8, 100 - friction: 150, 101 - tension: 2000, 102 - }, 98 + config: {}, 103 99 }); 104 100 105 101 return transitions((style, item) => { 106 102 return item ? ( 107 103 <animated.div 108 104 style={style} 109 - className={`toastAnimationWrapper fixed bottom-0 right-0 left-0 z-50 h-fit`} 105 + className={`toastAnimationWrapper fixed top-0 bottom-0 right-0 left-0 z-50 h-fit`} 110 106 > 111 107 <div 112 108 className={`toast absolute right-2 w-max shadow-md px-3 py-1 flex flex-row gap-2 rounded-full border text-center ${
+16 -8
drizzle/relations.ts
··· 1 1 import { relations } from "drizzle-orm/relations"; 2 - import { identities, publications, documents, comments_on_documents, bsky_profiles, entity_sets, entities, facts, email_auth_tokens, poll_votes_on_entity, permission_tokens, phone_rsvps_to_entity, custom_domains, custom_domain_routes, email_subscriptions_to_entity, atp_poll_records, atp_poll_votes, bsky_follows, subscribers_to_publications, permission_token_on_homepage, documents_in_publications, document_mentions_in_bsky, bsky_posts, publication_domains, leaflets_in_publications, publication_subscriptions, permission_token_rights } from "./schema"; 2 + import { identities, notifications, publications, documents, comments_on_documents, bsky_profiles, entity_sets, entities, facts, email_auth_tokens, poll_votes_on_entity, permission_tokens, phone_rsvps_to_entity, custom_domains, custom_domain_routes, email_subscriptions_to_entity, atp_poll_records, atp_poll_votes, bsky_follows, subscribers_to_publications, permission_token_on_homepage, documents_in_publications, document_mentions_in_bsky, bsky_posts, publication_domains, leaflets_in_publications, publication_subscriptions, permission_token_rights } from "./schema"; 3 3 4 - export const publicationsRelations = relations(publications, ({one, many}) => ({ 4 + export const notificationsRelations = relations(notifications, ({one}) => ({ 5 5 identity: one(identities, { 6 - fields: [publications.identity_did], 6 + fields: [notifications.recipient], 7 7 references: [identities.atp_did] 8 8 }), 9 - subscribers_to_publications: many(subscribers_to_publications), 10 - documents_in_publications: many(documents_in_publications), 11 - publication_domains: many(publication_domains), 12 - leaflets_in_publications: many(leaflets_in_publications), 13 - publication_subscriptions: many(publication_subscriptions), 14 9 })); 15 10 16 11 export const identitiesRelations = relations(identities, ({one, many}) => ({ 12 + notifications: many(notifications), 17 13 publications: many(publications), 18 14 email_auth_tokens: many(email_auth_tokens), 19 15 bsky_profiles: many(bsky_profiles), ··· 36 32 subscribers_to_publications: many(subscribers_to_publications), 37 33 permission_token_on_homepages: many(permission_token_on_homepage), 38 34 publication_domains: many(publication_domains), 35 + publication_subscriptions: many(publication_subscriptions), 36 + })); 37 + 38 + export const publicationsRelations = relations(publications, ({one, many}) => ({ 39 + identity: one(identities, { 40 + fields: [publications.identity_did], 41 + references: [identities.atp_did] 42 + }), 43 + subscribers_to_publications: many(subscribers_to_publications), 44 + documents_in_publications: many(documents_in_publications), 45 + publication_domains: many(publication_domains), 46 + leaflets_in_publications: many(leaflets_in_publications), 39 47 publication_subscriptions: many(publication_subscriptions), 40 48 })); 41 49
+10 -3
drizzle/schema.ts
··· 1 - import { pgTable, pgEnum, text, jsonb, index, foreignKey, timestamp, uuid, bigint, boolean, unique, uniqueIndex, smallint, primaryKey } from "drizzle-orm/pg-core" 1 + import { pgTable, pgEnum, text, jsonb, foreignKey, timestamp, boolean, uuid, index, bigint, unique, uniqueIndex, smallint, primaryKey } from "drizzle-orm/pg-core" 2 2 import { sql } from "drizzle-orm" 3 3 4 4 export const aal_level = pgEnum("aal_level", ['aal1', 'aal2', 'aal3']) ··· 15 15 export const rsvp_status = pgEnum("rsvp_status", ['GOING', 'NOT_GOING', 'MAYBE']) 16 16 export const action = pgEnum("action", ['INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'ERROR']) 17 17 export const equality_op = pgEnum("equality_op", ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'in']) 18 - export const buckettype = pgEnum("buckettype", ['STANDARD', 'ANALYTICS']) 18 + export const buckettype = pgEnum("buckettype", ['STANDARD', 'ANALYTICS', 'VECTOR']) 19 19 20 20 21 21 export const oauth_state_store = pgTable("oauth_state_store", { 22 22 key: text("key").primaryKey().notNull(), 23 23 state: jsonb("state").notNull(), 24 + }); 25 + 26 + export const notifications = pgTable("notifications", { 27 + recipient: text("recipient").notNull().references(() => identities.atp_did, { onDelete: "cascade", onUpdate: "cascade" } ), 28 + created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 29 + read: boolean("read").default(false).notNull(), 30 + data: jsonb("data").notNull(), 31 + id: uuid("id").primaryKey().notNull(), 24 32 }); 25 33 26 34 export const publications = pgTable("publications", { ··· 210 218 voter_did: text("voter_did").notNull(), 211 219 poll_uri: text("poll_uri").notNull().references(() => atp_poll_records.uri, { onDelete: "cascade", onUpdate: "cascade" } ), 212 220 poll_cid: text("poll_cid").notNull(), 213 - option: text("option").notNull(), 214 221 indexed_at: timestamp("indexed_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 215 222 }, 216 223 (table) => {
+1 -1
feeds/index.ts
··· 92 92 .from("documents") 93 93 .select( 94 94 `*, 95 - documents_in_publications!inner(publications!inner(*))`, 95 + documents_in_publications(publications(*))`, 96 96 ) 97 97 .or( 98 98 "record->preferences->showInDiscover.is.null,record->preferences->>showInDiscover.eq.true",
+4
lexicons/api/index.ts
··· 25 25 import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' 26 26 import * as PubLeafletBlocksBlockquote from './types/pub/leaflet/blocks/blockquote' 27 27 import * as PubLeafletBlocksBskyPost from './types/pub/leaflet/blocks/bskyPost' 28 + import * as PubLeafletBlocksButton from './types/pub/leaflet/blocks/button' 28 29 import * as PubLeafletBlocksCode from './types/pub/leaflet/blocks/code' 29 30 import * as PubLeafletBlocksHeader from './types/pub/leaflet/blocks/header' 30 31 import * as PubLeafletBlocksHorizontalRule from './types/pub/leaflet/blocks/horizontalRule' ··· 64 65 export * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' 65 66 export * as PubLeafletBlocksBlockquote from './types/pub/leaflet/blocks/blockquote' 66 67 export * as PubLeafletBlocksBskyPost from './types/pub/leaflet/blocks/bskyPost' 68 + export * as PubLeafletBlocksButton from './types/pub/leaflet/blocks/button' 67 69 export * as PubLeafletBlocksCode from './types/pub/leaflet/blocks/code' 68 70 export * as PubLeafletBlocksHeader from './types/pub/leaflet/blocks/header' 69 71 export * as PubLeafletBlocksHorizontalRule from './types/pub/leaflet/blocks/horizontalRule' ··· 96 98 'pub.leaflet.pages.linearDocument#textAlignCenter', 97 99 LinearDocumentTextAlignRight: 98 100 'pub.leaflet.pages.linearDocument#textAlignRight', 101 + LinearDocumentTextAlignJustify: 102 + 'pub.leaflet.pages.linearDocument#textAlignJustify', 99 103 } 100 104 101 105 export class AtpBaseClient extends XrpcClient {
+30 -1
lexicons/api/lexicons.ts
··· 1052 1052 }, 1053 1053 }, 1054 1054 }, 1055 + PubLeafletBlocksButton: { 1056 + lexicon: 1, 1057 + id: 'pub.leaflet.blocks.button', 1058 + defs: { 1059 + main: { 1060 + type: 'object', 1061 + required: ['text', 'url'], 1062 + properties: { 1063 + text: { 1064 + type: 'string', 1065 + }, 1066 + url: { 1067 + type: 'string', 1068 + format: 'uri', 1069 + }, 1070 + }, 1071 + }, 1072 + }, 1073 + }, 1055 1074 PubLeafletBlocksCode: { 1056 1075 lexicon: 1, 1057 1076 id: 'pub.leaflet.blocks.code', ··· 1389 1408 description: 'Record containing a document', 1390 1409 record: { 1391 1410 type: 'object', 1392 - required: ['pages', 'author', 'title', 'publication'], 1411 + required: ['pages', 'author', 'title'], 1393 1412 properties: { 1394 1413 title: { 1395 1414 type: 'string', ··· 1417 1436 type: 'string', 1418 1437 format: 'at-identifier', 1419 1438 }, 1439 + theme: { 1440 + type: 'ref', 1441 + ref: 'lex:pub.leaflet.publication#theme', 1442 + }, 1420 1443 pages: { 1421 1444 type: 'array', 1422 1445 items: { ··· 1493 1516 'lex:pub.leaflet.blocks.bskyPost', 1494 1517 'lex:pub.leaflet.blocks.page', 1495 1518 'lex:pub.leaflet.blocks.poll', 1519 + 'lex:pub.leaflet.blocks.button', 1496 1520 ], 1497 1521 }, 1498 1522 x: { ··· 1593 1617 'lex:pub.leaflet.blocks.bskyPost', 1594 1618 'lex:pub.leaflet.blocks.page', 1595 1619 'lex:pub.leaflet.blocks.poll', 1620 + 'lex:pub.leaflet.blocks.button', 1596 1621 ], 1597 1622 }, 1598 1623 alignment: { ··· 1613 1638 type: 'token', 1614 1639 }, 1615 1640 textAlignRight: { 1641 + type: 'token', 1642 + }, 1643 + textAlignJustify: { 1616 1644 type: 'token', 1617 1645 }, 1618 1646 quote: { ··· 2050 2078 ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', 2051 2079 PubLeafletBlocksBlockquote: 'pub.leaflet.blocks.blockquote', 2052 2080 PubLeafletBlocksBskyPost: 'pub.leaflet.blocks.bskyPost', 2081 + PubLeafletBlocksButton: 'pub.leaflet.blocks.button', 2053 2082 PubLeafletBlocksCode: 'pub.leaflet.blocks.code', 2054 2083 PubLeafletBlocksHeader: 'pub.leaflet.blocks.header', 2055 2084 PubLeafletBlocksHorizontalRule: 'pub.leaflet.blocks.horizontalRule',
+31
lexicons/api/types/pub/leaflet/blocks/button.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../../lexicons' 7 + import { 8 + type $Typed, 9 + is$typed as _is$typed, 10 + type OmitKey, 11 + } from '../../../../util' 12 + 13 + const is$typed = _is$typed, 14 + validate = _validate 15 + const id = 'pub.leaflet.blocks.button' 16 + 17 + export interface Main { 18 + $type?: 'pub.leaflet.blocks.button' 19 + text: string 20 + url: string 21 + } 22 + 23 + const hashMain = 'main' 24 + 25 + export function isMain<V>(v: V) { 26 + return is$typed(v, id, hashMain) 27 + } 28 + 29 + export function validateMain<V>(v: V) { 30 + return validate<Main & V>(v, id, hashMain) 31 + }
+3 -1
lexicons/api/types/pub/leaflet/document.ts
··· 6 6 import { validate as _validate } from '../../../lexicons' 7 7 import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 8 import type * as ComAtprotoRepoStrongRef from '../../com/atproto/repo/strongRef' 9 + import type * as PubLeafletPublication from './publication' 9 10 import type * as PubLeafletPagesLinearDocument from './pages/linearDocument' 10 11 import type * as PubLeafletPagesCanvas from './pages/canvas' 11 12 ··· 19 20 postRef?: ComAtprotoRepoStrongRef.Main 20 21 description?: string 21 22 publishedAt?: string 22 - publication: string 23 + publication?: string 23 24 author: string 25 + theme?: PubLeafletPublication.Theme 24 26 pages: ( 25 27 | $Typed<PubLeafletPagesLinearDocument.Main> 26 28 | $Typed<PubLeafletPagesCanvas.Main>
+2
lexicons/api/types/pub/leaflet/pages/canvas.ts
··· 22 22 import type * as PubLeafletBlocksBskyPost from '../blocks/bskyPost' 23 23 import type * as PubLeafletBlocksPage from '../blocks/page' 24 24 import type * as PubLeafletBlocksPoll from '../blocks/poll' 25 + import type * as PubLeafletBlocksButton from '../blocks/button' 25 26 26 27 const is$typed = _is$typed, 27 28 validate = _validate ··· 59 60 | $Typed<PubLeafletBlocksBskyPost.Main> 60 61 | $Typed<PubLeafletBlocksPage.Main> 61 62 | $Typed<PubLeafletBlocksPoll.Main> 63 + | $Typed<PubLeafletBlocksButton.Main> 62 64 | { $type: string } 63 65 x: number 64 66 y: number
+3
lexicons/api/types/pub/leaflet/pages/linearDocument.ts
··· 22 22 import type * as PubLeafletBlocksBskyPost from '../blocks/bskyPost' 23 23 import type * as PubLeafletBlocksPage from '../blocks/page' 24 24 import type * as PubLeafletBlocksPoll from '../blocks/poll' 25 + import type * as PubLeafletBlocksButton from '../blocks/button' 25 26 26 27 const is$typed = _is$typed, 27 28 validate = _validate ··· 59 60 | $Typed<PubLeafletBlocksBskyPost.Main> 60 61 | $Typed<PubLeafletBlocksPage.Main> 61 62 | $Typed<PubLeafletBlocksPoll.Main> 63 + | $Typed<PubLeafletBlocksButton.Main> 62 64 | { $type: string } 63 65 alignment?: 64 66 | 'lex:pub.leaflet.pages.linearDocument#textAlignLeft' ··· 81 83 export const TEXTALIGNLEFT = `${id}#textAlignLeft` 82 84 export const TEXTALIGNCENTER = `${id}#textAlignCenter` 83 85 export const TEXTALIGNRIGHT = `${id}#textAlignRight` 86 + export const TEXTALIGNJUSTIFY = `${id}#textAlignJustify` 84 87 85 88 export interface Quote { 86 89 $type?: 'pub.leaflet.pages.linearDocument#quote'
+22
lexicons/pub/leaflet/blocks/button.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "pub.leaflet.blocks.button", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "required": [ 8 + "text", 9 + "url" 10 + ], 11 + "properties": { 12 + "text": { 13 + "type": "string" 14 + }, 15 + "url": { 16 + "type": "string", 17 + "format": "uri" 18 + } 19 + } 20 + } 21 + } 22 + }
+5 -2
lexicons/pub/leaflet/document.json
··· 13 13 "required": [ 14 14 "pages", 15 15 "author", 16 - "title", 17 - "publication" 16 + "title" 18 17 ], 19 18 "properties": { 20 19 "title": { ··· 42 41 "author": { 43 42 "type": "string", 44 43 "format": "at-identifier" 44 + }, 45 + "theme": { 46 + "type": "ref", 47 + "ref": "pub.leaflet.publication#theme" 45 48 }, 46 49 "pages": { 47 50 "type": "array",
+2 -1
lexicons/pub/leaflet/pages/canvas.json
··· 44 44 "pub.leaflet.blocks.horizontalRule", 45 45 "pub.leaflet.blocks.bskyPost", 46 46 "pub.leaflet.blocks.page", 47 - "pub.leaflet.blocks.poll" 47 + "pub.leaflet.blocks.poll", 48 + "pub.leaflet.blocks.button" 48 49 ] 49 50 }, 50 51 "x": {
+5 -1
lexicons/pub/leaflet/pages/linearDocument.json
··· 41 41 "pub.leaflet.blocks.horizontalRule", 42 42 "pub.leaflet.blocks.bskyPost", 43 43 "pub.leaflet.blocks.page", 44 - "pub.leaflet.blocks.poll" 44 + "pub.leaflet.blocks.poll", 45 + "pub.leaflet.blocks.button" 45 46 ] 46 47 }, 47 48 "alignment": { ··· 62 63 "type": "token" 63 64 }, 64 65 "textAlignRight": { 66 + "type": "token" 67 + }, 68 + "textAlignJustify": { 65 69 "type": "token" 66 70 }, 67 71 "quote": {
+16
lexicons/src/blocks.ts
··· 279 279 }, 280 280 }; 281 281 282 + export const PubLeafletBlocksButton: LexiconDoc = { 283 + lexicon: 1, 284 + id: "pub.leaflet.blocks.button", 285 + defs: { 286 + main: { 287 + type: "object", 288 + required: ["text", "url"], 289 + properties: { 290 + text: { type: "string" }, 291 + url: { type: "string", format: "uri" }, 292 + }, 293 + }, 294 + }, 295 + }; 296 + 282 297 export const BlockLexicons = [ 283 298 PubLeafletBlocksIFrame, 284 299 PubLeafletBlocksText, ··· 293 308 PubLeafletBlocksBskyPost, 294 309 PubLeafletBlocksPage, 295 310 PubLeafletBlocksPoll, 311 + PubLeafletBlocksButton, 296 312 ]; 297 313 export const BlockUnion: LexRefUnion = { 298 314 type: "union",
+2 -1
lexicons/src/document.ts
··· 14 14 description: "Record containing a document", 15 15 record: { 16 16 type: "object", 17 - required: ["pages", "author", "title", "publication"], 17 + required: ["pages", "author", "title"], 18 18 properties: { 19 19 title: { type: "string", maxLength: 1280, maxGraphemes: 128 }, 20 20 postRef: { type: "ref", ref: "com.atproto.repo.strongRef" }, ··· 22 22 publishedAt: { type: "string", format: "datetime" }, 23 23 publication: { type: "string", format: "at-uri" }, 24 24 author: { type: "string", format: "at-identifier" }, 25 + theme: { type: "ref", ref: "pub.leaflet.publication#theme" }, 25 26 pages: { 26 27 type: "array", 27 28 items: {
+1
lexicons/src/pages/LinearDocument.ts
··· 32 32 textAlignLeft: { type: "token" }, 33 33 textAlignCenter: { type: "token" }, 34 34 textAlignRight: { type: "token" }, 35 + textAlignJustify: { type: "token" }, 35 36 quote: { 36 37 type: "object", 37 38 required: ["start", "end"],
+1 -1
next-env.d.ts
··· 1 1 /// <reference types="next" /> 2 2 /// <reference types="next/image-types/global" /> 3 - /// <reference path="./.next/types/routes.d.ts" /> 3 + import "./.next/dev/types/routes.d.ts"; 4 4 5 5 // NOTE: This file should not be edited 6 6 // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+2 -1
next.config.js
··· 21 21 }, 22 22 ]; 23 23 }, 24 + serverExternalPackages: ["yjs", "pino"], 24 25 pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"], 25 26 images: { 26 27 loader: "custom", ··· 30 31 { protocol: "https", hostname: "bdefzwcumgzjwllsnaej.supabase.co" }, 31 32 ], 32 33 }, 34 + reactCompiler: true, 33 35 experimental: { 34 - reactCompiler: true, 35 36 serverActions: { 36 37 bodySizeLimit: "5mb", 37 38 },
+2335 -460
package-lock.json
··· 21 21 "@hono/node-server": "^1.14.3", 22 22 "@mdx-js/loader": "^3.1.0", 23 23 "@mdx-js/react": "^3.1.0", 24 - "@next/bundle-analyzer": "^15.3.2", 25 - "@next/mdx": "15.3.2", 24 + "@next/bundle-analyzer": "16.0.3", 25 + "@next/mdx": "16.0.3", 26 26 "@radix-ui/react-dialog": "^1.1.15", 27 27 "@radix-ui/react-dropdown-menu": "^2.1.16", 28 28 "@radix-ui/react-popover": "^1.1.15", ··· 44 44 "feed": "^5.1.0", 45 45 "fractional-indexing": "^3.2.0", 46 46 "hono": "^4.7.11", 47 + "immer": "^10.2.0", 47 48 "inngest": "^3.40.1", 48 49 "ioredis": "^5.6.1", 49 50 "katex": "^0.16.22", 50 51 "linkifyjs": "^4.2.0", 51 52 "luxon": "^3.7.2", 52 53 "multiformats": "^13.3.2", 53 - "next": "^15.5.3", 54 + "next": "^16.0.7", 54 55 "pg": "^8.16.3", 55 56 "prosemirror-commands": "^1.5.2", 56 57 "prosemirror-inputrules": "^1.4.0", ··· 58 59 "prosemirror-model": "^1.21.0", 59 60 "prosemirror-schema-basic": "^1.2.2", 60 61 "prosemirror-state": "^1.4.3", 61 - "react": "^19.1.1", 62 + "react": "19.2.0", 62 63 "react-aria-components": "^1.8.0", 63 64 "react-day-picker": "^9.3.0", 64 - "react-dom": "^19.1.1", 65 + "react-dom": "19.2.0", 65 66 "react-use-measure": "^2.1.1", 66 67 "redlock": "^5.0.0-beta.2", 67 68 "rehype-parse": "^9.0.0", ··· 91 92 "@types/katex": "^0.16.7", 92 93 "@types/luxon": "^3.7.1", 93 94 "@types/node": "^22.15.17", 94 - "@types/react": "19.1.3", 95 - "@types/react-dom": "19.1.3", 95 + "@types/react": "19.2.6", 96 + "@types/react-dom": "19.2.3", 96 97 "@types/uuid": "^10.0.0", 97 98 "drizzle-kit": "^0.21.2", 98 99 "esbuild": "^0.25.4", 99 - "eslint": "8.57.0", 100 - "eslint-config-next": "^15.5.3", 100 + "eslint": "^9.39.1", 101 + "eslint-config-next": "16.0.3", 101 102 "postcss": "^8.4.38", 102 103 "prettier": "3.2.5", 103 104 "supabase": "^1.187.3", ··· 566 567 "node": ">=18.7.0" 567 568 } 568 569 }, 570 + "node_modules/@babel/code-frame": { 571 + "version": "7.27.1", 572 + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", 573 + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", 574 + "dev": true, 575 + "dependencies": { 576 + "@babel/helper-validator-identifier": "^7.27.1", 577 + "js-tokens": "^4.0.0", 578 + "picocolors": "^1.1.1" 579 + }, 580 + "engines": { 581 + "node": ">=6.9.0" 582 + } 583 + }, 584 + "node_modules/@babel/compat-data": { 585 + "version": "7.28.5", 586 + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", 587 + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", 588 + "dev": true, 589 + "engines": { 590 + "node": ">=6.9.0" 591 + } 592 + }, 593 + "node_modules/@babel/core": { 594 + "version": "7.28.5", 595 + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", 596 + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", 597 + "dev": true, 598 + "dependencies": { 599 + "@babel/code-frame": "^7.27.1", 600 + "@babel/generator": "^7.28.5", 601 + "@babel/helper-compilation-targets": "^7.27.2", 602 + "@babel/helper-module-transforms": "^7.28.3", 603 + "@babel/helpers": "^7.28.4", 604 + "@babel/parser": "^7.28.5", 605 + "@babel/template": "^7.27.2", 606 + "@babel/traverse": "^7.28.5", 607 + "@babel/types": "^7.28.5", 608 + "@jridgewell/remapping": "^2.3.5", 609 + "convert-source-map": "^2.0.0", 610 + "debug": "^4.1.0", 611 + "gensync": "^1.0.0-beta.2", 612 + "json5": "^2.2.3", 613 + "semver": "^6.3.1" 614 + }, 615 + "engines": { 616 + "node": ">=6.9.0" 617 + }, 618 + "funding": { 619 + "type": "opencollective", 620 + "url": "https://opencollective.com/babel" 621 + } 622 + }, 623 + "node_modules/@babel/core/node_modules/json5": { 624 + "version": "2.2.3", 625 + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", 626 + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", 627 + "dev": true, 628 + "bin": { 629 + "json5": "lib/cli.js" 630 + }, 631 + "engines": { 632 + "node": ">=6" 633 + } 634 + }, 635 + "node_modules/@babel/core/node_modules/semver": { 636 + "version": "6.3.1", 637 + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", 638 + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", 639 + "dev": true, 640 + "bin": { 641 + "semver": "bin/semver.js" 642 + } 643 + }, 644 + "node_modules/@babel/generator": { 645 + "version": "7.28.5", 646 + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", 647 + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", 648 + "dev": true, 649 + "dependencies": { 650 + "@babel/parser": "^7.28.5", 651 + "@babel/types": "^7.28.5", 652 + "@jridgewell/gen-mapping": "^0.3.12", 653 + "@jridgewell/trace-mapping": "^0.3.28", 654 + "jsesc": "^3.0.2" 655 + }, 656 + "engines": { 657 + "node": ">=6.9.0" 658 + } 659 + }, 660 + "node_modules/@babel/helper-compilation-targets": { 661 + "version": "7.27.2", 662 + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", 663 + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", 664 + "dev": true, 665 + "dependencies": { 666 + "@babel/compat-data": "^7.27.2", 667 + "@babel/helper-validator-option": "^7.27.1", 668 + "browserslist": "^4.24.0", 669 + "lru-cache": "^5.1.1", 670 + "semver": "^6.3.1" 671 + }, 672 + "engines": { 673 + "node": ">=6.9.0" 674 + } 675 + }, 676 + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { 677 + "version": "5.1.1", 678 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", 679 + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", 680 + "dev": true, 681 + "dependencies": { 682 + "yallist": "^3.0.2" 683 + } 684 + }, 685 + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { 686 + "version": "6.3.1", 687 + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", 688 + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", 689 + "dev": true, 690 + "bin": { 691 + "semver": "bin/semver.js" 692 + } 693 + }, 694 + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { 695 + "version": "3.1.1", 696 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 697 + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", 698 + "dev": true 699 + }, 700 + "node_modules/@babel/helper-globals": { 701 + "version": "7.28.0", 702 + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", 703 + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", 704 + "dev": true, 705 + "engines": { 706 + "node": ">=6.9.0" 707 + } 708 + }, 709 + "node_modules/@babel/helper-module-imports": { 710 + "version": "7.27.1", 711 + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", 712 + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", 713 + "dev": true, 714 + "dependencies": { 715 + "@babel/traverse": "^7.27.1", 716 + "@babel/types": "^7.27.1" 717 + }, 718 + "engines": { 719 + "node": ">=6.9.0" 720 + } 721 + }, 722 + "node_modules/@babel/helper-module-transforms": { 723 + "version": "7.28.3", 724 + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", 725 + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", 726 + "dev": true, 727 + "dependencies": { 728 + "@babel/helper-module-imports": "^7.27.1", 729 + "@babel/helper-validator-identifier": "^7.27.1", 730 + "@babel/traverse": "^7.28.3" 731 + }, 732 + "engines": { 733 + "node": ">=6.9.0" 734 + }, 735 + "peerDependencies": { 736 + "@babel/core": "^7.0.0" 737 + } 738 + }, 569 739 "node_modules/@babel/helper-string-parser": { 570 740 "version": "7.27.1", 571 741 "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", ··· 576 746 } 577 747 }, 578 748 "node_modules/@babel/helper-validator-identifier": { 749 + "version": "7.28.5", 750 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", 751 + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", 752 + "engines": { 753 + "node": ">=6.9.0" 754 + } 755 + }, 756 + "node_modules/@babel/helper-validator-option": { 579 757 "version": "7.27.1", 580 - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", 581 - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", 582 - "license": "MIT", 758 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", 759 + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", 760 + "dev": true, 761 + "engines": { 762 + "node": ">=6.9.0" 763 + } 764 + }, 765 + "node_modules/@babel/helpers": { 766 + "version": "7.28.4", 767 + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", 768 + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", 769 + "dev": true, 770 + "dependencies": { 771 + "@babel/template": "^7.27.2", 772 + "@babel/types": "^7.28.4" 773 + }, 774 + "engines": { 775 + "node": ">=6.9.0" 776 + } 777 + }, 778 + "node_modules/@babel/parser": { 779 + "version": "7.28.5", 780 + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", 781 + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", 782 + "dev": true, 783 + "dependencies": { 784 + "@babel/types": "^7.28.5" 785 + }, 786 + "bin": { 787 + "parser": "bin/babel-parser.js" 788 + }, 789 + "engines": { 790 + "node": ">=6.0.0" 791 + } 792 + }, 793 + "node_modules/@babel/template": { 794 + "version": "7.27.2", 795 + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", 796 + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", 797 + "dev": true, 798 + "dependencies": { 799 + "@babel/code-frame": "^7.27.1", 800 + "@babel/parser": "^7.27.2", 801 + "@babel/types": "^7.27.1" 802 + }, 803 + "engines": { 804 + "node": ">=6.9.0" 805 + } 806 + }, 807 + "node_modules/@babel/traverse": { 808 + "version": "7.28.5", 809 + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", 810 + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", 811 + "dev": true, 812 + "dependencies": { 813 + "@babel/code-frame": "^7.27.1", 814 + "@babel/generator": "^7.28.5", 815 + "@babel/helper-globals": "^7.28.0", 816 + "@babel/parser": "^7.28.5", 817 + "@babel/template": "^7.27.2", 818 + "@babel/types": "^7.28.5", 819 + "debug": "^4.3.1" 820 + }, 583 821 "engines": { 584 822 "node": ">=6.9.0" 585 823 } 586 824 }, 587 825 "node_modules/@babel/types": { 588 - "version": "7.27.1", 589 - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", 590 - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", 591 - "license": "MIT", 826 + "version": "7.28.5", 827 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", 828 + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", 592 829 "dependencies": { 593 830 "@babel/helper-string-parser": "^7.27.1", 594 - "@babel/helper-validator-identifier": "^7.27.1" 831 + "@babel/helper-validator-identifier": "^7.28.5" 595 832 }, 596 833 "engines": { 597 834 "node": ">=6.9.0" ··· 713 950 "source-map-support": "^0.5.21" 714 951 } 715 952 }, 953 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { 954 + "version": "0.18.20", 955 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", 956 + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", 957 + "cpu": [ 958 + "arm" 959 + ], 960 + "dev": true, 961 + "optional": true, 962 + "os": [ 963 + "android" 964 + ], 965 + "engines": { 966 + "node": ">=12" 967 + } 968 + }, 969 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { 970 + "version": "0.18.20", 971 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", 972 + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", 973 + "cpu": [ 974 + "arm64" 975 + ], 976 + "dev": true, 977 + "optional": true, 978 + "os": [ 979 + "android" 980 + ], 981 + "engines": { 982 + "node": ">=12" 983 + } 984 + }, 985 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { 986 + "version": "0.18.20", 987 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", 988 + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", 989 + "cpu": [ 990 + "x64" 991 + ], 992 + "dev": true, 993 + "optional": true, 994 + "os": [ 995 + "android" 996 + ], 997 + "engines": { 998 + "node": ">=12" 999 + } 1000 + }, 1001 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { 1002 + "version": "0.18.20", 1003 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", 1004 + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", 1005 + "cpu": [ 1006 + "arm64" 1007 + ], 1008 + "dev": true, 1009 + "optional": true, 1010 + "os": [ 1011 + "darwin" 1012 + ], 1013 + "engines": { 1014 + "node": ">=12" 1015 + } 1016 + }, 1017 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { 1018 + "version": "0.18.20", 1019 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", 1020 + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", 1021 + "cpu": [ 1022 + "x64" 1023 + ], 1024 + "dev": true, 1025 + "optional": true, 1026 + "os": [ 1027 + "darwin" 1028 + ], 1029 + "engines": { 1030 + "node": ">=12" 1031 + } 1032 + }, 1033 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { 1034 + "version": "0.18.20", 1035 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", 1036 + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", 1037 + "cpu": [ 1038 + "arm64" 1039 + ], 1040 + "dev": true, 1041 + "optional": true, 1042 + "os": [ 1043 + "freebsd" 1044 + ], 1045 + "engines": { 1046 + "node": ">=12" 1047 + } 1048 + }, 1049 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { 1050 + "version": "0.18.20", 1051 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", 1052 + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", 1053 + "cpu": [ 1054 + "x64" 1055 + ], 1056 + "dev": true, 1057 + "optional": true, 1058 + "os": [ 1059 + "freebsd" 1060 + ], 1061 + "engines": { 1062 + "node": ">=12" 1063 + } 1064 + }, 1065 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { 1066 + "version": "0.18.20", 1067 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", 1068 + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", 1069 + "cpu": [ 1070 + "arm" 1071 + ], 1072 + "dev": true, 1073 + "optional": true, 1074 + "os": [ 1075 + "linux" 1076 + ], 1077 + "engines": { 1078 + "node": ">=12" 1079 + } 1080 + }, 1081 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { 1082 + "version": "0.18.20", 1083 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", 1084 + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", 1085 + "cpu": [ 1086 + "arm64" 1087 + ], 1088 + "dev": true, 1089 + "optional": true, 1090 + "os": [ 1091 + "linux" 1092 + ], 1093 + "engines": { 1094 + "node": ">=12" 1095 + } 1096 + }, 1097 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { 1098 + "version": "0.18.20", 1099 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", 1100 + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", 1101 + "cpu": [ 1102 + "ia32" 1103 + ], 1104 + "dev": true, 1105 + "optional": true, 1106 + "os": [ 1107 + "linux" 1108 + ], 1109 + "engines": { 1110 + "node": ">=12" 1111 + } 1112 + }, 1113 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { 1114 + "version": "0.18.20", 1115 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", 1116 + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", 1117 + "cpu": [ 1118 + "loong64" 1119 + ], 1120 + "dev": true, 1121 + "optional": true, 1122 + "os": [ 1123 + "linux" 1124 + ], 1125 + "engines": { 1126 + "node": ">=12" 1127 + } 1128 + }, 1129 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { 1130 + "version": "0.18.20", 1131 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", 1132 + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", 1133 + "cpu": [ 1134 + "mips64el" 1135 + ], 1136 + "dev": true, 1137 + "optional": true, 1138 + "os": [ 1139 + "linux" 1140 + ], 1141 + "engines": { 1142 + "node": ">=12" 1143 + } 1144 + }, 1145 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { 1146 + "version": "0.18.20", 1147 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", 1148 + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", 1149 + "cpu": [ 1150 + "ppc64" 1151 + ], 1152 + "dev": true, 1153 + "optional": true, 1154 + "os": [ 1155 + "linux" 1156 + ], 1157 + "engines": { 1158 + "node": ">=12" 1159 + } 1160 + }, 1161 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { 1162 + "version": "0.18.20", 1163 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", 1164 + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", 1165 + "cpu": [ 1166 + "riscv64" 1167 + ], 1168 + "dev": true, 1169 + "optional": true, 1170 + "os": [ 1171 + "linux" 1172 + ], 1173 + "engines": { 1174 + "node": ">=12" 1175 + } 1176 + }, 1177 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { 1178 + "version": "0.18.20", 1179 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", 1180 + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", 1181 + "cpu": [ 1182 + "s390x" 1183 + ], 1184 + "dev": true, 1185 + "optional": true, 1186 + "os": [ 1187 + "linux" 1188 + ], 1189 + "engines": { 1190 + "node": ">=12" 1191 + } 1192 + }, 716 1193 "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { 717 1194 "version": "0.18.20", 718 1195 "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", ··· 729 1206 "node": ">=12" 730 1207 } 731 1208 }, 1209 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { 1210 + "version": "0.18.20", 1211 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", 1212 + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", 1213 + "cpu": [ 1214 + "x64" 1215 + ], 1216 + "dev": true, 1217 + "optional": true, 1218 + "os": [ 1219 + "netbsd" 1220 + ], 1221 + "engines": { 1222 + "node": ">=12" 1223 + } 1224 + }, 1225 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { 1226 + "version": "0.18.20", 1227 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", 1228 + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", 1229 + "cpu": [ 1230 + "x64" 1231 + ], 1232 + "dev": true, 1233 + "optional": true, 1234 + "os": [ 1235 + "openbsd" 1236 + ], 1237 + "engines": { 1238 + "node": ">=12" 1239 + } 1240 + }, 1241 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { 1242 + "version": "0.18.20", 1243 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", 1244 + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", 1245 + "cpu": [ 1246 + "x64" 1247 + ], 1248 + "dev": true, 1249 + "optional": true, 1250 + "os": [ 1251 + "sunos" 1252 + ], 1253 + "engines": { 1254 + "node": ">=12" 1255 + } 1256 + }, 1257 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { 1258 + "version": "0.18.20", 1259 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", 1260 + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", 1261 + "cpu": [ 1262 + "arm64" 1263 + ], 1264 + "dev": true, 1265 + "optional": true, 1266 + "os": [ 1267 + "win32" 1268 + ], 1269 + "engines": { 1270 + "node": ">=12" 1271 + } 1272 + }, 1273 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { 1274 + "version": "0.18.20", 1275 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", 1276 + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", 1277 + "cpu": [ 1278 + "ia32" 1279 + ], 1280 + "dev": true, 1281 + "optional": true, 1282 + "os": [ 1283 + "win32" 1284 + ], 1285 + "engines": { 1286 + "node": ">=12" 1287 + } 1288 + }, 1289 + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { 1290 + "version": "0.18.20", 1291 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", 1292 + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", 1293 + "cpu": [ 1294 + "x64" 1295 + ], 1296 + "dev": true, 1297 + "optional": true, 1298 + "os": [ 1299 + "win32" 1300 + ], 1301 + "engines": { 1302 + "node": ">=12" 1303 + } 1304 + }, 732 1305 "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { 733 1306 "version": "0.18.20", 734 1307 "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", ··· 798 1371 "esbuild": "*" 799 1372 } 800 1373 }, 1374 + "node_modules/@esbuild/aix-ppc64": { 1375 + "version": "0.25.4", 1376 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", 1377 + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", 1378 + "cpu": [ 1379 + "ppc64" 1380 + ], 1381 + "dev": true, 1382 + "optional": true, 1383 + "os": [ 1384 + "aix" 1385 + ], 1386 + "engines": { 1387 + "node": ">=18" 1388 + } 1389 + }, 1390 + "node_modules/@esbuild/android-arm": { 1391 + "version": "0.25.4", 1392 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", 1393 + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", 1394 + "cpu": [ 1395 + "arm" 1396 + ], 1397 + "dev": true, 1398 + "optional": true, 1399 + "os": [ 1400 + "android" 1401 + ], 1402 + "engines": { 1403 + "node": ">=18" 1404 + } 1405 + }, 1406 + "node_modules/@esbuild/android-arm64": { 1407 + "version": "0.25.4", 1408 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", 1409 + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", 1410 + "cpu": [ 1411 + "arm64" 1412 + ], 1413 + "dev": true, 1414 + "optional": true, 1415 + "os": [ 1416 + "android" 1417 + ], 1418 + "engines": { 1419 + "node": ">=18" 1420 + } 1421 + }, 1422 + "node_modules/@esbuild/android-x64": { 1423 + "version": "0.25.4", 1424 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", 1425 + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", 1426 + "cpu": [ 1427 + "x64" 1428 + ], 1429 + "dev": true, 1430 + "optional": true, 1431 + "os": [ 1432 + "android" 1433 + ], 1434 + "engines": { 1435 + "node": ">=18" 1436 + } 1437 + }, 1438 + "node_modules/@esbuild/darwin-x64": { 1439 + "version": "0.25.4", 1440 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", 1441 + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", 1442 + "cpu": [ 1443 + "x64" 1444 + ], 1445 + "dev": true, 1446 + "optional": true, 1447 + "os": [ 1448 + "darwin" 1449 + ], 1450 + "engines": { 1451 + "node": ">=18" 1452 + } 1453 + }, 1454 + "node_modules/@esbuild/freebsd-arm64": { 1455 + "version": "0.25.4", 1456 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", 1457 + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", 1458 + "cpu": [ 1459 + "arm64" 1460 + ], 1461 + "dev": true, 1462 + "optional": true, 1463 + "os": [ 1464 + "freebsd" 1465 + ], 1466 + "engines": { 1467 + "node": ">=18" 1468 + } 1469 + }, 1470 + "node_modules/@esbuild/freebsd-x64": { 1471 + "version": "0.25.4", 1472 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", 1473 + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", 1474 + "cpu": [ 1475 + "x64" 1476 + ], 1477 + "dev": true, 1478 + "optional": true, 1479 + "os": [ 1480 + "freebsd" 1481 + ], 1482 + "engines": { 1483 + "node": ">=18" 1484 + } 1485 + }, 1486 + "node_modules/@esbuild/linux-arm": { 1487 + "version": "0.25.4", 1488 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", 1489 + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", 1490 + "cpu": [ 1491 + "arm" 1492 + ], 1493 + "dev": true, 1494 + "optional": true, 1495 + "os": [ 1496 + "linux" 1497 + ], 1498 + "engines": { 1499 + "node": ">=18" 1500 + } 1501 + }, 1502 + "node_modules/@esbuild/linux-arm64": { 1503 + "version": "0.25.4", 1504 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", 1505 + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", 1506 + "cpu": [ 1507 + "arm64" 1508 + ], 1509 + "dev": true, 1510 + "optional": true, 1511 + "os": [ 1512 + "linux" 1513 + ], 1514 + "engines": { 1515 + "node": ">=18" 1516 + } 1517 + }, 1518 + "node_modules/@esbuild/linux-ia32": { 1519 + "version": "0.25.4", 1520 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", 1521 + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", 1522 + "cpu": [ 1523 + "ia32" 1524 + ], 1525 + "dev": true, 1526 + "optional": true, 1527 + "os": [ 1528 + "linux" 1529 + ], 1530 + "engines": { 1531 + "node": ">=18" 1532 + } 1533 + }, 1534 + "node_modules/@esbuild/linux-loong64": { 1535 + "version": "0.25.4", 1536 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", 1537 + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", 1538 + "cpu": [ 1539 + "loong64" 1540 + ], 1541 + "dev": true, 1542 + "optional": true, 1543 + "os": [ 1544 + "linux" 1545 + ], 1546 + "engines": { 1547 + "node": ">=18" 1548 + } 1549 + }, 1550 + "node_modules/@esbuild/linux-mips64el": { 1551 + "version": "0.25.4", 1552 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", 1553 + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", 1554 + "cpu": [ 1555 + "mips64el" 1556 + ], 1557 + "dev": true, 1558 + "optional": true, 1559 + "os": [ 1560 + "linux" 1561 + ], 1562 + "engines": { 1563 + "node": ">=18" 1564 + } 1565 + }, 1566 + "node_modules/@esbuild/linux-ppc64": { 1567 + "version": "0.25.4", 1568 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", 1569 + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", 1570 + "cpu": [ 1571 + "ppc64" 1572 + ], 1573 + "dev": true, 1574 + "optional": true, 1575 + "os": [ 1576 + "linux" 1577 + ], 1578 + "engines": { 1579 + "node": ">=18" 1580 + } 1581 + }, 1582 + "node_modules/@esbuild/linux-riscv64": { 1583 + "version": "0.25.4", 1584 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", 1585 + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", 1586 + "cpu": [ 1587 + "riscv64" 1588 + ], 1589 + "dev": true, 1590 + "optional": true, 1591 + "os": [ 1592 + "linux" 1593 + ], 1594 + "engines": { 1595 + "node": ">=18" 1596 + } 1597 + }, 1598 + "node_modules/@esbuild/linux-s390x": { 1599 + "version": "0.25.4", 1600 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", 1601 + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", 1602 + "cpu": [ 1603 + "s390x" 1604 + ], 1605 + "dev": true, 1606 + "optional": true, 1607 + "os": [ 1608 + "linux" 1609 + ], 1610 + "engines": { 1611 + "node": ">=18" 1612 + } 1613 + }, 801 1614 "node_modules/@esbuild/linux-x64": { 802 1615 "version": "0.25.4", 803 1616 "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", ··· 815 1628 "node": ">=18" 816 1629 } 817 1630 }, 1631 + "node_modules/@esbuild/netbsd-arm64": { 1632 + "version": "0.25.4", 1633 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", 1634 + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", 1635 + "cpu": [ 1636 + "arm64" 1637 + ], 1638 + "dev": true, 1639 + "optional": true, 1640 + "os": [ 1641 + "netbsd" 1642 + ], 1643 + "engines": { 1644 + "node": ">=18" 1645 + } 1646 + }, 1647 + "node_modules/@esbuild/netbsd-x64": { 1648 + "version": "0.25.4", 1649 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", 1650 + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", 1651 + "cpu": [ 1652 + "x64" 1653 + ], 1654 + "dev": true, 1655 + "optional": true, 1656 + "os": [ 1657 + "netbsd" 1658 + ], 1659 + "engines": { 1660 + "node": ">=18" 1661 + } 1662 + }, 1663 + "node_modules/@esbuild/openbsd-arm64": { 1664 + "version": "0.25.4", 1665 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", 1666 + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", 1667 + "cpu": [ 1668 + "arm64" 1669 + ], 1670 + "dev": true, 1671 + "optional": true, 1672 + "os": [ 1673 + "openbsd" 1674 + ], 1675 + "engines": { 1676 + "node": ">=18" 1677 + } 1678 + }, 1679 + "node_modules/@esbuild/openbsd-x64": { 1680 + "version": "0.25.4", 1681 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", 1682 + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", 1683 + "cpu": [ 1684 + "x64" 1685 + ], 1686 + "dev": true, 1687 + "optional": true, 1688 + "os": [ 1689 + "openbsd" 1690 + ], 1691 + "engines": { 1692 + "node": ">=18" 1693 + } 1694 + }, 1695 + "node_modules/@esbuild/sunos-x64": { 1696 + "version": "0.25.4", 1697 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", 1698 + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", 1699 + "cpu": [ 1700 + "x64" 1701 + ], 1702 + "dev": true, 1703 + "optional": true, 1704 + "os": [ 1705 + "sunos" 1706 + ], 1707 + "engines": { 1708 + "node": ">=18" 1709 + } 1710 + }, 1711 + "node_modules/@esbuild/win32-arm64": { 1712 + "version": "0.25.4", 1713 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", 1714 + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", 1715 + "cpu": [ 1716 + "arm64" 1717 + ], 1718 + "dev": true, 1719 + "optional": true, 1720 + "os": [ 1721 + "win32" 1722 + ], 1723 + "engines": { 1724 + "node": ">=18" 1725 + } 1726 + }, 1727 + "node_modules/@esbuild/win32-ia32": { 1728 + "version": "0.25.4", 1729 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", 1730 + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", 1731 + "cpu": [ 1732 + "ia32" 1733 + ], 1734 + "dev": true, 1735 + "optional": true, 1736 + "os": [ 1737 + "win32" 1738 + ], 1739 + "engines": { 1740 + "node": ">=18" 1741 + } 1742 + }, 1743 + "node_modules/@esbuild/win32-x64": { 1744 + "version": "0.25.4", 1745 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", 1746 + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", 1747 + "cpu": [ 1748 + "x64" 1749 + ], 1750 + "dev": true, 1751 + "optional": true, 1752 + "os": [ 1753 + "win32" 1754 + ], 1755 + "engines": { 1756 + "node": ">=18" 1757 + } 1758 + }, 818 1759 "node_modules/@eslint-community/eslint-utils": { 819 - "version": "4.7.0", 820 - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", 821 - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", 1760 + "version": "4.9.0", 1761 + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", 1762 + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", 822 1763 "dev": true, 823 - "license": "MIT", 824 1764 "dependencies": { 825 1765 "eslint-visitor-keys": "^3.4.3" 826 1766 }, ··· 834 1774 "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" 835 1775 } 836 1776 }, 1777 + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { 1778 + "version": "3.4.3", 1779 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", 1780 + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", 1781 + "dev": true, 1782 + "engines": { 1783 + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 1784 + }, 1785 + "funding": { 1786 + "url": "https://opencollective.com/eslint" 1787 + } 1788 + }, 837 1789 "node_modules/@eslint-community/regexpp": { 838 - "version": "4.10.0", 839 - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", 840 - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", 1790 + "version": "4.12.2", 1791 + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", 1792 + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", 841 1793 "dev": true, 842 1794 "engines": { 843 1795 "node": "^12.0.0 || ^14.0.0 || >=16.0.0" 844 1796 } 845 1797 }, 1798 + "node_modules/@eslint/config-array": { 1799 + "version": "0.21.1", 1800 + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", 1801 + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", 1802 + "dev": true, 1803 + "dependencies": { 1804 + "@eslint/object-schema": "^2.1.7", 1805 + "debug": "^4.3.1", 1806 + "minimatch": "^3.1.2" 1807 + }, 1808 + "engines": { 1809 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1810 + } 1811 + }, 1812 + "node_modules/@eslint/config-helpers": { 1813 + "version": "0.4.2", 1814 + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", 1815 + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", 1816 + "dev": true, 1817 + "dependencies": { 1818 + "@eslint/core": "^0.17.0" 1819 + }, 1820 + "engines": { 1821 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1822 + } 1823 + }, 1824 + "node_modules/@eslint/core": { 1825 + "version": "0.17.0", 1826 + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", 1827 + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", 1828 + "dev": true, 1829 + "dependencies": { 1830 + "@types/json-schema": "^7.0.15" 1831 + }, 1832 + "engines": { 1833 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1834 + } 1835 + }, 846 1836 "node_modules/@eslint/eslintrc": { 847 - "version": "2.1.4", 848 - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", 849 - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", 1837 + "version": "3.3.1", 1838 + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", 1839 + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", 850 1840 "dev": true, 851 1841 "dependencies": { 852 1842 "ajv": "^6.12.4", 853 1843 "debug": "^4.3.2", 854 - "espree": "^9.6.0", 855 - "globals": "^13.19.0", 1844 + "espree": "^10.0.1", 1845 + "globals": "^14.0.0", 856 1846 "ignore": "^5.2.0", 857 1847 "import-fresh": "^3.2.1", 858 1848 "js-yaml": "^4.1.0", ··· 860 1850 "strip-json-comments": "^3.1.1" 861 1851 }, 862 1852 "engines": { 863 - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 1853 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 864 1854 }, 865 1855 "funding": { 866 1856 "url": "https://opencollective.com/eslint" 867 1857 } 868 1858 }, 869 1859 "node_modules/@eslint/js": { 870 - "version": "8.57.0", 871 - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", 872 - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", 1860 + "version": "9.39.1", 1861 + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", 1862 + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", 1863 + "dev": true, 1864 + "engines": { 1865 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1866 + }, 1867 + "funding": { 1868 + "url": "https://eslint.org/donate" 1869 + } 1870 + }, 1871 + "node_modules/@eslint/object-schema": { 1872 + "version": "2.1.7", 1873 + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", 1874 + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", 1875 + "dev": true, 1876 + "engines": { 1877 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1878 + } 1879 + }, 1880 + "node_modules/@eslint/plugin-kit": { 1881 + "version": "0.4.1", 1882 + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", 1883 + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", 873 1884 "dev": true, 1885 + "dependencies": { 1886 + "@eslint/core": "^0.17.0", 1887 + "levn": "^0.4.1" 1888 + }, 874 1889 "engines": { 875 - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 1890 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 876 1891 } 877 1892 }, 878 1893 "node_modules/@fastify/busboy": { ··· 1016 2031 "hono": "^4" 1017 2032 } 1018 2033 }, 1019 - "node_modules/@humanwhocodes/config-array": { 1020 - "version": "0.11.14", 1021 - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", 1022 - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", 2034 + "node_modules/@humanfs/core": { 2035 + "version": "0.19.1", 2036 + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", 2037 + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", 2038 + "dev": true, 2039 + "engines": { 2040 + "node": ">=18.18.0" 2041 + } 2042 + }, 2043 + "node_modules/@humanfs/node": { 2044 + "version": "0.16.7", 2045 + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", 2046 + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", 1023 2047 "dev": true, 1024 2048 "dependencies": { 1025 - "@humanwhocodes/object-schema": "^2.0.2", 1026 - "debug": "^4.3.1", 1027 - "minimatch": "^3.0.5" 2049 + "@humanfs/core": "^0.19.1", 2050 + "@humanwhocodes/retry": "^0.4.0" 1028 2051 }, 1029 2052 "engines": { 1030 - "node": ">=10.10.0" 2053 + "node": ">=18.18.0" 1031 2054 } 1032 2055 }, 1033 2056 "node_modules/@humanwhocodes/module-importer": { ··· 1043 2066 "url": "https://github.com/sponsors/nzakas" 1044 2067 } 1045 2068 }, 1046 - "node_modules/@humanwhocodes/object-schema": { 1047 - "version": "2.0.3", 1048 - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", 1049 - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", 1050 - "dev": true 2069 + "node_modules/@humanwhocodes/retry": { 2070 + "version": "0.4.3", 2071 + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", 2072 + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", 2073 + "dev": true, 2074 + "engines": { 2075 + "node": ">=18.18" 2076 + }, 2077 + "funding": { 2078 + "type": "github", 2079 + "url": "https://github.com/sponsors/nzakas" 2080 + } 1051 2081 }, 1052 2082 "node_modules/@img/colour": { 1053 2083 "version": "1.0.0", ··· 1696 2726 } 1697 2727 }, 1698 2728 "node_modules/@next/bundle-analyzer": { 1699 - "version": "15.3.2", 1700 - "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-15.3.2.tgz", 1701 - "integrity": "sha512-zY5O1PNKNxWEjaFX8gKzm77z2oL0cnj+m5aiqNBgay9LPLCDO13Cf+FJONeNq/nJjeXptwHFT9EMmTecF9U4Iw==", 1702 - "license": "MIT", 2729 + "version": "16.0.3", 2730 + "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-16.0.3.tgz", 2731 + "integrity": "sha512-6Xo8f8/ZXtASfTPa6TH1aUn+xDg9Pkyl1YHVxu+89cVdLH7MnYjxv3rPOfEJ9BwCZCU2q4Flyw5MwltfD2pGbA==", 1703 2732 "dependencies": { 1704 2733 "webpack-bundle-analyzer": "4.10.1" 1705 2734 } 1706 2735 }, 1707 2736 "node_modules/@next/env": { 1708 - "version": "15.5.3", 1709 - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz", 1710 - "integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==", 2737 + "version": "16.0.7", 2738 + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.7.tgz", 2739 + "integrity": "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==", 1711 2740 "license": "MIT" 1712 2741 }, 1713 2742 "node_modules/@next/eslint-plugin-next": { 1714 - "version": "15.5.3", 1715 - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.3.tgz", 1716 - "integrity": "sha512-SdhaKdko6dpsSr0DldkESItVrnPYB1NS2NpShCSX5lc7SSQmLZt5Mug6t2xbiuVWEVDLZSuIAoQyYVBYp0dR5g==", 2743 + "version": "16.0.3", 2744 + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.3.tgz", 2745 + "integrity": "sha512-6sPWmZetzFWMsz7Dhuxsdmbu3fK+/AxKRtj7OB0/3OZAI2MHB/v2FeYh271LZ9abvnM1WIwWc/5umYjx0jo5sQ==", 1717 2746 "dev": true, 1718 - "license": "MIT", 1719 2747 "dependencies": { 1720 2748 "fast-glob": "3.3.1" 1721 2749 } ··· 1725 2753 "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", 1726 2754 "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", 1727 2755 "dev": true, 1728 - "license": "MIT", 1729 2756 "dependencies": { 1730 2757 "@nodelib/fs.stat": "^2.0.2", 1731 2758 "@nodelib/fs.walk": "^1.2.3", ··· 1742 2769 "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 1743 2770 "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 1744 2771 "dev": true, 1745 - "license": "ISC", 1746 2772 "dependencies": { 1747 2773 "is-glob": "^4.0.1" 1748 2774 }, ··· 1751 2777 } 1752 2778 }, 1753 2779 "node_modules/@next/mdx": { 1754 - "version": "15.3.2", 1755 - "resolved": "https://registry.npmjs.org/@next/mdx/-/mdx-15.3.2.tgz", 1756 - "integrity": "sha512-D6lSSbVzn1EiPwrBKG5QzXClcgdqiNCL8a3/6oROinzgZnYSxbVmnfs0UrqygtGSOmgW7sdJJSEOy555DoAwvw==", 1757 - "license": "MIT", 2780 + "version": "16.0.3", 2781 + "resolved": "https://registry.npmjs.org/@next/mdx/-/mdx-16.0.3.tgz", 2782 + "integrity": "sha512-uVl2JSEGAjBV+EVnpt1cZN88SK3lJ2n7Fc+iqTsgVx2g9+Y6ru+P6nuUgXd38OHPUIwzL6k2V1u4iV3kwuTySQ==", 1758 2783 "dependencies": { 1759 2784 "source-map": "^0.7.0" 1760 2785 }, ··· 1780 2805 } 1781 2806 }, 1782 2807 "node_modules/@next/swc-darwin-arm64": { 1783 - "version": "15.5.3", 1784 - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz", 1785 - "integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==", 2808 + "version": "16.0.7", 2809 + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.7.tgz", 2810 + "integrity": "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==", 1786 2811 "cpu": [ 1787 2812 "arm64" 1788 2813 ], ··· 1796 2821 } 1797 2822 }, 1798 2823 "node_modules/@next/swc-darwin-x64": { 1799 - "version": "15.5.3", 1800 - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz", 1801 - "integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==", 2824 + "version": "16.0.7", 2825 + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.7.tgz", 2826 + "integrity": "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==", 1802 2827 "cpu": [ 1803 2828 "x64" 1804 2829 ], ··· 1812 2837 } 1813 2838 }, 1814 2839 "node_modules/@next/swc-linux-arm64-gnu": { 1815 - "version": "15.5.3", 1816 - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz", 1817 - "integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==", 2840 + "version": "16.0.7", 2841 + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.7.tgz", 2842 + "integrity": "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==", 1818 2843 "cpu": [ 1819 2844 "arm64" 1820 2845 ], ··· 1828 2853 } 1829 2854 }, 1830 2855 "node_modules/@next/swc-linux-arm64-musl": { 1831 - "version": "15.5.3", 1832 - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz", 1833 - "integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==", 2856 + "version": "16.0.7", 2857 + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.7.tgz", 2858 + "integrity": "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==", 1834 2859 "cpu": [ 1835 2860 "arm64" 1836 2861 ], ··· 1844 2869 } 1845 2870 }, 1846 2871 "node_modules/@next/swc-linux-x64-gnu": { 1847 - "version": "15.5.3", 1848 - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz", 1849 - "integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==", 2872 + "version": "16.0.7", 2873 + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.7.tgz", 2874 + "integrity": "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==", 1850 2875 "cpu": [ 1851 2876 "x64" 1852 2877 ], ··· 1860 2885 } 1861 2886 }, 1862 2887 "node_modules/@next/swc-linux-x64-musl": { 1863 - "version": "15.5.3", 1864 - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz", 1865 - "integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==", 2888 + "version": "16.0.7", 2889 + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.7.tgz", 2890 + "integrity": "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==", 1866 2891 "cpu": [ 1867 2892 "x64" 1868 2893 ], ··· 1876 2901 } 1877 2902 }, 1878 2903 "node_modules/@next/swc-win32-arm64-msvc": { 1879 - "version": "15.5.3", 1880 - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz", 1881 - "integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==", 2904 + "version": "16.0.7", 2905 + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.7.tgz", 2906 + "integrity": "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==", 1882 2907 "cpu": [ 1883 2908 "arm64" 1884 2909 ], ··· 1892 2917 } 1893 2918 }, 1894 2919 "node_modules/@next/swc-win32-x64-msvc": { 1895 - "version": "15.5.3", 1896 - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz", 1897 - "integrity": "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==", 2920 + "version": "16.0.7", 2921 + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.7.tgz", 2922 + "integrity": "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==", 1898 2923 "cpu": [ 1899 2924 "x64" 1900 2925 ], ··· 5996 7021 "dev": true, 5997 7022 "license": "MIT" 5998 7023 }, 5999 - "node_modules/@rushstack/eslint-patch": { 6000 - "version": "1.10.3", 6001 - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", 6002 - "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", 6003 - "dev": true 6004 - }, 6005 7024 "node_modules/@shikijs/core": { 6006 7025 "version": "3.8.1", 6007 7026 "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.8.1.tgz", ··· 6596 7615 "@types/unist": "*" 6597 7616 } 6598 7617 }, 7618 + "node_modules/@types/json-schema": { 7619 + "version": "7.0.15", 7620 + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", 7621 + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", 7622 + "dev": true 7623 + }, 6599 7624 "node_modules/@types/json5": { 6600 7625 "version": "0.0.29", 6601 7626 "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", ··· 6722 7747 "integrity": "sha512-B34A7uot1Cv0XtaHRYDATltAdKx0BvVKNgYNqE4WjtPUa4VQJM7kxeXcVKaH+KS+kCmZ+6w+QaUdcljiheiBJA==" 6723 7748 }, 6724 7749 "node_modules/@types/react": { 6725 - "version": "19.1.3", 6726 - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz", 6727 - "integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==", 6728 - "license": "MIT", 7750 + "version": "19.2.6", 7751 + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz", 7752 + "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", 6729 7753 "dependencies": { 6730 - "csstype": "^3.0.2" 7754 + "csstype": "^3.2.2" 6731 7755 } 6732 7756 }, 6733 7757 "node_modules/@types/react-dom": { 6734 - "version": "19.1.3", 6735 - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.3.tgz", 6736 - "integrity": "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==", 7758 + "version": "19.2.3", 7759 + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", 7760 + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", 6737 7761 "devOptional": true, 6738 - "license": "MIT", 6739 7762 "peerDependencies": { 6740 - "@types/react": "^19.0.0" 7763 + "@types/react": "^19.2.0" 6741 7764 } 6742 7765 }, 6743 7766 "node_modules/@types/shimmer": { ··· 6775 7798 } 6776 7799 }, 6777 7800 "node_modules/@typescript-eslint/eslint-plugin": { 6778 - "version": "8.32.0", 6779 - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", 6780 - "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", 7801 + "version": "8.47.0", 7802 + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz", 7803 + "integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==", 6781 7804 "dev": true, 6782 - "license": "MIT", 6783 7805 "dependencies": { 6784 7806 "@eslint-community/regexpp": "^4.10.0", 6785 - "@typescript-eslint/scope-manager": "8.32.0", 6786 - "@typescript-eslint/type-utils": "8.32.0", 6787 - "@typescript-eslint/utils": "8.32.0", 6788 - "@typescript-eslint/visitor-keys": "8.32.0", 7807 + "@typescript-eslint/scope-manager": "8.47.0", 7808 + "@typescript-eslint/type-utils": "8.47.0", 7809 + "@typescript-eslint/utils": "8.47.0", 7810 + "@typescript-eslint/visitor-keys": "8.47.0", 6789 7811 "graphemer": "^1.4.0", 6790 - "ignore": "^5.3.1", 7812 + "ignore": "^7.0.0", 6791 7813 "natural-compare": "^1.4.0", 6792 7814 "ts-api-utils": "^2.1.0" 6793 7815 }, ··· 6799 7821 "url": "https://opencollective.com/typescript-eslint" 6800 7822 }, 6801 7823 "peerDependencies": { 6802 - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", 7824 + "@typescript-eslint/parser": "^8.47.0", 6803 7825 "eslint": "^8.57.0 || ^9.0.0", 6804 - "typescript": ">=4.8.4 <5.9.0" 7826 + "typescript": ">=4.8.4 <6.0.0" 7827 + } 7828 + }, 7829 + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { 7830 + "version": "7.0.5", 7831 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", 7832 + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", 7833 + "dev": true, 7834 + "engines": { 7835 + "node": ">= 4" 6805 7836 } 6806 7837 }, 6807 7838 "node_modules/@typescript-eslint/parser": { 6808 - "version": "8.32.0", 6809 - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", 6810 - "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", 7839 + "version": "8.47.0", 7840 + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.47.0.tgz", 7841 + "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", 6811 7842 "dev": true, 6812 - "license": "MIT", 6813 7843 "dependencies": { 6814 - "@typescript-eslint/scope-manager": "8.32.0", 6815 - "@typescript-eslint/types": "8.32.0", 6816 - "@typescript-eslint/typescript-estree": "8.32.0", 6817 - "@typescript-eslint/visitor-keys": "8.32.0", 7844 + "@typescript-eslint/scope-manager": "8.47.0", 7845 + "@typescript-eslint/types": "8.47.0", 7846 + "@typescript-eslint/typescript-estree": "8.47.0", 7847 + "@typescript-eslint/visitor-keys": "8.47.0", 6818 7848 "debug": "^4.3.4" 6819 7849 }, 6820 7850 "engines": { ··· 6826 7856 }, 6827 7857 "peerDependencies": { 6828 7858 "eslint": "^8.57.0 || ^9.0.0", 6829 - "typescript": ">=4.8.4 <5.9.0" 7859 + "typescript": ">=4.8.4 <6.0.0" 7860 + } 7861 + }, 7862 + "node_modules/@typescript-eslint/project-service": { 7863 + "version": "8.47.0", 7864 + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.47.0.tgz", 7865 + "integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==", 7866 + "dev": true, 7867 + "dependencies": { 7868 + "@typescript-eslint/tsconfig-utils": "^8.47.0", 7869 + "@typescript-eslint/types": "^8.47.0", 7870 + "debug": "^4.3.4" 7871 + }, 7872 + "engines": { 7873 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 7874 + }, 7875 + "funding": { 7876 + "type": "opencollective", 7877 + "url": "https://opencollective.com/typescript-eslint" 7878 + }, 7879 + "peerDependencies": { 7880 + "typescript": ">=4.8.4 <6.0.0" 6830 7881 } 6831 7882 }, 6832 7883 "node_modules/@typescript-eslint/scope-manager": { 6833 - "version": "8.32.0", 6834 - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", 6835 - "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", 7884 + "version": "8.47.0", 7885 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz", 7886 + "integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==", 6836 7887 "dev": true, 6837 - "license": "MIT", 6838 7888 "dependencies": { 6839 - "@typescript-eslint/types": "8.32.0", 6840 - "@typescript-eslint/visitor-keys": "8.32.0" 7889 + "@typescript-eslint/types": "8.47.0", 7890 + "@typescript-eslint/visitor-keys": "8.47.0" 6841 7891 }, 6842 7892 "engines": { 6843 7893 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 6847 7897 "url": "https://opencollective.com/typescript-eslint" 6848 7898 } 6849 7899 }, 7900 + "node_modules/@typescript-eslint/tsconfig-utils": { 7901 + "version": "8.47.0", 7902 + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz", 7903 + "integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==", 7904 + "dev": true, 7905 + "engines": { 7906 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 7907 + }, 7908 + "funding": { 7909 + "type": "opencollective", 7910 + "url": "https://opencollective.com/typescript-eslint" 7911 + }, 7912 + "peerDependencies": { 7913 + "typescript": ">=4.8.4 <6.0.0" 7914 + } 7915 + }, 6850 7916 "node_modules/@typescript-eslint/type-utils": { 6851 - "version": "8.32.0", 6852 - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", 6853 - "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", 7917 + "version": "8.47.0", 7918 + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz", 7919 + "integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==", 6854 7920 "dev": true, 6855 - "license": "MIT", 6856 7921 "dependencies": { 6857 - "@typescript-eslint/typescript-estree": "8.32.0", 6858 - "@typescript-eslint/utils": "8.32.0", 7922 + "@typescript-eslint/types": "8.47.0", 7923 + "@typescript-eslint/typescript-estree": "8.47.0", 7924 + "@typescript-eslint/utils": "8.47.0", 6859 7925 "debug": "^4.3.4", 6860 7926 "ts-api-utils": "^2.1.0" 6861 7927 }, ··· 6868 7934 }, 6869 7935 "peerDependencies": { 6870 7936 "eslint": "^8.57.0 || ^9.0.0", 6871 - "typescript": ">=4.8.4 <5.9.0" 7937 + "typescript": ">=4.8.4 <6.0.0" 6872 7938 } 6873 7939 }, 6874 7940 "node_modules/@typescript-eslint/types": { 6875 - "version": "8.32.0", 6876 - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", 6877 - "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", 7941 + "version": "8.47.0", 7942 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz", 7943 + "integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==", 6878 7944 "dev": true, 6879 - "license": "MIT", 6880 7945 "engines": { 6881 7946 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 6882 7947 }, ··· 6886 7951 } 6887 7952 }, 6888 7953 "node_modules/@typescript-eslint/typescript-estree": { 6889 - "version": "8.32.0", 6890 - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", 6891 - "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", 7954 + "version": "8.47.0", 7955 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz", 7956 + "integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==", 6892 7957 "dev": true, 6893 - "license": "MIT", 6894 7958 "dependencies": { 6895 - "@typescript-eslint/types": "8.32.0", 6896 - "@typescript-eslint/visitor-keys": "8.32.0", 7959 + "@typescript-eslint/project-service": "8.47.0", 7960 + "@typescript-eslint/tsconfig-utils": "8.47.0", 7961 + "@typescript-eslint/types": "8.47.0", 7962 + "@typescript-eslint/visitor-keys": "8.47.0", 6897 7963 "debug": "^4.3.4", 6898 7964 "fast-glob": "^3.3.2", 6899 7965 "is-glob": "^4.0.3", ··· 6909 7975 "url": "https://opencollective.com/typescript-eslint" 6910 7976 }, 6911 7977 "peerDependencies": { 6912 - "typescript": ">=4.8.4 <5.9.0" 7978 + "typescript": ">=4.8.4 <6.0.0" 6913 7979 } 6914 7980 }, 6915 7981 "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { 6916 - "version": "2.0.1", 6917 - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 6918 - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 7982 + "version": "2.0.2", 7983 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", 7984 + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", 6919 7985 "dev": true, 6920 - "license": "MIT", 6921 7986 "dependencies": { 6922 7987 "balanced-match": "^1.0.0" 6923 7988 } ··· 6927 7992 "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", 6928 7993 "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", 6929 7994 "dev": true, 6930 - "license": "ISC", 6931 7995 "dependencies": { 6932 7996 "brace-expansion": "^2.0.1" 6933 7997 }, ··· 6939 8003 } 6940 8004 }, 6941 8005 "node_modules/@typescript-eslint/utils": { 6942 - "version": "8.32.0", 6943 - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", 6944 - "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", 8006 + "version": "8.47.0", 8007 + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.47.0.tgz", 8008 + "integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==", 6945 8009 "dev": true, 6946 - "license": "MIT", 6947 8010 "dependencies": { 6948 8011 "@eslint-community/eslint-utils": "^4.7.0", 6949 - "@typescript-eslint/scope-manager": "8.32.0", 6950 - "@typescript-eslint/types": "8.32.0", 6951 - "@typescript-eslint/typescript-estree": "8.32.0" 8012 + "@typescript-eslint/scope-manager": "8.47.0", 8013 + "@typescript-eslint/types": "8.47.0", 8014 + "@typescript-eslint/typescript-estree": "8.47.0" 6952 8015 }, 6953 8016 "engines": { 6954 8017 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 6959 8022 }, 6960 8023 "peerDependencies": { 6961 8024 "eslint": "^8.57.0 || ^9.0.0", 6962 - "typescript": ">=4.8.4 <5.9.0" 8025 + "typescript": ">=4.8.4 <6.0.0" 6963 8026 } 6964 8027 }, 6965 8028 "node_modules/@typescript-eslint/visitor-keys": { 6966 - "version": "8.32.0", 6967 - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", 6968 - "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", 8029 + "version": "8.47.0", 8030 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz", 8031 + "integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==", 6969 8032 "dev": true, 6970 - "license": "MIT", 6971 8033 "dependencies": { 6972 - "@typescript-eslint/types": "8.32.0", 6973 - "eslint-visitor-keys": "^4.2.0" 8034 + "@typescript-eslint/types": "8.47.0", 8035 + "eslint-visitor-keys": "^4.2.1" 6974 8036 }, 6975 8037 "engines": { 6976 8038 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 6980 8042 "url": "https://opencollective.com/typescript-eslint" 6981 8043 } 6982 8044 }, 6983 - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { 6984 - "version": "4.2.0", 6985 - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", 6986 - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", 6987 - "dev": true, 6988 - "license": "Apache-2.0", 6989 - "engines": { 6990 - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 6991 - }, 6992 - "funding": { 6993 - "url": "https://opencollective.com/eslint" 6994 - } 6995 - }, 6996 8045 "node_modules/@ungap/structured-clone": { 6997 8046 "version": "1.2.0", 6998 8047 "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", ··· 7166 8215 "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", 7167 8216 "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", 7168 8217 "dev": true, 7169 - "license": "MIT", 7170 8218 "dependencies": { 7171 8219 "fast-deep-equal": "^3.1.3", 7172 8220 "fast-uri": "^3.0.1", ··· 7263 8311 "license": "MIT" 7264 8312 }, 7265 8313 "node_modules/array-includes": { 7266 - "version": "3.1.8", 7267 - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", 7268 - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", 8314 + "version": "3.1.9", 8315 + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", 8316 + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", 7269 8317 "dev": true, 7270 8318 "dependencies": { 7271 - "call-bind": "^1.0.7", 8319 + "call-bind": "^1.0.8", 8320 + "call-bound": "^1.0.4", 7272 8321 "define-properties": "^1.2.1", 7273 - "es-abstract": "^1.23.2", 7274 - "es-object-atoms": "^1.0.0", 7275 - "get-intrinsic": "^1.2.4", 7276 - "is-string": "^1.0.7" 8322 + "es-abstract": "^1.24.0", 8323 + "es-object-atoms": "^1.1.1", 8324 + "get-intrinsic": "^1.3.0", 8325 + "is-string": "^1.1.1", 8326 + "math-intrinsics": "^1.1.0" 7277 8327 }, 7278 8328 "engines": { 7279 8329 "node": ">= 0.4" ··· 7304 8354 } 7305 8355 }, 7306 8356 "node_modules/array.prototype.findlastindex": { 7307 - "version": "1.2.5", 7308 - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", 7309 - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", 8357 + "version": "1.2.6", 8358 + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", 8359 + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", 7310 8360 "dev": true, 7311 8361 "dependencies": { 7312 - "call-bind": "^1.0.7", 8362 + "call-bind": "^1.0.8", 8363 + "call-bound": "^1.0.4", 7313 8364 "define-properties": "^1.2.1", 7314 - "es-abstract": "^1.23.2", 8365 + "es-abstract": "^1.23.9", 7315 8366 "es-errors": "^1.3.0", 7316 - "es-object-atoms": "^1.0.0", 7317 - "es-shim-unscopables": "^1.0.2" 8367 + "es-object-atoms": "^1.1.1", 8368 + "es-shim-unscopables": "^1.1.0" 7318 8369 }, 7319 8370 "engines": { 7320 8371 "node": ">= 0.4" ··· 7324 8375 } 7325 8376 }, 7326 8377 "node_modules/array.prototype.flat": { 7327 - "version": "1.3.2", 7328 - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", 7329 - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", 8378 + "version": "1.3.3", 8379 + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", 8380 + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", 7330 8381 "dev": true, 7331 8382 "dependencies": { 7332 - "call-bind": "^1.0.2", 7333 - "define-properties": "^1.2.0", 7334 - "es-abstract": "^1.22.1", 7335 - "es-shim-unscopables": "^1.0.0" 8383 + "call-bind": "^1.0.8", 8384 + "define-properties": "^1.2.1", 8385 + "es-abstract": "^1.23.5", 8386 + "es-shim-unscopables": "^1.0.2" 7336 8387 }, 7337 8388 "engines": { 7338 8389 "node": ">= 0.4" ··· 7543 8594 } 7544 8595 ] 7545 8596 }, 8597 + "node_modules/baseline-browser-mapping": { 8598 + "version": "2.8.30", 8599 + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz", 8600 + "integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==", 8601 + "dev": true, 8602 + "bin": { 8603 + "baseline-browser-mapping": "dist/cli.js" 8604 + } 8605 + }, 7546 8606 "node_modules/bignumber.js": { 7547 8607 "version": "9.3.1", 7548 8608 "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", ··· 7661 8721 "node": ">=8" 7662 8722 } 7663 8723 }, 8724 + "node_modules/browserslist": { 8725 + "version": "4.28.0", 8726 + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", 8727 + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", 8728 + "dev": true, 8729 + "funding": [ 8730 + { 8731 + "type": "opencollective", 8732 + "url": "https://opencollective.com/browserslist" 8733 + }, 8734 + { 8735 + "type": "tidelift", 8736 + "url": "https://tidelift.com/funding/github/npm/browserslist" 8737 + }, 8738 + { 8739 + "type": "github", 8740 + "url": "https://github.com/sponsors/ai" 8741 + } 8742 + ], 8743 + "dependencies": { 8744 + "baseline-browser-mapping": "^2.8.25", 8745 + "caniuse-lite": "^1.0.30001754", 8746 + "electron-to-chromium": "^1.5.249", 8747 + "node-releases": "^2.0.27", 8748 + "update-browserslist-db": "^1.1.4" 8749 + }, 8750 + "bin": { 8751 + "browserslist": "cli.js" 8752 + }, 8753 + "engines": { 8754 + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" 8755 + } 8756 + }, 7664 8757 "node_modules/buffer": { 7665 8758 "version": "6.0.3", 7666 8759 "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", ··· 7764 8857 } 7765 8858 }, 7766 8859 "node_modules/caniuse-lite": { 7767 - "version": "1.0.30001717", 7768 - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz", 7769 - "integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==", 8860 + "version": "1.0.30001756", 8861 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz", 8862 + "integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==", 7770 8863 "funding": [ 7771 8864 { 7772 8865 "type": "opencollective", ··· 7780 8873 "type": "github", 7781 8874 "url": "https://github.com/sponsors/ai" 7782 8875 } 7783 - ], 7784 - "license": "CC-BY-4.0" 8876 + ] 7785 8877 }, 7786 8878 "node_modules/canonicalize": { 7787 8879 "version": "1.0.8", ··· 8104 9196 "node": ">= 0.6" 8105 9197 } 8106 9198 }, 9199 + "node_modules/convert-source-map": { 9200 + "version": "2.0.0", 9201 + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", 9202 + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", 9203 + "dev": true 9204 + }, 8107 9205 "node_modules/cookie": { 8108 9206 "version": "0.5.0", 8109 9207 "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", ··· 8155 9253 } 8156 9254 }, 8157 9255 "node_modules/cross-spawn": { 8158 - "version": "7.0.3", 8159 - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 8160 - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 9256 + "version": "7.0.6", 9257 + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 9258 + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 8161 9259 "dev": true, 8162 9260 "dependencies": { 8163 9261 "path-key": "^3.1.0", ··· 8169 9267 } 8170 9268 }, 8171 9269 "node_modules/csstype": { 8172 - "version": "3.1.3", 8173 - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", 8174 - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" 9270 + "version": "3.2.3", 9271 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", 9272 + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" 8175 9273 }, 8176 9274 "node_modules/d": { 8177 9275 "version": "1.0.2", ··· 8433 9531 "node": "*" 8434 9532 } 8435 9533 }, 8436 - "node_modules/doctrine": { 8437 - "version": "3.0.0", 8438 - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", 8439 - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", 8440 - "dev": true, 8441 - "dependencies": { 8442 - "esutils": "^2.0.2" 8443 - }, 8444 - "engines": { 8445 - "node": ">=6.0.0" 8446 - } 8447 - }, 8448 9534 "node_modules/dreamopt": { 8449 9535 "version": "0.8.0", 8450 9536 "resolved": "https://registry.npmjs.org/dreamopt/-/dreamopt-0.8.0.tgz", ··· 8477 9563 "drizzle-kit": "bin.cjs" 8478 9564 } 8479 9565 }, 9566 + "node_modules/drizzle-kit/node_modules/@esbuild/aix-ppc64": { 9567 + "version": "0.19.12", 9568 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", 9569 + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", 9570 + "cpu": [ 9571 + "ppc64" 9572 + ], 9573 + "dev": true, 9574 + "optional": true, 9575 + "os": [ 9576 + "aix" 9577 + ], 9578 + "engines": { 9579 + "node": ">=12" 9580 + } 9581 + }, 9582 + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm": { 9583 + "version": "0.19.12", 9584 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", 9585 + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", 9586 + "cpu": [ 9587 + "arm" 9588 + ], 9589 + "dev": true, 9590 + "optional": true, 9591 + "os": [ 9592 + "android" 9593 + ], 9594 + "engines": { 9595 + "node": ">=12" 9596 + } 9597 + }, 9598 + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm64": { 9599 + "version": "0.19.12", 9600 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", 9601 + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", 9602 + "cpu": [ 9603 + "arm64" 9604 + ], 9605 + "dev": true, 9606 + "optional": true, 9607 + "os": [ 9608 + "android" 9609 + ], 9610 + "engines": { 9611 + "node": ">=12" 9612 + } 9613 + }, 9614 + "node_modules/drizzle-kit/node_modules/@esbuild/android-x64": { 9615 + "version": "0.19.12", 9616 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", 9617 + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", 9618 + "cpu": [ 9619 + "x64" 9620 + ], 9621 + "dev": true, 9622 + "optional": true, 9623 + "os": [ 9624 + "android" 9625 + ], 9626 + "engines": { 9627 + "node": ">=12" 9628 + } 9629 + }, 9630 + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-arm64": { 9631 + "version": "0.19.12", 9632 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", 9633 + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", 9634 + "cpu": [ 9635 + "arm64" 9636 + ], 9637 + "dev": true, 9638 + "optional": true, 9639 + "os": [ 9640 + "darwin" 9641 + ], 9642 + "engines": { 9643 + "node": ">=12" 9644 + } 9645 + }, 9646 + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-x64": { 9647 + "version": "0.19.12", 9648 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", 9649 + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", 9650 + "cpu": [ 9651 + "x64" 9652 + ], 9653 + "dev": true, 9654 + "optional": true, 9655 + "os": [ 9656 + "darwin" 9657 + ], 9658 + "engines": { 9659 + "node": ">=12" 9660 + } 9661 + }, 9662 + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-arm64": { 9663 + "version": "0.19.12", 9664 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", 9665 + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", 9666 + "cpu": [ 9667 + "arm64" 9668 + ], 9669 + "dev": true, 9670 + "optional": true, 9671 + "os": [ 9672 + "freebsd" 9673 + ], 9674 + "engines": { 9675 + "node": ">=12" 9676 + } 9677 + }, 9678 + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-x64": { 9679 + "version": "0.19.12", 9680 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", 9681 + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", 9682 + "cpu": [ 9683 + "x64" 9684 + ], 9685 + "dev": true, 9686 + "optional": true, 9687 + "os": [ 9688 + "freebsd" 9689 + ], 9690 + "engines": { 9691 + "node": ">=12" 9692 + } 9693 + }, 9694 + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm": { 9695 + "version": "0.19.12", 9696 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", 9697 + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", 9698 + "cpu": [ 9699 + "arm" 9700 + ], 9701 + "dev": true, 9702 + "optional": true, 9703 + "os": [ 9704 + "linux" 9705 + ], 9706 + "engines": { 9707 + "node": ">=12" 9708 + } 9709 + }, 9710 + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm64": { 9711 + "version": "0.19.12", 9712 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", 9713 + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", 9714 + "cpu": [ 9715 + "arm64" 9716 + ], 9717 + "dev": true, 9718 + "optional": true, 9719 + "os": [ 9720 + "linux" 9721 + ], 9722 + "engines": { 9723 + "node": ">=12" 9724 + } 9725 + }, 9726 + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ia32": { 9727 + "version": "0.19.12", 9728 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", 9729 + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", 9730 + "cpu": [ 9731 + "ia32" 9732 + ], 9733 + "dev": true, 9734 + "optional": true, 9735 + "os": [ 9736 + "linux" 9737 + ], 9738 + "engines": { 9739 + "node": ">=12" 9740 + } 9741 + }, 9742 + "node_modules/drizzle-kit/node_modules/@esbuild/linux-loong64": { 9743 + "version": "0.19.12", 9744 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", 9745 + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", 9746 + "cpu": [ 9747 + "loong64" 9748 + ], 9749 + "dev": true, 9750 + "optional": true, 9751 + "os": [ 9752 + "linux" 9753 + ], 9754 + "engines": { 9755 + "node": ">=12" 9756 + } 9757 + }, 9758 + "node_modules/drizzle-kit/node_modules/@esbuild/linux-mips64el": { 9759 + "version": "0.19.12", 9760 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", 9761 + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", 9762 + "cpu": [ 9763 + "mips64el" 9764 + ], 9765 + "dev": true, 9766 + "optional": true, 9767 + "os": [ 9768 + "linux" 9769 + ], 9770 + "engines": { 9771 + "node": ">=12" 9772 + } 9773 + }, 9774 + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ppc64": { 9775 + "version": "0.19.12", 9776 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", 9777 + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", 9778 + "cpu": [ 9779 + "ppc64" 9780 + ], 9781 + "dev": true, 9782 + "optional": true, 9783 + "os": [ 9784 + "linux" 9785 + ], 9786 + "engines": { 9787 + "node": ">=12" 9788 + } 9789 + }, 9790 + "node_modules/drizzle-kit/node_modules/@esbuild/linux-riscv64": { 9791 + "version": "0.19.12", 9792 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", 9793 + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", 9794 + "cpu": [ 9795 + "riscv64" 9796 + ], 9797 + "dev": true, 9798 + "optional": true, 9799 + "os": [ 9800 + "linux" 9801 + ], 9802 + "engines": { 9803 + "node": ">=12" 9804 + } 9805 + }, 9806 + "node_modules/drizzle-kit/node_modules/@esbuild/linux-s390x": { 9807 + "version": "0.19.12", 9808 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", 9809 + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", 9810 + "cpu": [ 9811 + "s390x" 9812 + ], 9813 + "dev": true, 9814 + "optional": true, 9815 + "os": [ 9816 + "linux" 9817 + ], 9818 + "engines": { 9819 + "node": ">=12" 9820 + } 9821 + }, 8480 9822 "node_modules/drizzle-kit/node_modules/@esbuild/linux-x64": { 8481 9823 "version": "0.19.12", 8482 9824 "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", ··· 8494 9836 "node": ">=12" 8495 9837 } 8496 9838 }, 9839 + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-x64": { 9840 + "version": "0.19.12", 9841 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", 9842 + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", 9843 + "cpu": [ 9844 + "x64" 9845 + ], 9846 + "dev": true, 9847 + "optional": true, 9848 + "os": [ 9849 + "netbsd" 9850 + ], 9851 + "engines": { 9852 + "node": ">=12" 9853 + } 9854 + }, 9855 + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-x64": { 9856 + "version": "0.19.12", 9857 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", 9858 + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", 9859 + "cpu": [ 9860 + "x64" 9861 + ], 9862 + "dev": true, 9863 + "optional": true, 9864 + "os": [ 9865 + "openbsd" 9866 + ], 9867 + "engines": { 9868 + "node": ">=12" 9869 + } 9870 + }, 9871 + "node_modules/drizzle-kit/node_modules/@esbuild/sunos-x64": { 9872 + "version": "0.19.12", 9873 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", 9874 + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", 9875 + "cpu": [ 9876 + "x64" 9877 + ], 9878 + "dev": true, 9879 + "optional": true, 9880 + "os": [ 9881 + "sunos" 9882 + ], 9883 + "engines": { 9884 + "node": ">=12" 9885 + } 9886 + }, 9887 + "node_modules/drizzle-kit/node_modules/@esbuild/win32-arm64": { 9888 + "version": "0.19.12", 9889 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", 9890 + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", 9891 + "cpu": [ 9892 + "arm64" 9893 + ], 9894 + "dev": true, 9895 + "optional": true, 9896 + "os": [ 9897 + "win32" 9898 + ], 9899 + "engines": { 9900 + "node": ">=12" 9901 + } 9902 + }, 9903 + "node_modules/drizzle-kit/node_modules/@esbuild/win32-ia32": { 9904 + "version": "0.19.12", 9905 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", 9906 + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", 9907 + "cpu": [ 9908 + "ia32" 9909 + ], 9910 + "dev": true, 9911 + "optional": true, 9912 + "os": [ 9913 + "win32" 9914 + ], 9915 + "engines": { 9916 + "node": ">=12" 9917 + } 9918 + }, 9919 + "node_modules/drizzle-kit/node_modules/@esbuild/win32-x64": { 9920 + "version": "0.19.12", 9921 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", 9922 + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", 9923 + "cpu": [ 9924 + "x64" 9925 + ], 9926 + "dev": true, 9927 + "optional": true, 9928 + "os": [ 9929 + "win32" 9930 + ], 9931 + "engines": { 9932 + "node": ">=12" 9933 + } 9934 + }, 8497 9935 "node_modules/drizzle-kit/node_modules/esbuild": { 8498 9936 "version": "0.19.12", 8499 9937 "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", ··· 8677 10115 "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 8678 10116 "license": "MIT" 8679 10117 }, 10118 + "node_modules/electron-to-chromium": { 10119 + "version": "1.5.258", 10120 + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.258.tgz", 10121 + "integrity": "sha512-rHUggNV5jKQ0sSdWwlaRDkFc3/rRJIVnOSe9yR4zrR07m3ZxhP4N27Hlg8VeJGGYgFTxK5NqDmWI4DSH72vIJg==", 10122 + "dev": true 10123 + }, 8680 10124 "node_modules/emoji-regex": { 8681 10125 "version": "9.2.2", 8682 10126 "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", ··· 8730 10174 } 8731 10175 }, 8732 10176 "node_modules/es-abstract": { 8733 - "version": "1.23.9", 8734 - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", 8735 - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", 10177 + "version": "1.24.0", 10178 + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", 10179 + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", 8736 10180 "dev": true, 8737 - "license": "MIT", 8738 10181 "dependencies": { 8739 10182 "array-buffer-byte-length": "^1.0.2", 8740 10183 "arraybuffer.prototype.slice": "^1.0.4", 8741 10184 "available-typed-arrays": "^1.0.7", 8742 10185 "call-bind": "^1.0.8", 8743 - "call-bound": "^1.0.3", 10186 + "call-bound": "^1.0.4", 8744 10187 "data-view-buffer": "^1.0.2", 8745 10188 "data-view-byte-length": "^1.0.2", 8746 10189 "data-view-byte-offset": "^1.0.1", 8747 10190 "es-define-property": "^1.0.1", 8748 10191 "es-errors": "^1.3.0", 8749 - "es-object-atoms": "^1.0.0", 10192 + "es-object-atoms": "^1.1.1", 8750 10193 "es-set-tostringtag": "^2.1.0", 8751 10194 "es-to-primitive": "^1.3.0", 8752 10195 "function.prototype.name": "^1.1.8", 8753 - "get-intrinsic": "^1.2.7", 8754 - "get-proto": "^1.0.0", 10196 + "get-intrinsic": "^1.3.0", 10197 + "get-proto": "^1.0.1", 8755 10198 "get-symbol-description": "^1.1.0", 8756 10199 "globalthis": "^1.0.4", 8757 10200 "gopd": "^1.2.0", ··· 8763 10206 "is-array-buffer": "^3.0.5", 8764 10207 "is-callable": "^1.2.7", 8765 10208 "is-data-view": "^1.0.2", 10209 + "is-negative-zero": "^2.0.3", 8766 10210 "is-regex": "^1.2.1", 10211 + "is-set": "^2.0.3", 8767 10212 "is-shared-array-buffer": "^1.0.4", 8768 10213 "is-string": "^1.1.1", 8769 10214 "is-typed-array": "^1.1.15", 8770 - "is-weakref": "^1.1.0", 10215 + "is-weakref": "^1.1.1", 8771 10216 "math-intrinsics": "^1.1.0", 8772 - "object-inspect": "^1.13.3", 10217 + "object-inspect": "^1.13.4", 8773 10218 "object-keys": "^1.1.1", 8774 10219 "object.assign": "^4.1.7", 8775 10220 "own-keys": "^1.0.1", 8776 - "regexp.prototype.flags": "^1.5.3", 10221 + "regexp.prototype.flags": "^1.5.4", 8777 10222 "safe-array-concat": "^1.1.3", 8778 10223 "safe-push-apply": "^1.0.0", 8779 10224 "safe-regex-test": "^1.1.0", 8780 10225 "set-proto": "^1.0.0", 10226 + "stop-iteration-iterator": "^1.1.0", 8781 10227 "string.prototype.trim": "^1.2.10", 8782 10228 "string.prototype.trimend": "^1.0.9", 8783 10229 "string.prototype.trimstart": "^1.0.8", ··· 8786 10232 "typed-array-byte-offset": "^1.0.4", 8787 10233 "typed-array-length": "^1.0.7", 8788 10234 "unbox-primitive": "^1.1.0", 8789 - "which-typed-array": "^1.1.18" 10235 + "which-typed-array": "^1.1.19" 8790 10236 }, 8791 10237 "engines": { 8792 10238 "node": ">= 0.4" ··· 8869 10315 } 8870 10316 }, 8871 10317 "node_modules/es-shim-unscopables": { 8872 - "version": "1.0.2", 8873 - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", 8874 - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", 10318 + "version": "1.1.0", 10319 + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", 10320 + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", 8875 10321 "dev": true, 8876 10322 "dependencies": { 8877 - "hasown": "^2.0.0" 10323 + "hasown": "^2.0.2" 10324 + }, 10325 + "engines": { 10326 + "node": ">= 0.4" 8878 10327 } 8879 10328 }, 8880 10329 "node_modules/es-to-primitive": { ··· 9030 10479 "esbuild": ">=0.12 <1" 9031 10480 } 9032 10481 }, 10482 + "node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { 10483 + "version": "0.25.4", 10484 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", 10485 + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", 10486 + "cpu": [ 10487 + "arm64" 10488 + ], 10489 + "dev": true, 10490 + "optional": true, 10491 + "os": [ 10492 + "darwin" 10493 + ], 10494 + "engines": { 10495 + "node": ">=18" 10496 + } 10497 + }, 9033 10498 "node_modules/escalade": { 9034 - "version": "3.1.2", 9035 - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", 9036 - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", 10499 + "version": "3.2.0", 10500 + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 10501 + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 9037 10502 "engines": { 9038 10503 "node": ">=6" 9039 10504 } ··· 9056 10521 } 9057 10522 }, 9058 10523 "node_modules/eslint": { 9059 - "version": "8.57.0", 9060 - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", 9061 - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", 10524 + "version": "9.39.1", 10525 + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", 10526 + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", 9062 10527 "dev": true, 9063 10528 "dependencies": { 9064 - "@eslint-community/eslint-utils": "^4.2.0", 9065 - "@eslint-community/regexpp": "^4.6.1", 9066 - "@eslint/eslintrc": "^2.1.4", 9067 - "@eslint/js": "8.57.0", 9068 - "@humanwhocodes/config-array": "^0.11.14", 10529 + "@eslint-community/eslint-utils": "^4.8.0", 10530 + "@eslint-community/regexpp": "^4.12.1", 10531 + "@eslint/config-array": "^0.21.1", 10532 + "@eslint/config-helpers": "^0.4.2", 10533 + "@eslint/core": "^0.17.0", 10534 + "@eslint/eslintrc": "^3.3.1", 10535 + "@eslint/js": "9.39.1", 10536 + "@eslint/plugin-kit": "^0.4.1", 10537 + "@humanfs/node": "^0.16.6", 9069 10538 "@humanwhocodes/module-importer": "^1.0.1", 9070 - "@nodelib/fs.walk": "^1.2.8", 9071 - "@ungap/structured-clone": "^1.2.0", 10539 + "@humanwhocodes/retry": "^0.4.2", 10540 + "@types/estree": "^1.0.6", 9072 10541 "ajv": "^6.12.4", 9073 10542 "chalk": "^4.0.0", 9074 - "cross-spawn": "^7.0.2", 10543 + "cross-spawn": "^7.0.6", 9075 10544 "debug": "^4.3.2", 9076 - "doctrine": "^3.0.0", 9077 10545 "escape-string-regexp": "^4.0.0", 9078 - "eslint-scope": "^7.2.2", 9079 - "eslint-visitor-keys": "^3.4.3", 9080 - "espree": "^9.6.1", 9081 - "esquery": "^1.4.2", 10546 + "eslint-scope": "^8.4.0", 10547 + "eslint-visitor-keys": "^4.2.1", 10548 + "espree": "^10.4.0", 10549 + "esquery": "^1.5.0", 9082 10550 "esutils": "^2.0.2", 9083 10551 "fast-deep-equal": "^3.1.3", 9084 - "file-entry-cache": "^6.0.1", 10552 + "file-entry-cache": "^8.0.0", 9085 10553 "find-up": "^5.0.0", 9086 10554 "glob-parent": "^6.0.2", 9087 - "globals": "^13.19.0", 9088 - "graphemer": "^1.4.0", 9089 10555 "ignore": "^5.2.0", 9090 10556 "imurmurhash": "^0.1.4", 9091 10557 "is-glob": "^4.0.0", 9092 - "is-path-inside": "^3.0.3", 9093 - "js-yaml": "^4.1.0", 9094 10558 "json-stable-stringify-without-jsonify": "^1.0.1", 9095 - "levn": "^0.4.1", 9096 10559 "lodash.merge": "^4.6.2", 9097 10560 "minimatch": "^3.1.2", 9098 10561 "natural-compare": "^1.4.0", 9099 - "optionator": "^0.9.3", 9100 - "strip-ansi": "^6.0.1", 9101 - "text-table": "^0.2.0" 10562 + "optionator": "^0.9.3" 9102 10563 }, 9103 10564 "bin": { 9104 10565 "eslint": "bin/eslint.js" 9105 10566 }, 9106 10567 "engines": { 9107 - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 10568 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 9108 10569 }, 9109 10570 "funding": { 9110 - "url": "https://opencollective.com/eslint" 10571 + "url": "https://eslint.org/donate" 10572 + }, 10573 + "peerDependencies": { 10574 + "jiti": "*" 10575 + }, 10576 + "peerDependenciesMeta": { 10577 + "jiti": { 10578 + "optional": true 10579 + } 9111 10580 } 9112 10581 }, 9113 10582 "node_modules/eslint-config-next": { 9114 - "version": "15.5.3", 9115 - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.3.tgz", 9116 - "integrity": "sha512-e6j+QhQFOr5pfsc8VJbuTD9xTXJaRvMHYjEeLPA2pFkheNlgPLCkxdvhxhfuM4KGcqSZj2qEnpHisdTVs3BxuQ==", 10583 + "version": "16.0.3", 10584 + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.3.tgz", 10585 + "integrity": "sha512-5F6qDjcZldf0Y0ZbqvWvap9xzYUxyDf7/of37aeyhvkrQokj/4bT1JYWZdlWUr283aeVa+s52mPq9ogmGg+5dw==", 9117 10586 "dev": true, 9118 - "license": "MIT", 9119 10587 "dependencies": { 9120 - "@next/eslint-plugin-next": "15.5.3", 9121 - "@rushstack/eslint-patch": "^1.10.3", 9122 - "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", 9123 - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", 10588 + "@next/eslint-plugin-next": "16.0.3", 9124 10589 "eslint-import-resolver-node": "^0.3.6", 9125 10590 "eslint-import-resolver-typescript": "^3.5.2", 9126 - "eslint-plugin-import": "^2.31.0", 10591 + "eslint-plugin-import": "^2.32.0", 9127 10592 "eslint-plugin-jsx-a11y": "^6.10.0", 9128 10593 "eslint-plugin-react": "^7.37.0", 9129 - "eslint-plugin-react-hooks": "^5.0.0" 10594 + "eslint-plugin-react-hooks": "^7.0.0", 10595 + "globals": "16.4.0", 10596 + "typescript-eslint": "^8.46.0" 9130 10597 }, 9131 10598 "peerDependencies": { 9132 - "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", 10599 + "eslint": ">=9.0.0", 9133 10600 "typescript": ">=3.3.1" 9134 10601 }, 9135 10602 "peerDependenciesMeta": { 9136 10603 "typescript": { 9137 10604 "optional": true 9138 10605 } 10606 + } 10607 + }, 10608 + "node_modules/eslint-config-next/node_modules/globals": { 10609 + "version": "16.4.0", 10610 + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", 10611 + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", 10612 + "dev": true, 10613 + "engines": { 10614 + "node": ">=18" 10615 + }, 10616 + "funding": { 10617 + "url": "https://github.com/sponsors/sindresorhus" 9139 10618 } 9140 10619 }, 9141 10620 "node_modules/eslint-import-resolver-node": { ··· 9184 10663 } 9185 10664 }, 9186 10665 "node_modules/eslint-module-utils": { 9187 - "version": "2.12.0", 9188 - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", 9189 - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", 10666 + "version": "2.12.1", 10667 + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", 10668 + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", 9190 10669 "dev": true, 9191 - "license": "MIT", 9192 10670 "dependencies": { 9193 10671 "debug": "^3.2.7" 9194 10672 }, ··· 9211 10689 } 9212 10690 }, 9213 10691 "node_modules/eslint-plugin-import": { 9214 - "version": "2.31.0", 9215 - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", 9216 - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", 10692 + "version": "2.32.0", 10693 + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", 10694 + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", 9217 10695 "dev": true, 9218 - "license": "MIT", 9219 10696 "dependencies": { 9220 10697 "@rtsao/scc": "^1.1.0", 9221 - "array-includes": "^3.1.8", 9222 - "array.prototype.findlastindex": "^1.2.5", 9223 - "array.prototype.flat": "^1.3.2", 9224 - "array.prototype.flatmap": "^1.3.2", 10698 + "array-includes": "^3.1.9", 10699 + "array.prototype.findlastindex": "^1.2.6", 10700 + "array.prototype.flat": "^1.3.3", 10701 + "array.prototype.flatmap": "^1.3.3", 9225 10702 "debug": "^3.2.7", 9226 10703 "doctrine": "^2.1.0", 9227 10704 "eslint-import-resolver-node": "^0.3.9", 9228 - "eslint-module-utils": "^2.12.0", 10705 + "eslint-module-utils": "^2.12.1", 9229 10706 "hasown": "^2.0.2", 9230 - "is-core-module": "^2.15.1", 10707 + "is-core-module": "^2.16.1", 9231 10708 "is-glob": "^4.0.3", 9232 10709 "minimatch": "^3.1.2", 9233 10710 "object.fromentries": "^2.0.8", 9234 10711 "object.groupby": "^1.0.3", 9235 - "object.values": "^1.2.0", 10712 + "object.values": "^1.2.1", 9236 10713 "semver": "^6.3.1", 9237 - "string.prototype.trimend": "^1.0.8", 10714 + "string.prototype.trimend": "^1.0.9", 9238 10715 "tsconfig-paths": "^3.15.0" 9239 10716 }, 9240 10717 "engines": { ··· 9338 10815 } 9339 10816 }, 9340 10817 "node_modules/eslint-plugin-react-hooks": { 9341 - "version": "5.2.0", 9342 - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", 9343 - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", 10818 + "version": "7.0.1", 10819 + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", 10820 + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", 9344 10821 "dev": true, 9345 - "license": "MIT", 10822 + "dependencies": { 10823 + "@babel/core": "^7.24.4", 10824 + "@babel/parser": "^7.24.4", 10825 + "hermes-parser": "^0.25.1", 10826 + "zod": "^3.25.0 || ^4.0.0", 10827 + "zod-validation-error": "^3.5.0 || ^4.0.0" 10828 + }, 9346 10829 "engines": { 9347 - "node": ">=10" 10830 + "node": ">=18" 9348 10831 }, 9349 10832 "peerDependencies": { 9350 10833 "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" 9351 10834 } 9352 10835 }, 10836 + "node_modules/eslint-plugin-react-hooks/node_modules/zod": { 10837 + "version": "4.1.12", 10838 + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", 10839 + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", 10840 + "dev": true, 10841 + "funding": { 10842 + "url": "https://github.com/sponsors/colinhacks" 10843 + } 10844 + }, 10845 + "node_modules/eslint-plugin-react-hooks/node_modules/zod-validation-error": { 10846 + "version": "4.0.2", 10847 + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", 10848 + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", 10849 + "dev": true, 10850 + "engines": { 10851 + "node": ">=18.0.0" 10852 + }, 10853 + "peerDependencies": { 10854 + "zod": "^3.25.0 || ^4.0.0" 10855 + } 10856 + }, 9353 10857 "node_modules/eslint-plugin-react/node_modules/doctrine": { 9354 10858 "version": "2.1.0", 9355 10859 "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", ··· 9392 10896 } 9393 10897 }, 9394 10898 "node_modules/eslint-scope": { 9395 - "version": "7.2.2", 9396 - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", 9397 - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", 10899 + "version": "8.4.0", 10900 + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", 10901 + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", 9398 10902 "dev": true, 9399 10903 "dependencies": { 9400 10904 "esrecurse": "^4.3.0", 9401 10905 "estraverse": "^5.2.0" 9402 10906 }, 9403 10907 "engines": { 9404 - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 10908 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 9405 10909 }, 9406 10910 "funding": { 9407 10911 "url": "https://opencollective.com/eslint" 9408 10912 } 9409 10913 }, 9410 10914 "node_modules/eslint-visitor-keys": { 9411 - "version": "3.4.3", 9412 - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", 9413 - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", 10915 + "version": "4.2.1", 10916 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", 10917 + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", 9414 10918 "dev": true, 9415 10919 "engines": { 9416 - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 10920 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 9417 10921 }, 9418 10922 "funding": { 9419 10923 "url": "https://opencollective.com/eslint" ··· 9435 10939 } 9436 10940 }, 9437 10941 "node_modules/espree": { 9438 - "version": "9.6.1", 9439 - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", 9440 - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", 10942 + "version": "10.4.0", 10943 + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", 10944 + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", 9441 10945 "dev": true, 9442 10946 "dependencies": { 9443 - "acorn": "^8.9.0", 10947 + "acorn": "^8.15.0", 9444 10948 "acorn-jsx": "^5.3.2", 9445 - "eslint-visitor-keys": "^3.4.1" 10949 + "eslint-visitor-keys": "^4.2.1" 9446 10950 }, 9447 10951 "engines": { 9448 - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 10952 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 9449 10953 }, 9450 10954 "funding": { 9451 10955 "url": "https://opencollective.com/eslint" ··· 9801 11305 } 9802 11306 }, 9803 11307 "node_modules/fast-uri": { 9804 - "version": "3.0.5", 9805 - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz", 9806 - "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==", 11308 + "version": "3.1.0", 11309 + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", 11310 + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", 9807 11311 "dev": true, 9808 11312 "funding": [ 9809 11313 { ··· 9814 11318 "type": "opencollective", 9815 11319 "url": "https://opencollective.com/fastify" 9816 11320 } 9817 - ], 9818 - "license": "BSD-3-Clause" 11321 + ] 9819 11322 }, 9820 11323 "node_modules/fastq": { 9821 11324 "version": "1.17.1", ··· 9863 11366 } 9864 11367 }, 9865 11368 "node_modules/file-entry-cache": { 9866 - "version": "6.0.1", 9867 - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", 9868 - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", 11369 + "version": "8.0.0", 11370 + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", 11371 + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", 9869 11372 "dev": true, 9870 11373 "dependencies": { 9871 - "flat-cache": "^3.0.4" 11374 + "flat-cache": "^4.0.0" 9872 11375 }, 9873 11376 "engines": { 9874 - "node": "^10.12.0 || >=12.0.0" 11377 + "node": ">=16.0.0" 9875 11378 } 9876 11379 }, 9877 11380 "node_modules/fill-range": { ··· 9936 11439 } 9937 11440 }, 9938 11441 "node_modules/flat-cache": { 9939 - "version": "3.2.0", 9940 - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", 9941 - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", 11442 + "version": "4.0.1", 11443 + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", 11444 + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", 9942 11445 "dev": true, 9943 11446 "dependencies": { 9944 11447 "flatted": "^3.2.9", 9945 - "keyv": "^4.5.3", 9946 - "rimraf": "^3.0.2" 11448 + "keyv": "^4.5.4" 9947 11449 }, 9948 11450 "engines": { 9949 - "node": "^10.12.0 || >=12.0.0" 11451 + "node": ">=16" 9950 11452 } 9951 11453 }, 9952 11454 "node_modules/flatted": { 9953 - "version": "3.3.1", 9954 - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", 9955 - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", 11455 + "version": "3.3.3", 11456 + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", 11457 + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", 9956 11458 "dev": true 9957 11459 }, 9958 11460 "node_modules/follow-redirects": { ··· 10152 11654 }, 10153 11655 "engines": { 10154 11656 "node": ">=14" 11657 + } 11658 + }, 11659 + "node_modules/gensync": { 11660 + "version": "1.0.0-beta.2", 11661 + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", 11662 + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", 11663 + "dev": true, 11664 + "engines": { 11665 + "node": ">=6.9.0" 10155 11666 } 10156 11667 }, 10157 11668 "node_modules/get-caller-file": { ··· 10315 11826 } 10316 11827 }, 10317 11828 "node_modules/globals": { 10318 - "version": "13.24.0", 10319 - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", 10320 - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", 11829 + "version": "14.0.0", 11830 + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", 11831 + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", 10321 11832 "dev": true, 10322 - "dependencies": { 10323 - "type-fest": "^0.20.2" 10324 - }, 10325 11833 "engines": { 10326 - "node": ">=8" 11834 + "node": ">=18" 10327 11835 }, 10328 11836 "funding": { 10329 11837 "url": "https://github.com/sponsors/sindresorhus" ··· 10783 12291 "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", 10784 12292 "dev": true 10785 12293 }, 12294 + "node_modules/hermes-estree": { 12295 + "version": "0.25.1", 12296 + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", 12297 + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", 12298 + "dev": true 12299 + }, 12300 + "node_modules/hermes-parser": { 12301 + "version": "0.25.1", 12302 + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", 12303 + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", 12304 + "dev": true, 12305 + "dependencies": { 12306 + "hermes-estree": "0.25.1" 12307 + } 12308 + }, 10786 12309 "node_modules/hono": { 10787 12310 "version": "4.7.11", 10788 12311 "resolved": "https://registry.npmjs.org/hono/-/hono-4.7.11.tgz", ··· 10877 12400 } 10878 12401 }, 10879 12402 "node_modules/immer": { 10880 - "version": "10.1.1", 10881 - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", 10882 - "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", 10883 - "optional": true, 10884 - "peer": true, 12403 + "version": "10.2.0", 12404 + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", 12405 + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", 12406 + "license": "MIT", 10885 12407 "funding": { 10886 12408 "type": "opencollective", 10887 12409 "url": "https://opencollective.com/immer" 10888 12410 } 10889 12411 }, 10890 12412 "node_modules/import-fresh": { 10891 - "version": "3.3.0", 10892 - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", 10893 - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", 12413 + "version": "3.3.1", 12414 + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", 12415 + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", 10894 12416 "dev": true, 10895 12417 "dependencies": { 10896 12418 "parent-module": "^1.0.0", ··· 11375 12897 "url": "https://github.com/sponsors/ljharb" 11376 12898 } 11377 12899 }, 12900 + "node_modules/is-negative-zero": { 12901 + "version": "2.0.3", 12902 + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", 12903 + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", 12904 + "dev": true, 12905 + "engines": { 12906 + "node": ">= 0.4" 12907 + }, 12908 + "funding": { 12909 + "url": "https://github.com/sponsors/ljharb" 12910 + } 12911 + }, 11378 12912 "node_modules/is-number": { 11379 12913 "version": "7.0.0", 11380 12914 "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", ··· 11399 12933 }, 11400 12934 "funding": { 11401 12935 "url": "https://github.com/sponsors/ljharb" 11402 - } 11403 - }, 11404 - "node_modules/is-path-inside": { 11405 - "version": "3.0.3", 11406 - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", 11407 - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", 11408 - "dev": true, 11409 - "engines": { 11410 - "node": ">=8" 11411 12936 } 11412 12937 }, 11413 12938 "node_modules/is-plain-obj": { ··· 11666 13191 "license": "MIT" 11667 13192 }, 11668 13193 "node_modules/js-yaml": { 11669 - "version": "4.1.0", 11670 - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 11671 - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 13194 + "version": "4.1.1", 13195 + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", 13196 + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", 11672 13197 "dev": true, 11673 13198 "dependencies": { 11674 13199 "argparse": "^2.0.1" 11675 13200 }, 11676 13201 "bin": { 11677 13202 "js-yaml": "bin/js-yaml.js" 13203 + } 13204 + }, 13205 + "node_modules/jsesc": { 13206 + "version": "3.1.0", 13207 + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", 13208 + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", 13209 + "dev": true, 13210 + "bin": { 13211 + "jsesc": "bin/jsesc" 13212 + }, 13213 + "engines": { 13214 + "node": ">=6" 11678 13215 } 11679 13216 }, 11680 13217 "node_modules/json-bigint": { ··· 11713 13250 "version": "1.0.0", 11714 13251 "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", 11715 13252 "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", 11716 - "dev": true, 11717 - "license": "MIT" 13253 + "dev": true 11718 13254 }, 11719 13255 "node_modules/json-stable-stringify-without-jsonify": { 11720 13256 "version": "1.0.1", ··· 13581 15117 } 13582 15118 }, 13583 15119 "node_modules/next": { 13584 - "version": "15.5.3", 13585 - "resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz", 13586 - "integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==", 15120 + "version": "16.0.7", 15121 + "resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz", 15122 + "integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==", 13587 15123 "license": "MIT", 13588 15124 "dependencies": { 13589 - "@next/env": "15.5.3", 15125 + "@next/env": "16.0.7", 13590 15126 "@swc/helpers": "0.5.15", 13591 15127 "caniuse-lite": "^1.0.30001579", 13592 15128 "postcss": "8.4.31", ··· 13596 15132 "next": "dist/bin/next" 13597 15133 }, 13598 15134 "engines": { 13599 - "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" 15135 + "node": ">=20.9.0" 13600 15136 }, 13601 15137 "optionalDependencies": { 13602 - "@next/swc-darwin-arm64": "15.5.3", 13603 - "@next/swc-darwin-x64": "15.5.3", 13604 - "@next/swc-linux-arm64-gnu": "15.5.3", 13605 - "@next/swc-linux-arm64-musl": "15.5.3", 13606 - "@next/swc-linux-x64-gnu": "15.5.3", 13607 - "@next/swc-linux-x64-musl": "15.5.3", 13608 - "@next/swc-win32-arm64-msvc": "15.5.3", 13609 - "@next/swc-win32-x64-msvc": "15.5.3", 13610 - "sharp": "^0.34.3" 15138 + "@next/swc-darwin-arm64": "16.0.7", 15139 + "@next/swc-darwin-x64": "16.0.7", 15140 + "@next/swc-linux-arm64-gnu": "16.0.7", 15141 + "@next/swc-linux-arm64-musl": "16.0.7", 15142 + "@next/swc-linux-x64-gnu": "16.0.7", 15143 + "@next/swc-linux-x64-musl": "16.0.7", 15144 + "@next/swc-win32-arm64-msvc": "16.0.7", 15145 + "@next/swc-win32-x64-msvc": "16.0.7", 15146 + "sharp": "^0.34.4" 13611 15147 }, 13612 15148 "peerDependencies": { 13613 15149 "@opentelemetry/api": "^1.1.0", ··· 13731 15267 "node-gyp-build-optional-packages-optional": "optional.js", 13732 15268 "node-gyp-build-optional-packages-test": "build-test.js" 13733 15269 } 15270 + }, 15271 + "node_modules/node-releases": { 15272 + "version": "2.0.27", 15273 + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", 15274 + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", 15275 + "dev": true 13734 15276 }, 13735 15277 "node_modules/normalize-path": { 13736 15278 "version": "3.0.0", ··· 14099 15641 "dev": true, 14100 15642 "engines": { 14101 15643 "node": ">=8" 14102 - } 14103 - }, 14104 - "node_modules/path-is-absolute": { 14105 - "version": "1.0.1", 14106 - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 14107 - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 14108 - "dev": true, 14109 - "engines": { 14110 - "node": ">=0.10.0" 14111 15644 } 14112 15645 }, 14113 15646 "node_modules/path-key": { ··· 14798 16331 } 14799 16332 }, 14800 16333 "node_modules/react": { 14801 - "version": "19.1.1", 14802 - "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", 14803 - "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", 14804 - "license": "MIT", 16334 + "version": "19.2.0", 16335 + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", 16336 + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", 14805 16337 "engines": { 14806 16338 "node": ">=0.10.0" 14807 16339 } ··· 14920 16452 } 14921 16453 }, 14922 16454 "node_modules/react-dom": { 14923 - "version": "19.1.1", 14924 - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", 14925 - "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", 14926 - "license": "MIT", 16455 + "version": "19.2.0", 16456 + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", 16457 + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", 14927 16458 "dependencies": { 14928 - "scheduler": "^0.26.0" 16459 + "scheduler": "^0.27.0" 14929 16460 }, 14930 16461 "peerDependencies": { 14931 - "react": "^19.1.1" 16462 + "react": "^19.2.0" 14932 16463 } 14933 16464 }, 14934 16465 "node_modules/react-is": { ··· 15441 16972 "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", 15442 16973 "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", 15443 16974 "dev": true, 15444 - "license": "MIT", 15445 16975 "engines": { 15446 16976 "node": ">=0.10.0" 15447 16977 } ··· 15513 17043 "node": ">=0.10.0" 15514 17044 } 15515 17045 }, 15516 - "node_modules/rimraf": { 15517 - "version": "3.0.2", 15518 - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 15519 - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 15520 - "deprecated": "Rimraf versions prior to v4 are no longer supported", 15521 - "dev": true, 15522 - "dependencies": { 15523 - "glob": "^7.1.3" 15524 - }, 15525 - "bin": { 15526 - "rimraf": "bin.js" 15527 - }, 15528 - "funding": { 15529 - "url": "https://github.com/sponsors/isaacs" 15530 - } 15531 - }, 15532 - "node_modules/rimraf/node_modules/glob": { 15533 - "version": "7.2.3", 15534 - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 15535 - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 15536 - "deprecated": "Glob versions prior to v9 are no longer supported", 15537 - "dev": true, 15538 - "dependencies": { 15539 - "fs.realpath": "^1.0.0", 15540 - "inflight": "^1.0.4", 15541 - "inherits": "2", 15542 - "minimatch": "^3.1.1", 15543 - "once": "^1.3.0", 15544 - "path-is-absolute": "^1.0.0" 15545 - }, 15546 - "engines": { 15547 - "node": "*" 15548 - }, 15549 - "funding": { 15550 - "url": "https://github.com/sponsors/isaacs" 15551 - } 15552 - }, 15553 17046 "node_modules/rollup-plugin-inject": { 15554 17047 "version": "3.0.2", 15555 17048 "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", ··· 15707 17200 "license": "ISC" 15708 17201 }, 15709 17202 "node_modules/scheduler": { 15710 - "version": "0.26.0", 15711 - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", 15712 - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", 15713 - "license": "MIT" 17203 + "version": "0.27.0", 17204 + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", 17205 + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" 15714 17206 }, 15715 17207 "node_modules/scmp": { 15716 17208 "version": "2.1.0", ··· 16155 17647 "node": ">= 0.8" 16156 17648 } 16157 17649 }, 17650 + "node_modules/stop-iteration-iterator": { 17651 + "version": "1.1.0", 17652 + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", 17653 + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", 17654 + "dev": true, 17655 + "dependencies": { 17656 + "es-errors": "^1.3.0", 17657 + "internal-slot": "^1.1.0" 17658 + }, 17659 + "engines": { 17660 + "node": ">= 0.4" 17661 + } 17662 + }, 16158 17663 "node_modules/stoppable": { 16159 17664 "version": "1.1.0", 16160 17665 "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", ··· 16482 17987 "integrity": "sha512-lDMFv4nKQrSjlkHKAlHVqKrBG4DyFfa9F74cmBZ3Iy3ed8yvWnlWSIdi4IKfSqwmazAohBNwiN64qGx4y5Q3IQ==", 16483 17988 "license": "ISC" 16484 17989 }, 16485 - "node_modules/text-table": { 16486 - "version": "0.2.0", 16487 - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", 16488 - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", 16489 - "dev": true 16490 - }, 16491 17990 "node_modules/thread-stream": { 16492 17991 "version": "2.7.0", 16493 17992 "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", ··· 16643 18142 "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", 16644 18143 "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", 16645 18144 "dev": true, 16646 - "license": "MIT", 16647 18145 "engines": { 16648 18146 "node": ">=18.12" 16649 18147 }, ··· 16761 18259 "node": ">= 0.8.0" 16762 18260 } 16763 18261 }, 16764 - "node_modules/type-fest": { 16765 - "version": "0.20.2", 16766 - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", 16767 - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", 16768 - "dev": true, 16769 - "engines": { 16770 - "node": ">=10" 16771 - }, 16772 - "funding": { 16773 - "url": "https://github.com/sponsors/sindresorhus" 16774 - } 16775 - }, 16776 18262 "node_modules/type-is": { 16777 18263 "version": "1.6.18", 16778 18264 "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", ··· 16875 18361 }, 16876 18362 "engines": { 16877 18363 "node": ">=14.17" 18364 + } 18365 + }, 18366 + "node_modules/typescript-eslint": { 18367 + "version": "8.47.0", 18368 + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.47.0.tgz", 18369 + "integrity": "sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==", 18370 + "dev": true, 18371 + "dependencies": { 18372 + "@typescript-eslint/eslint-plugin": "8.47.0", 18373 + "@typescript-eslint/parser": "8.47.0", 18374 + "@typescript-eslint/typescript-estree": "8.47.0", 18375 + "@typescript-eslint/utils": "8.47.0" 18376 + }, 18377 + "engines": { 18378 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 18379 + }, 18380 + "funding": { 18381 + "type": "opencollective", 18382 + "url": "https://opencollective.com/typescript-eslint" 18383 + }, 18384 + "peerDependencies": { 18385 + "eslint": "^8.57.0 || ^9.0.0", 18386 + "typescript": ">=4.8.4 <6.0.0" 16878 18387 } 16879 18388 }, 16880 18389 "node_modules/uc.micro": { ··· 17048 18557 "node": ">= 0.8" 17049 18558 } 17050 18559 }, 18560 + "node_modules/update-browserslist-db": { 18561 + "version": "1.1.4", 18562 + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", 18563 + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", 18564 + "dev": true, 18565 + "funding": [ 18566 + { 18567 + "type": "opencollective", 18568 + "url": "https://opencollective.com/browserslist" 18569 + }, 18570 + { 18571 + "type": "tidelift", 18572 + "url": "https://tidelift.com/funding/github/npm/browserslist" 18573 + }, 18574 + { 18575 + "type": "github", 18576 + "url": "https://github.com/sponsors/ai" 18577 + } 18578 + ], 18579 + "dependencies": { 18580 + "escalade": "^3.2.0", 18581 + "picocolors": "^1.1.1" 18582 + }, 18583 + "bin": { 18584 + "update-browserslist-db": "cli.js" 18585 + }, 18586 + "peerDependencies": { 18587 + "browserslist": ">= 4.21.0" 18588 + } 18589 + }, 17051 18590 "node_modules/use-callback-ref": { 17052 18591 "version": "1.3.3", 17053 18592 "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", ··· 17457 18996 } 17458 18997 } 17459 18998 }, 18999 + "node_modules/wrangler/node_modules/@esbuild/android-arm": { 19000 + "version": "0.17.19", 19001 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", 19002 + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", 19003 + "cpu": [ 19004 + "arm" 19005 + ], 19006 + "dev": true, 19007 + "optional": true, 19008 + "os": [ 19009 + "android" 19010 + ], 19011 + "engines": { 19012 + "node": ">=12" 19013 + } 19014 + }, 19015 + "node_modules/wrangler/node_modules/@esbuild/android-arm64": { 19016 + "version": "0.17.19", 19017 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", 19018 + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", 19019 + "cpu": [ 19020 + "arm64" 19021 + ], 19022 + "dev": true, 19023 + "optional": true, 19024 + "os": [ 19025 + "android" 19026 + ], 19027 + "engines": { 19028 + "node": ">=12" 19029 + } 19030 + }, 19031 + "node_modules/wrangler/node_modules/@esbuild/android-x64": { 19032 + "version": "0.17.19", 19033 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", 19034 + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", 19035 + "cpu": [ 19036 + "x64" 19037 + ], 19038 + "dev": true, 19039 + "optional": true, 19040 + "os": [ 19041 + "android" 19042 + ], 19043 + "engines": { 19044 + "node": ">=12" 19045 + } 19046 + }, 19047 + "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { 19048 + "version": "0.17.19", 19049 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", 19050 + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", 19051 + "cpu": [ 19052 + "arm64" 19053 + ], 19054 + "dev": true, 19055 + "optional": true, 19056 + "os": [ 19057 + "darwin" 19058 + ], 19059 + "engines": { 19060 + "node": ">=12" 19061 + } 19062 + }, 19063 + "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { 19064 + "version": "0.17.19", 19065 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", 19066 + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", 19067 + "cpu": [ 19068 + "x64" 19069 + ], 19070 + "dev": true, 19071 + "optional": true, 19072 + "os": [ 19073 + "darwin" 19074 + ], 19075 + "engines": { 19076 + "node": ">=12" 19077 + } 19078 + }, 19079 + "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { 19080 + "version": "0.17.19", 19081 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", 19082 + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", 19083 + "cpu": [ 19084 + "arm64" 19085 + ], 19086 + "dev": true, 19087 + "optional": true, 19088 + "os": [ 19089 + "freebsd" 19090 + ], 19091 + "engines": { 19092 + "node": ">=12" 19093 + } 19094 + }, 19095 + "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { 19096 + "version": "0.17.19", 19097 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", 19098 + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", 19099 + "cpu": [ 19100 + "x64" 19101 + ], 19102 + "dev": true, 19103 + "optional": true, 19104 + "os": [ 19105 + "freebsd" 19106 + ], 19107 + "engines": { 19108 + "node": ">=12" 19109 + } 19110 + }, 19111 + "node_modules/wrangler/node_modules/@esbuild/linux-arm": { 19112 + "version": "0.17.19", 19113 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", 19114 + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", 19115 + "cpu": [ 19116 + "arm" 19117 + ], 19118 + "dev": true, 19119 + "optional": true, 19120 + "os": [ 19121 + "linux" 19122 + ], 19123 + "engines": { 19124 + "node": ">=12" 19125 + } 19126 + }, 19127 + "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { 19128 + "version": "0.17.19", 19129 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", 19130 + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", 19131 + "cpu": [ 19132 + "arm64" 19133 + ], 19134 + "dev": true, 19135 + "optional": true, 19136 + "os": [ 19137 + "linux" 19138 + ], 19139 + "engines": { 19140 + "node": ">=12" 19141 + } 19142 + }, 19143 + "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { 19144 + "version": "0.17.19", 19145 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", 19146 + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", 19147 + "cpu": [ 19148 + "ia32" 19149 + ], 19150 + "dev": true, 19151 + "optional": true, 19152 + "os": [ 19153 + "linux" 19154 + ], 19155 + "engines": { 19156 + "node": ">=12" 19157 + } 19158 + }, 19159 + "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { 19160 + "version": "0.17.19", 19161 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", 19162 + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", 19163 + "cpu": [ 19164 + "loong64" 19165 + ], 19166 + "dev": true, 19167 + "optional": true, 19168 + "os": [ 19169 + "linux" 19170 + ], 19171 + "engines": { 19172 + "node": ">=12" 19173 + } 19174 + }, 19175 + "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { 19176 + "version": "0.17.19", 19177 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", 19178 + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", 19179 + "cpu": [ 19180 + "mips64el" 19181 + ], 19182 + "dev": true, 19183 + "optional": true, 19184 + "os": [ 19185 + "linux" 19186 + ], 19187 + "engines": { 19188 + "node": ">=12" 19189 + } 19190 + }, 19191 + "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { 19192 + "version": "0.17.19", 19193 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", 19194 + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", 19195 + "cpu": [ 19196 + "ppc64" 19197 + ], 19198 + "dev": true, 19199 + "optional": true, 19200 + "os": [ 19201 + "linux" 19202 + ], 19203 + "engines": { 19204 + "node": ">=12" 19205 + } 19206 + }, 19207 + "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { 19208 + "version": "0.17.19", 19209 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", 19210 + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", 19211 + "cpu": [ 19212 + "riscv64" 19213 + ], 19214 + "dev": true, 19215 + "optional": true, 19216 + "os": [ 19217 + "linux" 19218 + ], 19219 + "engines": { 19220 + "node": ">=12" 19221 + } 19222 + }, 19223 + "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { 19224 + "version": "0.17.19", 19225 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", 19226 + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", 19227 + "cpu": [ 19228 + "s390x" 19229 + ], 19230 + "dev": true, 19231 + "optional": true, 19232 + "os": [ 19233 + "linux" 19234 + ], 19235 + "engines": { 19236 + "node": ">=12" 19237 + } 19238 + }, 17460 19239 "node_modules/wrangler/node_modules/@esbuild/linux-x64": { 17461 19240 "version": "0.17.19", 17462 19241 "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", ··· 17468 19247 "optional": true, 17469 19248 "os": [ 17470 19249 "linux" 19250 + ], 19251 + "engines": { 19252 + "node": ">=12" 19253 + } 19254 + }, 19255 + "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { 19256 + "version": "0.17.19", 19257 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", 19258 + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", 19259 + "cpu": [ 19260 + "x64" 19261 + ], 19262 + "dev": true, 19263 + "optional": true, 19264 + "os": [ 19265 + "netbsd" 19266 + ], 19267 + "engines": { 19268 + "node": ">=12" 19269 + } 19270 + }, 19271 + "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { 19272 + "version": "0.17.19", 19273 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", 19274 + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", 19275 + "cpu": [ 19276 + "x64" 19277 + ], 19278 + "dev": true, 19279 + "optional": true, 19280 + "os": [ 19281 + "openbsd" 19282 + ], 19283 + "engines": { 19284 + "node": ">=12" 19285 + } 19286 + }, 19287 + "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { 19288 + "version": "0.17.19", 19289 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", 19290 + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", 19291 + "cpu": [ 19292 + "x64" 19293 + ], 19294 + "dev": true, 19295 + "optional": true, 19296 + "os": [ 19297 + "sunos" 19298 + ], 19299 + "engines": { 19300 + "node": ">=12" 19301 + } 19302 + }, 19303 + "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { 19304 + "version": "0.17.19", 19305 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", 19306 + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", 19307 + "cpu": [ 19308 + "arm64" 19309 + ], 19310 + "dev": true, 19311 + "optional": true, 19312 + "os": [ 19313 + "win32" 19314 + ], 19315 + "engines": { 19316 + "node": ">=12" 19317 + } 19318 + }, 19319 + "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { 19320 + "version": "0.17.19", 19321 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", 19322 + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", 19323 + "cpu": [ 19324 + "ia32" 19325 + ], 19326 + "dev": true, 19327 + "optional": true, 19328 + "os": [ 19329 + "win32" 19330 + ], 19331 + "engines": { 19332 + "node": ">=12" 19333 + } 19334 + }, 19335 + "node_modules/wrangler/node_modules/@esbuild/win32-x64": { 19336 + "version": "0.17.19", 19337 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", 19338 + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", 19339 + "cpu": [ 19340 + "x64" 19341 + ], 19342 + "dev": true, 19343 + "optional": true, 19344 + "os": [ 19345 + "win32" 17471 19346 ], 17472 19347 "engines": { 17473 19348 "node": ">=12"
+13 -12
package.json
··· 4 4 "description": "", 5 5 "main": "index.js", 6 6 "scripts": { 7 - "dev": "next dev --turbo", 7 + "dev": "TZ=UTC next dev --turbo", 8 8 "publish-lexicons": "tsx lexicons/publish.ts", 9 9 "generate-db-types": "supabase gen types --local > supabase/database.types.ts && drizzle-kit introspect && rm -rf ./drizzle/*.sql ./drizzle/meta", 10 10 "lexgen": "tsx ./lexicons/build.ts && lex gen-api ./lexicons/api ./lexicons/pub/leaflet/* ./lexicons/pub/leaflet/*/* ./lexicons/com/atproto/*/* ./lexicons/app/bsky/*/* --yes && find './lexicons/api' -type f -exec sed -i 's/\\.js'/'/g' {} \\;", ··· 31 31 "@hono/node-server": "^1.14.3", 32 32 "@mdx-js/loader": "^3.1.0", 33 33 "@mdx-js/react": "^3.1.0", 34 - "@next/bundle-analyzer": "^15.3.2", 35 - "@next/mdx": "15.3.2", 34 + "@next/bundle-analyzer": "16.0.3", 35 + "@next/mdx": "16.0.3", 36 36 "@radix-ui/react-dialog": "^1.1.15", 37 37 "@radix-ui/react-dropdown-menu": "^2.1.16", 38 38 "@radix-ui/react-popover": "^1.1.15", ··· 54 54 "feed": "^5.1.0", 55 55 "fractional-indexing": "^3.2.0", 56 56 "hono": "^4.7.11", 57 + "immer": "^10.2.0", 57 58 "inngest": "^3.40.1", 58 59 "ioredis": "^5.6.1", 59 60 "katex": "^0.16.22", 60 61 "linkifyjs": "^4.2.0", 61 62 "luxon": "^3.7.2", 62 63 "multiformats": "^13.3.2", 63 - "next": "^15.5.3", 64 + "next": "^16.0.7", 64 65 "pg": "^8.16.3", 65 66 "prosemirror-commands": "^1.5.2", 66 67 "prosemirror-inputrules": "^1.4.0", ··· 68 69 "prosemirror-model": "^1.21.0", 69 70 "prosemirror-schema-basic": "^1.2.2", 70 71 "prosemirror-state": "^1.4.3", 71 - "react": "^19.1.1", 72 + "react": "19.2.0", 72 73 "react-aria-components": "^1.8.0", 73 74 "react-day-picker": "^9.3.0", 74 - "react-dom": "^19.1.1", 75 + "react-dom": "19.2.0", 75 76 "react-use-measure": "^2.1.1", 76 77 "redlock": "^5.0.0-beta.2", 77 78 "rehype-parse": "^9.0.0", ··· 101 102 "@types/katex": "^0.16.7", 102 103 "@types/luxon": "^3.7.1", 103 104 "@types/node": "^22.15.17", 104 - "@types/react": "19.1.3", 105 - "@types/react-dom": "19.1.3", 105 + "@types/react": "19.2.6", 106 + "@types/react-dom": "19.2.3", 106 107 "@types/uuid": "^10.0.0", 107 108 "drizzle-kit": "^0.21.2", 108 109 "esbuild": "^0.25.4", 109 - "eslint": "8.57.0", 110 - "eslint-config-next": "^15.5.3", 110 + "eslint": "^9.39.1", 111 + "eslint-config-next": "16.0.3", 111 112 "postcss": "^8.4.38", 112 113 "prettier": "3.2.5", 113 114 "supabase": "^1.187.3", ··· 119 120 "overrides": { 120 121 "ajv": "^8.17.1", 121 122 "whatwg-url": "^14.0.0", 122 - "@types/react": "19.1.3", 123 - "@types/react-dom": "19.1.3" 123 + "@types/react": "19.2.6", 124 + "@types/react-dom": "19.2.3" 124 125 } 125 126 }
+144
patterns/notifications.md
··· 1 + # Notification System 2 + 3 + ## Overview 4 + 5 + Notifications are stored in the database and hydrated with related data before being rendered. The system supports multiple notification types (comments, subscriptions, etc.) that are processed in parallel. 6 + 7 + ## Key Files 8 + 9 + - **`src/notifications.ts`** - Core notification types and hydration logic 10 + - **`app/(home-pages)/notifications/NotificationList.tsx`** - Renders all notification types 11 + - **`app/(home-pages)/notifications/Notification.tsx`** - Base notification component 12 + - Individual notification components (e.g., `CommentNotification.tsx`, `FollowNotification.tsx`) 13 + 14 + ## Adding a New Notification Type 15 + 16 + ### 1. Update Notification Data Types (`src/notifications.ts`) 17 + 18 + Add your type to the `NotificationData` union: 19 + 20 + ```typescript 21 + export type NotificationData = 22 + | { type: "comment"; comment_uri: string; parent_uri?: string } 23 + | { type: "subscribe"; subscription_uri: string } 24 + | { type: "your_type"; your_field: string }; // Add here 25 + ``` 26 + 27 + Add to the `HydratedNotification` union: 28 + 29 + ```typescript 30 + export type HydratedNotification = 31 + | HydratedCommentNotification 32 + | HydratedSubscribeNotification 33 + | HydratedYourNotification; // Add here 34 + ``` 35 + 36 + ### 2. Create Hydration Function (`src/notifications.ts`) 37 + 38 + ```typescript 39 + export type HydratedYourNotification = Awaited< 40 + ReturnType<typeof hydrateYourNotifications> 41 + >[0]; 42 + 43 + async function hydrateYourNotifications(notifications: NotificationRow[]) { 44 + const yourNotifications = notifications.filter( 45 + (n): n is NotificationRow & { data: ExtractNotificationType<"your_type"> } => 46 + (n.data as NotificationData)?.type === "your_type", 47 + ); 48 + 49 + if (yourNotifications.length === 0) return []; 50 + 51 + // Fetch related data with joins 52 + const { data } = await supabaseServerClient 53 + .from("your_table") 54 + .select("*, related_table(*)") 55 + .in("uri", yourNotifications.map((n) => n.data.your_field)); 56 + 57 + return yourNotifications.map((notification) => ({ 58 + id: notification.id, 59 + recipient: notification.recipient, 60 + created_at: notification.created_at, 61 + type: "your_type" as const, 62 + your_field: notification.data.your_field, 63 + yourData: data?.find((d) => d.uri === notification.data.your_field)!, 64 + })); 65 + } 66 + ``` 67 + 68 + Add to `hydrateNotifications` parallel array: 69 + 70 + ```typescript 71 + const [commentNotifications, subscribeNotifications, yourNotifications] = await Promise.all([ 72 + hydrateCommentNotifications(notifications), 73 + hydrateSubscribeNotifications(notifications), 74 + hydrateYourNotifications(notifications), // Add here 75 + ]); 76 + 77 + const allHydrated = [...commentNotifications, ...subscribeNotifications, ...yourNotifications]; 78 + ``` 79 + 80 + ### 3. Trigger the Notification (in your action file) 81 + 82 + ```typescript 83 + import { Notification, pingIdentityToUpdateNotification } from "src/notifications"; 84 + import { v7 } from "uuid"; 85 + 86 + // When the event occurs: 87 + const recipient = /* determine who should receive it */; 88 + if (recipient !== currentUser) { 89 + const notification: Notification = { 90 + id: v7(), 91 + recipient, 92 + data: { 93 + type: "your_type", 94 + your_field: "value", 95 + }, 96 + }; 97 + await supabaseServerClient.from("notifications").insert(notification); 98 + await pingIdentityToUpdateNotification(recipient); 99 + } 100 + ``` 101 + 102 + ### 4. Create Notification Component 103 + 104 + Create a new component (e.g., `YourNotification.tsx`): 105 + 106 + ```typescript 107 + import { HydratedYourNotification } from "src/notifications"; 108 + import { Notification } from "./Notification"; 109 + 110 + export const YourNotification = (props: HydratedYourNotification) => { 111 + // Extract data from props.yourData 112 + 113 + return ( 114 + <Notification 115 + timestamp={props.created_at} 116 + href={/* link to relevant page */} 117 + icon={/* icon or avatar */} 118 + actionText={<>Message to display</>} 119 + content={/* optional additional content */} 120 + /> 121 + ); 122 + }; 123 + ``` 124 + 125 + ### 5. Update NotificationList (`NotificationList.tsx`) 126 + 127 + Import and render your notification type: 128 + 129 + ```typescript 130 + import { YourNotification } from "./YourNotification"; 131 + 132 + // In the map function: 133 + if (n.type === "your_type") { 134 + return <YourNotification key={n.id} {...n} />; 135 + } 136 + ``` 137 + 138 + ## Example: Subscribe Notifications 139 + 140 + See the implementation in: 141 + - `src/notifications.ts:88-125` - Hydration logic 142 + - `app/lish/subscribeToPublication.ts:55-68` - Trigger 143 + - `app/(home-pages)/notifications/FollowNotification.tsx` - Component 144 + - `app/(home-pages)/notifications/NotificationList.tsx:40-42` - Rendering
+5 -5
src/hooks/useLocalizedDate.ts
··· 2 2 import { useContext, useMemo } from "react"; 3 3 import { DateTime } from "luxon"; 4 4 import { RequestHeadersContext } from "components/Providers/RequestHeadersProvider"; 5 - import { useInitialPageLoad } from "components/InitialPageLoadProvider"; 5 + import { useHasPageLoaded } from "components/InitialPageLoadProvider"; 6 6 7 7 /** 8 8 * Hook that formats a date string using Luxon with timezone and locale from request headers. ··· 20 20 options?: Intl.DateTimeFormatOptions, 21 21 ): string { 22 22 const { timezone, language } = useContext(RequestHeadersContext); 23 - const isInitialPageLoad = useInitialPageLoad(); 23 + const hasPageLoaded = useHasPageLoaded(); 24 24 25 25 return useMemo(() => { 26 26 // Parse the date string to Luxon DateTime 27 27 let dateTime = DateTime.fromISO(dateString); 28 28 29 29 // On initial page load, use header timezone. After hydration, use system timezone 30 - const effectiveTimezone = isInitialPageLoad 30 + const effectiveTimezone = !hasPageLoaded 31 31 ? timezone 32 32 : Intl.DateTimeFormat().resolvedOptions().timeZone; 33 33 ··· 39 39 // On initial page load, use header locale. After hydration, use system locale 40 40 // Parse locale from accept-language header (take first locale) 41 41 // accept-language format: "en-US,en;q=0.9,es;q=0.8" 42 - const effectiveLocale = isInitialPageLoad 42 + const effectiveLocale = !hasPageLoaded 43 43 ? language?.split(",")[0]?.split(";")[0]?.trim() || "en-US" 44 44 : Intl.DateTimeFormat().resolvedOptions().locale; 45 45 ··· 49 49 // Fallback to en-US if locale is invalid 50 50 return dateTime.toLocaleString(options, { locale: "en-US" }); 51 51 } 52 - }, [dateString, options, timezone, language, isInitialPageLoad]); 52 + }, [dateString, options, timezone, language, hasPageLoaded]); 53 53 }
+176
src/notifications.ts
··· 1 + "use server"; 2 + 3 + import { supabaseServerClient } from "supabase/serverClient"; 4 + import { Tables, TablesInsert } from "supabase/database.types"; 5 + 6 + type NotificationRow = Tables<"notifications">; 7 + 8 + export type Notification = Omit<TablesInsert<"notifications">, "data"> & { 9 + data: NotificationData; 10 + }; 11 + 12 + export type NotificationData = 13 + | { type: "comment"; comment_uri: string; parent_uri?: string } 14 + | { type: "subscribe"; subscription_uri: string } 15 + | { type: "quote"; bsky_post_uri: string; document_uri: string }; 16 + 17 + export type HydratedNotification = 18 + | HydratedCommentNotification 19 + | HydratedSubscribeNotification 20 + | HydratedQuoteNotification; 21 + export async function hydrateNotifications( 22 + notifications: NotificationRow[], 23 + ): Promise<Array<HydratedNotification>> { 24 + // Call all hydrators in parallel 25 + const [commentNotifications, subscribeNotifications, quoteNotifications] = await Promise.all([ 26 + hydrateCommentNotifications(notifications), 27 + hydrateSubscribeNotifications(notifications), 28 + hydrateQuoteNotifications(notifications), 29 + ]); 30 + 31 + // Combine all hydrated notifications 32 + const allHydrated = [...commentNotifications, ...subscribeNotifications, ...quoteNotifications]; 33 + 34 + // Sort by created_at to maintain order 35 + allHydrated.sort( 36 + (a, b) => 37 + new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), 38 + ); 39 + 40 + return allHydrated; 41 + } 42 + 43 + // Type guard to extract notification type 44 + type ExtractNotificationType<T extends NotificationData["type"]> = Extract< 45 + NotificationData, 46 + { type: T } 47 + >; 48 + 49 + export type HydratedCommentNotification = Awaited< 50 + ReturnType<typeof hydrateCommentNotifications> 51 + >[0]; 52 + 53 + async function hydrateCommentNotifications(notifications: NotificationRow[]) { 54 + const commentNotifications = notifications.filter( 55 + (n): n is NotificationRow & { data: ExtractNotificationType<"comment"> } => 56 + (n.data as NotificationData)?.type === "comment", 57 + ); 58 + 59 + if (commentNotifications.length === 0) { 60 + return []; 61 + } 62 + 63 + // Fetch comment data from the database 64 + const commentUris = commentNotifications.flatMap((n) => 65 + n.data.parent_uri 66 + ? [n.data.comment_uri, n.data.parent_uri] 67 + : [n.data.comment_uri], 68 + ); 69 + const { data: comments } = await supabaseServerClient 70 + .from("comments_on_documents") 71 + .select( 72 + "*,bsky_profiles(*), documents(*, documents_in_publications(publications(*)))", 73 + ) 74 + .in("uri", commentUris); 75 + 76 + return commentNotifications.map((notification) => ({ 77 + id: notification.id, 78 + recipient: notification.recipient, 79 + created_at: notification.created_at, 80 + type: "comment" as const, 81 + comment_uri: notification.data.comment_uri, 82 + parentData: notification.data.parent_uri 83 + ? comments?.find((c) => c.uri === notification.data.parent_uri)! 84 + : undefined, 85 + commentData: comments?.find( 86 + (c) => c.uri === notification.data.comment_uri, 87 + )!, 88 + })); 89 + } 90 + 91 + export type HydratedSubscribeNotification = Awaited< 92 + ReturnType<typeof hydrateSubscribeNotifications> 93 + >[0]; 94 + 95 + async function hydrateSubscribeNotifications(notifications: NotificationRow[]) { 96 + const subscribeNotifications = notifications.filter( 97 + ( 98 + n, 99 + ): n is NotificationRow & { data: ExtractNotificationType<"subscribe"> } => 100 + (n.data as NotificationData)?.type === "subscribe", 101 + ); 102 + 103 + if (subscribeNotifications.length === 0) { 104 + return []; 105 + } 106 + 107 + // Fetch subscription data from the database with related data 108 + const subscriptionUris = subscribeNotifications.map( 109 + (n) => n.data.subscription_uri, 110 + ); 111 + const { data: subscriptions } = await supabaseServerClient 112 + .from("publication_subscriptions") 113 + .select("*, identities(bsky_profiles(*)), publications(*)") 114 + .in("uri", subscriptionUris); 115 + 116 + return subscribeNotifications.map((notification) => ({ 117 + id: notification.id, 118 + recipient: notification.recipient, 119 + created_at: notification.created_at, 120 + type: "subscribe" as const, 121 + subscription_uri: notification.data.subscription_uri, 122 + subscriptionData: subscriptions?.find( 123 + (s) => s.uri === notification.data.subscription_uri, 124 + )!, 125 + })); 126 + } 127 + 128 + export type HydratedQuoteNotification = Awaited< 129 + ReturnType<typeof hydrateQuoteNotifications> 130 + >[0]; 131 + 132 + async function hydrateQuoteNotifications(notifications: NotificationRow[]) { 133 + const quoteNotifications = notifications.filter( 134 + (n): n is NotificationRow & { data: ExtractNotificationType<"quote"> } => 135 + (n.data as NotificationData)?.type === "quote", 136 + ); 137 + 138 + if (quoteNotifications.length === 0) { 139 + return []; 140 + } 141 + 142 + // Fetch bsky post data and document data 143 + const bskyPostUris = quoteNotifications.map((n) => n.data.bsky_post_uri); 144 + const documentUris = quoteNotifications.map((n) => n.data.document_uri); 145 + 146 + const { data: bskyPosts } = await supabaseServerClient 147 + .from("bsky_posts") 148 + .select("*") 149 + .in("uri", bskyPostUris); 150 + 151 + const { data: documents } = await supabaseServerClient 152 + .from("documents") 153 + .select("*, documents_in_publications(publications(*))") 154 + .in("uri", documentUris); 155 + 156 + return quoteNotifications.map((notification) => ({ 157 + id: notification.id, 158 + recipient: notification.recipient, 159 + created_at: notification.created_at, 160 + type: "quote" as const, 161 + bsky_post_uri: notification.data.bsky_post_uri, 162 + document_uri: notification.data.document_uri, 163 + bskyPost: bskyPosts?.find((p) => p.uri === notification.data.bsky_post_uri)!, 164 + document: documents?.find((d) => d.uri === notification.data.document_uri)!, 165 + })); 166 + } 167 + 168 + export async function pingIdentityToUpdateNotification(did: string) { 169 + let channel = supabaseServerClient.channel(`identity.atp_did:${did}`); 170 + await channel.send({ 171 + type: "broadcast", 172 + event: "notification", 173 + payload: { message: "poke" }, 174 + }); 175 + await supabaseServerClient.removeChannel(channel); 176 + }
+1
src/utils/codeLanguageStorage.ts
··· 1 + export const LAST_USED_CODE_LANGUAGE_KEY = "lastUsedCodeLanguage";
+3 -3
src/utils/getCurrentDeploymentDomain.ts
··· 1 - import { headers, type UnsafeUnwrappedHeaders } from "next/headers"; 2 - export function getCurrentDeploymentDomain() { 3 - const headersList = (headers() as unknown as UnsafeUnwrappedHeaders); 1 + import { headers } from "next/headers"; 2 + export async function getCurrentDeploymentDomain() { 3 + const headersList = await headers(); 4 4 const hostname = headersList.get("x-forwarded-host"); 5 5 let protocol = headersList.get("x-forwarded-proto"); 6 6 return `${protocol}://${hostname}/`;
+50 -19
src/utils/getMicroLinkOgImage.ts
··· 2 2 3 3 export async function getMicroLinkOgImage( 4 4 path: string, 5 - options?: { width?: number; height?: number; deviceScaleFactor?: number }, 5 + options?: { 6 + width?: number; 7 + height?: number; 8 + deviceScaleFactor?: number; 9 + noCache?: boolean; 10 + }, 6 11 ) { 7 12 const headersList = await headers(); 8 - const hostname = headersList.get("x-forwarded-host"); 13 + let hostname = headersList.get("x-forwarded-host"); 9 14 let protocol = headersList.get("x-forwarded-proto"); 15 + if (process.env.NODE_ENV === "development") { 16 + protocol === "https"; 17 + hostname = "leaflet.pub"; 18 + } 10 19 let full_path = `${protocol}://${hostname}${path}`; 20 + return getWebpageImage(full_path, options); 21 + } 22 + 23 + export async function getWebpageImage( 24 + url: string, 25 + options?: { 26 + width?: number; 27 + height?: number; 28 + deviceScaleFactor?: number; 29 + noCache?: boolean; 30 + }, 31 + ) { 11 32 let response = await fetch( 12 - `https://pro.microlink.io/?url=${encodeURIComponent(full_path)}&screenshot=true&viewport.width=${options?.width || 1400}&viewport.height=${options?.height || 733}&viewport.deviceScaleFactor=${options?.deviceScaleFactor || 1}&meta=false&embed=screenshot.url&force=true`, 33 + `https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDFLARE_ACCOUNT}/browser-rendering/screenshot`, 13 34 { 35 + method: "POST", 14 36 headers: { 15 - "x-api-key": process.env.MICROLINK_API_KEY!, 37 + "Content-type": "application/json", 38 + Authorization: `Bearer ${process.env.CLOUDFLARE_API_TOKEN}`, 16 39 }, 17 - next: { 18 - revalidate: 600, 19 - }, 40 + body: JSON.stringify({ 41 + url, 42 + scrollPage: true, 43 + addStyleTag: [ 44 + { 45 + content: `* {scrollbar-width:none; }`, 46 + }, 47 + ], 48 + gotoOptions: { 49 + waitUntil: "load", 50 + }, 51 + viewport: { 52 + width: options?.width || 1400, 53 + height: options?.height || 733, 54 + deviceScaleFactor: options?.deviceScaleFactor, 55 + }, 56 + }), 57 + next: !options?.noCache 58 + ? undefined 59 + : { 60 + revalidate: 600, 61 + }, 20 62 }, 21 63 ); 22 - const clonedResponse = response.clone(); 23 - if (clonedResponse.status == 200) { 24 - clonedResponse.headers.set( 25 - "CDN-Cache-Control", 26 - "s-maxage=600, stale-while-revalidate=3600", 27 - ); 28 - clonedResponse.headers.set( 29 - "Cache-Control", 30 - "s-maxage=600, stale-while-revalidate=3600", 31 - ); 32 - } 33 64 34 - return clonedResponse; 65 + return response; 35 66 }
+50
src/utils/getPublicationMetadataFromLeafletData.ts
··· 1 + import { GetLeafletDataReturnType } from "app/api/rpc/[command]/get_leaflet_data"; 2 + import { Json } from "supabase/database.types"; 3 + 4 + export function getPublicationMetadataFromLeafletData( 5 + data?: GetLeafletDataReturnType["result"]["data"], 6 + ) { 7 + if (!data) return null; 8 + 9 + let pubData: 10 + | { 11 + description: string; 12 + title: string; 13 + leaflet: string; 14 + doc: string | null; 15 + publications: { 16 + identity_did: string; 17 + name: string; 18 + indexed_at: string; 19 + record: Json | null; 20 + uri: string; 21 + } | null; 22 + documents: { 23 + data: Json; 24 + indexed_at: string; 25 + uri: string; 26 + } | null; 27 + } 28 + | undefined 29 + | null = 30 + data?.leaflets_in_publications?.[0] || 31 + data?.permission_token_rights[0].entity_sets?.permission_tokens?.find( 32 + (p) => p.leaflets_in_publications?.length, 33 + )?.leaflets_in_publications?.[0]; 34 + 35 + // If not found, check for standalone documents 36 + let standaloneDoc = 37 + data?.leaflets_to_documents?.[0] || 38 + data?.permission_token_rights[0].entity_sets?.permission_tokens.find( 39 + (p) => p.leaflets_to_documents?.length, 40 + )?.leaflets_to_documents?.[0]; 41 + if (!pubData && standaloneDoc) { 42 + // Transform standalone document data to match the expected format 43 + pubData = { 44 + ...standaloneDoc, 45 + publications: null, // No publication for standalone docs 46 + doc: standaloneDoc.document, 47 + }; 48 + } 49 + return pubData; 50 + }
-16
src/utils/isBot.ts
··· 1 - import { cookies, headers, type UnsafeUnwrappedHeaders } from "next/headers"; 2 - export function getIsBot() { 3 - const userAgent = 4 - (headers() as unknown as UnsafeUnwrappedHeaders).get("user-agent") || ""; 5 - const botPatterns = [ 6 - /bot/i, 7 - /crawler/i, 8 - /spider/i, 9 - /googlebot/i, 10 - /bingbot/i, 11 - /yahoo/i, 12 - // Add more patterns as needed 13 - ]; 14 - 15 - return botPatterns.some((pattern) => pattern.test(userAgent)); 16 - }
+77
supabase/database.types.ts
··· 580 580 } 581 581 leaflets_in_publications: { 582 582 Row: { 583 + archived: boolean | null 583 584 description: string 584 585 doc: string | null 585 586 leaflet: string ··· 587 588 title: string 588 589 } 589 590 Insert: { 591 + archived?: boolean | null 590 592 description?: string 591 593 doc?: string | null 592 594 leaflet: string ··· 594 596 title?: string 595 597 } 596 598 Update: { 599 + archived?: boolean | null 597 600 description?: string 598 601 doc?: string | null 599 602 leaflet?: string ··· 624 627 }, 625 628 ] 626 629 } 630 + leaflets_to_documents: { 631 + Row: { 632 + created_at: string 633 + description: string 634 + document: string 635 + leaflet: string 636 + title: string 637 + } 638 + Insert: { 639 + created_at?: string 640 + description?: string 641 + document: string 642 + leaflet: string 643 + title?: string 644 + } 645 + Update: { 646 + created_at?: string 647 + description?: string 648 + document?: string 649 + leaflet?: string 650 + title?: string 651 + } 652 + Relationships: [ 653 + { 654 + foreignKeyName: "leaflets_to_documents_document_fkey" 655 + columns: ["document"] 656 + isOneToOne: false 657 + referencedRelation: "documents" 658 + referencedColumns: ["uri"] 659 + }, 660 + { 661 + foreignKeyName: "leaflets_to_documents_leaflet_fkey" 662 + columns: ["leaflet"] 663 + isOneToOne: false 664 + referencedRelation: "permission_tokens" 665 + referencedColumns: ["id"] 666 + }, 667 + ] 668 + } 669 + notifications: { 670 + Row: { 671 + created_at: string 672 + data: Json 673 + id: string 674 + read: boolean 675 + recipient: string 676 + } 677 + Insert: { 678 + created_at?: string 679 + data: Json 680 + id: string 681 + read?: boolean 682 + recipient: string 683 + } 684 + Update: { 685 + created_at?: string 686 + data?: Json 687 + id?: string 688 + read?: boolean 689 + recipient?: string 690 + } 691 + Relationships: [ 692 + { 693 + foreignKeyName: "notifications_recipient_fkey" 694 + columns: ["recipient"] 695 + isOneToOne: false 696 + referencedRelation: "identities" 697 + referencedColumns: ["atp_did"] 698 + }, 699 + ] 700 + } 627 701 oauth_session_store: { 628 702 Row: { 629 703 key: string ··· 656 730 } 657 731 permission_token_on_homepage: { 658 732 Row: { 733 + archived: boolean | null 659 734 created_at: string 660 735 identity: string 661 736 token: string 662 737 } 663 738 Insert: { 739 + archived?: boolean | null 664 740 created_at?: string 665 741 identity: string 666 742 token: string 667 743 } 668 744 Update: { 745 + archived?: boolean | null 669 746 created_at?: string 670 747 identity?: string 671 748 token?: string
+60
supabase/migrations/20251030215033_add_notifications_table.sql
··· 1 + create table "public"."notifications" ( 2 + "recipient" text not null, 3 + "created_at" timestamp with time zone not null default now(), 4 + "read" boolean not null default false, 5 + "data" jsonb not null, 6 + "id" uuid not null 7 + ); 8 + 9 + 10 + alter table "public"."notifications" enable row level security; 11 + 12 + CREATE UNIQUE INDEX notifications_pkey ON public.notifications USING btree (id); 13 + 14 + alter table "public"."notifications" add constraint "notifications_pkey" PRIMARY KEY using index "notifications_pkey"; 15 + 16 + alter table "public"."notifications" add constraint "notifications_recipient_fkey" FOREIGN KEY (recipient) REFERENCES identities(atp_did) ON UPDATE CASCADE ON DELETE CASCADE not valid; 17 + 18 + alter table "public"."notifications" validate constraint "notifications_recipient_fkey"; 19 + 20 + grant delete on table "public"."notifications" to "anon"; 21 + 22 + grant insert on table "public"."notifications" to "anon"; 23 + 24 + grant references on table "public"."notifications" to "anon"; 25 + 26 + grant select on table "public"."notifications" to "anon"; 27 + 28 + grant trigger on table "public"."notifications" to "anon"; 29 + 30 + grant truncate on table "public"."notifications" to "anon"; 31 + 32 + grant update on table "public"."notifications" to "anon"; 33 + 34 + grant delete on table "public"."notifications" to "authenticated"; 35 + 36 + grant insert on table "public"."notifications" to "authenticated"; 37 + 38 + grant references on table "public"."notifications" to "authenticated"; 39 + 40 + grant select on table "public"."notifications" to "authenticated"; 41 + 42 + grant trigger on table "public"."notifications" to "authenticated"; 43 + 44 + grant truncate on table "public"."notifications" to "authenticated"; 45 + 46 + grant update on table "public"."notifications" to "authenticated"; 47 + 48 + grant delete on table "public"."notifications" to "service_role"; 49 + 50 + grant insert on table "public"."notifications" to "service_role"; 51 + 52 + grant references on table "public"."notifications" to "service_role"; 53 + 54 + grant select on table "public"."notifications" to "service_role"; 55 + 56 + grant trigger on table "public"."notifications" to "service_role"; 57 + 58 + grant truncate on table "public"."notifications" to "service_role"; 59 + 60 + grant update on table "public"."notifications" to "service_role";
+63
supabase/migrations/20251118185507_add_leaflets_to_documents.sql
··· 1 + create table "public"."leaflets_to_documents" ( 2 + "leaflet" uuid not null, 3 + "document" text not null, 4 + "created_at" timestamp with time zone not null default now(), 5 + "title" text not null default ''::text, 6 + "description" text not null default ''::text 7 + ); 8 + 9 + alter table "public"."leaflets_to_documents" enable row level security; 10 + 11 + CREATE UNIQUE INDEX leaflets_to_documents_pkey ON public.leaflets_to_documents USING btree (leaflet, document); 12 + 13 + alter table "public"."leaflets_to_documents" add constraint "leaflets_to_documents_pkey" PRIMARY KEY using index "leaflets_to_documents_pkey"; 14 + 15 + alter table "public"."leaflets_to_documents" add constraint "leaflets_to_documents_document_fkey" FOREIGN KEY (document) REFERENCES documents(uri) ON UPDATE CASCADE ON DELETE CASCADE not valid; 16 + 17 + alter table "public"."leaflets_to_documents" validate constraint "leaflets_to_documents_document_fkey"; 18 + 19 + alter table "public"."leaflets_to_documents" add constraint "leaflets_to_documents_leaflet_fkey" FOREIGN KEY (leaflet) REFERENCES permission_tokens(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; 20 + 21 + alter table "public"."leaflets_to_documents" validate constraint "leaflets_to_documents_leaflet_fkey"; 22 + 23 + grant delete on table "public"."leaflets_to_documents" to "anon"; 24 + 25 + grant insert on table "public"."leaflets_to_documents" to "anon"; 26 + 27 + grant references on table "public"."leaflets_to_documents" to "anon"; 28 + 29 + grant select on table "public"."leaflets_to_documents" to "anon"; 30 + 31 + grant trigger on table "public"."leaflets_to_documents" to "anon"; 32 + 33 + grant truncate on table "public"."leaflets_to_documents" to "anon"; 34 + 35 + grant update on table "public"."leaflets_to_documents" to "anon"; 36 + 37 + grant delete on table "public"."leaflets_to_documents" to "authenticated"; 38 + 39 + grant insert on table "public"."leaflets_to_documents" to "authenticated"; 40 + 41 + grant references on table "public"."leaflets_to_documents" to "authenticated"; 42 + 43 + grant select on table "public"."leaflets_to_documents" to "authenticated"; 44 + 45 + grant trigger on table "public"."leaflets_to_documents" to "authenticated"; 46 + 47 + grant truncate on table "public"."leaflets_to_documents" to "authenticated"; 48 + 49 + grant update on table "public"."leaflets_to_documents" to "authenticated"; 50 + 51 + grant delete on table "public"."leaflets_to_documents" to "service_role"; 52 + 53 + grant insert on table "public"."leaflets_to_documents" to "service_role"; 54 + 55 + grant references on table "public"."leaflets_to_documents" to "service_role"; 56 + 57 + grant select on table "public"."leaflets_to_documents" to "service_role"; 58 + 59 + grant trigger on table "public"."leaflets_to_documents" to "service_role"; 60 + 61 + grant truncate on table "public"."leaflets_to_documents" to "service_role"; 62 + 63 + grant update on table "public"."leaflets_to_documents" to "service_role";
+1
supabase/migrations/20251119191717_add_archived_to_permission_tokens_on_homepage.sql
··· 1 + alter table "public"."permission_token_on_homepage" add column "archived" boolean;
+1
supabase/migrations/20251120215250_add_archived_col_to_leaflets_in_publications.sql
··· 1 + alter table "public"."leaflets_in_publications" add column "archived" boolean;
+15
supabase/migrations/20251122220118_add_cascade_on_update_to_pt_relations.sql
··· 1 + alter table "public"."permission_token_on_homepage" drop constraint "permission_token_creator_token_fkey"; 2 + 3 + alter table "public"."leaflets_in_publications" drop constraint "leaflets_in_publications_leaflet_fkey"; 4 + 5 + alter table "public"."leaflets_in_publications" drop column "archived"; 6 + 7 + alter table "public"."permission_token_on_homepage" drop column "archived"; 8 + 9 + alter table "public"."permission_token_on_homepage" add constraint "permission_token_on_homepage_token_fkey" FOREIGN KEY (token) REFERENCES permission_tokens(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; 10 + 11 + alter table "public"."permission_token_on_homepage" validate constraint "permission_token_on_homepage_token_fkey"; 12 + 13 + alter table "public"."leaflets_in_publications" add constraint "leaflets_in_publications_leaflet_fkey" FOREIGN KEY (leaflet) REFERENCES permission_tokens(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; 14 + 15 + alter table "public"."leaflets_in_publications" validate constraint "leaflets_in_publications_leaflet_fkey";
+2
supabase/migrations/20251124214105_add_back_archived_cols.sql
··· 1 + alter table "public"."permission_token_on_homepage" add column "archived" boolean; 2 + alter table "public"."leaflets_in_publications" add column "archived" boolean;
+14 -5
tsconfig.json
··· 1 1 { 2 2 "compilerOptions": { 3 - "lib": ["dom", "dom.iterable", "esnext"], 4 - "types": ["@cloudflare/workers-types"], 3 + "lib": [ 4 + "dom", 5 + "dom.iterable", 6 + "esnext" 7 + ], 8 + "types": [ 9 + "@cloudflare/workers-types" 10 + ], 5 11 "baseUrl": ".", 6 12 "allowJs": true, 7 13 "skipLibCheck": true, ··· 15 21 "moduleResolution": "node", 16 22 "resolveJsonModule": true, 17 23 "isolatedModules": true, 18 - "jsx": "preserve", 24 + "jsx": "react-jsx", 19 25 "plugins": [ 20 26 { 21 27 "name": "next" ··· 30 36 "**/*.js", 31 37 "**/*.ts", 32 38 "**/*.tsx", 33 - "**/*.mdx" 39 + "**/*.mdx", 40 + ".next/dev/types/**/*.ts" 34 41 ], 35 - "exclude": ["node_modules"] 42 + "exclude": [ 43 + "node_modules" 44 + ] 36 45 }