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

facelift profile page

Natalie B a68f66f8 b83ec791

+453 -66
+1
package.json
··· 14 14 "@atcute/bluesky-richtext-segmenter": "^1.0.5", 15 15 "@atcute/client": "^2.0.9", 16 16 "@atcute/oauth-browser-client": "^1.0.17", 17 + "@radix-ui/react-accordion": "^1.2.8", 17 18 "@radix-ui/react-avatar": "^1.1.4", 18 19 "@radix-ui/react-dialog": "^1.1.7", 19 20 "@radix-ui/react-dropdown-menu": "^2.1.7",
+146
pnpm-lock.yaml
··· 20 20 '@atcute/oauth-browser-client': 21 21 specifier: ^1.0.17 22 22 version: 1.0.17 23 + '@radix-ui/react-accordion': 24 + specifier: ^1.2.8 25 + version: 1.2.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 23 26 '@radix-ui/react-avatar': 24 27 specifier: ^1.1.4 25 28 version: 1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) ··· 498 501 '@radix-ui/primitive@1.1.2': 499 502 resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} 500 503 504 + '@radix-ui/react-accordion@1.2.8': 505 + resolution: {integrity: sha512-c7OKBvO36PfQIUGIjj1Wko0hH937pYFU2tR5zbIJDUsmTzHoZVHHt4bmb7OOJbzTaWJtVELKWojBHa7OcnUHmQ==} 506 + peerDependencies: 507 + '@types/react': '*' 508 + '@types/react-dom': '*' 509 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 510 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 511 + peerDependenciesMeta: 512 + '@types/react': 513 + optional: true 514 + '@types/react-dom': 515 + optional: true 516 + 501 517 '@radix-ui/react-arrow@1.1.3': 502 518 resolution: {integrity: sha512-2dvVU4jva0qkNZH6HHWuSz5FN5GeU5tymvCgutF8WaXz9WnD1NgUhy73cqzkjkN4Zkn8lfTPv5JIfrC221W+Nw==} 503 519 peerDependencies: ··· 524 540 '@types/react-dom': 525 541 optional: true 526 542 543 + '@radix-ui/react-collapsible@1.1.8': 544 + resolution: {integrity: sha512-hxEsLvK9WxIAPyxdDRULL4hcaSjMZCfP7fHB0Z1uUnDoDBat1Zh46hwYfa69DeZAbJrPckjf0AGAtEZyvDyJbw==} 545 + peerDependencies: 546 + '@types/react': '*' 547 + '@types/react-dom': '*' 548 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 549 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 550 + peerDependenciesMeta: 551 + '@types/react': 552 + optional: true 553 + '@types/react-dom': 554 + optional: true 555 + 527 556 '@radix-ui/react-collection@1.1.3': 528 557 resolution: {integrity: sha512-mM2pxoQw5HJ49rkzwOs7Y6J4oYH22wS8BfK2/bBxROlI4xuR0c4jEenQP63LlTlDkO6Buj2Vt+QYAYcOgqtrXA==} 558 + peerDependencies: 559 + '@types/react': '*' 560 + '@types/react-dom': '*' 561 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 562 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 563 + peerDependenciesMeta: 564 + '@types/react': 565 + optional: true 566 + '@types/react-dom': 567 + optional: true 568 + 569 + '@radix-ui/react-collection@1.1.4': 570 + resolution: {integrity: sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==} 529 571 peerDependencies: 530 572 '@types/react': '*' 531 573 '@types/react-dom': '*' ··· 699 741 '@types/react-dom': 700 742 optional: true 701 743 744 + '@radix-ui/react-presence@1.1.4': 745 + resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} 746 + peerDependencies: 747 + '@types/react': '*' 748 + '@types/react-dom': '*' 749 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 750 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 751 + peerDependenciesMeta: 752 + '@types/react': 753 + optional: true 754 + '@types/react-dom': 755 + optional: true 756 + 702 757 '@radix-ui/react-primitive@2.0.3': 703 758 resolution: {integrity: sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==} 759 + peerDependencies: 760 + '@types/react': '*' 761 + '@types/react-dom': '*' 762 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 763 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 764 + peerDependenciesMeta: 765 + '@types/react': 766 + optional: true 767 + '@types/react-dom': 768 + optional: true 769 + 770 + '@radix-ui/react-primitive@2.1.0': 771 + resolution: {integrity: sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==} 704 772 peerDependencies: 705 773 '@types/react': '*' 706 774 '@types/react-dom': '*' ··· 810 878 811 879 '@radix-ui/react-use-controllable-state@1.1.1': 812 880 resolution: {integrity: sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==} 881 + peerDependencies: 882 + '@types/react': '*' 883 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 884 + peerDependenciesMeta: 885 + '@types/react': 886 + optional: true 887 + 888 + '@radix-ui/react-use-controllable-state@1.2.2': 889 + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} 890 + peerDependencies: 891 + '@types/react': '*' 892 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 893 + peerDependenciesMeta: 894 + '@types/react': 895 + optional: true 896 + 897 + '@radix-ui/react-use-effect-event@0.0.2': 898 + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} 813 899 peerDependencies: 814 900 '@types/react': '*' 815 901 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc ··· 2439 2525 2440 2526 '@radix-ui/primitive@1.1.2': {} 2441 2527 2528 + '@radix-ui/react-accordion@1.2.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2529 + dependencies: 2530 + '@radix-ui/primitive': 1.1.2 2531 + '@radix-ui/react-collapsible': 1.1.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2532 + '@radix-ui/react-collection': 1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2533 + '@radix-ui/react-compose-refs': 1.1.2(react@19.0.0) 2534 + '@radix-ui/react-context': 1.1.2(react@19.0.0) 2535 + '@radix-ui/react-direction': 1.1.1(react@19.0.0) 2536 + '@radix-ui/react-id': 1.1.1(react@19.0.0) 2537 + '@radix-ui/react-primitive': 2.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2538 + '@radix-ui/react-use-controllable-state': 1.2.2(react@19.0.0) 2539 + react: 19.0.0 2540 + react-dom: 19.0.0(react@19.0.0) 2541 + 2442 2542 '@radix-ui/react-arrow@1.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2443 2543 dependencies: 2444 2544 '@radix-ui/react-primitive': 2.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) ··· 2454 2554 react: 19.0.0 2455 2555 react-dom: 19.0.0(react@19.0.0) 2456 2556 2557 + '@radix-ui/react-collapsible@1.1.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2558 + dependencies: 2559 + '@radix-ui/primitive': 1.1.2 2560 + '@radix-ui/react-compose-refs': 1.1.2(react@19.0.0) 2561 + '@radix-ui/react-context': 1.1.2(react@19.0.0) 2562 + '@radix-ui/react-id': 1.1.1(react@19.0.0) 2563 + '@radix-ui/react-presence': 1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2564 + '@radix-ui/react-primitive': 2.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2565 + '@radix-ui/react-use-controllable-state': 1.2.2(react@19.0.0) 2566 + '@radix-ui/react-use-layout-effect': 1.1.1(react@19.0.0) 2567 + react: 19.0.0 2568 + react-dom: 19.0.0(react@19.0.0) 2569 + 2457 2570 '@radix-ui/react-collection@1.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2458 2571 dependencies: 2459 2572 '@radix-ui/react-compose-refs': 1.1.2(react@19.0.0) ··· 2463 2576 react: 19.0.0 2464 2577 react-dom: 19.0.0(react@19.0.0) 2465 2578 2579 + '@radix-ui/react-collection@1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2580 + dependencies: 2581 + '@radix-ui/react-compose-refs': 1.1.2(react@19.0.0) 2582 + '@radix-ui/react-context': 1.1.2(react@19.0.0) 2583 + '@radix-ui/react-primitive': 2.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2584 + '@radix-ui/react-slot': 1.2.0(react@19.0.0) 2585 + react: 19.0.0 2586 + react-dom: 19.0.0(react@19.0.0) 2587 + 2466 2588 '@radix-ui/react-compose-refs@1.1.2(react@19.0.0)': 2467 2589 dependencies: 2468 2590 react: 19.0.0 ··· 2605 2727 react: 19.0.0 2606 2728 react-dom: 19.0.0(react@19.0.0) 2607 2729 2730 + '@radix-ui/react-presence@1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2731 + dependencies: 2732 + '@radix-ui/react-compose-refs': 1.1.2(react@19.0.0) 2733 + '@radix-ui/react-use-layout-effect': 1.1.1(react@19.0.0) 2734 + react: 19.0.0 2735 + react-dom: 19.0.0(react@19.0.0) 2736 + 2608 2737 '@radix-ui/react-primitive@2.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2738 + dependencies: 2739 + '@radix-ui/react-slot': 1.2.0(react@19.0.0) 2740 + react: 19.0.0 2741 + react-dom: 19.0.0(react@19.0.0) 2742 + 2743 + '@radix-ui/react-primitive@2.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2609 2744 dependencies: 2610 2745 '@radix-ui/react-slot': 1.2.0(react@19.0.0) 2611 2746 react: 19.0.0 ··· 2692 2827 '@radix-ui/react-use-controllable-state@1.1.1(react@19.0.0)': 2693 2828 dependencies: 2694 2829 '@radix-ui/react-use-callback-ref': 1.1.1(react@19.0.0) 2830 + react: 19.0.0 2831 + 2832 + '@radix-ui/react-use-controllable-state@1.2.2(react@19.0.0)': 2833 + dependencies: 2834 + '@radix-ui/react-use-effect-event': 0.0.2(react@19.0.0) 2835 + '@radix-ui/react-use-layout-effect': 1.1.1(react@19.0.0) 2836 + react: 19.0.0 2837 + 2838 + '@radix-ui/react-use-effect-event@0.0.2(react@19.0.0)': 2839 + dependencies: 2840 + '@radix-ui/react-use-layout-effect': 1.1.1(react@19.0.0) 2695 2841 react: 19.0.0 2696 2842 2697 2843 '@radix-ui/react-use-escape-keydown@1.1.1(react@19.0.0)':
+2 -2
src/components/allBacklinksViewer.tsx
··· 80 80 return ( 81 81 <> 82 82 <h2 className="text-2xl pt-6 font-semibold leading-3">Backlinks</h2> 83 - <div className="text-lg text-muted-foreground"> 83 + <div className="text-sm text-muted-foreground"> 84 84 Interaction Statistics from{" "} 85 85 <a 86 86 className="text-blue-500 hover:underline" ··· 152 152 <p className="text-muted-foreground w-max"> 153 153 Nothing doing! No links indexed for this target! 154 154 </p> 155 - <span className="text-muted-foreground"> 155 + <span className="text-muted-foreground text-xs"> 156 156 You can{" "} 157 157 <a 158 158 href={`https://constellation.microcosm.blue/links/all?target=${encodeURIComponent(aturi)}`}
+60
src/components/ui/accordion.tsx
··· 1 + "use client" 2 + 3 + import * as React from "react" 4 + import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 + import { ChevronDown } from "lucide-react" 6 + 7 + import { cn } from "@/lib/utils" 8 + 9 + const Accordion = AccordionPrimitive.Root 10 + 11 + const AccordionItem = React.forwardRef< 12 + React.ElementRef<typeof AccordionPrimitive.Item>, 13 + React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> 14 + >(({ className, ...props }, ref) => ( 15 + <AccordionPrimitive.Item 16 + ref={ref} 17 + className={cn("border-b", className)} 18 + {...props} 19 + /> 20 + )) 21 + AccordionItem.displayName = "AccordionItem" 22 + 23 + const AccordionTrigger = React.forwardRef< 24 + React.ElementRef<typeof AccordionPrimitive.Trigger>, 25 + React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> 26 + >(({ className, children, ...props }, ref) => ( 27 + <AccordionPrimitive.Header className="flex"> 28 + <AccordionPrimitive.Trigger 29 + ref={ref} 30 + className={cn( 31 + "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180", 32 + className 33 + )} 34 + {...props} 35 + > 36 + {children} 37 + <ChevronDown className="h-4 w-4 transition-transform duration-200" /> 38 + </AccordionPrimitive.Trigger> 39 + </AccordionPrimitive.Header> 40 + )) 41 + AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 42 + 43 + const AccordionContent = React.forwardRef< 44 + React.ElementRef<typeof AccordionPrimitive.Content>, 45 + React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> 46 + >(({ className, children, ...props }, ref) => ( 47 + <AccordionPrimitive.Content 48 + ref={ref} 49 + className={cn( 50 + "overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down", 51 + className 52 + )} 53 + {...props} 54 + > 55 + <div className="pb-4 pt-0">{children}</div> 56 + </AccordionPrimitive.Content> 57 + )) 58 + AccordionContent.displayName = AccordionPrimitive.Content.displayName 59 + 60 + export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
+84
src/components/ui/click-to-copy.tsx
··· 1 + import { useState } from "preact/compat"; 2 + import { Check, Copy } from "lucide-react"; 3 + import { Button } from "@/components/ui/button"; // Assuming you have a Button component from shadcn/ui 4 + import { cn } from "@/lib/utils"; // Assuming you have a utility for class names 5 + 6 + interface ClickToCopyProps { 7 + value?: string; 8 + children?: any; // Allow wrapping other elements if needed, defaults to showing the value 9 + className?: string; 10 + buttonClassName?: string; 11 + iconSize?: number; 12 + } 13 + 14 + export function ClickToCopy({ 15 + value, 16 + children, 17 + className, 18 + buttonClassName, 19 + iconSize = 16, // Default icon size 20 + }: ClickToCopyProps) { 21 + //const [isHovering, setIsHovering] = useState(false); 22 + const [isCopied, setIsCopied] = useState(false); 23 + 24 + if (!value) return children; 25 + 26 + const handleCopy = async (event: MouseEvent) => { 27 + event.stopPropagation(); // Prevent potential parent click handlers 28 + try { 29 + await navigator.clipboard.writeText(value); 30 + setIsCopied(true); 31 + setTimeout(() => setIsCopied(false), 1500); 32 + } catch (err) { 33 + console.error("Failed to copy text: ", err); 34 + } 35 + }; 36 + 37 + const buttonLabel = isCopied ? "Copied!" : "Copy to clipboard"; 38 + 39 + return ( 40 + <div 41 + className={cn("relative flex items-center group", className)} 42 + //onMouseEnter={() => setIsHovering(true)} 43 + //onMouseLeave={() => setIsHovering(false)} 44 + > 45 + <Button 46 + variant="link" 47 + size="icon" 48 + className={cn( 49 + "absolute -left-8 top-1/2 transform -translate-y-1/2 p-1 h-full w-auto bg-green-500", 50 + "opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity duration-150", 51 + isCopied 52 + ? "text-green-600 hover:text-green-700" 53 + : "text-muted-foreground hover:text-foreground", 54 + buttonClassName, 55 + )} 56 + onClick={handleCopy} 57 + aria-label={buttonLabel} 58 + title={buttonLabel} 59 + > 60 + <div className="relative flex items-center justify-center"> 61 + <Copy 62 + size={iconSize} 63 + className={cn( 64 + "transition-all duration-200 ease-in-out", 65 + isCopied ? "opacity-0 scale-75" : "opacity-100 scale-100", 66 + )} 67 + /> 68 + {/* Check Icon */} 69 + <Check 70 + size={iconSize} 71 + className={cn( 72 + "absolute transition-all duration-200 ease-in-out", 73 + isCopied ? "opacity-100 scale-100" : "opacity-0 scale-75", 74 + )} 75 + /> 76 + </div> 77 + </Button> 78 + 79 + <span className="truncate">{children ?? value}</span> 80 + </div> 81 + ); 82 + } 83 + 84 + export default ClickToCopy;
+6 -2
src/components/views/app-bsky/embed.tsx
··· 112 112 const rkey = splits[splits.length - 1]; 113 113 try { 114 114 const rpc = new QtClient(new URL("https://public.api.bsky.app")); 115 - const response = await rpc 115 + const responsePromise = rpc 116 116 .getXrpcClient() 117 117 .get("com.atproto.repo.getRecord", { 118 118 params: { repo: did, collection, rkey }, 119 119 signal: abortController.signal, 120 120 }); 121 - const actor = await rpc 121 + const actorPromise = rpc 122 122 .getXrpcClient() 123 123 .get("app.bsky.actor.getProfile", { 124 124 params: { actor: did }, 125 125 signal: abortController.signal, 126 126 }); 127 + const [response, actor] = await Promise.all([ 128 + responsePromise, 129 + actorPromise, 130 + ]); 127 131 setData({ 128 132 actorProfile: actor.data, 129 133 post: response.data.value as AppBskyFeedPost.Record,
+4
src/index.css
··· 201 201 .scrollbar-hide::-webkit-scrollbar { 202 202 display: none; /* Chrome, Safari, and Opera */ 203 203 } 204 + 205 + * { 206 + text-decoration-line: none !important; 207 + }
+140 -58
src/routes/at:/$handle.index.tsx
··· 1 + import AllBacklinksViewer from "@/components/allBacklinksViewer"; 1 2 import ShowError from "@/components/error"; 2 3 import { RenderJson } from "@/components/renderJson"; 3 4 import RepoIcons from "@/components/repoIcons"; 5 + import { 6 + Accordion, 7 + AccordionContent, 8 + AccordionItem, 9 + AccordionTrigger, 10 + } from "@/components/ui/accordion"; // Import Shadcn accordion components 11 + import ClickToCopy from "@/components/ui/click-to-copy"; 4 12 import { Loader } from "@/components/ui/loader"; 5 13 import { useDocumentTitle } from "@/hooks/useDocumentTitle"; 14 + import { useStoredState } from "@/hooks/useStoredState"; // Import the hook 6 15 import getDidDoc from "@/lib/getDidDoc"; 7 - import { QtClient, useXrpc } from "@/providers/qtprovider"; 16 + import { QtClient } from "@/providers/qtprovider"; 8 17 import "@atcute/bluesky/lexicons"; 9 18 import { 10 19 AppBskyActorGetProfile, ··· 33 42 } 34 43 35 44 function useRepoData(handle: string): RepoData { 36 - const xrpc = useXrpc(); 45 + //const xrpc = useXrpc(); 37 46 const [state, setState] = useState<RepoData>({ 38 47 data: undefined, 39 48 isLoading: true, ··· 49 58 50 59 async function fetchRepoData() { 51 60 try { 52 - setState((prev) => ({ ...prev, isLoading: true })); 61 + setState((prev) => ({ ...prev, isLoading: true, error: null })); // Reset error state 53 62 54 63 let id; 55 64 try { 56 65 id = await resolveFromIdentity(handle); 57 66 } catch (err: any) { 58 - console.log("BSLKDJFSL"); 59 67 throw new Error("Unable to resolve identity: " + err.message); 60 68 } 61 69 // we dont use the main authenticated client here ··· 71 79 try { 72 80 doc = await getDidDoc(id.identity.id); 73 81 } catch (error) { 74 - console.log("sdf"); 75 82 console.error("Failed to fetch DID document:", error); 83 + // Don't throw here, allow the rest of the data to load 76 84 } 77 85 // can we get bsky data? 78 86 if (response.data.collections.includes("app.bsky.actor.profile")) { 79 - // reuse client dumbass 87 + // Fetch Bluesky profile data using a public client 80 88 const bskyData = await new QtClient( 81 89 new URL("https://public.api.bsky.app"), 82 90 ) 83 91 .getXrpcClient() 84 92 .get("app.bsky.actor.getProfile", { 85 93 params: { actor: id.identity.id }, 94 + signal: abortController.signal, // Pass signal here too 86 95 }); 87 96 88 97 setState({ ··· 103 112 error: null, 104 113 }); 105 114 } 106 - // todo: actual errors 107 115 } catch (err: any) { 108 116 if (err.name === "AbortError") return; 109 117 118 + console.error("Failed to fetch repo data:", err); // Log the error for debugging 110 119 setState({ 111 120 data: undefined, 121 + blueSkyData: undefined, // Clear blueSkyData on error 122 + identity: undefined, // Clear identity on error 112 123 isLoading: false, 113 - error: err instanceof Error ? err : new Error("An error occurred"), 124 + didDoc: undefined, // Clear didDoc on error 125 + error: 126 + err instanceof Error ? err : new Error("An unknown error occurred"), 114 127 }); 115 128 } 116 129 } ··· 120 133 return () => { 121 134 abortController.abort(); 122 135 }; 123 - }, [handle, xrpc]); 136 + }, [handle]); // Removed xrpc dependency as it's not directly used in effect logic 124 137 125 138 return state; 126 139 } ··· 133 146 const { handle } = Route.useParams(); 134 147 const { blueSkyData, data, identity, isLoading, error, didDoc } = 135 148 useRepoData(handle); 149 + 150 + // State for accordion collapse, using the handle in the key for uniqueness 151 + const [didDocOpenValue, setDidDocOpenValue] = useStoredState<string>( 152 + `did-doc-open-${handle}`, // Unique key per handle 153 + "", // Default to closed (empty string value) 154 + ); 155 + 136 156 if (error) { 137 157 return <ShowError error={error} />; 138 158 } 139 159 140 - if (isLoading && !blueSkyData) { 160 + // Show loader only if essential data (repo description) is still loading 161 + if (isLoading && !data) { 141 162 return <Loader className="max-h-[calc(100vh-5rem)] h-screen" />; 142 163 } 143 164 165 + // Handle case where repo description loaded but identity resolution failed earlier 166 + if (!identity && !isLoading) { 167 + return ( 168 + <ShowError 169 + error={ 170 + new Error("Failed to resolve identity or fetch repository data.") 171 + } 172 + /> 173 + ); 174 + } 175 + 144 176 return ( 145 - <div className="flex flex-row justify-center w-full max-h-[calc(100vh-5rem)]"> 146 - <div className="max-w-md lg:max-w-2xl w-[90vw] mx-4 md:mt-16 space-y-2"> 177 + <div className="flex flex-row justify-center w-full min-h-[calc(100vh-5rem)]"> 178 + <div className="max-w-md lg:max-w-2xl w-[90vw] mx-4 my-4 md:mt-8 space-y-2"> 147 179 {blueSkyData ? ( 148 180 blueSkyData?.banner ? ( 149 181 <div className="relative mb-12 md:mb-16"> ··· 168 200 <AtSign className="w-16 h-16" /> 169 201 </div> 170 202 )} 171 - <h1 className="text-2xl md:text-3xl font-bold"> 172 - {blueSkyData?.displayName}{" "} 173 - <span className="text-muted-foreground font-normal"> 203 + <ClickToCopy 204 + className="text-2xl md:text-3xl font-bold pt-2" 205 + value={data?.handle} 206 + > 207 + {blueSkyData?.displayName || data?.handle}{" "} 208 + <span className="text-muted-foreground font-normal block md:inline"> 174 209 @{data?.handle} 175 - {data?.handleIsCorrect ? "" : " (invalid handle)"} 210 + {data?.handleIsCorrect === false && ( 211 + <span className="text-orange-600 dark:text-orange-400"> 212 + (unverified handle) 213 + </span> 214 + )} 176 215 </span> 177 - </h1> 178 - 179 - {data?.collections && ( 180 - <div className="flex flex-row pb-2"> 216 + </ClickToCopy> 217 + {data?.collections && identity && ( 218 + <div className="flex flex-row pb-2 flex-wrap gap-0"> 181 219 <RepoIcons 182 - collections={data?.collections} 183 - handle={data?.handle} 184 - did={identity?.identity.id} 220 + collections={data.collections} 221 + handle={data.handle} 222 + did={identity.identity.id} 185 223 /> 186 224 </div> 187 225 )} 188 - <code>{data?.did}</code> 189 - <br /> 190 - 191 - <div> 192 - PDS:{" "} 193 - {identity?.identity.pds.hostname.includes("bsky.network") && "🍄"}{" "} 194 - {identity?.identity.pds.hostname} 195 - </div> 196 - 197 - <div> 198 - <h2 className="text-xl font-bold">Collections</h2> 199 - <ul> 200 - {data?.collections.map((c) => ( 201 - <li key={c} className="text-blue-500"> 202 - <Link 203 - to="/at:/$handle/$collection" 204 - params={{ 205 - handle: handle, 206 - collection: c, 207 - }} 226 + {data?.did && ( 227 + <ClickToCopy 228 + className="block text-sm text-muted-foreground break-all" 229 + value={data.did} 230 + > 231 + {data.did} 232 + </ClickToCopy> 233 + )} 234 + {identity?.identity.pds && ( 235 + <ClickToCopy 236 + className="text-sm" 237 + value={identity.identity.pds.hostname} 238 + > 239 + PDS:{" "} 240 + {identity.identity.pds.hostname.includes("bsky.network") && "🍄"}{" "} 241 + <a 242 + href={`https://${identity.identity.pds.hostname}`} 243 + target="_blank" 244 + rel="noopener noreferrer" 245 + className="text-blue-500 hover:underline" 246 + > 247 + {identity.identity.pds.hostname} 248 + </a> 249 + </ClickToCopy> 250 + )} 251 + {data?.collections && data.collections.length > 0 && ( 252 + <div className="pt-2"> 253 + <h2 className="text-xl font-bold mb-1">Collections</h2> 254 + <ul className="list-inside space-y-1"> 255 + {data.collections.map((c) => ( 256 + <li 257 + key={c} 258 + className="text-blue-500 hover:no-underline border-b hover:border-border border-transparent w-min" 208 259 > 209 - {c} 210 - </Link> 211 - </li> 212 - ))} 213 - </ul> 214 - </div> 215 - <div className="pt-2"> 216 - <h2 className="text-xl font-bold">DID Document</h2> 217 - <div className="w-full overflow-x-auto"> 218 - <RenderJson 219 - data={didDoc} 220 - did={identity?.identity.id!} 221 - pds={identity?.identity.pds.toString()!} 222 - /> 260 + <Link 261 + to="/at:/$handle/$collection" 262 + params={{ 263 + handle: handle, // Use original handle for navigation consistency 264 + collection: c, 265 + }} 266 + > 267 + {c} 268 + </Link> 269 + </li> 270 + ))} 271 + </ul> 272 + </div> 273 + )} 274 + {/* Collapsible DID Document Section */} 275 + {didDoc && identity && ( 276 + <Accordion 277 + type="single" 278 + collapsible 279 + className="w-full pt-2" 280 + value={didDocOpenValue} 281 + onValueChange={setDidDocOpenValue} 282 + > 283 + <AccordionItem value="did-doc"> 284 + <AccordionTrigger className="text-2xl font-semibold hover:no-underline py-2"> 285 + DID Document 286 + </AccordionTrigger> 287 + <AccordionContent> 288 + <div className="w-full overflow-x-auto rounded-md border bg-muted/30 p-2 mt-1"> 289 + {" "} 290 + {/* Added background and padding */} 291 + <RenderJson 292 + data={didDoc} 293 + did={identity.identity.id} 294 + pds={identity.identity.pds.toString()} 295 + /> 296 + </div> 297 + </AccordionContent> 298 + </AccordionItem> 299 + </Accordion> 300 + )} 301 + {/* Backlinks Section */} 302 + {data?.did && ( 303 + <div className="pt-4 pb-8 flex flex-col gap-2"> 304 + <AllBacklinksViewer aturi={`at://${data.did}`} /> 223 305 </div> 224 - </div> 306 + )} 225 307 </div> 226 308 </div> 227 309 );
+10 -3
src/routes/at:/$handle/$collection.$rkey.lazy.tsx
··· 56 56 // we dont use the main authenticated client here 57 57 const rpc = new QtClient(id.identity.pds); 58 58 // get the PDS 59 - const response = await rpc 59 + // Start both requests without awaiting them individually 60 + const getRecordPromise = rpc 60 61 .getXrpcClient() 61 62 .get("com.atproto.repo.getRecord", { 62 63 params: { repo: id.identity.id, collection, rkey }, 63 64 signal: abortController.signal, 64 65 }); 65 - // get the if we don't have it 66 - const record = await rpc 66 + 67 + const describeRepoPromise = rpc 67 68 .getXrpcClient() 68 69 .get("com.atproto.repo.describeRepo", { 69 70 params: { repo: id.identity.id }, 70 71 signal: abortController.signal, 71 72 }); 73 + 74 + // Wait for both promises to complete in parallel 75 + const [response, record] = await Promise.all([ 76 + getRecordPromise, 77 + describeRepoPromise, 78 + ]); 72 79 // todo: actual errors 73 80 setState({ 74 81 data: response.data,
-1
src/routes/at:/$handle/$collection.index.lazy.tsx
··· 43 43 try { 44 44 id = await resolveFromIdentity(handle); 45 45 } catch (err: any) { 46 - console.log("BSLKDJFSL"); 47 46 throw new Error("Unable to resolve identity: " + err.message); 48 47 } 49 48 const rpc = new QtClient(id.identity.pds);