Read-it-later social network

i did a lot things, its better i think

+234 -125
bun.lockb

This is a binary file and will not be displayed.

+2
package.json
··· 19 19 "@tailwindcss/typography": "^0.5.19", 20 20 "autoprefixer": "^10.4.21", 21 21 "drizzle-kit": "^0.31.4", 22 + "open-graph-scraper": "^6.11.0", 22 23 "svelte": "^5.39.6", 23 24 "svelte-check": "^4.3.2", 24 25 "tailwindcss": "^4.1.13", ··· 30 31 "@atproto/oauth-client-node": "^0.3.8", 31 32 "@oslojs/encoding": "^1.1.0", 32 33 "@tailwindcss/vite": "^4.1.13", 34 + "@tanstack/svelte-query": "^6.0.9", 33 35 "drizzle-orm": "^0.44.5", 34 36 "postgres": "^3.4.7", 35 37 "valibot": "^1.1.0"
+1
src/app.css
··· 18 18 @utility border-groove { 19 19 border-style: groove; 20 20 } 21 +
+35 -11
src/lib/components/BookmarkCard.svelte
··· 12 12 let { isOwner = false, bookmark, onTagClick, onTagDeleteClick }: BookmarkCardProps = $props(); 13 13 </script> 14 14 15 - <article class="flex flex-col gap-4 border border-dashed hover:border-solid hover:shadow-lg px-4 py-3 w-fit"> 16 - <a href={bookmark.subject} class="break-all hover:underline hover:cursor-pointer hover:text-shadow-md text-xl visited:text-violet-600">{bookmark.subject}</a> 17 - {#if bookmark.tags && bookmark.tags.length > 0} 18 - <div class="flex gap-5 flex-wrap"> 19 - {#each bookmark.tags as tag} 20 - <TagPill {tag} showDeleteButton={isOwner} {onTagClick} {onTagDeleteClick} /> 21 - {/each} 15 + <span class="flex border-3 border-double w-full rounded hover:shadow-lg"> 16 + <article class="flex flex-col gap-4 px-4 py-3 w-full h-fit"> 17 + <div class="flex gap-4 items-center"> 18 + {#if bookmark.$enriched?.favicon} 19 + <img src={bookmark.$enriched.favicon} alt={bookmark.$enriched.title} class="size-8 bg-neutral-300 rounded p-1" /> 20 + {/if} 21 + <h1 class="font-semibold">{bookmark.$enriched?.title}</h1> 22 22 </div> 23 - {:else} 24 - <p class="text-sm italic">No tags</p> 25 - {/if} 26 - </article> 23 + 24 + <a href={bookmark.subject} class="break-all hover:underline underline-offset-4 hover:cursor-pointer text-xl visited:text-violet-600"> 25 + {bookmark.subject} 26 + </a> 27 + {#if bookmark.$enriched?.description} 28 + <p>{bookmark.$enriched.description}</p> 29 + {/if} 30 + {#if bookmark.tags && bookmark.tags.length > 0} 31 + <div class="flex gap-5 flex-wrap"> 32 + {#each bookmark.tags as tag} 33 + <TagPill {tag} showDeleteButton={isOwner} {onTagClick} {onTagDeleteClick} /> 34 + {/each} 35 + </div> 36 + {:else} 37 + <p class="text-sm italic">No tags</p> 38 + {/if} 39 + </article> 40 + 41 + <nav class="w-fit border-l grid grid-rows-3 divide-y-1"> 42 + <button class="px-4">💛</button> 43 + <button class="px-4">💬</button> 44 + {#if isOwner} 45 + <button class="px-4">🗑️</button> 46 + {:else} 47 + <button class="px-4">🔖</button> 48 + {/if} 49 + </nav> 50 + </span>
+2 -2
src/lib/components/TagPill.svelte
··· 10 10 let { tag, variant, showDeleteButton, onTagClick, onTagDeleteClick }: TagPillProps = $props(); 11 11 </script> 12 12 13 - <div class="relative group flex"> 13 + <div class="relative group flex w-fit"> 14 14 {#if showDeleteButton && variant !== "menu"} 15 15 <button 16 16 onclick={() => onTagDeleteClick?.(tag)} ··· 23 23 onclick={() => onTagClick?.(tag)} 24 24 class={[ 25 25 variant === "menu" && "hover:bg-red-300", 26 - "bg-gray-200 w-fit px-2 py-1 hover:cursor-pointer font-comico text-sm" 26 + "bg-gray-200 w-fit px-2 py-1 hover:cursor-pointer text-sm" 27 27 ]} 28 28 > 29 29 {tag}
+11 -6
src/lib/server/api.ts
··· 1 - import { SLICES_NETWORK_ACCESS_TOKEN } from "$env/static/private"; 1 + import { SLICES_BEARER_TOKEN, SLICES_NETWORK_ACCESS_TOKEN } from "$env/static/private"; 2 2 import type { LexiconCommunityBookmark, SliceItem, SliceList } from "$lib/utils"; 3 3 4 4 const SLICES_NETWORK_SLICE_URI = "at://did:plc:gotnvwkr56ibs33l4hwgfoet/network.slices.slice/3m26tswgbi42i" 5 5 6 - const baseUrl = "https://api.slices.network/xrpc/"; 6 + const baseUrl = "https://slices-api.fly.dev/xrpc/"; 7 7 8 8 type GetListProps = { 9 9 limit?: number; // default: 50, max: 100 10 - cursor?: string; 10 + cursor?: string | null; 11 11 where?: { 12 12 [key: string]: { eq?: string, contains?: string, in?: string[] } 13 13 }; ··· 24 24 this.sliceUri = sliceUri; 25 25 } 26 26 27 + /** 27 28 async getRecord({ uri }: { uri: string }) { 28 - const searchParams = new URLSearchParams({ slice: SLICES_NETWORK_SLICE_URI, uri }); 29 29 const response = await fetch(`${baseUrl}${this.collection}.getRecord?${searchParams.toString()}`); 30 30 return await response.json() as SliceItem<T>; 31 31 } 32 + **/ 32 33 33 34 async getList(body: GetListProps) { 34 35 const response = await fetch(`${baseUrl}${this.collection}.getRecords`, { 35 36 method: "POST", 36 37 headers: { 38 + // "Accept": "*/*", 37 39 "Content-Type": "application/json", 38 - "Authorization": SLICES_NETWORK_ACCESS_TOKEN 40 + // "Authorization": `Bearer ${SLICES_BEARER_TOKEN}` 39 41 }, 40 42 body: JSON.stringify({ ...body, slice: SLICES_NETWORK_SLICE_URI }) 41 43 }); 42 44 const data = await response.json() as SliceList<T>; 43 - console.log({ data }); 45 + for (const d of data.records) { 46 + console.log(d); 47 + } 48 + console.log(data.cursor); 44 49 return data; 45 50 } 46 51 }
+12 -1
src/lib/utils.ts
··· 11 11 subject: string; 12 12 createdAt: string; 13 13 tags?: string[]; 14 + $enriched?: { 15 + description: string; 16 + favicon: string; 17 + title: string; 18 + } 14 19 }; 15 20 16 21 export type LexiconCommunityLike = { ··· 21 26 22 27 export type SliceItem<T> = CommonSliceFields & { value: T }; 23 28 24 - export type SliceList<T> = CommonSliceFields & { 29 + export type SliceList<T> = { 25 30 cursor: string; 26 31 records: (CommonSliceFields & { did: string, value: T })[]; 27 32 } ··· 35 40 rkey: groups?.rkey 36 41 } 37 42 } 43 + 44 + export async function resolveHandle(handle: string) { 45 + const result = await fetch(`https://slingshot.microcosm.blue/xrpc/com.atproto.identity.resolveHandle?handle=${handle}`) 46 + const info = await result.json(); 47 + return info.did; 48 + }
+54 -45
src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 2 import { page } from '$app/state'; 3 3 import '../app.css'; 4 + import { browser } from '$app/environment'; 5 + import { QueryClient, QueryClientProvider } from "@tanstack/svelte-query"; 4 6 5 7 let { data, children } = $props(); 6 8 const user = $derived(data.user); 9 + const queryClient = new QueryClient({ 10 + defaultOptions: { 11 + queries: { 12 + enabled: true 13 + } 14 + } 15 + }); 7 16 </script> 8 17 9 - <div class="flex flex-col gap-8 w-screen h-full min-h-screen font-neco"> 10 - <header class="flex flex-col lg:flex-row lg:items-center w-full gap-4 px-8 py-4 border-b lg:border-none justify-between"> 11 - <a href="/" class="font-comico text-2xl hover:text-shadow-md">potatonet.app</a> 18 + <QueryClientProvider client={queryClient}> 19 + <div class="flex flex-col gap-8 w-screen h-full min-h-screen font-neco"> 20 + <header class="flex flex-col lg:flex-row lg:items-center w-full gap-4 px-8 py-4 border-b lg:border-none justify-between"> 21 + <a href="/" class="text-2xl hover:text-shadow-md">potatonet.app</a> 12 22 13 - <div class="flex gap-4 items-center text-lg flex-wrap"> 14 - <nav class="text-lg flex gap-4 flex-wrap items-center border-3 border-groove px-3 py-1.5"> 15 - <a href="/" class="hover:text-shadow-lg hover:underline" title="explore" aria-label="explore">🛰️ explore</a> 16 - <a href="https://tangled.sh/@zeu.dev/potatonet-app" class="hover:text-shadow-lg" title="source code" aria-label="source code">🧶 source code</a> 17 - <a href="https://bsky.app/profile/zeu.dev" class="hover:text-shadow-lg" title="maker's bluesky" aria-label="maker's bluesky">🦋 maker's bluesky</a> 23 + <div class="flex gap-4 items-center text-lg flex-wrap"> 24 + <nav class="text-lg flex gap-4 flex-wrap items-center border-3 border-groove px-3 py-1.5"> 25 + <a href="/" class="hover:text-shadow-lg hover:underline" title="explore" aria-label="explore">🛰️ explore</a> 26 + <a href="https://tangled.sh/@zeu.dev/potatonet-app" class="hover:text-shadow-lg" title="source code" aria-label="source code">🧶 source code</a> 27 + {#if user} 28 + <a href={`/${user.handle}/bookmarks`} class="hover:text-shadow-lg" aria-label="logged in user's bookmarks">🔖 your bookmarks</a> 29 + {/if} 30 + </nav> 18 31 {#if user} 19 - <a href={`/${user.handle}/bookmarks`} class="hover:text-shadow-lg" aria-label="logged in user's bookmarks">🔖 your bookmarks</a> 32 + <form action="/?/logout" method="POST"> 33 + <button type="submit" class="hover:text-shadow-lg hover:cursor-pointer"> 34 + Logout 35 + </button> 36 + </form> 37 + {:else} 38 + <form action="/?/login" method="POST" class="flex gap-4 lg:basis-0"> 39 + <input 40 + name="handle" 41 + type="text" 42 + placeholder="Handle (eg: zeu.dev)" 43 + class="border border-black border-dashed text-sm px-3 py-2 hover:shadow-lg focus:shadow-lg" 44 + /> 45 + <button type="submit" class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2"> 46 + Login 47 + </button> 48 + </form> 20 49 {/if} 21 - </nav> 22 - {#if user} 23 - <form action="/?/logout" method="POST"> 24 - <button type="submit" class="hover:text-shadow-lg hover:cursor-pointer font-comico"> 25 - Logout 26 - </button> 27 - </form> 28 - {:else} 29 - <form action="/?/login" method="POST" class="flex gap-4 lg:basis-0"> 30 - <input 31 - name="handle" 32 - type="text" 33 - placeholder="Handle (eg: zeu.dev)" 34 - class="border border-black border-dashed text-sm px-3 py-2 hover:shadow-lg focus:shadow-lg" 35 - /> 36 - <button type="submit" class="font-comico bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2"> 37 - Login 38 - </button> 39 - </form> 40 - {/if} 41 - </div> 42 - </header> 43 - 44 - {#key page.url.pathname} 45 - <main class="flex flex-col gap-4 p-8 pt-0 lg:pt-8"> 46 - <svelte:boundary> 47 - {@render children()} 48 - 49 - {#snippet pending()} 50 - <p>Loading...</p> 51 - {/snippet} 52 - </svelte:boundary> 53 - </main> 54 - {/key} 55 - </div> 56 - 50 + </div> 51 + </header> 52 + 53 + {#key page.url.pathname} 54 + <main class="flex flex-col gap-4 p-8 pt-0 lg:pt-8"> 55 + <svelte:boundary> 56 + {@render children()} 57 + 58 + {#snippet pending()} 59 + <p>Loading...</p> 60 + {/snippet} 61 + </svelte:boundary> 62 + </main> 63 + {/key} 64 + </div> 65 + </QueryClientProvider>
+43 -23
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import BookmarkCard from "$lib/components/BookmarkCard.svelte"; 3 - import TagPill from "$lib/components/TagPill.svelte"; 3 + import TagPill from "$lib/components/TagPill.svelte"; 4 + import { createInfiniteQuery } from "@tanstack/svelte-query"; 4 5 import { getAllBookmarks } from "./api/bookmarks/data.remote"; 5 6 6 7 let { data } = $props(); 7 - let cursor = $state(""); 8 - const userBookmarksQuery = $derived(getAllBookmarks({ cursor })); 9 - const queryData = $derived(userBookmarksQuery.current); 10 - 11 8 let query = $state(""); 12 9 let filterTags = $state<string[]>([]); 10 + 11 + let bookmarkPage = $state(0); 12 + const exploreBookmarksQuery = createInfiniteQuery(() => ({ 13 + queryKey: ["explore"], 14 + queryFn: ({ pageParam }) => getAllBookmarks({ cursor: pageParam }), 15 + initialPageParam: "", 16 + getNextPageParam: (lastPage) => lastPage.cursor, 17 + select: (data) => data.pages.map((page) => page.list).flat(), 18 + staleTime: 600 19 + })); 20 + let bookmarks = $derived(exploreBookmarksQuery.data ?? []); 13 21 14 22 function onTagClick(tag: string) { 15 - const index = filterTags.findIndex((t) => t === tag); 23 + const index = filterTags.findIndex((t) => t.toLowerCase() === tag.toLowerCase()); 16 24 if (index >= 0) { filterTags.splice(index, 1); } 17 - else { filterTags.push(tag); 25 + else { filterTags.push(tag.toLowerCase()); 18 26 } 19 27 } 20 28 21 29 function onTagDeleteClick(tag: string) { 22 30 console.log("DELETE", tag); 23 31 } 32 + 33 + $inspect(bookmarkPage, bookmarks.slice(bookmarkPage*50)); 24 34 </script> 25 35 26 36 <div class="flex gap-4 items-center"> 27 - <h1 class="text-2xl lg:text-3xl font-comico">Explore</h1> 28 - <h2 class="text-lg italic">recent 50</h2> 37 + <h1 class="text-2xl lg:text-3xl">Explore</h1> 29 38 </div> 30 39 31 40 <menu class="flex flex-col lg:flex-row w-full gap-4"> ··· 44 53 {/each} 45 54 {/if} 46 55 </label> 47 - <button 48 - onclick={() => userBookmarksQuery.refresh()} 49 - class="font-comico bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2" 50 - > 51 - Refresh 56 + 57 + <button onclick={() => { exploreBookmarksQuery.fetchPreviousPage(); bookmarkPage--; }} disabled={!exploreBookmarksQuery.hasPreviousPage}> 58 + Prev Page 59 + </button> 60 + <button onclick={() => { exploreBookmarksQuery.fetchNextPage(); bookmarkPage++; }} disabled={!exploreBookmarksQuery.hasNextPage}> 61 + Next Page 52 62 </button> 63 + 53 64 {#if data.user} 54 - <button class="justify-self-end font-comico bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2"> 65 + <button class="justify-self-end bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2"> 55 66 🔖 New Bookmark 56 67 </button> 57 68 {/if} ··· 59 70 </menu> 60 71 <hr /> 61 72 62 - {#if userBookmarksQuery.loading} 73 + {#if exploreBookmarksQuery.isPending} 63 74 <p>Loading...</p> 64 - {:else if userBookmarksQuery.error} 75 + {:else if exploreBookmarksQuery.isError} 65 76 <p>Error</p> 66 - {:else if queryData} 77 + {:else if exploreBookmarksQuery.isSuccess} 67 78 <div class="flex flex-wrap gap-4"> 68 - {#each queryData.bookmarks as bookmark} 69 - {#if bookmark.subject.includes(query) && (bookmark.tags?.some(t => filterTags.length > 0 ? filterTags.includes(t) : true))} 70 - <BookmarkCard {bookmark} {onTagClick} {onTagDeleteClick} /> 71 - {/if} 72 - {/each} 79 + {#if bookmarks} 80 + {@const pagedBookmarks = bookmarks.slice(bookmarkPage*50)} 81 + {#each pagedBookmarks as info} 82 + {@const bookmark = info.bookmark} 83 + {#if bookmark.subject.includes(query)} 84 + {#if (bookmark.tags && bookmark.tags.length > 0 85 + && bookmark.tags.some(t => filterTags.length > 0 ? filterTags.includes(t.toLowerCase()) : true) 86 + ) 87 + || (bookmark.tags && bookmark.tags.length === 0 && filterTags.length === 0)} 88 + <BookmarkCard {bookmark} {onTagClick} {onTagDeleteClick} /> 89 + {/if} 90 + {/if} 91 + {/each} 92 + {/if} 73 93 </div> 74 94 {/if}
+48 -27
src/routes/[handle]/bookmarks/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { page } from "$app/state"; 3 2 import BookmarkCard from "$lib/components/BookmarkCard.svelte"; 4 3 import TagPill from "$lib/components/TagPill.svelte"; 5 - import { getUserBookmarks } from "../../api/bookmarks/data.remote"; 4 + import { createInfiniteQuery } from "@tanstack/svelte-query"; 5 + import { getUserBookmarks } from "../../api/bookmarks/data.remote.js"; 6 + import { page } from "$app/state"; 6 7 7 - let { data } = $props(); 8 - const { handle } = page.params; 9 - let isOwner = $derived(data.user?.handle === handle); 10 - let cursor = $state(""); 11 - const userBookmarksQuery = $derived(getUserBookmarks({ handle: handle as string, cursor })); 12 8 9 + let { data } = $props(); 13 10 let query = $state(""); 14 11 let filterTags = $state<string[]>([]); 12 + 13 + let bookmarkPage = $state(0); 14 + const userBookmarksQuery = createInfiniteQuery(() => ({ 15 + queryKey: ["user", page.params.handle], 16 + queryFn: ({ pageParam }) => getUserBookmarks({ handle: page.params.handle!, cursor: pageParam }), 17 + initialPageParam: "", 18 + getNextPageParam: (lastPage) => lastPage.cursor, 19 + select: (data) => data.pages.map((page) => page.list).flat(), 20 + staleTime: 600 21 + })); 22 + let bookmarks = $derived(userBookmarksQuery.data ?? []); 15 23 16 24 function onTagClick(tag: string) { 17 - const index = filterTags.findIndex((t) => t === tag); 25 + const index = filterTags.findIndex((t) => t.toLowerCase() === tag.toLowerCase()); 18 26 if (index >= 0) { filterTags.splice(index, 1); } 19 - else { 20 - filterTags.push(tag); 27 + else { filterTags.push(tag.toLowerCase()); 21 28 } 22 29 } 23 30 24 31 function onTagDeleteClick(tag: string) { 25 32 console.log("DELETE", tag); 26 33 } 34 + 35 + $inspect(bookmarkPage, bookmarks.slice(bookmarkPage*50)); 27 36 </script> 28 37 29 - <h1 class="text-2xl lg:text-3xl font-comico">Bookmarks by @{handle}</h1> 38 + <div class="flex gap-4 items-center"> 39 + <h1 class="text-2xl lg:text-3xl">Bookmarks by {page.params.handle}</h1> 40 + </div> 30 41 31 42 <menu class="flex flex-col lg:flex-row w-full gap-4"> 32 43 <label class="flex items-center gap-2"> ··· 44 55 {/each} 45 56 {/if} 46 57 </label> 47 - <button 48 - onclick={() => userBookmarksQuery.refresh()} 49 - class="font-comico bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2" 50 - > 51 - Refresh 58 + 59 + <button onclick={() => { userBookmarksQuery.fetchPreviousPage(); bookmarkPage--; }} disabled={!userBookmarksQuery.hasPreviousPage}> 60 + Prev Page 61 + </button> 62 + <button onclick={() => { userBookmarksQuery.fetchNextPage(); bookmarkPage++; }} disabled={!userBookmarksQuery.hasNextPage}> 63 + Next Page 52 64 </button> 53 - {#if isOwner} 54 - <button class="justify-self-end font-comico bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2"> 65 + 66 + {#if data.user} 67 + <button class="justify-self-end bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2"> 55 68 🔖 New Bookmark 56 69 </button> 57 70 {/if} ··· 59 72 </menu> 60 73 <hr /> 61 74 62 - {#if userBookmarksQuery.loading} 75 + {#if userBookmarksQuery.isPending} 63 76 <p>Loading...</p> 64 - {:else if userBookmarksQuery.error} 77 + {:else if userBookmarksQuery.isError} 65 78 <p>Error</p> 66 - {:else} 67 - {@const { cursor: returnedCursor, bookmarks } = userBookmarksQuery.current || { cursor: "", bookmarks: []}} 79 + {:else if userBookmarksQuery.isSuccess} 68 80 <div class="flex flex-wrap gap-4"> 69 - {#each bookmarks as bookmark} 70 - {#if bookmark.subject.includes(query) && (bookmark.tags?.every(t => filterTags.length > 0 ? filterTags.includes(t) : true))} 71 - <BookmarkCard {isOwner} {bookmark} {onTagClick} {onTagDeleteClick} /> 72 - {/if} 73 - {/each} 81 + {#if bookmarks} 82 + {@const pagedBookmarks = bookmarks.slice(bookmarkPage*50)} 83 + {#each pagedBookmarks as info} 84 + {@const bookmark = info.bookmark} 85 + {#if bookmark.subject.includes(query)} 86 + {#if (bookmark.tags && bookmark.tags.length > 0 87 + && bookmark.tags.some(t => filterTags.length > 0 ? filterTags.includes(t.toLowerCase()) : true) 88 + ) 89 + || (bookmark.tags && bookmark.tags.length === 0 && filterTags.length === 0)} 90 + <BookmarkCard {bookmark} {onTagClick} {onTagDeleteClick} /> 91 + {/if} 92 + {/if} 93 + {/each} 94 + {/if} 74 95 </div> 75 96 {/if}
+15 -10
src/routes/api/bookmarks/data.remote.ts
··· 1 1 import * as v from "valibot"; 2 - import { getRequestEvent, query } from "$app/server" 2 + import { query } from "$app/server" 3 3 import { LexiconBookmarkSlicesAPI } from "$lib/server/api" 4 - import { Agent } from "@atproto/api"; 5 4 6 5 const GetUserBookmarksValidator = v.object({ 7 6 handle: v.string(), ··· 9 8 }); 10 9 11 10 export const getUserBookmarks = query(GetUserBookmarksValidator, async ({ handle, cursor }) => { 12 - const { locals } = getRequestEvent(); 13 - const agent = locals.authedAgent ?? new Agent({ service: "https://api.bsky.app" }); 14 - const result = await agent.resolveHandle({ handle }); 15 - if (!result.success) { throw Error() }; 11 + const result = await fetch(`https://slingshot.microcosm.blue/xrpc/com.atproto.identity.resolveHandle?handle=${handle}`) 12 + const info = await result.json(); 13 + 14 + if (!info) { throw Error(); } 16 15 17 16 const data = await LexiconBookmarkSlicesAPI.getList({ 18 - cursor, 17 + cursor: !cursor ? null : cursor, 19 18 where: { 20 - did: { eq: result.data.did } 19 + did: { eq: info.did } 21 20 } 22 21 }); 23 22 24 - return { cursor: data.cursor, bookmarks: data.records.map((r) => r.value )}; 23 + console.log(info); 24 + 25 + return { cursor: data.cursor, list: data.records.map((r) => { 26 + return { did: r.did, bookmark: r.value } 27 + })}; 25 28 }); 26 29 27 30 ··· 32 35 export const getAllBookmarks = query(GetAllBookmarksValidator, async ({ cursor }) => { 33 36 const data = await LexiconBookmarkSlicesAPI.getList({ cursor }); 34 37 35 - return { cursor: data.cursor, bookmarks: data.records.map((r) => r.value )}; 38 + return { cursor: data.cursor, list: data.records.map((r) => { 39 + return { did: r.did, bookmark: r.value } 40 + })}; 36 41 });
+11
src/routes/api/metadata.remote.ts
··· 1 + import * as v from "valibot"; 2 + import ogs from "open-graph-scraper"; 3 + import { query } from "$app/server"; 4 + import { error } from "@sveltejs/kit"; 5 + 6 + export const getMetadata = query(v.string(), async (url) => { 7 + if (url === "/") { return error(401); } 8 + const response = await ogs({ url }); 9 + if (response.error) { return error(404); } 10 + return response.result; 11 + });