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