Read-it-later social network

add tanstack query, hardcoded selfhosted.social agent for now, render bookmark subjects

bun.lockb

This is a binary file and will not be displayed.

+8 -8
package.json
··· 14 }, 15 "devDependencies": { 16 "@sveltejs/adapter-netlify": "^5.2.3", 17 - "@sveltejs/kit": "^2.42.1", 18 - "@sveltejs/vite-plugin-svelte": "^6.2.0", 19 - "@tailwindcss/typography": "^0.5.16", 20 "autoprefixer": "^10.4.21", 21 "drizzle-kit": "^0.31.4", 22 - "svelte": "^5.39.2", 23 - "svelte-check": "^4.3.1", 24 "tailwindcss": "^4.1.13", 25 "typescript": "^5.9.2", 26 - "vite": "^7.1.6" 27 }, 28 "dependencies": { 29 - "@atproto/api": "^0.16.9", 30 "@atproto/oauth-client-node": "^0.3.8", 31 "@oslojs/encoding": "^1.1.0", 32 "@tailwindcss/vite": "^4.1.13", 33 - "@tanstack/svelte-query": "^5.89.0", 34 "drizzle-orm": "^0.44.5", 35 "postgres": "^3.4.7" 36 }
··· 14 }, 15 "devDependencies": { 16 "@sveltejs/adapter-netlify": "^5.2.3", 17 + "@sveltejs/kit": "^2.43.4", 18 + "@sveltejs/vite-plugin-svelte": "^6.2.1", 19 + "@tailwindcss/typography": "^0.5.19", 20 "autoprefixer": "^10.4.21", 21 "drizzle-kit": "^0.31.4", 22 + "svelte": "^5.39.6", 23 + "svelte-check": "^4.3.2", 24 "tailwindcss": "^4.1.13", 25 "typescript": "^5.9.2", 26 + "vite": "^7.1.7" 27 }, 28 "dependencies": { 29 + "@atproto/api": "^0.16.10", 30 "@atproto/oauth-client-node": "^0.3.8", 31 "@oslojs/encoding": "^1.1.0", 32 "@tailwindcss/vite": "^4.1.13", 33 + "@tanstack/svelte-query": "^5.90.2", 34 "drizzle-orm": "^0.44.5", 35 "postgres": "^3.4.7" 36 }
+2 -2
src/app.d.ts
··· 1 // See https://svelte.dev/docs/kit/types#app.d.ts 2 3 - import type { Agent, AtpBaseClient } from "@atproto/api"; 4 import type { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 5 6 // for information about these interfaces ··· 10 11 // set on `hooks.server.ts`, available on server functions 12 interface Locals { 13 - agent: Agent | AtpBaseClient | undefined; 14 user: ProfileViewDetailed | undefined; 15 } 16
··· 1 // See https://svelte.dev/docs/kit/types#app.d.ts 2 3 + import type { Agent } from "@atproto/api"; 4 import type { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 5 6 // for information about these interfaces ··· 10 11 // set on `hooks.server.ts`, available on server functions 12 interface Locals { 13 + authedAgent: Agent | undefined; 14 user: ProfileViewDetailed | undefined; 15 } 16
+4 -11
src/hooks.server.ts
··· 1 import { atclient } from "$lib/atproto"; 2 - import { AtpBaseClient, Agent } from "@atproto/api"; 3 4 import { decryptToString } from "$lib/server/encryption"; 5 import { decodeBase64, decodeBase64urlIgnorePadding } from "@oslojs/encoding"; ··· 25 const oauthSession = await atclient.restore(decrypted); 26 27 // set the authed agent 28 - const agent = new Agent(oauthSession); 29 - event.locals.agent = agent; 30 31 // set the authed user with decrypted session DID 32 - const user = await agent.getProfile({ actor: decrypted }); 33 event.locals.user = user.data; 34 - } 35 - else { 36 - // set public API agent 37 - const agent = new AtpBaseClient({ 38 - service: "https://slingshot.microcosm.blue" 39 - }); 40 - event.locals.agent = agent; 41 } 42 43 return resolve(event);
··· 1 + import { Agent } from "@atproto/api"; 2 import { atclient } from "$lib/atproto"; 3 4 import { decryptToString } from "$lib/server/encryption"; 5 import { decodeBase64, decodeBase64urlIgnorePadding } from "@oslojs/encoding"; ··· 25 const oauthSession = await atclient.restore(decrypted); 26 27 // set the authed agent 28 + const authedAgent = new Agent(oauthSession); 29 + event.locals.authedAgent = authedAgent; 30 31 // set the authed user with decrypted session DID 32 + const user = await authedAgent.getProfile({ actor: decrypted }); 33 event.locals.user = user.data; 34 } 35 36 return resolve(event);
+24
src/lib/utils.ts
···
··· 1 + // --- UTILITIES --- 2 + 3 + export type LexiconCommunityBookmark = { 4 + $type: "community.lexicon.bookmarks.bookmark"; 5 + subject: string; 6 + createdAt: string; 7 + tags?: string[]; 8 + }; 9 + 10 + export type LexiconCommunityLike = { 11 + $type: "community.lexicon.interaction.like"; 12 + subject: string; 13 + createdAt: string; 14 + } 15 + 16 + export function parseAtUri(uri: string) { 17 + const regex = /at:\/\/(?<did>did.*)\/(?<lexi>.*)\/(?<rkey>.*)/; 18 + const groups = regex.exec(uri)?.groups; 19 + return { 20 + did: groups?.did, 21 + lexi: groups?.lexi, 22 + rkey: groups?.rkey 23 + } 24 + }
+1 -1
src/routes/+layout.server.ts
··· 2 3 export async function load({ locals }: ServerLoadEvent) { 4 // have user available throughout the app via LayoutData 5 - return { user: locals.user }; 6 }
··· 2 3 export async function load({ locals }: ServerLoadEvent) { 4 // have user available throughout the app via LayoutData 5 + return { user: locals.user, authedAgent: locals.authedAgent }; 6 }
+45 -34
src/routes/+layout.svelte
··· 1 <script lang="ts"> 2 import '../app.css'; 3 let { data, children } = $props(); 4 const user = $derived(data.user); 5 </script> 6 7 - <div class="flex flex-col gap-8 w-screen h-full min-h-screen font-neco"> 8 - <header class="flex items-center w-full gap-4 px-8 py-4 justify-between"> 9 - <nav class="text-lg flex gap-4 items-center"> 10 - <a href="/" class="font-comico text-2xl hover:text-shadow-md">potatonet.app</a> 11 - <a href="https://tangled.sh/@zeu.dev/potatonet-app" class="hover:text-shadow-lg">🧶</a> 12 - <a href="https://bsky.app/profile/zeu.dev" class="hover:text-shadow-lg">🦋</a> 13 - </nav> 14 15 - <div class="flex gap-4 items-center"> 16 - {#if user} 17 - <a href={`/${user.handle}/home`} class="hover:text-shadow-lg">🏡</a> 18 - <form action="/?/logout" method="POST"> 19 - <button type="submit" class="hover:text-shadow-lg hover:cursor-pointer font-comico"> 20 - Logout 21 - </button> 22 - </form> 23 - {:else} 24 - <form action="/?/login" method="POST"> 25 - <input 26 - name="handle" 27 - type="text" 28 - placeholder="Handle (eg: zeu.dev)" 29 - class="border border-black border-dashed px-3 py-2 hover:shadow-lg focus:shadow-lg" 30 - /> 31 - <button type="submit" class="hover:text-shadow-lg hover:cursor-pointer font-comico"> 32 - Login 33 - </button> 34 - </form> 35 - {/if} 36 - </div> 37 - </header> 38 39 - <main class="flex flex-col gap-4 p-8"> 40 - {@render children()} 41 - </main> 42 - </div>
··· 1 <script lang="ts"> 2 import '../app.css'; 3 + import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query'; 4 + 5 let { data, children } = $props(); 6 const user = $derived(data.user); 7 + const queryClient = new QueryClient(); 8 </script> 9 10 + <QueryClientProvider client={queryClient}> 11 + <div class="flex flex-col gap-8 w-screen h-full min-h-screen font-neco"> 12 + <header class="flex items-center w-full gap-4 px-8 py-4 justify-between"> 13 + <nav class="text-lg flex gap-4 items-center"> 14 + <a href="/" class="font-comico text-2xl hover:text-shadow-md">potatonet.app</a> 15 + <a href="https://tangled.sh/@zeu.dev/potatonet-app" class="hover:text-shadow-lg">🧶</a> 16 + <a href="https://bsky.app/profile/zeu.dev" class="hover:text-shadow-lg">🦋</a> 17 + </nav> 18 19 + <div class="flex gap-4 items-center text-lg"> 20 + {#if user} 21 + <a href={`/${user.handle}/home`} class="hover:text-shadow-lg">🏡</a> 22 + <form action="/?/logout" method="POST"> 23 + <button type="submit" class="hover:text-shadow-lg hover:cursor-pointer font-comico"> 24 + Logout 25 + </button> 26 + </form> 27 + {:else} 28 + <form action="/?/login" method="POST"> 29 + <input 30 + name="handle" 31 + type="text" 32 + placeholder="Handle (eg: zeu.dev)" 33 + class="border border-black border-dashed px-3 py-2 hover:shadow-lg focus:shadow-lg" 34 + /> 35 + <button type="submit" class="hover:text-shadow-lg hover:cursor-pointer font-comico"> 36 + Login 37 + </button> 38 + </form> 39 + {/if} 40 + </div> 41 + </header> 42 43 + <main class="flex flex-col gap-4 p-8"> 44 + <svelte:boundary> 45 + {@render children()} 46 + 47 + {#snippet pending()} 48 + <p>Page loading...</p> 49 + {/snippet} 50 + </svelte:boundary> 51 + </main> 52 + </div> 53 + </QueryClientProvider>
+2
src/routes/+page.svelte
···
··· 1 + <h1 class="text-3xl font-bold font-comico">explore</h1> 2 + <p>coming soon...</p>
+31 -1
src/routes/[handle]/home/+page.svelte
··· 1 <script lang="ts"> 2 import { page } from "$app/state"; 3 4 - const handle = $derived(page.params.handle); 5 </script>
··· 1 <script lang="ts"> 2 import { page } from "$app/state"; 3 + import { Agent } from "@atproto/api"; 4 + import { createQuery } from "@tanstack/svelte-query"; 5 + import type { LexiconCommunityBookmark } from "$lib/utils"; 6 7 + const { handle } = page.params; 8 + const agent = new Agent({ service: "https://selfhosted.social" }); 9 + 10 + const bookmarksQuery = createQuery({ 11 + queryKey: ["bookmarks", handle], 12 + queryFn: async () => { 13 + if (!handle) { throw Error } 14 + const result = await agent.com.atproto.repo.listRecords({ 15 + repo: handle, 16 + collection: "community.lexicon.bookmarks.bookmark" 17 + }); 18 + if (!result.success) { throw Error } 19 + console.log({ result }); 20 + return result.data as unknown as { cursor: string, records: { uri: string, cid: string, value: LexiconCommunityBookmark }[] }; 21 + }, 22 + staleTime: 3000 23 + }); 24 </script> 25 + 26 + {#if $bookmarksQuery.isLoading} 27 + <p>Loading...</p> 28 + {:else if $bookmarksQuery.isError} 29 + <p>Error</p> 30 + {:else if $bookmarksQuery.isSuccess} 31 + {@const bookmarks = $bookmarksQuery.data.records} 32 + {#each bookmarks as { uri, cid, value: bookmark }} 33 + <p>{bookmark.subject}</p> 34 + {/each} 35 + {/if}
+1 -1
src/routes/oauth/callback/+server.ts
··· 30 error(500, { message: (err as Error).message }); 31 } 32 33 - redirect(301, "/farm"); 34 }
··· 30 error(500, { message: (err as Error).message }); 31 } 32 33 + redirect(301, `/`); 34 }
+7 -1
svelte.config.js
··· 9 10 kit: { 11 adapter: adapter() 12 - } 13 }; 14 15 export default config;
··· 9 10 kit: { 11 adapter: adapter() 12 + }, 13 + 14 + compilerOptions: { 15 + experimental: { 16 + async: true 17 + } 18 + } 19 }; 20 21 export default config;