tangled
alpha
login
or
join now
zeu.dev
/
potatonet-app
12
fork
atom
Read-it-later social network
12
fork
atom
overview
issues
pulls
pipelines
update queries with authed viewer subscription
zeu.dev
1 month ago
793c0e4d
cf0f4aff
+106
-69
9 changed files
expand all
collapse all
unified
split
src
lib
components
PublicationCard.svelte
utils.ts
routes
+error.svelte
+layout.svelte
+layout.ts
+page.svelte
[handle]
[pubRkey]
+page.svelte
explore
+page.svelte
home
+page.svelte
+16
-34
src/lib/components/PublicationCard.svelte
···
2
import { getContext } from "svelte";
3
import { createQuery } from "@tanstack/svelte-query";
4
import type { QuicksliceClient } from "quickslice-client-js";
5
-
import { parseAtUri, resolveHandle, type MiniDoc, type PublicationNode } from "$lib/utils";
6
7
const user = getContext("user") as MiniDoc;
8
const atclient = getContext("atclient") as QuicksliceClient;
9
10
-
let { publication, showEmpty = false }: { publication: PublicationNode, showEmpty?: boolean } = $props();
0
0
0
0
11
12
let disableSubscribeButton = $state(false);
13
let isSubscribeButtonHovered = $state(false);
···
44
},
45
}));
46
47
-
const subscriptionQuery = createQuery(() => ({
48
-
queryKey: ["isSubscribed", publication.uri, user && user.did],
49
-
queryFn: async () => {
50
-
if (!user.did) {
51
-
return { records: [] }
52
-
}
53
-
const constellationUrl = new URL("https://constellation.microcosm.blue/xrpc/blue.microcosm.links.getBacklinks");
54
-
constellationUrl.searchParams.set("subject", publication.uri);
55
-
constellationUrl.searchParams.set("source", "site.standard.graph.subscription:publication");
56
-
constellationUrl.searchParams.set("did", user.did);
57
-
const response = await fetch(constellationUrl, {
58
-
headers: {
59
-
"Accept": "application/json"
60
-
}
61
-
});
62
-
63
-
64
-
const json = await response.json() as { records: { did: string, collection: string, rkey: string }[] };
65
-
return json;
66
-
},
67
-
select: (data) => data.records[0] && data.records[0].rkey
68
-
}));
69
-
70
let documents = $derived(countQuery.data?.documents || 0);
71
let subscribers = $derived(countQuery.data?.subscribers || 0);
72
-
let subscriptionRkey = $derived(subscriptionQuery.data);
73
-
let blobSyncUrl = $derived((`${miniDocQuery.data?.pds}/xrpc/com.atproto.sync.getBlob?did=${publication.did}&cid=${publication.value.icon?.ref.$link}`));
74
-
const theme = publication.value.basicTheme || {
75
$type: "site.standard.theme.basic",
76
background: {
77
$type: "site.standard.theme.color#rgb",
···
167
`}
168
>
169
<div class="flex flex-1 flex-col items-center justify-center gap-3 p-8">
170
-
{#if publication.value.icon}
171
<img
172
src={blobSyncUrl.toString()}
173
-
alt={publication.value.name}
174
class="size-24 rounded-xl hover:-rotate-15 transition-transform duration-150"
175
/>
176
{/if}
177
<h3 class="text-xl font-semibold text-center text-balance">
178
-
{publication.value.name}
179
</h3>
180
<a
181
href={`https://bsky.app/profile/${publication.actorHandle}`}
···
187
by @{publication.actorHandle}
188
</a>
189
<p class="text-xs text-center max-w-md leading-relaxed font-neco">
190
-
{publication.value.description}
191
</p>
192
</div>
193
194
<div class="flex w-full lg:w-32 border-t lg:flex-col lg:border-t-0 lg:border-l bg-muted/50">
195
-
<div
0
196
class="group flex flex-1 flex-col items-center justify-center gap-1 border-r lg:border-r-0 lg:border-b border-border p-4 hover:cursor-pointer"
197
style={`
198
background-color: rgb(${theme.accent.r},${theme.accent.g},${theme.accent.b});
···
206
Documents
207
<span class="group-hover:rotate-45 transition-transform duration-150">↗</span>
208
</span>
209
-
</div>
210
<button
211
onclick={toggleSubscribe}
212
disabled={disableSubscribeButton}
···
2
import { getContext } from "svelte";
3
import { createQuery } from "@tanstack/svelte-query";
4
import type { QuicksliceClient } from "quickslice-client-js";
5
+
import { parseAtUri, resolveHandle, type MiniDoc, type PublicationNode, type SubscriptionNode } from "$lib/utils";
6
7
const user = getContext("user") as MiniDoc;
8
const atclient = getContext("atclient") as QuicksliceClient;
9
10
+
let { publication, showEmpty = false }: {
11
+
publication: PublicationNode & { viewerSiteStandardGraphSubscriptionViaPublication?: SubscriptionNode | null }, showEmpty?: boolean
12
+
} = $props();
13
+
14
+
const { rkey: pubRkey } = parseAtUri(publication.uri);
15
16
let disableSubscribeButton = $state(false);
17
let isSubscribeButtonHovered = $state(false);
···
48
},
49
}));
50
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
51
let documents = $derived(countQuery.data?.documents || 0);
52
let subscribers = $derived(countQuery.data?.subscribers || 0);
53
+
let subscriptionRkey = $derived(parseAtUri(publication.viewerSiteStandardGraphSubscriptionViaPublication?.uri || "").rkey);
54
+
let blobSyncUrl = $derived(`${miniDocQuery.data?.pds}/xrpc/com.atproto.sync.getBlob?did=${publication.did}&cid=${publication.icon?.ref}`);
55
+
const theme = publication.basicTheme || {
56
$type: "site.standard.theme.basic",
57
background: {
58
$type: "site.standard.theme.color#rgb",
···
148
`}
149
>
150
<div class="flex flex-1 flex-col items-center justify-center gap-3 p-8">
151
+
{#if publication.icon}
152
<img
153
src={blobSyncUrl.toString()}
154
+
alt={publication.name}
155
class="size-24 rounded-xl hover:-rotate-15 transition-transform duration-150"
156
/>
157
{/if}
158
<h3 class="text-xl font-semibold text-center text-balance">
159
+
{publication.name}
160
</h3>
161
<a
162
href={`https://bsky.app/profile/${publication.actorHandle}`}
···
168
by @{publication.actorHandle}
169
</a>
170
<p class="text-xs text-center max-w-md leading-relaxed font-neco">
171
+
{publication.description}
172
</p>
173
</div>
174
175
<div class="flex w-full lg:w-32 border-t lg:flex-col lg:border-t-0 lg:border-l bg-muted/50">
176
+
<a
177
+
href={`/${miniDocQuery.data?.handle}/${pubRkey}`}
178
class="group flex flex-1 flex-col items-center justify-center gap-1 border-r lg:border-r-0 lg:border-b border-border p-4 hover:cursor-pointer"
179
style={`
180
background-color: rgb(${theme.accent.r},${theme.accent.g},${theme.accent.b});
···
188
Documents
189
<span class="group-hover:rotate-45 transition-transform duration-150">↗</span>
190
</span>
191
+
</a>
192
<button
193
onclick={toggleSubscribe}
194
disabled={disableSubscribeButton}
+7
-3
src/lib/utils.ts
···
39
40
export type ATBlob = {
41
$type: string;
42
-
ref: { $link: string; };
43
mimeType: string;
44
size: number;
45
}
···
51
r: number;
52
}
53
54
-
export type PublicationNode = Node & { value: {
55
url: string;
56
name: string;
57
description: string;
···
67
accent: StandardSiteThemeColorRGB;
68
accentForeground: StandardSiteThemeColorRGB;
69
};
70
-
}}
0
0
0
0
71
72
export type DocumentNode = Node & { value: {
73
title: string;
···
39
40
export type ATBlob = {
41
$type: string;
42
+
ref: string;
43
mimeType: string;
44
size: number;
45
}
···
51
r: number;
52
}
53
54
+
export type PublicationNode = Node & {
55
url: string;
56
name: string;
57
description: string;
···
67
accent: StandardSiteThemeColorRGB;
68
accentForeground: StandardSiteThemeColorRGB;
69
};
70
+
}
71
+
72
+
export type SubscriptionNode = Node & {
73
+
publication: string;
74
+
}
75
76
export type DocumentNode = Node & { value: {
77
title: string;
+5
src/routes/+error.svelte
···
0
0
0
0
0
···
1
+
<script lang="ts">
2
+
import { page } from "$app/state";
3
+
</script>
4
+
5
+
<p>{page.error.message}</p>
+3
-12
src/routes/+layout.svelte
···
1
<script lang="ts">
2
import '../app.css';
3
import { page } from '$app/state';
4
-
import { onMount, setContext } from 'svelte';
5
import { QueryClient, QueryClientProvider } from "@tanstack/svelte-query";
6
import { SvelteQueryDevtools } from "@tanstack/svelte-query-devtools";
7
-
import { goto } from '$app/navigation';
8
9
let { data, children } = $props();
10
const { atclient, user } = data;
···
32
}
33
}
34
});
35
-
36
-
/**
37
-
onMount(() => {
38
-
if (user) {
39
-
goto("/home");
40
-
}
41
-
});
42
-
**/
43
</script>
44
45
<QueryClientProvider client={queryClient}>
···
50
51
<div class="flex gap-4 items-center flex-wrap">
52
<nav class="flex gap-4 flex-wrap items-center px-3 py-1.5">
53
-
<a href="/explore" class="hover:text-shadow-sm" title="explore" aria-label="explore">🛰️ explore</a>
54
{#if user}
55
-
<a href="/home" class="hover:text-shadow-sm" title="explore" aria-label="explore">🏠 {user.handle}</a>
56
{/if}
57
</nav>
58
{#if user}
···
1
<script lang="ts">
2
import '../app.css';
3
import { page } from '$app/state';
4
+
import { setContext } from 'svelte';
5
import { QueryClient, QueryClientProvider } from "@tanstack/svelte-query";
6
import { SvelteQueryDevtools } from "@tanstack/svelte-query-devtools";
0
7
8
let { data, children } = $props();
9
const { atclient, user } = data;
···
31
}
32
}
33
});
0
0
0
0
0
0
0
0
34
</script>
35
36
<QueryClientProvider client={queryClient}>
···
41
42
<div class="flex gap-4 items-center flex-wrap">
43
<nav class="flex gap-4 flex-wrap items-center px-3 py-1.5">
44
+
<a href="/explore" class="hover:text-shadow-sm" title="explore" aria-label="explore">🛰️ Explore</a>
45
{#if user}
46
+
<a href="/home" class="hover:text-shadow-sm" title="explore" aria-label="explore">🏠 Home</a>
47
{/if}
48
</nav>
49
{#if user}
+1
-1
src/routes/+layout.ts
···
13
14
if (url.searchParams.has("code")) {
15
await atclient.handleRedirectCallback();
16
-
redirect(302, "/");
17
}
18
19
const isAuthed = await atclient.isAuthenticated();
···
13
14
if (url.searchParams.has("code")) {
15
await atclient.handleRedirectCallback();
16
+
redirect(302, "/home");
17
}
18
19
const isAuthed = await atclient.isAuthenticated();
+24
-2
src/routes/+page.svelte
···
1
<script lang="ts">
0
2
import LeafletIcon from "$lib/components/LeafletIcon.svelte";
3
import OffprintIcon from "$lib/components/OffprintIcon.svelte";
4
import PcktIcon from "$lib/components/PcktIcon.svelte";
5
-
</script>
6
0
0
0
7
0
0
0
0
0
0
8
9
<section class="flex flex-col gap-4 my-8">
10
<h2 class="text-amber-400 text-2xl font-bold font-neco">Talk about what everyone's reading today</h2>
···
39
<h2 class="text-center text-amber-400 text-3xl font-bold font-neco">Find your next read on potatonet</h2>
40
<div class="flex gap-4">
41
<a href="/explore" class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2">🛰️ Explore</a>
42
-
<button class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2">Login</button>
0
0
0
0
0
0
0
0
0
0
0
0
43
</div>
44
<pre class="text-xs tracking-widest">
45
···
1
<script lang="ts">
2
+
import { getContext } from "svelte";
3
import LeafletIcon from "$lib/components/LeafletIcon.svelte";
4
import OffprintIcon from "$lib/components/OffprintIcon.svelte";
5
import PcktIcon from "$lib/components/PcktIcon.svelte";
6
+
import type { QuicksliceClient } from "quickslice-client-js";
7
8
+
const user = getContext("user");
9
+
const atclient = getContext("atclient") as QuicksliceClient;
10
+
let handleInput = $state("");
11
12
+
async function login() {
13
+
if (handleInput) {
14
+
await atclient.loginWithRedirect({ handle: handleInput });
15
+
}
16
+
}
17
+
</script>
18
19
<section class="flex flex-col gap-4 my-8">
20
<h2 class="text-amber-400 text-2xl font-bold font-neco">Talk about what everyone's reading today</h2>
···
49
<h2 class="text-center text-amber-400 text-3xl font-bold font-neco">Find your next read on potatonet</h2>
50
<div class="flex gap-4">
51
<a href="/explore" class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2">🛰️ Explore</a>
52
+
{#if user}
53
+
<a href="/home" class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2">🏠 Home</a>
54
+
{:else}
55
+
<input
56
+
type="text"
57
+
bind:value={handleInput}
58
+
placeholder="Handle (eg: zeu.dev)"
59
+
class="border border-black border-dashed text-sm px-3 py-2 hover:shadow-lg focus:shadow-lg"
60
+
/>
61
+
<button onclick={login} class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2">
62
+
Login
63
+
</button>
64
+
{/if}
65
</div>
66
<pre class="text-xs tracking-widest">
67
-7
src/routes/[handle]/[pubRkey]/+page.svelte
···
1
-
<script lang="ts">
2
-
import { page } from "$app/state";
3
-
4
-
const { handle, pubRkey } = page.params;
5
-
</script>
6
-
7
-
<p>{handle} {pubRkey}</p>
···
0
0
0
0
0
0
0
+31
-5
src/routes/explore/+page.svelte
···
1
<script lang="ts">
2
import { Debounced } from "runed";
3
-
import type { PublicationNode } from '$lib/utils';
4
import { createInfiniteQuery } from '@tanstack/svelte-query';
5
import PublicationCard from '$lib/components/PublicationCard.svelte';
6
···
23
actorHandle: { contains: "${debouncedSearchTerm.current}" }
24
}]
25
}`}) {
26
-
edges {}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
27
pageInfo {
28
hasNextPage
29
endCursor
···
31
}
32
}
33
`;
34
-
const data = await atclient.publicQuery(query);
0
0
0
0
0
0
35
return data as {
36
siteStandardPublication: {
37
-
edges: { node: PublicationNode, cursor: string }[],
0
0
0
38
pageInfo: {
39
hasNextPage: boolean;
40
endCursor: string;
···
98
{#if currentPage?.length === 0}
99
There are no publications based onb the current filters
100
{/if}
101
-
{#each currentPage as publication (publication.uri)}
102
<PublicationCard {publication} />
103
{/each}
104
{/if}
···
1
<script lang="ts">
2
import { Debounced } from "runed";
3
+
import type { PublicationNode, SubscriptionNode } from '$lib/utils';
4
import { createInfiniteQuery } from '@tanstack/svelte-query';
5
import PublicationCard from '$lib/components/PublicationCard.svelte';
6
···
23
actorHandle: { contains: "${debouncedSearchTerm.current}" }
24
}]
25
}`}) {
26
+
edges {
27
+
node {
28
+
viewerSiteStandardGraphSubscriptionViaPublication {}
29
+
uri
30
+
indexedAt
31
+
cid
32
+
did
33
+
url
34
+
name
35
+
description
36
+
icon {}
37
+
actorHandle
38
+
preferences {
39
+
showInDiscover
40
+
}
41
+
basicTheme {}
42
+
}
43
+
}
44
pageInfo {
45
hasNextPage
46
endCursor
···
48
}
49
}
50
`;
51
+
let data;
52
+
if (user) {
53
+
data = await atclient.query(query);
54
+
}
55
+
else {
56
+
data = await atclient.publicQuery(query);
57
+
}
58
return data as {
59
siteStandardPublication: {
60
+
edges: {
61
+
node: PublicationNode & { viewerSiteStandardGraphSubscriptionViaPublication: SubscriptionNode | null},
62
+
cursor: string
63
+
}[],
64
pageInfo: {
65
hasNextPage: boolean;
66
endCursor: string;
···
124
{#if currentPage?.length === 0}
125
There are no publications based onb the current filters
126
{/if}
127
+
{#each currentPage as publication, i (i)}
128
<PublicationCard {publication} />
129
{/each}
130
{/if}
+19
-5
src/routes/home/+page.svelte
···
1
<script lang="ts">
2
-
import { Debounced } from "runed";
3
import { goto } from "$app/navigation";
4
import { getContext, onMount } from "svelte";
5
import type { MiniDoc, PublicationNode } from "$lib/utils";
···
28
}) {
29
edges {
30
node {
31
-
publicationResolved {}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
32
}
33
}
34
pageInfo {
···
39
}
40
`;
41
42
-
const data = await atclient.publicQuery(query);
43
return data as {
44
siteStandardGraphSubscription: {
45
-
edges: { node: { publicationResolved: PublicationNode }}[],
46
pageInfo: {
47
hasNextPage: boolean;
48
endCursor: string;
···
54
getNextPageParam: (lastPage) => lastPage.siteStandardGraphSubscription.pageInfo.endCursor,
55
select: (data) => {
56
const items = data.pages.map((page) => page.siteStandardGraphSubscription.edges).flat();
57
-
const nodes = items.map((i) => i.node.publicationResolved);
58
return nodes;
59
}
60
}));
···
1
<script lang="ts">
0
2
import { goto } from "$app/navigation";
3
import { getContext, onMount } from "svelte";
4
import type { MiniDoc, PublicationNode } from "$lib/utils";
···
27
}) {
28
edges {
29
node {
30
+
uri
31
+
publicationResolved {
32
+
uri
33
+
indexedAt
34
+
cid
35
+
did
36
+
url
37
+
name
38
+
description
39
+
icon {}
40
+
actorHandle
41
+
preferences {
42
+
showInDiscover
43
+
}
44
+
basicTheme {}
45
+
}
46
}
47
}
48
pageInfo {
···
53
}
54
`;
55
56
+
const data = await atclient.query(query);
57
return data as {
58
siteStandardGraphSubscription: {
59
+
edges: { node: { uri: string, publicationResolved: PublicationNode }}[],
60
pageInfo: {
61
hasNextPage: boolean;
62
endCursor: string;
···
68
getNextPageParam: (lastPage) => lastPage.siteStandardGraphSubscription.pageInfo.endCursor,
69
select: (data) => {
70
const items = data.pages.map((page) => page.siteStandardGraphSubscription.edges).flat();
71
+
const nodes = items.map((i) => { return { ...(i.node.publicationResolved), viewerSiteStandardGraphSubscriptionViaPublication: { uri: i.node.uri }} });
72
return nodes;
73
}
74
}));