Fork of atp.tools as a universal profile for people on the ATmosphere
1import { useEffect, useState } from "react";
2import {
3 Card,
4 CardContent,
5 CardDescription,
6 CardHeader,
7 CardTitle,
8} from "@/components/ui/card";
9import { Skeleton } from "@/components/ui/skeleton";
10import { Link } from "@tanstack/react-router";
11
12interface LinkData {
13 links: {
14 [key: string]: {
15 [key: string]: {
16 records: number;
17 distinct_dids: number;
18 };
19 };
20 };
21}
22
23export 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 }, [aturi]);
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-sm 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 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 className="text-muted-foreground text-xs">
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 view the api response
162 </a>
163 .
164 </span>
165 </div>
166 )}
167 </div>
168 </>
169 );
170}
171
172// Helper function to format category names
173const 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
182const formatStatName = (name: string) => {
183 return name.split(".").filter(Boolean).join(".");
184};
185
186export default AllBacklinksViewer;