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
facelift profile page
Natalie B
11 months ago
a68f66f8
b83ec791
+453
-66
10 changed files
expand all
collapse all
unified
split
package.json
pnpm-lock.yaml
src
components
allBacklinksViewer.tsx
ui
accordion.tsx
click-to-copy.tsx
views
app-bsky
embed.tsx
index.css
routes
at:
$handle
$collection.$rkey.lazy.tsx
$collection.index.lazy.tsx
$handle.index.tsx
+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
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
23
+
'@radix-ui/react-accordion':
24
24
+
specifier: ^1.2.8
25
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
504
+
'@radix-ui/react-accordion@1.2.8':
505
505
+
resolution: {integrity: sha512-c7OKBvO36PfQIUGIjj1Wko0hH937pYFU2tR5zbIJDUsmTzHoZVHHt4bmb7OOJbzTaWJtVELKWojBHa7OcnUHmQ==}
506
506
+
peerDependencies:
507
507
+
'@types/react': '*'
508
508
+
'@types/react-dom': '*'
509
509
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
510
510
+
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
511
511
+
peerDependenciesMeta:
512
512
+
'@types/react':
513
513
+
optional: true
514
514
+
'@types/react-dom':
515
515
+
optional: true
516
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
543
+
'@radix-ui/react-collapsible@1.1.8':
544
544
+
resolution: {integrity: sha512-hxEsLvK9WxIAPyxdDRULL4hcaSjMZCfP7fHB0Z1uUnDoDBat1Zh46hwYfa69DeZAbJrPckjf0AGAtEZyvDyJbw==}
545
545
+
peerDependencies:
546
546
+
'@types/react': '*'
547
547
+
'@types/react-dom': '*'
548
548
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
549
549
+
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
550
550
+
peerDependenciesMeta:
551
551
+
'@types/react':
552
552
+
optional: true
553
553
+
'@types/react-dom':
554
554
+
optional: true
555
555
+
527
556
'@radix-ui/react-collection@1.1.3':
528
557
resolution: {integrity: sha512-mM2pxoQw5HJ49rkzwOs7Y6J4oYH22wS8BfK2/bBxROlI4xuR0c4jEenQP63LlTlDkO6Buj2Vt+QYAYcOgqtrXA==}
558
558
+
peerDependencies:
559
559
+
'@types/react': '*'
560
560
+
'@types/react-dom': '*'
561
561
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
562
562
+
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
563
563
+
peerDependenciesMeta:
564
564
+
'@types/react':
565
565
+
optional: true
566
566
+
'@types/react-dom':
567
567
+
optional: true
568
568
+
569
569
+
'@radix-ui/react-collection@1.1.4':
570
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
744
+
'@radix-ui/react-presence@1.1.4':
745
745
+
resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==}
746
746
+
peerDependencies:
747
747
+
'@types/react': '*'
748
748
+
'@types/react-dom': '*'
749
749
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
750
750
+
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
751
751
+
peerDependenciesMeta:
752
752
+
'@types/react':
753
753
+
optional: true
754
754
+
'@types/react-dom':
755
755
+
optional: true
756
756
+
702
757
'@radix-ui/react-primitive@2.0.3':
703
758
resolution: {integrity: sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==}
759
759
+
peerDependencies:
760
760
+
'@types/react': '*'
761
761
+
'@types/react-dom': '*'
762
762
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
763
763
+
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
764
764
+
peerDependenciesMeta:
765
765
+
'@types/react':
766
766
+
optional: true
767
767
+
'@types/react-dom':
768
768
+
optional: true
769
769
+
770
770
+
'@radix-ui/react-primitive@2.1.0':
771
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
881
+
peerDependencies:
882
882
+
'@types/react': '*'
883
883
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
884
884
+
peerDependenciesMeta:
885
885
+
'@types/react':
886
886
+
optional: true
887
887
+
888
888
+
'@radix-ui/react-use-controllable-state@1.2.2':
889
889
+
resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
890
890
+
peerDependencies:
891
891
+
'@types/react': '*'
892
892
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
893
893
+
peerDependenciesMeta:
894
894
+
'@types/react':
895
895
+
optional: true
896
896
+
897
897
+
'@radix-ui/react-use-effect-event@0.0.2':
898
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
2528
+
'@radix-ui/react-accordion@1.2.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
2529
2529
+
dependencies:
2530
2530
+
'@radix-ui/primitive': 1.1.2
2531
2531
+
'@radix-ui/react-collapsible': 1.1.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
2532
2532
+
'@radix-ui/react-collection': 1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
2533
2533
+
'@radix-ui/react-compose-refs': 1.1.2(react@19.0.0)
2534
2534
+
'@radix-ui/react-context': 1.1.2(react@19.0.0)
2535
2535
+
'@radix-ui/react-direction': 1.1.1(react@19.0.0)
2536
2536
+
'@radix-ui/react-id': 1.1.1(react@19.0.0)
2537
2537
+
'@radix-ui/react-primitive': 2.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
2538
2538
+
'@radix-ui/react-use-controllable-state': 1.2.2(react@19.0.0)
2539
2539
+
react: 19.0.0
2540
2540
+
react-dom: 19.0.0(react@19.0.0)
2541
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
2557
+
'@radix-ui/react-collapsible@1.1.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
2558
2558
+
dependencies:
2559
2559
+
'@radix-ui/primitive': 1.1.2
2560
2560
+
'@radix-ui/react-compose-refs': 1.1.2(react@19.0.0)
2561
2561
+
'@radix-ui/react-context': 1.1.2(react@19.0.0)
2562
2562
+
'@radix-ui/react-id': 1.1.1(react@19.0.0)
2563
2563
+
'@radix-ui/react-presence': 1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
2564
2564
+
'@radix-ui/react-primitive': 2.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
2565
2565
+
'@radix-ui/react-use-controllable-state': 1.2.2(react@19.0.0)
2566
2566
+
'@radix-ui/react-use-layout-effect': 1.1.1(react@19.0.0)
2567
2567
+
react: 19.0.0
2568
2568
+
react-dom: 19.0.0(react@19.0.0)
2569
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
2579
+
'@radix-ui/react-collection@1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
2580
2580
+
dependencies:
2581
2581
+
'@radix-ui/react-compose-refs': 1.1.2(react@19.0.0)
2582
2582
+
'@radix-ui/react-context': 1.1.2(react@19.0.0)
2583
2583
+
'@radix-ui/react-primitive': 2.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
2584
2584
+
'@radix-ui/react-slot': 1.2.0(react@19.0.0)
2585
2585
+
react: 19.0.0
2586
2586
+
react-dom: 19.0.0(react@19.0.0)
2587
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
2730
+
'@radix-ui/react-presence@1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
2731
2731
+
dependencies:
2732
2732
+
'@radix-ui/react-compose-refs': 1.1.2(react@19.0.0)
2733
2733
+
'@radix-ui/react-use-layout-effect': 1.1.1(react@19.0.0)
2734
2734
+
react: 19.0.0
2735
2735
+
react-dom: 19.0.0(react@19.0.0)
2736
2736
+
2608
2737
'@radix-ui/react-primitive@2.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
2738
2738
+
dependencies:
2739
2739
+
'@radix-ui/react-slot': 1.2.0(react@19.0.0)
2740
2740
+
react: 19.0.0
2741
2741
+
react-dom: 19.0.0(react@19.0.0)
2742
2742
+
2743
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
2830
+
react: 19.0.0
2831
2831
+
2832
2832
+
'@radix-ui/react-use-controllable-state@1.2.2(react@19.0.0)':
2833
2833
+
dependencies:
2834
2834
+
'@radix-ui/react-use-effect-event': 0.0.2(react@19.0.0)
2835
2835
+
'@radix-ui/react-use-layout-effect': 1.1.1(react@19.0.0)
2836
2836
+
react: 19.0.0
2837
2837
+
2838
2838
+
'@radix-ui/react-use-effect-event@0.0.2(react@19.0.0)':
2839
2839
+
dependencies:
2840
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
83
-
<div className="text-lg text-muted-foreground">
83
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
155
-
<span className="text-muted-foreground">
155
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
1
+
"use client"
2
2
+
3
3
+
import * as React from "react"
4
4
+
import * as AccordionPrimitive from "@radix-ui/react-accordion"
5
5
+
import { ChevronDown } from "lucide-react"
6
6
+
7
7
+
import { cn } from "@/lib/utils"
8
8
+
9
9
+
const Accordion = AccordionPrimitive.Root
10
10
+
11
11
+
const AccordionItem = React.forwardRef<
12
12
+
React.ElementRef<typeof AccordionPrimitive.Item>,
13
13
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
14
14
+
>(({ className, ...props }, ref) => (
15
15
+
<AccordionPrimitive.Item
16
16
+
ref={ref}
17
17
+
className={cn("border-b", className)}
18
18
+
{...props}
19
19
+
/>
20
20
+
))
21
21
+
AccordionItem.displayName = "AccordionItem"
22
22
+
23
23
+
const AccordionTrigger = React.forwardRef<
24
24
+
React.ElementRef<typeof AccordionPrimitive.Trigger>,
25
25
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
26
26
+
>(({ className, children, ...props }, ref) => (
27
27
+
<AccordionPrimitive.Header className="flex">
28
28
+
<AccordionPrimitive.Trigger
29
29
+
ref={ref}
30
30
+
className={cn(
31
31
+
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
32
32
+
className
33
33
+
)}
34
34
+
{...props}
35
35
+
>
36
36
+
{children}
37
37
+
<ChevronDown className="h-4 w-4 transition-transform duration-200" />
38
38
+
</AccordionPrimitive.Trigger>
39
39
+
</AccordionPrimitive.Header>
40
40
+
))
41
41
+
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42
42
+
43
43
+
const AccordionContent = React.forwardRef<
44
44
+
React.ElementRef<typeof AccordionPrimitive.Content>,
45
45
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
46
46
+
>(({ className, children, ...props }, ref) => (
47
47
+
<AccordionPrimitive.Content
48
48
+
ref={ref}
49
49
+
className={cn(
50
50
+
"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
51
51
+
className
52
52
+
)}
53
53
+
{...props}
54
54
+
>
55
55
+
<div className="pb-4 pt-0">{children}</div>
56
56
+
</AccordionPrimitive.Content>
57
57
+
))
58
58
+
AccordionContent.displayName = AccordionPrimitive.Content.displayName
59
59
+
60
60
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
+84
src/components/ui/click-to-copy.tsx
···
1
1
+
import { useState } from "preact/compat";
2
2
+
import { Check, Copy } from "lucide-react";
3
3
+
import { Button } from "@/components/ui/button"; // Assuming you have a Button component from shadcn/ui
4
4
+
import { cn } from "@/lib/utils"; // Assuming you have a utility for class names
5
5
+
6
6
+
interface ClickToCopyProps {
7
7
+
value?: string;
8
8
+
children?: any; // Allow wrapping other elements if needed, defaults to showing the value
9
9
+
className?: string;
10
10
+
buttonClassName?: string;
11
11
+
iconSize?: number;
12
12
+
}
13
13
+
14
14
+
export function ClickToCopy({
15
15
+
value,
16
16
+
children,
17
17
+
className,
18
18
+
buttonClassName,
19
19
+
iconSize = 16, // Default icon size
20
20
+
}: ClickToCopyProps) {
21
21
+
//const [isHovering, setIsHovering] = useState(false);
22
22
+
const [isCopied, setIsCopied] = useState(false);
23
23
+
24
24
+
if (!value) return children;
25
25
+
26
26
+
const handleCopy = async (event: MouseEvent) => {
27
27
+
event.stopPropagation(); // Prevent potential parent click handlers
28
28
+
try {
29
29
+
await navigator.clipboard.writeText(value);
30
30
+
setIsCopied(true);
31
31
+
setTimeout(() => setIsCopied(false), 1500);
32
32
+
} catch (err) {
33
33
+
console.error("Failed to copy text: ", err);
34
34
+
}
35
35
+
};
36
36
+
37
37
+
const buttonLabel = isCopied ? "Copied!" : "Copy to clipboard";
38
38
+
39
39
+
return (
40
40
+
<div
41
41
+
className={cn("relative flex items-center group", className)}
42
42
+
//onMouseEnter={() => setIsHovering(true)}
43
43
+
//onMouseLeave={() => setIsHovering(false)}
44
44
+
>
45
45
+
<Button
46
46
+
variant="link"
47
47
+
size="icon"
48
48
+
className={cn(
49
49
+
"absolute -left-8 top-1/2 transform -translate-y-1/2 p-1 h-full w-auto bg-green-500",
50
50
+
"opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity duration-150",
51
51
+
isCopied
52
52
+
? "text-green-600 hover:text-green-700"
53
53
+
: "text-muted-foreground hover:text-foreground",
54
54
+
buttonClassName,
55
55
+
)}
56
56
+
onClick={handleCopy}
57
57
+
aria-label={buttonLabel}
58
58
+
title={buttonLabel}
59
59
+
>
60
60
+
<div className="relative flex items-center justify-center">
61
61
+
<Copy
62
62
+
size={iconSize}
63
63
+
className={cn(
64
64
+
"transition-all duration-200 ease-in-out",
65
65
+
isCopied ? "opacity-0 scale-75" : "opacity-100 scale-100",
66
66
+
)}
67
67
+
/>
68
68
+
{/* Check Icon */}
69
69
+
<Check
70
70
+
size={iconSize}
71
71
+
className={cn(
72
72
+
"absolute transition-all duration-200 ease-in-out",
73
73
+
isCopied ? "opacity-100 scale-100" : "opacity-0 scale-75",
74
74
+
)}
75
75
+
/>
76
76
+
</div>
77
77
+
</Button>
78
78
+
79
79
+
<span className="truncate">{children ?? value}</span>
80
80
+
</div>
81
81
+
);
82
82
+
}
83
83
+
84
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
115
-
const response = await rpc
115
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
121
-
const actor = await rpc
121
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
127
+
const [response, actor] = await Promise.all([
128
128
+
responsePromise,
129
129
+
actorPromise,
130
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
204
+
205
205
+
* {
206
206
+
text-decoration-line: none !important;
207
207
+
}
+140
-58
src/routes/at:/$handle.index.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 RepoIcons from "@/components/repoIcons";
5
5
+
import {
6
6
+
Accordion,
7
7
+
AccordionContent,
8
8
+
AccordionItem,
9
9
+
AccordionTrigger,
10
10
+
} from "@/components/ui/accordion"; // Import Shadcn accordion components
11
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
14
+
import { useStoredState } from "@/hooks/useStoredState"; // Import the hook
6
15
import getDidDoc from "@/lib/getDidDoc";
7
7
-
import { QtClient, useXrpc } from "@/providers/qtprovider";
16
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
36
-
const xrpc = useXrpc();
45
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
52
-
setState((prev) => ({ ...prev, isLoading: true }));
61
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
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
74
-
console.log("sdf");
75
82
console.error("Failed to fetch DID document:", error);
83
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
79
-
// reuse client dumbass
87
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
94
+
signal: abortController.signal, // Pass signal here too
86
95
});
87
96
88
97
setState({
···
103
112
error: null,
104
113
});
105
114
}
106
106
-
// todo: actual errors
107
115
} catch (err: any) {
108
116
if (err.name === "AbortError") return;
109
117
118
118
+
console.error("Failed to fetch repo data:", err); // Log the error for debugging
110
119
setState({
111
120
data: undefined,
121
121
+
blueSkyData: undefined, // Clear blueSkyData on error
122
122
+
identity: undefined, // Clear identity on error
112
123
isLoading: false,
113
113
-
error: err instanceof Error ? err : new Error("An error occurred"),
124
124
+
didDoc: undefined, // Clear didDoc on error
125
125
+
error:
126
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
123
-
}, [handle, xrpc]);
136
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
149
+
150
150
+
// State for accordion collapse, using the handle in the key for uniqueness
151
151
+
const [didDocOpenValue, setDidDocOpenValue] = useStoredState<string>(
152
152
+
`did-doc-open-${handle}`, // Unique key per handle
153
153
+
"", // Default to closed (empty string value)
154
154
+
);
155
155
+
136
156
if (error) {
137
157
return <ShowError error={error} />;
138
158
}
139
159
140
140
-
if (isLoading && !blueSkyData) {
160
160
+
// Show loader only if essential data (repo description) is still loading
161
161
+
if (isLoading && !data) {
141
162
return <Loader className="max-h-[calc(100vh-5rem)] h-screen" />;
142
163
}
143
164
165
165
+
// Handle case where repo description loaded but identity resolution failed earlier
166
166
+
if (!identity && !isLoading) {
167
167
+
return (
168
168
+
<ShowError
169
169
+
error={
170
170
+
new Error("Failed to resolve identity or fetch repository data.")
171
171
+
}
172
172
+
/>
173
173
+
);
174
174
+
}
175
175
+
144
176
return (
145
145
-
<div className="flex flex-row justify-center w-full max-h-[calc(100vh-5rem)]">
146
146
-
<div className="max-w-md lg:max-w-2xl w-[90vw] mx-4 md:mt-16 space-y-2">
177
177
+
<div className="flex flex-row justify-center w-full min-h-[calc(100vh-5rem)]">
178
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
171
-
<h1 className="text-2xl md:text-3xl font-bold">
172
172
-
{blueSkyData?.displayName}{" "}
173
173
-
<span className="text-muted-foreground font-normal">
203
203
+
<ClickToCopy
204
204
+
className="text-2xl md:text-3xl font-bold pt-2"
205
205
+
value={data?.handle}
206
206
+
>
207
207
+
{blueSkyData?.displayName || data?.handle}{" "}
208
208
+
<span className="text-muted-foreground font-normal block md:inline">
174
209
@{data?.handle}
175
175
-
{data?.handleIsCorrect ? "" : " (invalid handle)"}
210
210
+
{data?.handleIsCorrect === false && (
211
211
+
<span className="text-orange-600 dark:text-orange-400">
212
212
+
(unverified handle)
213
213
+
</span>
214
214
+
)}
176
215
</span>
177
177
-
</h1>
178
178
-
179
179
-
{data?.collections && (
180
180
-
<div className="flex flex-row pb-2">
216
216
+
</ClickToCopy>
217
217
+
{data?.collections && identity && (
218
218
+
<div className="flex flex-row pb-2 flex-wrap gap-0">
181
219
<RepoIcons
182
182
-
collections={data?.collections}
183
183
-
handle={data?.handle}
184
184
-
did={identity?.identity.id}
220
220
+
collections={data.collections}
221
221
+
handle={data.handle}
222
222
+
did={identity.identity.id}
185
223
/>
186
224
</div>
187
225
)}
188
188
-
<code>{data?.did}</code>
189
189
-
<br />
190
190
-
191
191
-
<div>
192
192
-
PDS:{" "}
193
193
-
{identity?.identity.pds.hostname.includes("bsky.network") && "🍄"}{" "}
194
194
-
{identity?.identity.pds.hostname}
195
195
-
</div>
196
196
-
197
197
-
<div>
198
198
-
<h2 className="text-xl font-bold">Collections</h2>
199
199
-
<ul>
200
200
-
{data?.collections.map((c) => (
201
201
-
<li key={c} className="text-blue-500">
202
202
-
<Link
203
203
-
to="/at:/$handle/$collection"
204
204
-
params={{
205
205
-
handle: handle,
206
206
-
collection: c,
207
207
-
}}
226
226
+
{data?.did && (
227
227
+
<ClickToCopy
228
228
+
className="block text-sm text-muted-foreground break-all"
229
229
+
value={data.did}
230
230
+
>
231
231
+
{data.did}
232
232
+
</ClickToCopy>
233
233
+
)}
234
234
+
{identity?.identity.pds && (
235
235
+
<ClickToCopy
236
236
+
className="text-sm"
237
237
+
value={identity.identity.pds.hostname}
238
238
+
>
239
239
+
PDS:{" "}
240
240
+
{identity.identity.pds.hostname.includes("bsky.network") && "🍄"}{" "}
241
241
+
<a
242
242
+
href={`https://${identity.identity.pds.hostname}`}
243
243
+
target="_blank"
244
244
+
rel="noopener noreferrer"
245
245
+
className="text-blue-500 hover:underline"
246
246
+
>
247
247
+
{identity.identity.pds.hostname}
248
248
+
</a>
249
249
+
</ClickToCopy>
250
250
+
)}
251
251
+
{data?.collections && data.collections.length > 0 && (
252
252
+
<div className="pt-2">
253
253
+
<h2 className="text-xl font-bold mb-1">Collections</h2>
254
254
+
<ul className="list-inside space-y-1">
255
255
+
{data.collections.map((c) => (
256
256
+
<li
257
257
+
key={c}
258
258
+
className="text-blue-500 hover:no-underline border-b hover:border-border border-transparent w-min"
208
259
>
209
209
-
{c}
210
210
-
</Link>
211
211
-
</li>
212
212
-
))}
213
213
-
</ul>
214
214
-
</div>
215
215
-
<div className="pt-2">
216
216
-
<h2 className="text-xl font-bold">DID Document</h2>
217
217
-
<div className="w-full overflow-x-auto">
218
218
-
<RenderJson
219
219
-
data={didDoc}
220
220
-
did={identity?.identity.id!}
221
221
-
pds={identity?.identity.pds.toString()!}
222
222
-
/>
260
260
+
<Link
261
261
+
to="/at:/$handle/$collection"
262
262
+
params={{
263
263
+
handle: handle, // Use original handle for navigation consistency
264
264
+
collection: c,
265
265
+
}}
266
266
+
>
267
267
+
{c}
268
268
+
</Link>
269
269
+
</li>
270
270
+
))}
271
271
+
</ul>
272
272
+
</div>
273
273
+
)}
274
274
+
{/* Collapsible DID Document Section */}
275
275
+
{didDoc && identity && (
276
276
+
<Accordion
277
277
+
type="single"
278
278
+
collapsible
279
279
+
className="w-full pt-2"
280
280
+
value={didDocOpenValue}
281
281
+
onValueChange={setDidDocOpenValue}
282
282
+
>
283
283
+
<AccordionItem value="did-doc">
284
284
+
<AccordionTrigger className="text-2xl font-semibold hover:no-underline py-2">
285
285
+
DID Document
286
286
+
</AccordionTrigger>
287
287
+
<AccordionContent>
288
288
+
<div className="w-full overflow-x-auto rounded-md border bg-muted/30 p-2 mt-1">
289
289
+
{" "}
290
290
+
{/* Added background and padding */}
291
291
+
<RenderJson
292
292
+
data={didDoc}
293
293
+
did={identity.identity.id}
294
294
+
pds={identity.identity.pds.toString()}
295
295
+
/>
296
296
+
</div>
297
297
+
</AccordionContent>
298
298
+
</AccordionItem>
299
299
+
</Accordion>
300
300
+
)}
301
301
+
{/* Backlinks Section */}
302
302
+
{data?.did && (
303
303
+
<div className="pt-4 pb-8 flex flex-col gap-2">
304
304
+
<AllBacklinksViewer aturi={`at://${data.did}`} />
223
305
</div>
224
224
-
</div>
306
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
59
-
const response = await rpc
59
59
+
// Start both requests without awaiting them individually
60
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
65
-
// get the if we don't have it
66
66
-
const record = await rpc
66
66
+
67
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
73
+
74
74
+
// Wait for both promises to complete in parallel
75
75
+
const [response, record] = await Promise.all([
76
76
+
getRecordPromise,
77
77
+
describeRepoPromise,
78
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
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);