tangled
alpha
login
or
join now
tynanpurdy.com
/
atprofile
3
fork
atom
Fork of atp.tools as a universal profile for people on the ATmosphere
3
fork
atom
overview
issues
pulls
pipelines
add a bit of microcosm
Natalie B.
1 year ago
aaa1af4e
79321e12
+717
-83
12 changed files
expand all
collapse all
unified
split
src
components
allBacklinksViewer.tsx
rnfgrertt
resultsView.tsx
views
app-bsky
feedLike.tsx
feedPost.tsx
feedRepost.tsx
lib
tid.ts
routeTree.gen.ts
routes
at:
$handle
$collection.$rkey.lazy.tsx
$collection.index.lazy.tsx
$handle.index.tsx
constellation
dids.$collection.tsx
links.$collection.tsx
+186
src/components/allBacklinksViewer.tsx
···
1
1
+
import { useEffect, useState } from "react";
2
2
+
import {
3
3
+
Card,
4
4
+
CardContent,
5
5
+
CardDescription,
6
6
+
CardHeader,
7
7
+
CardTitle,
8
8
+
} from "@/components/ui/card";
9
9
+
import { Skeleton } from "@/components/ui/skeleton";
10
10
+
import { Link } from "@tanstack/react-router";
11
11
+
12
12
+
interface LinkData {
13
13
+
links: {
14
14
+
[key: string]: {
15
15
+
[key: string]: {
16
16
+
records: number;
17
17
+
distinct_dids: number;
18
18
+
};
19
19
+
};
20
20
+
};
21
21
+
}
22
22
+
23
23
+
export function AllBacklinksViewer({ aturi }: { aturi: string }) {
24
24
+
const [data, setData] = useState<LinkData | null>(null);
25
25
+
const [loading, setLoading] = useState(true);
26
26
+
const [error, setError] = useState<string | null>(null);
27
27
+
28
28
+
useEffect(() => {
29
29
+
const fetchData = async () => {
30
30
+
try {
31
31
+
const response = await fetch(
32
32
+
`https://constellation.microcosm.blue/links/all?target=${encodeURIComponent(aturi)}`,
33
33
+
);
34
34
+
const jsonData = await response.json();
35
35
+
setData(jsonData);
36
36
+
setLoading(false);
37
37
+
} catch (err) {
38
38
+
setError("Error fetching data");
39
39
+
setLoading(false);
40
40
+
}
41
41
+
};
42
42
+
43
43
+
fetchData();
44
44
+
}, []);
45
45
+
46
46
+
if (loading) {
47
47
+
return (
48
48
+
<>
49
49
+
<h2 className="text-xl font-bold">Backlinks</h2>
50
50
+
<div className="grid gap-4 mt-4 md:grid-cols-1 lg:grid-cols-2">
51
51
+
{[...Array(6)].map((_, i) => (
52
52
+
<Card key={i}>
53
53
+
<CardHeader>
54
54
+
<Skeleton className="h-4 w-[250px]" />
55
55
+
<Skeleton className="h-4 w-[200px]" />
56
56
+
</CardHeader>
57
57
+
<CardContent>
58
58
+
<Skeleton className="h-20 w-full" />
59
59
+
</CardContent>
60
60
+
</Card>
61
61
+
))}
62
62
+
</div>
63
63
+
</>
64
64
+
);
65
65
+
}
66
66
+
67
67
+
if (error) {
68
68
+
return (
69
69
+
<Card className="m-4">
70
70
+
<CardHeader>
71
71
+
<CardTitle className="text-red-500">Error</CardTitle>
72
72
+
<CardDescription>{error}</CardDescription>
73
73
+
</CardHeader>
74
74
+
</Card>
75
75
+
);
76
76
+
}
77
77
+
78
78
+
if (!data) return null;
79
79
+
80
80
+
return (
81
81
+
<>
82
82
+
<h2 className="text-2xl pt-6 font-semibold leading-3">Backlinks</h2>
83
83
+
<div className="text-lg text-muted-foreground">
84
84
+
Interaction Statistics from{" "}
85
85
+
<a
86
86
+
className="text-blue-500 hover:underline"
87
87
+
href="https://constellation.microcosm.blue/"
88
88
+
target="_blank"
89
89
+
rel="noopener noreferrer"
90
90
+
>
91
91
+
microcosm constellation
92
92
+
</a>
93
93
+
</div>
94
94
+
<div className="grid gap-4 md:grid-cols-1 lg:grid-cols-2 leading-snug">
95
95
+
{Object.entries(data.links).map(([category, stats]) => (
96
96
+
<Card key={category} className="flex flex-col">
97
97
+
<CardContent className="flex-1 mt-4">
98
98
+
<CardTitle className="mb-2">
99
99
+
{formatCategoryName(category)}
100
100
+
</CardTitle>
101
101
+
<div className="space-y-4">
102
102
+
{Object.entries(stats).map(([stat, values]) => (
103
103
+
<div key={stat} className="space-y-2">
104
104
+
<h4 className="font-medium text-sm text-muted-foreground">
105
105
+
{formatStatName(stat)}
106
106
+
</h4>
107
107
+
<div className="grid grid-cols-2 gap-2 text-sm">
108
108
+
<Link
109
109
+
to={"/constellation/links/$collection"}
110
110
+
params={{
111
111
+
collection: category,
112
112
+
}}
113
113
+
search={{
114
114
+
path: stat,
115
115
+
target: aturi,
116
116
+
}}
117
117
+
className="flex justify-between text-blue-700 dark:text-blue-300"
118
118
+
>
119
119
+
<span>Records:</span>
120
120
+
<span>
121
121
+
<span className="font-medium">{values.records}</span>
122
122
+
<span className="border-l w-0 ml-2" />
123
123
+
</span>
124
124
+
</Link>
125
125
+
<div className="flex justify-between">
126
126
+
<Link
127
127
+
to={"/constellation/dids/$collection"}
128
128
+
params={{
129
129
+
collection: category,
130
130
+
}}
131
131
+
search={{
132
132
+
path: stat,
133
133
+
target: aturi,
134
134
+
}}
135
135
+
className="flex justify-between w-full text-blue-700 dark:text-blue-300"
136
136
+
>
137
137
+
<span>Distinct DIDs:</span>
138
138
+
<span className="font-medium">
139
139
+
{values.distinct_dids}
140
140
+
</span>
141
141
+
</Link>
142
142
+
</div>
143
143
+
</div>
144
144
+
</div>
145
145
+
))}
146
146
+
</div>
147
147
+
</CardContent>
148
148
+
</Card>
149
149
+
))}
150
150
+
{Object.entries(data.links).length == 0 && (
151
151
+
<div className="flex flex-col items-start justify-start">
152
152
+
<p className="text-muted-foreground w-max">
153
153
+
Nothing doing! No links indexed for this target!
154
154
+
</p>
155
155
+
<span>
156
156
+
You can{" "}
157
157
+
<a
158
158
+
href={`https://constellation.microcosm.blue/links/all?target=${encodeURIComponent(aturi)}`}
159
159
+
className="text-blue-500 hover:underline"
160
160
+
>
161
161
+
check for updates here
162
162
+
</a>
163
163
+
.
164
164
+
</span>
165
165
+
</div>
166
166
+
)}
167
167
+
</div>
168
168
+
</>
169
169
+
);
170
170
+
}
171
171
+
172
172
+
// Helper function to format category names
173
173
+
const formatCategoryName = (name: string) => {
174
174
+
return name
175
175
+
.split(".")
176
176
+
.pop()
177
177
+
?.replace(/([A-Z])/g, " $1")
178
178
+
.trim();
179
179
+
};
180
180
+
181
181
+
// Helper function to format stat names
182
182
+
const formatStatName = (name: string) => {
183
183
+
return name.split(".").filter(Boolean).join(" → ");
184
184
+
};
185
185
+
186
186
+
export default AllBacklinksViewer;
+5
-11
src/components/rnfgrertt/resultsView.tsx
···
1
1
-
import {
2
2
-
Camera,
3
3
-
Check,
4
4
-
Clipboard,
5
5
-
Loader2,
6
6
-
RefreshCw,
7
7
-
} from "lucide-react";
1
1
+
import { Camera, Check, Clipboard, Loader2, RefreshCw } from "lucide-react";
8
2
import { useState, useRef, useEffect, useContext } from "preact/hooks";
9
3
import {
10
4
CartesianGrid,
···
120
114
"com.atproto.repo.putRecord",
121
115
{
122
116
data: {
123
123
-
rkey: generateTid(),
117
117
+
rkey: generateTid().toString(),
124
118
repo: qt.currentAgent.sub,
125
119
record: result,
126
120
collection: "tools.atp.typing.test",
···
169
163
);
170
164
}
171
165
172
172
-
export const ResultsView = ({
166
166
+
export function ResultsView({
173
167
stats,
174
168
wpmData,
175
169
resetTest,
···
183
177
textData: string | TextMeta;
184
178
testConfig: TestConfig;
185
179
userInput: string;
186
186
-
}) => {
180
180
+
}) {
187
181
const [isSaving, setIsSaving] = useState(false);
188
182
const resultsRef = useRef<HTMLDivElement>(null);
189
183
···
298
292
</div>
299
293
</div>
300
294
);
301
301
-
};
295
295
+
}
302
296
303
297
const StatBox = ({
304
298
label,
+6
-1
src/components/views/app-bsky/feedLike.tsx
···
19
19
stroke="#ba5678"
20
20
className="inline mb-0.5 mr-1"
21
21
/>{" "}
22
22
-
{repoData?.handle} liked
22
22
+
<span className="inline-flex">
23
23
+
<span className="max-w-64 lg:max-w-xl w-min pr-1 inline overflow-hidden text-ellipsis whitespace-nowrap">
24
24
+
{repoData?.handle}
25
25
+
</span>{" "}
26
26
+
liked
27
27
+
</span>
23
28
</p>
24
29
<BlueskyPostWithoutEmbed showEmbeddedPost uri={post.subject.uri} />
25
30
<div className="mt-4 text-muted-foreground text-sm flex align-center gap-2 border p-2 rounded-lg">
+12
-4
src/components/views/app-bsky/feedPost.tsx
···
21
21
return (
22
22
<div className="border p-6 py-3 rounded-md">
23
23
{post.reply?.root && post.reply?.root.uri !== post.reply.parent.uri && (
24
24
-
<div className="text-sm text-muted-foreground">
24
24
+
<div className="text-sm text-muted-foreground max-w-min">
25
25
Root post:{" "}
26
26
-
<Link to={"/" + post.reply?.root.uri}>{post.reply?.root.uri}</Link>
26
26
+
<Link
27
27
+
className="overflow-hidden text-ellipsis whitespace-nowrap block"
28
28
+
to={"/" + post.reply?.root.uri}
29
29
+
>
30
30
+
{post.reply?.root.uri}
31
31
+
</Link>
27
32
</div>
28
33
)}
29
34
{post.reply?.parent && (
30
30
-
<div className="text-sm text-muted-foreground mb-3">
35
35
+
<div className="text-sm text-muted-foreground mb-3 max-w-full">
31
36
Parent post:{" "}
32
32
-
<Link to={"/" + post.reply?.parent.uri}>
37
37
+
<Link
38
38
+
to={"/" + post.reply?.parent.uri}
39
39
+
className="overflow-hidden text-ellipsis whitespace-nowrap block"
40
40
+
>
33
41
{post.reply?.parent.uri}
34
42
</Link>
35
43
</div>
+7
-1
src/components/views/app-bsky/feedRepost.tsx
···
14
14
<>
15
15
<p className="py-1 text-muted-foreground">
16
16
{" "}
17
17
-
<Repeat2 className="inline mb-0.5 mr-1" /> {repoData?.handle} reposted{" "}
17
17
+
<Repeat2 className="inline mb-0.5 mr-1" />{" "}
18
18
+
<span className="inline-flex">
19
19
+
<span className="max-w-64 lg:max-w-xl w-min pr-1 inline overflow-hidden text-ellipsis whitespace-nowrap">
20
20
+
{repoData?.handle}
21
21
+
</span>{" "}
22
22
+
reposted
23
23
+
</span>
18
24
</p>
19
25
<BlueskyPostWithoutEmbed showEmbeddedPost uri={post.subject.uri} />
20
26
</>
+44
-54
src/lib/tid.ts
···
1
1
-
export const createRfc4648Encode = (
2
2
-
alphabet: string,
3
3
-
bitsPerChar: number,
4
4
-
pad: boolean,
5
5
-
) => {
6
6
-
return (bytes: Uint8Array): string => {
7
7
-
const mask = (1 << bitsPerChar) - 1;
8
8
-
let str = "";
1
1
+
/**
2
2
+
* TID (Transaction ID) implementation for ATProto
3
3
+
* Based on the original Go implementation from github.com/bluesky-social/indigo
4
4
+
*/
9
5
10
10
-
let bits = 0; // Number of bits currently in the buffer
11
11
-
let buffer = 0; // Bits waiting to be written out, MSB first
12
12
-
for (let i = 0; i < bytes.length; ++i) {
13
13
-
// Slurp data into the buffer:
14
14
-
buffer = (buffer << 8) | bytes[i];
15
15
-
bits += 8;
6
6
+
// Base32 alphabet used for sorting
7
7
+
const BASE32_SORT_ALPHABET = "234567abcdefghijklmnopqrstuvwxyz";
16
8
17
17
-
// Write out as much as we can:
18
18
-
while (bits > bitsPerChar) {
19
19
-
bits -= bitsPerChar;
20
20
-
str += alphabet[mask & (buffer >> bits)];
21
21
-
}
22
22
-
}
9
9
+
// Constants for bit operations
10
10
+
const CLOCK_ID_MASK = 0x3ff;
11
11
+
const MICROS_MASK = 0x1ffffffffffffn;
12
12
+
const INTEGER_MASK = 0x7fffffffffffffffn;
23
13
24
24
-
// Partial character:
25
25
-
if (bits !== 0) {
26
26
-
str += alphabet[mask & (buffer << (bitsPerChar - bits))];
27
27
-
}
14
14
+
class TransactionId {
15
15
+
private readonly value: string;
28
16
29
29
-
// Add padding characters until we hit a byte boundary:
30
30
-
if (pad) {
31
31
-
while (((str.length * bitsPerChar) & 7) !== 0) {
32
32
-
str += "=";
33
33
-
}
34
34
-
}
17
17
+
constructor(value: string) {
18
18
+
this.value = value;
19
19
+
}
35
20
36
36
-
return str;
37
37
-
};
38
38
-
};
21
21
+
toString(): string {
22
22
+
return this.value;
23
23
+
}
24
24
+
25
25
+
static create(unixMicros: bigint, clockId: number): TransactionId {
26
26
+
const clockIdBig = BigInt(clockId & CLOCK_ID_MASK);
27
27
+
const v = ((unixMicros & MICROS_MASK) << 10n) | clockIdBig;
28
28
+
return TransactionId.fromInteger(v);
29
29
+
}
30
30
+
31
31
+
static createNow(clockId: number): TransactionId {
32
32
+
const nowMicros = BigInt(Date.now()) * 1000n; // Convert ms to μs
33
33
+
return TransactionId.create(nowMicros, clockId);
34
34
+
}
39
35
40
40
-
const BASE32_SORTABLE_CHARSET = "234567abcdefghijklmnopqrstuvwxyz";
36
36
+
private static fromInteger(value: bigint): TransactionId {
37
37
+
value = INTEGER_MASK & value;
38
38
+
let result = "";
41
39
42
42
-
export const toBase32Sortable = createRfc4648Encode(
43
43
-
BASE32_SORTABLE_CHARSET,
44
44
-
5,
45
45
-
false,
46
46
-
);
40
40
+
for (let i = 0; i < 13; i++) {
41
41
+
result = BASE32_SORT_ALPHABET[Number(value & 0x1fn)] + result;
42
42
+
value = value >> 5n;
43
43
+
}
47
44
48
48
-
function intToArray(i: number) {
49
49
-
return Uint8Array.of(
50
50
-
(i & 0xff000000) >> 24,
51
51
-
(i & 0x00ff0000) >> 16,
52
52
-
(i & 0x0000ff00) >> 8,
53
53
-
(i & 0x000000ff) >> 0,
54
54
-
);
45
45
+
return new TransactionId(result);
46
46
+
}
55
47
}
56
48
57
57
-
/*
58
58
-
* Generates an ATProto TID using the current timestamp.
59
59
-
* Encoded as b32-sortable.
49
49
+
/**
50
50
+
* Generates a new Transaction ID with a random clock ID
51
51
+
* @returns TransactionId
60
52
*/
61
61
-
export function generateTid() {
62
62
-
let ms = new Date().getMilliseconds();
63
63
-
let bytes = intToArray(ms);
64
64
-
65
65
-
return toBase32Sortable(bytes);
53
53
+
export function generateTid(): TransactionId {
54
54
+
const clockId = Math.floor(Math.random() * 64 + 512);
55
55
+
return TransactionId.createNow(clockId);
66
56
}
+54
src/routeTree.gen.ts
···
14
14
15
15
import { Route as rootRoute } from './routes/__root'
16
16
import { Route as AtHandleIndexImport } from './routes/at:/$handle.index'
17
17
+
import { Route as ConstellationLinksCollectionImport } from './routes/constellation/links.$collection'
18
18
+
import { Route as ConstellationDidsCollectionImport } from './routes/constellation/dids.$collection'
17
19
18
20
// Create Virtual Routes
19
21
···
101
103
getParentRoute: () => rootRoute,
102
104
} as any)
103
105
106
106
+
const ConstellationLinksCollectionRoute =
107
107
+
ConstellationLinksCollectionImport.update({
108
108
+
id: '/constellation/links/$collection',
109
109
+
path: '/constellation/links/$collection',
110
110
+
getParentRoute: () => rootRoute,
111
111
+
} as any)
112
112
+
113
113
+
const ConstellationDidsCollectionRoute =
114
114
+
ConstellationDidsCollectionImport.update({
115
115
+
id: '/constellation/dids/$collection',
116
116
+
path: '/constellation/dids/$collection',
117
117
+
getParentRoute: () => rootRoute,
118
118
+
} as any)
119
119
+
104
120
const AtHandleCollectionIndexLazyRoute =
105
121
AtHandleCollectionIndexLazyImport.update({
106
122
id: '/at:/$handle/$collection/',
···
180
196
preLoaderRoute: typeof RnfgrerttIndexLazyImport
181
197
parentRoute: typeof rootRoute
182
198
}
199
199
+
'/constellation/dids/$collection': {
200
200
+
id: '/constellation/dids/$collection'
201
201
+
path: '/constellation/dids/$collection'
202
202
+
fullPath: '/constellation/dids/$collection'
203
203
+
preLoaderRoute: typeof ConstellationDidsCollectionImport
204
204
+
parentRoute: typeof rootRoute
205
205
+
}
206
206
+
'/constellation/links/$collection': {
207
207
+
id: '/constellation/links/$collection'
208
208
+
path: '/constellation/links/$collection'
209
209
+
fullPath: '/constellation/links/$collection'
210
210
+
preLoaderRoute: typeof ConstellationLinksCollectionImport
211
211
+
parentRoute: typeof rootRoute
212
212
+
}
183
213
'/at:/$handle/': {
184
214
id: '/at:/$handle/'
185
215
path: '/at:/$handle'
···
222
252
'/auth/login': typeof AuthLoginLazyRoute
223
253
'/rnfgrertt/typing': typeof RnfgrerttTypingLazyRoute
224
254
'/rnfgrertt': typeof RnfgrerttIndexLazyRoute
255
255
+
'/constellation/dids/$collection': typeof ConstellationDidsCollectionRoute
256
256
+
'/constellation/links/$collection': typeof ConstellationLinksCollectionRoute
225
257
'/at:/$handle': typeof AtHandleIndexRoute
226
258
'/pds/$url': typeof PdsUrlIndexLazyRoute
227
259
'/at:/$handle/$collection/$rkey': typeof AtHandleCollectionRkeyLazyRoute
···
237
269
'/auth/login': typeof AuthLoginLazyRoute
238
270
'/rnfgrertt/typing': typeof RnfgrerttTypingLazyRoute
239
271
'/rnfgrertt': typeof RnfgrerttIndexLazyRoute
272
272
+
'/constellation/dids/$collection': typeof ConstellationDidsCollectionRoute
273
273
+
'/constellation/links/$collection': typeof ConstellationLinksCollectionRoute
240
274
'/at:/$handle': typeof AtHandleIndexRoute
241
275
'/pds/$url': typeof PdsUrlIndexLazyRoute
242
276
'/at:/$handle/$collection/$rkey': typeof AtHandleCollectionRkeyLazyRoute
···
253
287
'/auth/login': typeof AuthLoginLazyRoute
254
288
'/rnfgrertt/typing': typeof RnfgrerttTypingLazyRoute
255
289
'/rnfgrertt/': typeof RnfgrerttIndexLazyRoute
290
290
+
'/constellation/dids/$collection': typeof ConstellationDidsCollectionRoute
291
291
+
'/constellation/links/$collection': typeof ConstellationLinksCollectionRoute
256
292
'/at:/$handle/': typeof AtHandleIndexRoute
257
293
'/pds/$url/': typeof PdsUrlIndexLazyRoute
258
294
'/at:/$handle/$collection/$rkey': typeof AtHandleCollectionRkeyLazyRoute
···
270
306
| '/auth/login'
271
307
| '/rnfgrertt/typing'
272
308
| '/rnfgrertt'
309
309
+
| '/constellation/dids/$collection'
310
310
+
| '/constellation/links/$collection'
273
311
| '/at:/$handle'
274
312
| '/pds/$url'
275
313
| '/at:/$handle/$collection/$rkey'
···
284
322
| '/auth/login'
285
323
| '/rnfgrertt/typing'
286
324
| '/rnfgrertt'
325
325
+
| '/constellation/dids/$collection'
326
326
+
| '/constellation/links/$collection'
287
327
| '/at:/$handle'
288
328
| '/pds/$url'
289
329
| '/at:/$handle/$collection/$rkey'
···
298
338
| '/auth/login'
299
339
| '/rnfgrertt/typing'
300
340
| '/rnfgrertt/'
341
341
+
| '/constellation/dids/$collection'
342
342
+
| '/constellation/links/$collection'
301
343
| '/at:/$handle/'
302
344
| '/pds/$url/'
303
345
| '/at:/$handle/$collection/$rkey'
···
314
356
AuthLoginLazyRoute: typeof AuthLoginLazyRoute
315
357
RnfgrerttTypingLazyRoute: typeof RnfgrerttTypingLazyRoute
316
358
RnfgrerttIndexLazyRoute: typeof RnfgrerttIndexLazyRoute
359
359
+
ConstellationDidsCollectionRoute: typeof ConstellationDidsCollectionRoute
360
360
+
ConstellationLinksCollectionRoute: typeof ConstellationLinksCollectionRoute
317
361
AtHandleIndexRoute: typeof AtHandleIndexRoute
318
362
PdsUrlIndexLazyRoute: typeof PdsUrlIndexLazyRoute
319
363
AtHandleCollectionRkeyLazyRoute: typeof AtHandleCollectionRkeyLazyRoute
···
329
373
AuthLoginLazyRoute: AuthLoginLazyRoute,
330
374
RnfgrerttTypingLazyRoute: RnfgrerttTypingLazyRoute,
331
375
RnfgrerttIndexLazyRoute: RnfgrerttIndexLazyRoute,
376
376
+
ConstellationDidsCollectionRoute: ConstellationDidsCollectionRoute,
377
377
+
ConstellationLinksCollectionRoute: ConstellationLinksCollectionRoute,
332
378
AtHandleIndexRoute: AtHandleIndexRoute,
333
379
PdsUrlIndexLazyRoute: PdsUrlIndexLazyRoute,
334
380
AtHandleCollectionRkeyLazyRoute: AtHandleCollectionRkeyLazyRoute,
···
353
399
"/auth/login",
354
400
"/rnfgrertt/typing",
355
401
"/rnfgrertt/",
402
402
+
"/constellation/dids/$collection",
403
403
+
"/constellation/links/$collection",
356
404
"/at:/$handle/",
357
405
"/pds/$url/",
358
406
"/at:/$handle/$collection/$rkey",
···
382
430
},
383
431
"/rnfgrertt/": {
384
432
"filePath": "rnfgrertt/index.lazy.tsx"
433
433
+
},
434
434
+
"/constellation/dids/$collection": {
435
435
+
"filePath": "constellation/dids.$collection.tsx"
436
436
+
},
437
437
+
"/constellation/links/$collection": {
438
438
+
"filePath": "constellation/links.$collection.tsx"
385
439
},
386
440
"/at:/$handle/": {
387
441
"filePath": "at:/$handle.index.tsx"
+9
-7
src/routes/at:/$handle.index.tsx
···
130
130
}
131
131
132
132
return (
133
133
-
<div className="flex flex-row justify-center w-full">
134
134
-
<div className="max-w-2xl w-screen p-4 md:mt-16 space-y-2">
133
133
+
<div className="flex flex-row justify-center w-full max-h-[calc(100vh-5rem)]">
134
134
+
<div className="max-w-md lg:max-w-2xl w-[90vw] mx-4 md:mt-16 space-y-2">
135
135
{blueSkyData ? (
136
136
blueSkyData?.banner ? (
137
137
<div className="relative mb-12 md:mb-16">
···
202
202
</div>
203
203
<div className="pt-2">
204
204
<h2 className="text-xl font-bold">DID Document</h2>
205
205
-
<RenderJson
206
206
-
data={didDoc}
207
207
-
did={identity?.identity.id!}
208
208
-
pds={identity?.identity.pds.toString()!}
209
209
-
/>
205
205
+
<div className="w-full overflow-x-auto">
206
206
+
<RenderJson
207
207
+
data={didDoc}
208
208
+
did={identity?.identity.id!}
209
209
+
pds={identity?.identity.pds.toString()!}
210
210
+
/>
211
211
+
</div>
210
212
</div>
211
213
</div>
212
214
</div>
+5
-3
src/routes/at:/$handle/$collection.$rkey.lazy.tsx
···
1
1
+
import AllBacklinksViewer from "@/components/allBacklinksViewer";
1
2
import ShowError from "@/components/error";
2
3
import { RenderJson } from "@/components/renderJson";
3
4
import { SplitText } from "@/components/segmentedText";
···
124
125
const View = getView((data.value as any).$type);
125
126
126
127
return (
127
127
-
<div className="flex flex-row justify-center w-full min-h-screen">
128
128
-
<div className="max-w-2xl w-screen p-4 md:mt-16 space-y-2">
128
128
+
<div className="flex flex-row justify-center w-full min-h-[calc(100vh-5rem)] max-w-[100vw]">
129
129
+
<div className="max-w-md lg:max-w-2xl w-[90vw] mx-4 md:mt-16 space-y-2">
129
130
<Link
130
131
to={`/at:/$handle`}
131
132
params={{
···
134
135
className=""
135
136
>
136
137
<div>
137
137
-
<h1 className="text-2xl md:text-3xl text-muted-foreground font-normal">
138
138
+
<h1 className="text-2xl md:text-3xl max-w-xs lg:max-w-2xl overflow-hidden text-ellipsis whitespace-nowrap text-muted-foreground font-normal">
138
139
@{repoInfo?.handle}
139
140
{repoInfo?.handleIsCorrect ? "" : " (invalid handle)"}
140
141
</h1>
···
225
226
</div>
226
227
</TabsContent>
227
228
</Tabs>
229
229
+
<AllBacklinksViewer aturi={`at://${handle}/${collection}/${rkey}`} />
228
230
</div>
229
231
</div>
230
232
);
+2
-2
src/routes/at:/$handle/$collection.index.lazy.tsx
···
120
120
}
121
121
122
122
return (
123
123
-
<div className="flex flex-row justify-center w-full min-h-screen">
124
124
-
<div className="max-w-2xl w-screen p-4 md:mt-16 space-y-2">
123
123
+
<div className="flex flex-row justify-center w-full min-h-[calc(100vh-5rem)]">
124
124
+
<div className="max-w-md lg:max-w-2xl w-[90vw] mx-4 md:mt-16 space-y-2">
125
125
<Link
126
126
to="/at:/$handle"
127
127
params={{ handle: identity?.raw ?? "" }}
+188
src/routes/constellation/dids.$collection.tsx
···
1
1
+
import ShowError from "@/components/error";
2
2
+
import { Loader } from "@/components/ui/loader";
3
3
+
import {
4
4
+
createFileRoute,
5
5
+
Link,
6
6
+
useParams,
7
7
+
useSearch,
8
8
+
} from "@tanstack/react-router";
9
9
+
import { useEffect, useRef, useState } from "preact/hooks";
10
10
+
11
11
+
export const Route = createFileRoute("/constellation/dids/$collection")({
12
12
+
component: RouteComponent,
13
13
+
validateSearch: (search: Record<string, unknown>) => {
14
14
+
console.log(search);
15
15
+
return {
16
16
+
target: String(search.target) || "",
17
17
+
path: String(search.path) || "",
18
18
+
};
19
19
+
},
20
20
+
});
21
21
+
22
22
+
interface ConstellationLinkState {
23
23
+
totalLinks: number;
24
24
+
links: string[];
25
25
+
cursor: string | null;
26
26
+
error: Error | null;
27
27
+
isLoading: boolean;
28
28
+
}
29
29
+
30
30
+
function useConstellationLink(
31
31
+
collection: string,
32
32
+
target: string,
33
33
+
path: string,
34
34
+
) {
35
35
+
const [link, setLink] = useState<ConstellationLinkState>({
36
36
+
totalLinks: 0,
37
37
+
links: [],
38
38
+
cursor: null,
39
39
+
error: null,
40
40
+
isLoading: true,
41
41
+
});
42
42
+
43
43
+
const fetchLink = async (cursor?: string) => {
44
44
+
// check for missing parameters
45
45
+
if (!collection || !target || !path) {
46
46
+
let missingParams = [];
47
47
+
for (let param in [collection, target, path]) {
48
48
+
if (!param) missingParams.push(param);
49
49
+
}
50
50
+
if (missingParams.length > 0) {
51
51
+
setLink({
52
52
+
...link,
53
53
+
error: new Error("Missing parameters: "),
54
54
+
isLoading: false,
55
55
+
});
56
56
+
return;
57
57
+
}
58
58
+
}
59
59
+
60
60
+
let response = await fetch(
61
61
+
`https://constellation.microcosm.blue/links/distinct-dids?target=${target}&collection=${collection}&path=${path}${cursor ? `&cursor=${cursor}` : ""}`,
62
62
+
);
63
63
+
64
64
+
let data = await response.json();
65
65
+
setLink((prev) => ({
66
66
+
...prev,
67
67
+
totalLinks: data.total,
68
68
+
links: [...(prev.links || []), ...data.linking_dids],
69
69
+
cursor: data.cursor,
70
70
+
error: data.error,
71
71
+
isLoading: false,
72
72
+
}));
73
73
+
};
74
74
+
75
75
+
useEffect(() => {
76
76
+
fetchLink();
77
77
+
}, [collection, target, path]);
78
78
+
79
79
+
return {
80
80
+
totalLinks: link.totalLinks,
81
81
+
links: link.links,
82
82
+
cursor: link.cursor,
83
83
+
error: link.error,
84
84
+
isLoading: link.isLoading,
85
85
+
fetchMore: async (cursor?: string) => {
86
86
+
if (!cursor) return;
87
87
+
await fetchLink(cursor);
88
88
+
},
89
89
+
};
90
90
+
}
91
91
+
92
92
+
function RouteComponent() {
93
93
+
// get route params
94
94
+
const { collection } = useParams({
95
95
+
from: "/constellation/dids/$collection",
96
96
+
});
97
97
+
// get query params
98
98
+
const params = useSearch({
99
99
+
from: "/constellation/dids/$collection",
100
100
+
});
101
101
+
102
102
+
const state = useConstellationLink(collection, params.target, params.path);
103
103
+
104
104
+
const loaderRef = useRef<HTMLDivElement>(null);
105
105
+
106
106
+
const { links, cursor, error, isLoading, fetchMore } = state;
107
107
+
108
108
+
useEffect(() => {
109
109
+
if (!loaderRef.current) return;
110
110
+
111
111
+
const observer = new IntersectionObserver(
112
112
+
(entries) => {
113
113
+
const target = entries[0];
114
114
+
if (target.isIntersecting && !isLoading && cursor) {
115
115
+
fetchMore(cursor);
116
116
+
}
117
117
+
},
118
118
+
{ threshold: 0.1, rootMargin: "50px" },
119
119
+
);
120
120
+
121
121
+
observer.observe(loaderRef.current);
122
122
+
return () => observer.disconnect();
123
123
+
}, [cursor, isLoading, fetchMore]);
124
124
+
125
125
+
if (error) {
126
126
+
return <ShowError error={error} />;
127
127
+
}
128
128
+
129
129
+
if (isLoading && !links.length) {
130
130
+
return <Loader />;
131
131
+
}
132
132
+
const splitColl = params.target.split("/");
133
133
+
return (
134
134
+
<div className="flex flex-row justify-center w-full min-h-screen">
135
135
+
<div className="max-w-md lg:max-w-2xl w-[90vw] mx-4 md:mt-16 space-y-2">
136
136
+
<h1 className="text-3xl font-bold">Links</h1>
137
137
+
<div className="text-muted-foreground flex-inline">
138
138
+
<span>View all dids mentioning </span>
139
139
+
<span className="flex">
140
140
+
<span>at://</span>
141
141
+
<span className="inline-block max-w-full overflow-hidden text-ellipsis whitespace-nowrap">
142
142
+
{splitColl[2]}
143
143
+
</span>
144
144
+
/
145
145
+
<span className="inline-block max-w-full overflow-hidden text-ellipsis whitespace-nowrap">
146
146
+
{splitColl[3]}
147
147
+
</span>
148
148
+
/<span>{splitColl[4]}</span>
149
149
+
</span>
150
150
+
</div>
151
151
+
{links.map((link) => (
152
152
+
<div className="w-min max-w-full">
153
153
+
<Link
154
154
+
key={link}
155
155
+
to="/at:/$handle"
156
156
+
params={{
157
157
+
handle: link,
158
158
+
}}
159
159
+
className="flex text-blue-700 dark:text-blue-300 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
160
160
+
>
161
161
+
at://
162
162
+
<span className="inline-block max-w-full overflow-hidden text-ellipsis whitespace-nowrap">
163
163
+
{link}
164
164
+
</span>
165
165
+
/
166
166
+
</Link>
167
167
+
</div>
168
168
+
))}
169
169
+
170
170
+
<div
171
171
+
ref={loaderRef}
172
172
+
className="flex flex-row justify-center h-10 -pt-16"
173
173
+
>
174
174
+
{isLoading && (
175
175
+
<div className="text-center text-sm text-muted-foreground mx-10">
176
176
+
Loading more...
177
177
+
</div>
178
178
+
)}
179
179
+
{!isLoading && !cursor && (
180
180
+
<div className="text-center text-sm text-muted-foreground mx-10 mt-2">
181
181
+
that's all, folks!
182
182
+
</div>
183
183
+
)}
184
184
+
</div>
185
185
+
</div>
186
186
+
</div>
187
187
+
);
188
188
+
}
+199
src/routes/constellation/links.$collection.tsx
···
1
1
+
import ShowError from "@/components/error";
2
2
+
import { Loader } from "@/components/ui/loader";
3
3
+
import {
4
4
+
createFileRoute,
5
5
+
Link,
6
6
+
useParams,
7
7
+
useSearch,
8
8
+
} from "@tanstack/react-router";
9
9
+
import { useEffect, useRef, useState } from "preact/hooks";
10
10
+
11
11
+
export const Route = createFileRoute("/constellation/links/$collection")({
12
12
+
component: RouteComponent,
13
13
+
validateSearch: (search: Record<string, unknown>) => {
14
14
+
return {
15
15
+
target: String(search.target) || "",
16
16
+
path: String(search.path) || "",
17
17
+
};
18
18
+
},
19
19
+
});
20
20
+
21
21
+
interface CLink {
22
22
+
did: string;
23
23
+
collection: string;
24
24
+
rkey: string;
25
25
+
}
26
26
+
27
27
+
interface ConstellationLinkState {
28
28
+
totalLinks: number;
29
29
+
links: CLink[];
30
30
+
cursor: string | null;
31
31
+
error: Error | null;
32
32
+
isLoading: boolean;
33
33
+
}
34
34
+
35
35
+
function useConstellationLink(
36
36
+
collection: string,
37
37
+
target: string,
38
38
+
path: string,
39
39
+
) {
40
40
+
const [link, setLink] = useState<ConstellationLinkState>({
41
41
+
totalLinks: 0,
42
42
+
links: [],
43
43
+
cursor: null,
44
44
+
error: null,
45
45
+
isLoading: true,
46
46
+
});
47
47
+
48
48
+
const fetchLink = async (cursor?: string) => {
49
49
+
// check for missing parameters
50
50
+
if (!collection || !target || !path) {
51
51
+
let missingParams = [];
52
52
+
for (let param in [collection, target, path]) {
53
53
+
if (!param) missingParams.push(param);
54
54
+
}
55
55
+
if (missingParams.length > 0) {
56
56
+
setLink({
57
57
+
...link,
58
58
+
error: new Error("Missing parameters: "),
59
59
+
isLoading: false,
60
60
+
});
61
61
+
return;
62
62
+
}
63
63
+
}
64
64
+
65
65
+
let response = await fetch(
66
66
+
`https://constellation.microcosm.blue/links?target=${target}&collection=${collection}&path=${path}${cursor ? `&cursor=${cursor}` : ""}`,
67
67
+
);
68
68
+
69
69
+
let data = await response.json();
70
70
+
setLink((prev) => ({
71
71
+
...prev,
72
72
+
totalLinks: data.total,
73
73
+
links: [...(prev.links || []), ...data.linking_records],
74
74
+
cursor: data.cursor,
75
75
+
error: data.error,
76
76
+
isLoading: false,
77
77
+
}));
78
78
+
};
79
79
+
80
80
+
useEffect(() => {
81
81
+
fetchLink();
82
82
+
}, [collection, target, path]);
83
83
+
84
84
+
return {
85
85
+
totalLinks: link.totalLinks,
86
86
+
links: link.links,
87
87
+
cursor: link.cursor,
88
88
+
error: link.error,
89
89
+
isLoading: link.isLoading,
90
90
+
fetchMore: async (cursor?: string) => {
91
91
+
if (!cursor) return;
92
92
+
await fetchLink(cursor);
93
93
+
},
94
94
+
};
95
95
+
}
96
96
+
97
97
+
function RouteComponent() {
98
98
+
// get route params
99
99
+
const { collection } = useParams({
100
100
+
from: "/constellation/links/$collection",
101
101
+
});
102
102
+
// get query params
103
103
+
const params = useSearch({
104
104
+
from: "/constellation/links/$collection",
105
105
+
});
106
106
+
107
107
+
const state = useConstellationLink(collection, params.target, params.path);
108
108
+
109
109
+
const loaderRef = useRef<HTMLDivElement>(null);
110
110
+
111
111
+
const { links, cursor, error, isLoading, fetchMore } = state;
112
112
+
113
113
+
useEffect(() => {
114
114
+
if (!loaderRef.current) return;
115
115
+
116
116
+
const observer = new IntersectionObserver(
117
117
+
(entries) => {
118
118
+
const target = entries[0];
119
119
+
if (target.isIntersecting && !isLoading && cursor) {
120
120
+
fetchMore(cursor);
121
121
+
}
122
122
+
},
123
123
+
{ threshold: 0.1, rootMargin: "50px" },
124
124
+
);
125
125
+
126
126
+
observer.observe(loaderRef.current);
127
127
+
return () => observer.disconnect();
128
128
+
}, [cursor, isLoading, fetchMore]);
129
129
+
130
130
+
if (error) {
131
131
+
return <ShowError error={error} />;
132
132
+
}
133
133
+
134
134
+
if (isLoading && !links.length) {
135
135
+
return <Loader />;
136
136
+
}
137
137
+
const splitColl = params.target.split("/");
138
138
+
return (
139
139
+
<div className="flex flex-row justify-center w-full min-h-screen">
140
140
+
<div className="max-w-md lg:max-w-2xl w-[90vw] mx-4 md:mt-16 space-y-2">
141
141
+
<h1 className="text-3xl font-bold">Links</h1>
142
142
+
<div className="text-muted-foreground flex-inline">
143
143
+
<span>View all links to </span>
144
144
+
<span className="flex">
145
145
+
<span>at://</span>
146
146
+
<span className="inline-block max-w-full overflow-hidden text-ellipsis whitespace-nowrap">
147
147
+
{splitColl[2]}
148
148
+
</span>
149
149
+
/
150
150
+
<span className="inline-block max-w-full overflow-hidden text-ellipsis whitespace-nowrap">
151
151
+
{splitColl[3]}
152
152
+
</span>
153
153
+
/<span>{splitColl[4]}</span>
154
154
+
</span>
155
155
+
</div>
156
156
+
{links.map((link) => (
157
157
+
<div className="w-min max-w-full">
158
158
+
<Link
159
159
+
key={link.rkey + link.did}
160
160
+
to="/at:/$handle/$collection/$rkey"
161
161
+
params={{
162
162
+
handle: link.did,
163
163
+
collection: link.collection,
164
164
+
rkey: link.rkey,
165
165
+
}}
166
166
+
className="flex text-blue-700 dark:text-blue-300 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
167
167
+
>
168
168
+
at://
169
169
+
<span className="inline-block max-w-full overflow-hidden text-ellipsis whitespace-nowrap">
170
170
+
{link.did}
171
171
+
</span>
172
172
+
/
173
173
+
<span className="inline-block max-w-full overflow-hidden text-ellipsis whitespace-nowrap">
174
174
+
{link.collection}
175
175
+
</span>
176
176
+
/<span>{link.rkey}</span>
177
177
+
</Link>
178
178
+
</div>
179
179
+
))}
180
180
+
181
181
+
<div
182
182
+
ref={loaderRef}
183
183
+
className="flex flex-row justify-center h-10 -pt-16"
184
184
+
>
185
185
+
{isLoading && (
186
186
+
<div className="text-center text-sm text-muted-foreground mx-10">
187
187
+
Loading more...
188
188
+
</div>
189
189
+
)}
190
190
+
{!isLoading && !cursor && (
191
191
+
<div className="text-center text-sm text-muted-foreground mx-10 mt-2">
192
192
+
that's all, folks!
193
193
+
</div>
194
194
+
)}
195
195
+
</div>
196
196
+
</div>
197
197
+
</div>
198
198
+
);
199
199
+
}