tangled
alpha
login
or
join now
isuggest.selfce.st
/
strand
3
fork
atom
alternative tangled frontend (extremely wip)
3
fork
atom
overview
issues
pulls
pipelines
feat: fetch repos
serenity
2 weeks ago
374eea5a
06a635ab
+120
-12
5 changed files
expand all
collapse all
unified
split
src
components
Profile
ProfileOverview.tsx
lib
queries
get-profile.ts
get-repos.ts
types
lexicons
sh
tangled
repo.ts
routes
_layout
$identifier
index.tsx
+18
-3
src/components/Profile/ProfileOverview.tsx
···
2
import { Avatar } from "@/components/Profile/Avatar";
3
import { useAvatarQuery } from "@/lib/queries/get-avatar";
4
import { useProfileQuery } from "@/lib/queries/get-profile";
0
5
import { useMiniDoc } from "@/lib/queries/resolve-minidoc";
0
6
7
export const ProfileOverview = ({ identifier }: { identifier: string }) => {
0
8
const {
9
isLoading: isMiniDocLoading,
10
error: miniDocQueryErr,
···
26
did: miniDocQueryData?.did ?? null,
27
repoUrl: miniDocQueryData ? new URL(miniDocQueryData.pds) : null,
28
});
0
0
0
0
0
0
0
0
0
29
30
const isLoading =
31
isMiniDocLoading ||
···
33
isAvatarLoading ||
34
!avatarQueryData ||
35
isProfileLoading ||
36
-
!profileQueryData;
37
-
const err = miniDocQueryErr ?? avatarQueryErr ?? profileQueryErr;
0
0
0
38
39
if (isLoading) return <Loading />;
40
if (err) return <p>{err.message}</p>;
···
42
const avatarUri = avatarQueryData;
43
44
return (
45
-
<div className="bg-surface0 w-fit">
46
<Avatar
47
uri={avatarUri}
48
className="outline-overlay0 h-48 rounded-full outline"
···
2
import { Avatar } from "@/components/Profile/Avatar";
3
import { useAvatarQuery } from "@/lib/queries/get-avatar";
4
import { useProfileQuery } from "@/lib/queries/get-profile";
5
+
import { useReposQuery } from "@/lib/queries/get-repos";
6
import { useMiniDoc } from "@/lib/queries/resolve-minidoc";
7
+
import { useState } from "react";
8
9
export const ProfileOverview = ({ identifier }: { identifier: string }) => {
10
+
const [reposQueryCursor, setReposQueryCursor] = useState(null);
11
const {
12
isLoading: isMiniDocLoading,
13
error: miniDocQueryErr,
···
29
did: miniDocQueryData?.did ?? null,
30
repoUrl: miniDocQueryData ? new URL(miniDocQueryData.pds) : null,
31
});
32
+
const {
33
+
isLoading: isReposLoading,
34
+
error: reposQueryErr,
35
+
data: reposQueryData,
36
+
} = useReposQuery({
37
+
did: miniDocQueryData?.did ?? null,
38
+
repoUrl: miniDocQueryData ? new URL(miniDocQueryData.pds) : null,
39
+
cursor: reposQueryCursor,
40
+
});
41
42
const isLoading =
43
isMiniDocLoading ||
···
45
isAvatarLoading ||
46
!avatarQueryData ||
47
isProfileLoading ||
48
+
!profileQueryData ||
49
+
isReposLoading ||
50
+
!reposQueryData;
51
+
const err =
52
+
miniDocQueryErr ?? avatarQueryErr ?? profileQueryErr ?? reposQueryErr;
53
54
if (isLoading) return <Loading />;
55
if (err) return <p>{err.message}</p>;
···
57
const avatarUri = avatarQueryData;
58
59
return (
60
+
<div className="bg-surface0 flex w-fit flex-col pt-8">
61
<Avatar
62
uri={avatarUri}
63
className="outline-overlay0 h-48 rounded-full outline"
+1
-1
src/lib/queries/get-profile.ts
···
78
return useQuery({
79
queryKey: profileQueryKey(did),
80
queryFn: () => {
81
-
if (!did || !repoUrl) return undefined;
82
return getProfile({ did, repoUrl });
83
},
84
});
···
78
return useQuery({
79
queryKey: profileQueryKey(did),
80
queryFn: () => {
81
+
if (!did || !repoUrl) return {};
82
return getProfile({ did, repoUrl });
83
},
84
});
+77
src/lib/queries/get-repos.ts
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { err } from "@/lib/result";
2
+
import { comAtprotoRepoGetRecordOutputSchema } from "@/lib/types/lexicons/com/atproto/repo/getRecord";
3
+
import { shTangledRepoSchema } from "@/lib/types/lexicons/sh/tangled/repo";
4
+
import { useQuery } from "@tanstack/react-query";
5
+
import { z } from "zod/v4";
6
+
7
+
export const getRepos = async ({
8
+
did,
9
+
repoUrl,
10
+
cursor,
11
+
}: {
12
+
did: string;
13
+
repoUrl: URL;
14
+
cursor: string | null;
15
+
}) => {
16
+
const repoUrlString = repoUrl.toString();
17
+
const cleanedUrl = repoUrlString.endsWith("/")
18
+
? repoUrlString.substring(0, repoUrlString.length - 1)
19
+
: repoUrlString;
20
+
21
+
const cursorParam = cursor ? `&cursor=${cursor}` : "";
22
+
23
+
const reposReq = new Request(
24
+
`${cleanedUrl}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=sh.tangled.repo&limit=20${cursorParam}`,
25
+
);
26
+
const res = await fetch(reposReq);
27
+
if (!res.ok)
28
+
throw new Error(`Fetching repos from PDS ${cleanedUrl} failed.`);
29
+
const data: unknown = await res.json();
30
+
31
+
const {
32
+
success,
33
+
error,
34
+
data: parseData,
35
+
} = z
36
+
.object({
37
+
cursor: z.string().optional(),
38
+
records: z.array(
39
+
comAtprotoRepoGetRecordOutputSchema(shTangledRepoSchema),
40
+
),
41
+
})
42
+
.safeParse(data);
43
+
44
+
console.log({ success, error, parseData });
45
+
46
+
if (!success) {
47
+
throw new Error(error.message);
48
+
}
49
+
50
+
return parseData.records;
51
+
};
52
+
53
+
const reposQueryKey = ({
54
+
did,
55
+
cursor,
56
+
}: {
57
+
did: string | null;
58
+
cursor: string | null;
59
+
}) => (cursor ? ["repos", did, cursor] : ["repos", did]);
60
+
61
+
export const useReposQuery = ({
62
+
did,
63
+
repoUrl,
64
+
cursor,
65
+
}: {
66
+
did: string | null;
67
+
repoUrl: URL | null;
68
+
cursor: string | null;
69
+
}) => {
70
+
return useQuery({
71
+
queryKey: reposQueryKey({ did, cursor }),
72
+
queryFn: () => {
73
+
if (!did || !repoUrl) return {};
74
+
return getRepos({ did, repoUrl, cursor });
75
+
},
76
+
});
77
+
};
+16
src/lib/types/lexicons/sh/tangled/repo.ts
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { z } from "zod/v4";
2
+
3
+
export const shTangledRepoSchema = z.object({
4
+
name: z.string(),
5
+
knot: z.string(),
6
+
createdAt: z.iso.datetime(),
7
+
8
+
spindle: z.string().optional(),
9
+
description: z.string().optional(),
10
+
website: z.url().optional(),
11
+
topics: z.array(z.string().min(1).max(50)).max(50).optional(),
12
+
source: z.string().optional(),
13
+
labels: z.array(z.string()).optional(),
14
+
});
15
+
16
+
export type ShTangledRepo = z.infer<typeof shTangledRepoSchema>;
+8
-8
src/routes/_layout/$identifier/index.tsx
···
28
}> = [
29
{
30
to: `/${identifier}`,
31
-
icon: <LucideBookOpen height={18} width={18} />,
32
label: "Overview",
33
isCurrent: currTab === "overview",
34
},
35
{
36
to: `/${identifier}?tab=repos`,
37
-
icon: <LucideBookMarked height={18} width={18} />,
38
-
label: "Repos",
39
isCurrent: currTab === "repos",
40
},
41
{
42
-
to: `/${identifier}?tab=starred`,
43
-
icon: <LucideStar height={18} width={18} />,
44
-
label: "Starred",
45
-
isCurrent: currTab === "starred",
46
},
47
];
48
···
66
icon={icon}
67
label={label}
68
underlineClassName="bg-text"
69
-
labelClassName={`text-text ${isCurrent ? "font-semibold" : ""}`}
70
iconClassName="text-text"
71
iconVariants={{}}
72
className={`hover:bg-surface1 rounded-t-lg p-2.5 px-3 transition-all ${isCurrent ? "bg-surface0" : ""}`}
···
28
}> = [
29
{
30
to: `/${identifier}`,
31
+
icon: <LucideBookOpen height={16} width={16} />,
32
label: "Overview",
33
isCurrent: currTab === "overview",
34
},
35
{
36
to: `/${identifier}?tab=repos`,
37
+
icon: <LucideBookMarked height={16} width={16} />,
38
+
label: "Repositories",
39
isCurrent: currTab === "repos",
40
},
41
{
42
+
to: `/${identifier}?tab=stars`,
43
+
icon: <LucideStar height={16} width={16} />,
44
+
label: "Stars",
45
+
isCurrent: currTab === "stars",
46
},
47
];
48
···
66
icon={icon}
67
label={label}
68
underlineClassName="bg-text"
69
+
labelClassName={`text-text pl-1 ${isCurrent ? "font-semibold" : ""}`}
70
iconClassName="text-text"
71
iconVariants={{}}
72
className={`hover:bg-surface1 rounded-t-lg p-2.5 px-3 transition-all ${isCurrent ? "bg-surface0" : ""}`}