alternative tangled frontend (extremely wip)

feat: fetch repos

serenity 374eea5a 06a635ab

+120 -12
+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 + import { useReposQuery } from "@/lib/queries/get-repos"; 5 6 import { useMiniDoc } from "@/lib/queries/resolve-minidoc"; 7 + import { useState } from "react"; 6 8 7 9 export const ProfileOverview = ({ identifier }: { identifier: string }) => { 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 + 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 + }); 29 41 30 42 const isLoading = 31 43 isMiniDocLoading || ··· 33 45 isAvatarLoading || 34 46 !avatarQueryData || 35 47 isProfileLoading || 36 - !profileQueryData; 37 - const err = miniDocQueryErr ?? avatarQueryErr ?? profileQueryErr; 48 + !profileQueryData || 49 + isReposLoading || 50 + !reposQueryData; 51 + const err = 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 - <div className="bg-surface0 w-fit"> 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 - if (!did || !repoUrl) return undefined; 81 + if (!did || !repoUrl) return {}; 82 82 return getProfile({ did, repoUrl }); 83 83 }, 84 84 });
+77
src/lib/queries/get-repos.ts
··· 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
··· 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 28 }> = [ 29 29 { 30 30 to: `/${identifier}`, 31 - icon: <LucideBookOpen height={18} width={18} />, 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 - icon: <LucideBookMarked height={18} width={18} />, 38 - label: "Repos", 37 + icon: <LucideBookMarked height={16} width={16} />, 38 + label: "Repositories", 39 39 isCurrent: currTab === "repos", 40 40 }, 41 41 { 42 - to: `/${identifier}?tab=starred`, 43 - icon: <LucideStar height={18} width={18} />, 44 - label: "Starred", 45 - isCurrent: currTab === "starred", 42 + to: `/${identifier}?tab=stars`, 43 + icon: <LucideStar height={16} width={16} />, 44 + label: "Stars", 45 + isCurrent: currTab === "stars", 46 46 }, 47 47 ]; 48 48 ··· 66 66 icon={icon} 67 67 label={label} 68 68 underlineClassName="bg-text" 69 - labelClassName={`text-text ${isCurrent ? "font-semibold" : ""}`} 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" : ""}`}