Fork of atp.tools as a universal profile for people on the ATmosphere

add a bit of microcosm

+717 -83
+186
src/components/allBacklinksViewer.tsx
··· 1 + import { useEffect, useState } from "react"; 2 + import { 3 + Card, 4 + CardContent, 5 + CardDescription, 6 + CardHeader, 7 + CardTitle, 8 + } from "@/components/ui/card"; 9 + import { Skeleton } from "@/components/ui/skeleton"; 10 + import { Link } from "@tanstack/react-router"; 11 + 12 + interface LinkData { 13 + links: { 14 + [key: string]: { 15 + [key: string]: { 16 + records: number; 17 + distinct_dids: number; 18 + }; 19 + }; 20 + }; 21 + } 22 + 23 + export function AllBacklinksViewer({ aturi }: { aturi: string }) { 24 + const [data, setData] = useState<LinkData | null>(null); 25 + const [loading, setLoading] = useState(true); 26 + const [error, setError] = useState<string | null>(null); 27 + 28 + useEffect(() => { 29 + const fetchData = async () => { 30 + try { 31 + const response = await fetch( 32 + `https://constellation.microcosm.blue/links/all?target=${encodeURIComponent(aturi)}`, 33 + ); 34 + const jsonData = await response.json(); 35 + setData(jsonData); 36 + setLoading(false); 37 + } catch (err) { 38 + setError("Error fetching data"); 39 + setLoading(false); 40 + } 41 + }; 42 + 43 + fetchData(); 44 + }, []); 45 + 46 + if (loading) { 47 + return ( 48 + <> 49 + <h2 className="text-xl font-bold">Backlinks</h2> 50 + <div className="grid gap-4 mt-4 md:grid-cols-1 lg:grid-cols-2"> 51 + {[...Array(6)].map((_, i) => ( 52 + <Card key={i}> 53 + <CardHeader> 54 + <Skeleton className="h-4 w-[250px]" /> 55 + <Skeleton className="h-4 w-[200px]" /> 56 + </CardHeader> 57 + <CardContent> 58 + <Skeleton className="h-20 w-full" /> 59 + </CardContent> 60 + </Card> 61 + ))} 62 + </div> 63 + </> 64 + ); 65 + } 66 + 67 + if (error) { 68 + return ( 69 + <Card className="m-4"> 70 + <CardHeader> 71 + <CardTitle className="text-red-500">Error</CardTitle> 72 + <CardDescription>{error}</CardDescription> 73 + </CardHeader> 74 + </Card> 75 + ); 76 + } 77 + 78 + if (!data) return null; 79 + 80 + return ( 81 + <> 82 + <h2 className="text-2xl pt-6 font-semibold leading-3">Backlinks</h2> 83 + <div className="text-lg text-muted-foreground"> 84 + Interaction Statistics from{" "} 85 + <a 86 + className="text-blue-500 hover:underline" 87 + href="https://constellation.microcosm.blue/" 88 + target="_blank" 89 + rel="noopener noreferrer" 90 + > 91 + microcosm constellation 92 + </a> 93 + </div> 94 + <div className="grid gap-4 md:grid-cols-1 lg:grid-cols-2 leading-snug"> 95 + {Object.entries(data.links).map(([category, stats]) => ( 96 + <Card key={category} className="flex flex-col"> 97 + <CardContent className="flex-1 mt-4"> 98 + <CardTitle className="mb-2"> 99 + {formatCategoryName(category)} 100 + </CardTitle> 101 + <div className="space-y-4"> 102 + {Object.entries(stats).map(([stat, values]) => ( 103 + <div key={stat} className="space-y-2"> 104 + <h4 className="font-medium text-sm text-muted-foreground"> 105 + {formatStatName(stat)} 106 + </h4> 107 + <div className="grid grid-cols-2 gap-2 text-sm"> 108 + <Link 109 + to={"/constellation/links/$collection"} 110 + params={{ 111 + collection: category, 112 + }} 113 + search={{ 114 + path: stat, 115 + target: aturi, 116 + }} 117 + className="flex justify-between text-blue-700 dark:text-blue-300" 118 + > 119 + <span>Records:</span> 120 + <span> 121 + <span className="font-medium">{values.records}</span> 122 + <span className="border-l w-0 ml-2" /> 123 + </span> 124 + </Link> 125 + <div className="flex justify-between"> 126 + <Link 127 + to={"/constellation/dids/$collection"} 128 + params={{ 129 + collection: category, 130 + }} 131 + search={{ 132 + path: stat, 133 + target: aturi, 134 + }} 135 + className="flex justify-between w-full text-blue-700 dark:text-blue-300" 136 + > 137 + <span>Distinct DIDs:</span> 138 + <span className="font-medium"> 139 + {values.distinct_dids} 140 + </span> 141 + </Link> 142 + </div> 143 + </div> 144 + </div> 145 + ))} 146 + </div> 147 + </CardContent> 148 + </Card> 149 + ))} 150 + {Object.entries(data.links).length == 0 && ( 151 + <div className="flex flex-col items-start justify-start"> 152 + <p className="text-muted-foreground w-max"> 153 + Nothing doing! No links indexed for this target! 154 + </p> 155 + <span> 156 + You can{" "} 157 + <a 158 + href={`https://constellation.microcosm.blue/links/all?target=${encodeURIComponent(aturi)}`} 159 + className="text-blue-500 hover:underline" 160 + > 161 + check for updates here 162 + </a> 163 + . 164 + </span> 165 + </div> 166 + )} 167 + </div> 168 + </> 169 + ); 170 + } 171 + 172 + // Helper function to format category names 173 + const formatCategoryName = (name: string) => { 174 + return name 175 + .split(".") 176 + .pop() 177 + ?.replace(/([A-Z])/g, " $1") 178 + .trim(); 179 + }; 180 + 181 + // Helper function to format stat names 182 + const formatStatName = (name: string) => { 183 + return name.split(".").filter(Boolean).join(" → "); 184 + }; 185 + 186 + export default AllBacklinksViewer;
+5 -11
src/components/rnfgrertt/resultsView.tsx
··· 1 - import { 2 - Camera, 3 - Check, 4 - Clipboard, 5 - Loader2, 6 - RefreshCw, 7 - } from "lucide-react"; 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 - rkey: generateTid(), 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 - export const ResultsView = ({ 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 - }) => { 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 - }; 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 - {repoData?.handle} liked 22 + <span className="inline-flex"> 23 + <span className="max-w-64 lg:max-w-xl w-min pr-1 inline overflow-hidden text-ellipsis whitespace-nowrap"> 24 + {repoData?.handle} 25 + </span>{" "} 26 + liked 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 - <div className="text-sm text-muted-foreground"> 24 + <div className="text-sm text-muted-foreground max-w-min"> 25 25 Root post:{" "} 26 - <Link to={"/" + post.reply?.root.uri}>{post.reply?.root.uri}</Link> 26 + <Link 27 + className="overflow-hidden text-ellipsis whitespace-nowrap block" 28 + to={"/" + post.reply?.root.uri} 29 + > 30 + {post.reply?.root.uri} 31 + </Link> 27 32 </div> 28 33 )} 29 34 {post.reply?.parent && ( 30 - <div className="text-sm text-muted-foreground mb-3"> 35 + <div className="text-sm text-muted-foreground mb-3 max-w-full"> 31 36 Parent post:{" "} 32 - <Link to={"/" + post.reply?.parent.uri}> 37 + <Link 38 + to={"/" + post.reply?.parent.uri} 39 + className="overflow-hidden text-ellipsis whitespace-nowrap block" 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 - <Repeat2 className="inline mb-0.5 mr-1" /> {repoData?.handle} reposted{" "} 17 + <Repeat2 className="inline mb-0.5 mr-1" />{" "} 18 + <span className="inline-flex"> 19 + <span className="max-w-64 lg:max-w-xl w-min pr-1 inline overflow-hidden text-ellipsis whitespace-nowrap"> 20 + {repoData?.handle} 21 + </span>{" "} 22 + reposted 23 + </span> 18 24 </p> 19 25 <BlueskyPostWithoutEmbed showEmbeddedPost uri={post.subject.uri} /> 20 26 </>
+44 -54
src/lib/tid.ts
··· 1 - export const createRfc4648Encode = ( 2 - alphabet: string, 3 - bitsPerChar: number, 4 - pad: boolean, 5 - ) => { 6 - return (bytes: Uint8Array): string => { 7 - const mask = (1 << bitsPerChar) - 1; 8 - let str = ""; 1 + /** 2 + * TID (Transaction ID) implementation for ATProto 3 + * Based on the original Go implementation from github.com/bluesky-social/indigo 4 + */ 9 5 10 - let bits = 0; // Number of bits currently in the buffer 11 - let buffer = 0; // Bits waiting to be written out, MSB first 12 - for (let i = 0; i < bytes.length; ++i) { 13 - // Slurp data into the buffer: 14 - buffer = (buffer << 8) | bytes[i]; 15 - bits += 8; 6 + // Base32 alphabet used for sorting 7 + const BASE32_SORT_ALPHABET = "234567abcdefghijklmnopqrstuvwxyz"; 16 8 17 - // Write out as much as we can: 18 - while (bits > bitsPerChar) { 19 - bits -= bitsPerChar; 20 - str += alphabet[mask & (buffer >> bits)]; 21 - } 22 - } 9 + // Constants for bit operations 10 + const CLOCK_ID_MASK = 0x3ff; 11 + const MICROS_MASK = 0x1ffffffffffffn; 12 + const INTEGER_MASK = 0x7fffffffffffffffn; 23 13 24 - // Partial character: 25 - if (bits !== 0) { 26 - str += alphabet[mask & (buffer << (bitsPerChar - bits))]; 27 - } 14 + class TransactionId { 15 + private readonly value: string; 28 16 29 - // Add padding characters until we hit a byte boundary: 30 - if (pad) { 31 - while (((str.length * bitsPerChar) & 7) !== 0) { 32 - str += "="; 33 - } 34 - } 17 + constructor(value: string) { 18 + this.value = value; 19 + } 35 20 36 - return str; 37 - }; 38 - }; 21 + toString(): string { 22 + return this.value; 23 + } 24 + 25 + static create(unixMicros: bigint, clockId: number): TransactionId { 26 + const clockIdBig = BigInt(clockId & CLOCK_ID_MASK); 27 + const v = ((unixMicros & MICROS_MASK) << 10n) | clockIdBig; 28 + return TransactionId.fromInteger(v); 29 + } 30 + 31 + static createNow(clockId: number): TransactionId { 32 + const nowMicros = BigInt(Date.now()) * 1000n; // Convert ms to μs 33 + return TransactionId.create(nowMicros, clockId); 34 + } 39 35 40 - const BASE32_SORTABLE_CHARSET = "234567abcdefghijklmnopqrstuvwxyz"; 36 + private static fromInteger(value: bigint): TransactionId { 37 + value = INTEGER_MASK & value; 38 + let result = ""; 41 39 42 - export const toBase32Sortable = createRfc4648Encode( 43 - BASE32_SORTABLE_CHARSET, 44 - 5, 45 - false, 46 - ); 40 + for (let i = 0; i < 13; i++) { 41 + result = BASE32_SORT_ALPHABET[Number(value & 0x1fn)] + result; 42 + value = value >> 5n; 43 + } 47 44 48 - function intToArray(i: number) { 49 - return Uint8Array.of( 50 - (i & 0xff000000) >> 24, 51 - (i & 0x00ff0000) >> 16, 52 - (i & 0x0000ff00) >> 8, 53 - (i & 0x000000ff) >> 0, 54 - ); 45 + return new TransactionId(result); 46 + } 55 47 } 56 48 57 - /* 58 - * Generates an ATProto TID using the current timestamp. 59 - * Encoded as b32-sortable. 49 + /** 50 + * Generates a new Transaction ID with a random clock ID 51 + * @returns TransactionId 60 52 */ 61 - export function generateTid() { 62 - let ms = new Date().getMilliseconds(); 63 - let bytes = intToArray(ms); 64 - 65 - return toBase32Sortable(bytes); 53 + export function generateTid(): TransactionId { 54 + const clockId = Math.floor(Math.random() * 64 + 512); 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 + import { Route as ConstellationLinksCollectionImport } from './routes/constellation/links.$collection' 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 + const ConstellationLinksCollectionRoute = 107 + ConstellationLinksCollectionImport.update({ 108 + id: '/constellation/links/$collection', 109 + path: '/constellation/links/$collection', 110 + getParentRoute: () => rootRoute, 111 + } as any) 112 + 113 + const ConstellationDidsCollectionRoute = 114 + ConstellationDidsCollectionImport.update({ 115 + id: '/constellation/dids/$collection', 116 + path: '/constellation/dids/$collection', 117 + getParentRoute: () => rootRoute, 118 + } as any) 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 + '/constellation/dids/$collection': { 200 + id: '/constellation/dids/$collection' 201 + path: '/constellation/dids/$collection' 202 + fullPath: '/constellation/dids/$collection' 203 + preLoaderRoute: typeof ConstellationDidsCollectionImport 204 + parentRoute: typeof rootRoute 205 + } 206 + '/constellation/links/$collection': { 207 + id: '/constellation/links/$collection' 208 + path: '/constellation/links/$collection' 209 + fullPath: '/constellation/links/$collection' 210 + preLoaderRoute: typeof ConstellationLinksCollectionImport 211 + parentRoute: typeof rootRoute 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 + '/constellation/dids/$collection': typeof ConstellationDidsCollectionRoute 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 + '/constellation/dids/$collection': typeof ConstellationDidsCollectionRoute 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 + '/constellation/dids/$collection': typeof ConstellationDidsCollectionRoute 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 + | '/constellation/dids/$collection' 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 + | '/constellation/dids/$collection' 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 + | '/constellation/dids/$collection' 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 + ConstellationDidsCollectionRoute: typeof ConstellationDidsCollectionRoute 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 + ConstellationDidsCollectionRoute: ConstellationDidsCollectionRoute, 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 + "/constellation/dids/$collection", 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 + }, 434 + "/constellation/dids/$collection": { 435 + "filePath": "constellation/dids.$collection.tsx" 436 + }, 437 + "/constellation/links/$collection": { 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 - <div className="flex flex-row justify-center w-full"> 134 - <div className="max-w-2xl w-screen p-4 md:mt-16 space-y-2"> 133 + <div className="flex flex-row justify-center w-full max-h-[calc(100vh-5rem)]"> 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 - <RenderJson 206 - data={didDoc} 207 - did={identity?.identity.id!} 208 - pds={identity?.identity.pds.toString()!} 209 - /> 205 + <div className="w-full overflow-x-auto"> 206 + <RenderJson 207 + data={didDoc} 208 + did={identity?.identity.id!} 209 + pds={identity?.identity.pds.toString()!} 210 + /> 211 + </div> 210 212 </div> 211 213 </div> 212 214 </div>
+5 -3
src/routes/at:/$handle/$collection.$rkey.lazy.tsx
··· 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 - <div className="flex flex-row justify-center w-full min-h-screen"> 128 - <div className="max-w-2xl w-screen p-4 md:mt-16 space-y-2"> 128 + <div className="flex flex-row justify-center w-full min-h-[calc(100vh-5rem)] max-w-[100vw]"> 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 - <h1 className="text-2xl md:text-3xl text-muted-foreground font-normal"> 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 + <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 - <div className="flex flex-row justify-center w-full min-h-screen"> 124 - <div className="max-w-2xl w-screen p-4 md:mt-16 space-y-2"> 123 + <div className="flex flex-row justify-center w-full min-h-[calc(100vh-5rem)]"> 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 + import ShowError from "@/components/error"; 2 + import { Loader } from "@/components/ui/loader"; 3 + import { 4 + createFileRoute, 5 + Link, 6 + useParams, 7 + useSearch, 8 + } from "@tanstack/react-router"; 9 + import { useEffect, useRef, useState } from "preact/hooks"; 10 + 11 + export const Route = createFileRoute("/constellation/dids/$collection")({ 12 + component: RouteComponent, 13 + validateSearch: (search: Record<string, unknown>) => { 14 + console.log(search); 15 + return { 16 + target: String(search.target) || "", 17 + path: String(search.path) || "", 18 + }; 19 + }, 20 + }); 21 + 22 + interface ConstellationLinkState { 23 + totalLinks: number; 24 + links: string[]; 25 + cursor: string | null; 26 + error: Error | null; 27 + isLoading: boolean; 28 + } 29 + 30 + function useConstellationLink( 31 + collection: string, 32 + target: string, 33 + path: string, 34 + ) { 35 + const [link, setLink] = useState<ConstellationLinkState>({ 36 + totalLinks: 0, 37 + links: [], 38 + cursor: null, 39 + error: null, 40 + isLoading: true, 41 + }); 42 + 43 + const fetchLink = async (cursor?: string) => { 44 + // check for missing parameters 45 + if (!collection || !target || !path) { 46 + let missingParams = []; 47 + for (let param in [collection, target, path]) { 48 + if (!param) missingParams.push(param); 49 + } 50 + if (missingParams.length > 0) { 51 + setLink({ 52 + ...link, 53 + error: new Error("Missing parameters: "), 54 + isLoading: false, 55 + }); 56 + return; 57 + } 58 + } 59 + 60 + let response = await fetch( 61 + `https://constellation.microcosm.blue/links/distinct-dids?target=${target}&collection=${collection}&path=${path}${cursor ? `&cursor=${cursor}` : ""}`, 62 + ); 63 + 64 + let data = await response.json(); 65 + setLink((prev) => ({ 66 + ...prev, 67 + totalLinks: data.total, 68 + links: [...(prev.links || []), ...data.linking_dids], 69 + cursor: data.cursor, 70 + error: data.error, 71 + isLoading: false, 72 + })); 73 + }; 74 + 75 + useEffect(() => { 76 + fetchLink(); 77 + }, [collection, target, path]); 78 + 79 + return { 80 + totalLinks: link.totalLinks, 81 + links: link.links, 82 + cursor: link.cursor, 83 + error: link.error, 84 + isLoading: link.isLoading, 85 + fetchMore: async (cursor?: string) => { 86 + if (!cursor) return; 87 + await fetchLink(cursor); 88 + }, 89 + }; 90 + } 91 + 92 + function RouteComponent() { 93 + // get route params 94 + const { collection } = useParams({ 95 + from: "/constellation/dids/$collection", 96 + }); 97 + // get query params 98 + const params = useSearch({ 99 + from: "/constellation/dids/$collection", 100 + }); 101 + 102 + const state = useConstellationLink(collection, params.target, params.path); 103 + 104 + const loaderRef = useRef<HTMLDivElement>(null); 105 + 106 + const { links, cursor, error, isLoading, fetchMore } = state; 107 + 108 + useEffect(() => { 109 + if (!loaderRef.current) return; 110 + 111 + const observer = new IntersectionObserver( 112 + (entries) => { 113 + const target = entries[0]; 114 + if (target.isIntersecting && !isLoading && cursor) { 115 + fetchMore(cursor); 116 + } 117 + }, 118 + { threshold: 0.1, rootMargin: "50px" }, 119 + ); 120 + 121 + observer.observe(loaderRef.current); 122 + return () => observer.disconnect(); 123 + }, [cursor, isLoading, fetchMore]); 124 + 125 + if (error) { 126 + return <ShowError error={error} />; 127 + } 128 + 129 + if (isLoading && !links.length) { 130 + return <Loader />; 131 + } 132 + const splitColl = params.target.split("/"); 133 + return ( 134 + <div className="flex flex-row justify-center w-full min-h-screen"> 135 + <div className="max-w-md lg:max-w-2xl w-[90vw] mx-4 md:mt-16 space-y-2"> 136 + <h1 className="text-3xl font-bold">Links</h1> 137 + <div className="text-muted-foreground flex-inline"> 138 + <span>View all dids mentioning </span> 139 + <span className="flex"> 140 + <span>at://</span> 141 + <span className="inline-block max-w-full overflow-hidden text-ellipsis whitespace-nowrap"> 142 + {splitColl[2]} 143 + </span> 144 + / 145 + <span className="inline-block max-w-full overflow-hidden text-ellipsis whitespace-nowrap"> 146 + {splitColl[3]} 147 + </span> 148 + /<span>{splitColl[4]}</span> 149 + </span> 150 + </div> 151 + {links.map((link) => ( 152 + <div className="w-min max-w-full"> 153 + <Link 154 + key={link} 155 + to="/at:/$handle" 156 + params={{ 157 + handle: link, 158 + }} 159 + className="flex text-blue-700 dark:text-blue-300 hover:text-blue-500 dark:hover:text-blue-400 transition-colors" 160 + > 161 + at:// 162 + <span className="inline-block max-w-full overflow-hidden text-ellipsis whitespace-nowrap"> 163 + {link} 164 + </span> 165 + / 166 + </Link> 167 + </div> 168 + ))} 169 + 170 + <div 171 + ref={loaderRef} 172 + className="flex flex-row justify-center h-10 -pt-16" 173 + > 174 + {isLoading && ( 175 + <div className="text-center text-sm text-muted-foreground mx-10"> 176 + Loading more... 177 + </div> 178 + )} 179 + {!isLoading && !cursor && ( 180 + <div className="text-center text-sm text-muted-foreground mx-10 mt-2"> 181 + that's all, folks! 182 + </div> 183 + )} 184 + </div> 185 + </div> 186 + </div> 187 + ); 188 + }
+199
src/routes/constellation/links.$collection.tsx
··· 1 + import ShowError from "@/components/error"; 2 + import { Loader } from "@/components/ui/loader"; 3 + import { 4 + createFileRoute, 5 + Link, 6 + useParams, 7 + useSearch, 8 + } from "@tanstack/react-router"; 9 + import { useEffect, useRef, useState } from "preact/hooks"; 10 + 11 + export const Route = createFileRoute("/constellation/links/$collection")({ 12 + component: RouteComponent, 13 + validateSearch: (search: Record<string, unknown>) => { 14 + return { 15 + target: String(search.target) || "", 16 + path: String(search.path) || "", 17 + }; 18 + }, 19 + }); 20 + 21 + interface CLink { 22 + did: string; 23 + collection: string; 24 + rkey: string; 25 + } 26 + 27 + interface ConstellationLinkState { 28 + totalLinks: number; 29 + links: CLink[]; 30 + cursor: string | null; 31 + error: Error | null; 32 + isLoading: boolean; 33 + } 34 + 35 + function useConstellationLink( 36 + collection: string, 37 + target: string, 38 + path: string, 39 + ) { 40 + const [link, setLink] = useState<ConstellationLinkState>({ 41 + totalLinks: 0, 42 + links: [], 43 + cursor: null, 44 + error: null, 45 + isLoading: true, 46 + }); 47 + 48 + const fetchLink = async (cursor?: string) => { 49 + // check for missing parameters 50 + if (!collection || !target || !path) { 51 + let missingParams = []; 52 + for (let param in [collection, target, path]) { 53 + if (!param) missingParams.push(param); 54 + } 55 + if (missingParams.length > 0) { 56 + setLink({ 57 + ...link, 58 + error: new Error("Missing parameters: "), 59 + isLoading: false, 60 + }); 61 + return; 62 + } 63 + } 64 + 65 + let response = await fetch( 66 + `https://constellation.microcosm.blue/links?target=${target}&collection=${collection}&path=${path}${cursor ? `&cursor=${cursor}` : ""}`, 67 + ); 68 + 69 + let data = await response.json(); 70 + setLink((prev) => ({ 71 + ...prev, 72 + totalLinks: data.total, 73 + links: [...(prev.links || []), ...data.linking_records], 74 + cursor: data.cursor, 75 + error: data.error, 76 + isLoading: false, 77 + })); 78 + }; 79 + 80 + useEffect(() => { 81 + fetchLink(); 82 + }, [collection, target, path]); 83 + 84 + return { 85 + totalLinks: link.totalLinks, 86 + links: link.links, 87 + cursor: link.cursor, 88 + error: link.error, 89 + isLoading: link.isLoading, 90 + fetchMore: async (cursor?: string) => { 91 + if (!cursor) return; 92 + await fetchLink(cursor); 93 + }, 94 + }; 95 + } 96 + 97 + function RouteComponent() { 98 + // get route params 99 + const { collection } = useParams({ 100 + from: "/constellation/links/$collection", 101 + }); 102 + // get query params 103 + const params = useSearch({ 104 + from: "/constellation/links/$collection", 105 + }); 106 + 107 + const state = useConstellationLink(collection, params.target, params.path); 108 + 109 + const loaderRef = useRef<HTMLDivElement>(null); 110 + 111 + const { links, cursor, error, isLoading, fetchMore } = state; 112 + 113 + useEffect(() => { 114 + if (!loaderRef.current) return; 115 + 116 + const observer = new IntersectionObserver( 117 + (entries) => { 118 + const target = entries[0]; 119 + if (target.isIntersecting && !isLoading && cursor) { 120 + fetchMore(cursor); 121 + } 122 + }, 123 + { threshold: 0.1, rootMargin: "50px" }, 124 + ); 125 + 126 + observer.observe(loaderRef.current); 127 + return () => observer.disconnect(); 128 + }, [cursor, isLoading, fetchMore]); 129 + 130 + if (error) { 131 + return <ShowError error={error} />; 132 + } 133 + 134 + if (isLoading && !links.length) { 135 + return <Loader />; 136 + } 137 + const splitColl = params.target.split("/"); 138 + return ( 139 + <div className="flex flex-row justify-center w-full min-h-screen"> 140 + <div className="max-w-md lg:max-w-2xl w-[90vw] mx-4 md:mt-16 space-y-2"> 141 + <h1 className="text-3xl font-bold">Links</h1> 142 + <div className="text-muted-foreground flex-inline"> 143 + <span>View all links to </span> 144 + <span className="flex"> 145 + <span>at://</span> 146 + <span className="inline-block max-w-full overflow-hidden text-ellipsis whitespace-nowrap"> 147 + {splitColl[2]} 148 + </span> 149 + / 150 + <span className="inline-block max-w-full overflow-hidden text-ellipsis whitespace-nowrap"> 151 + {splitColl[3]} 152 + </span> 153 + /<span>{splitColl[4]}</span> 154 + </span> 155 + </div> 156 + {links.map((link) => ( 157 + <div className="w-min max-w-full"> 158 + <Link 159 + key={link.rkey + link.did} 160 + to="/at:/$handle/$collection/$rkey" 161 + params={{ 162 + handle: link.did, 163 + collection: link.collection, 164 + rkey: link.rkey, 165 + }} 166 + className="flex text-blue-700 dark:text-blue-300 hover:text-blue-500 dark:hover:text-blue-400 transition-colors" 167 + > 168 + at:// 169 + <span className="inline-block max-w-full overflow-hidden text-ellipsis whitespace-nowrap"> 170 + {link.did} 171 + </span> 172 + / 173 + <span className="inline-block max-w-full overflow-hidden text-ellipsis whitespace-nowrap"> 174 + {link.collection} 175 + </span> 176 + /<span>{link.rkey}</span> 177 + </Link> 178 + </div> 179 + ))} 180 + 181 + <div 182 + ref={loaderRef} 183 + className="flex flex-row justify-center h-10 -pt-16" 184 + > 185 + {isLoading && ( 186 + <div className="text-center text-sm text-muted-foreground mx-10"> 187 + Loading more... 188 + </div> 189 + )} 190 + {!isLoading && !cursor && ( 191 + <div className="text-center text-sm text-muted-foreground mx-10 mt-2"> 192 + that's all, folks! 193 + </div> 194 + )} 195 + </div> 196 + </div> 197 + </div> 198 + ); 199 + }