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: trending feed
serenity
3 weeks ago
e8ce5e7b
70a32184
+176
-38
9 changed files
expand all
collapse all
unified
split
.cta.json
src
components
Animated
UnderlineIconRouterLink.tsx
Homepage
TrendingFeed.tsx
Icons
LucideBookMarked.tsx
LucideStar.tsx
Nav
NavBarAuthed.tsx
NavBarUnauthed.tsx
lib
queries
get-trending-from-stitch.ts
types
lexicons
app
bsky
feed
post.ts
+17
-17
.cta.json
···
1
{
2
-
"projectName": "strand",
3
-
"mode": "file-router",
4
-
"typescript": true,
5
-
"tailwind": true,
6
-
"packageManager": "pnpm",
7
-
"addOnOptions": {},
8
-
"git": true,
9
-
"version": 1,
10
-
"framework": "react-cra",
11
-
"chosenAddOns": [
12
-
"eslint",
13
-
"nitro",
14
-
"start",
15
-
"tanstack-query",
16
-
"compiler"
17
-
]
18
-
}
···
1
{
2
+
"projectName": "strand",
3
+
"mode": "file-router",
4
+
"typescript": true,
5
+
"tailwind": true,
6
+
"packageManager": "pnpm",
7
+
"addOnOptions": {},
8
+
"git": true,
9
+
"version": 1,
10
+
"framework": "react-cra",
11
+
"chosenAddOns": [
12
+
"eslint",
13
+
"nitro",
14
+
"start",
15
+
"tanstack-query",
16
+
"compiler"
17
+
]
18
+
}
+1
-1
src/components/Animated/UnderlineIconRouterLink.tsx
···
43
<motion.div initial="initial" whileHover="hover">
44
<Link
45
to={to}
46
-
className={`flex cursor-pointer items-center gap-1 pl-2 ${className}`}
47
target={target}
48
>
49
{position === "left" && iconElement}
···
43
<motion.div initial="initial" whileHover="hover">
44
<Link
45
to={to}
46
+
className={`flex cursor-pointer items-center gap-1 ${className}`}
47
target={target}
48
>
49
{position === "left" && iconElement}
+96
-13
src/components/Homepage/TrendingFeed.tsx
···
1
import { UnderlineLink } from "@/components/Animated/UnderlinedLink";
0
0
0
0
0
0
2
import { Link } from "@tanstack/react-router";
3
4
export const TrendingFeed = () => {
0
0
0
0
0
0
5
return (
6
-
<div>
7
-
<p>
8
-
Powered by{" "}
9
-
<UnderlineLink
10
-
href="https://catsky.social/profile/stitch.selfhosted.social"
11
-
target="_blank"
12
-
underlineColor="bg-accent"
13
-
className="text-accent"
14
-
>
15
-
stitch.selfhosted.social
16
-
</UnderlineLink>
17
-
.
18
-
</p>
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
19
</div>
20
);
21
};
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 { UnderlineLink } from "@/components/Animated/UnderlinedLink";
2
+
import { UnderlineIconRouterLink } from "@/components/Animated/UnderlineIconRouterLink";
3
+
import { Loading } from "@/components/Icons/Loading";
4
+
import { LucideBookMarked } from "@/components/Icons/LucideBookMarked";
5
+
import { LucideStar } from "@/components/Icons/LucideStar";
6
+
import { useTrendingQuery } from "@/lib/queries/get-trending-from-stitch";
7
+
import { AppBskyFeedPost } from "@/lib/types/lexicons/app/bsky/feed/post";
8
import { Link } from "@tanstack/react-router";
9
10
export const TrendingFeed = () => {
11
+
const {
12
+
isLoading: isTrendingLoading,
13
+
error: trendingQueryError,
14
+
data: trendingPosts,
15
+
} = useTrendingQuery();
16
+
17
return (
18
+
<div className="flex flex-col gap-4 p-4">
19
+
{isTrendingLoading ? (
20
+
<>
21
+
<p>
22
+
Powered by{" "}
23
+
<UnderlineLink
24
+
href="https://catsky.social/profile/stitch.selfhosted.social"
25
+
target="_blank"
26
+
underlineColor="bg-accent"
27
+
className="text-accent"
28
+
>
29
+
stitch.selfhosted.social
30
+
</UnderlineLink>
31
+
.
32
+
</p>
33
+
<Loading />
34
+
</>
35
+
) : trendingQueryError ? (
36
+
<p>{trendingQueryError.message}</p>
37
+
) : (
38
+
<>
39
+
<div>
40
+
<h1 className="text-xl font-semibold">Trending</h1>
41
+
<p>
42
+
Powered by{" "}
43
+
<UnderlineLink
44
+
href="https://catsky.social/profile/stitch.selfhosted.social"
45
+
target="_blank"
46
+
underlineColor="bg-accent"
47
+
className="text-accent"
48
+
>
49
+
stitch.selfhosted.social
50
+
</UnderlineLink>
51
+
.
52
+
</p>
53
+
</div>
54
+
<TrendingRepoInfo
55
+
posts={trendingPosts?.map((post) => post.value)}
56
+
/>
57
+
</>
58
+
)}
59
</div>
60
);
61
};
62
+
63
+
const TrendingRepoInfo = ({
64
+
posts,
65
+
}: {
66
+
posts: AppBskyFeedPost[] | undefined;
67
+
}) => {
68
+
if (!posts) return <p>:(</p>;
69
+
70
+
return posts.map((post) => {
71
+
const paragraphs = post.text.split("\n");
72
+
73
+
const repoInfo = paragraphs[0].split(" ");
74
+
const starInfo = paragraphs[1].split(" ");
75
+
76
+
const starCount = starInfo[starInfo.length - 1];
77
+
const repoName = repoInfo[0];
78
+
const repoDesc = repoInfo.slice(1, repoInfo.length).join(" ");
79
+
const repoUrlRelative = `/${repoInfo[0]}`;
80
+
81
+
// because sometimes stitch doesn't post the star counts
82
+
if (isNaN(parseInt(starCount))) return undefined;
83
+
84
+
return (
85
+
<div className="bg-surface0 border-overlay0 flex items-start justify-between rounded-sm border p-4">
86
+
<div>
87
+
<UnderlineIconRouterLink
88
+
to={repoUrlRelative}
89
+
icon={LucideBookMarked({})}
90
+
label={repoName}
91
+
iconClassName="text-text"
92
+
labelClassName="text-text"
93
+
underlineClassName="bg-text"
94
+
/>
95
+
<p className="text-subtext max-w-164">{repoDesc}</p>
96
+
</div>
97
+
<button className="flex items-center gap-1">
98
+
<LucideStar />
99
+
<p>{starCount}</p>
100
+
</button>
101
+
</div>
102
+
);
103
+
});
104
+
};
+25
src/components/Icons/LucideBookMarked.tsx
···
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 { SVGProps } from "react";
2
+
3
+
export function LucideBookMarked(props: SVGProps<SVGSVGElement>) {
4
+
return (
5
+
<svg
6
+
xmlns="http://www.w3.org/2000/svg"
7
+
width="1em"
8
+
height="1em"
9
+
viewBox="0 0 24 24"
10
+
{...props}
11
+
>
12
+
{/* Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE */}
13
+
<g
14
+
fill="none"
15
+
stroke="currentColor"
16
+
strokeLinecap="round"
17
+
strokeLinejoin="round"
18
+
strokeWidth="2"
19
+
>
20
+
<path d="M10 2v8l3-3l3 3V2" />
21
+
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20" />
22
+
</g>
23
+
</svg>
24
+
);
25
+
}
+23
src/components/Icons/LucideStar.tsx
···
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 { SVGProps } from "react";
2
+
3
+
export function LucideStar(props: SVGProps<SVGSVGElement>) {
4
+
return (
5
+
<svg
6
+
xmlns="http://www.w3.org/2000/svg"
7
+
width="1em"
8
+
height="1em"
9
+
viewBox="0 0 24 24"
10
+
{...props}
11
+
>
12
+
{/* Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE */}
13
+
<path
14
+
fill="none"
15
+
stroke="currentColor"
16
+
strokeLinecap="round"
17
+
strokeLinejoin="round"
18
+
strokeWidth="2"
19
+
d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.12 2.12 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.12 2.12 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.12 2.12 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.12 2.12 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.12 2.12 0 0 0 1.597-1.16z"
20
+
/>
21
+
</svg>
22
+
);
23
+
}
+1
-1
src/components/Nav/NavBarAuthed.tsx
···
29
iconClassName="text-text"
30
labelClassName="text-text"
31
underlineClassName="bg-text"
32
-
className="text-lg font-semibold"
33
/>
34
</div>
35
<div className="flex max-h-12 items-center gap-1">
···
29
iconClassName="text-text"
30
labelClassName="text-text"
31
underlineClassName="bg-text"
32
+
className="text-lg font-semibold pl-2"
33
/>
34
</div>
35
<div className="flex max-h-12 items-center gap-1">
+1
-1
src/components/Nav/NavBarUnauthed.tsx
···
13
iconClassName="text-text"
14
labelClassName="text-text"
15
underlineClassName="bg-text"
16
-
className="text-lg font-semibold"
17
/>
18
</div>
19
<div className="flex items-center gap-1">
···
13
iconClassName="text-text"
14
labelClassName="text-text"
15
underlineClassName="bg-text"
16
+
className="text-lg font-semibold pl-2"
17
/>
18
</div>
19
<div className="flex items-center gap-1">
+11
-4
src/lib/queries/get-trending-from-stitch.ts
···
7
const STITCH_PDS_URL = new URL("https://selfhosted.social");
8
9
export const getTrendingFromStitch = async () => {
0
0
0
0
0
10
const trendingReq = new Request(
11
-
`${STITCH_PDS_URL}/xrpc/com.atproto.listRecords?repo=${STITCH_DID}&collection=app.bsky.feed.post&limit=20`,
12
);
13
const res = await fetch(trendingReq);
14
if (!res.ok) throw new Error("Fetching posts from Stitch failed.");
15
-
const data: unknown = res.json();
16
17
const {
18
success,
···
20
data: parseData,
21
} = z
22
.object({
23
-
cursor: z.string(),
24
records: z.array(
25
comAtprotoRepoGetRecordOutputSchema(appBskyFeedPostSchema),
26
),
27
})
28
.safeParse(data);
29
30
-
if (!success) throw new Error(error.message);
0
0
31
32
return parseData.records;
33
};
···
7
const STITCH_PDS_URL = new URL("https://selfhosted.social");
8
9
export const getTrendingFromStitch = async () => {
10
+
const repoUrlString = STITCH_PDS_URL.toString();
11
+
const cleanedUrl = repoUrlString.endsWith("/")
12
+
? repoUrlString.substring(0, repoUrlString.length - 1)
13
+
: repoUrlString;
14
+
15
const trendingReq = new Request(
16
+
`${cleanedUrl}/xrpc/com.atproto.repo.listRecords?repo=${STITCH_DID}&collection=app.bsky.feed.post&limit=20`,
17
);
18
const res = await fetch(trendingReq);
19
if (!res.ok) throw new Error("Fetching posts from Stitch failed.");
20
+
const data: unknown = await res.json();
21
22
const {
23
success,
···
25
data: parseData,
26
} = z
27
.object({
28
+
cursor: z.string().optional(),
29
records: z.array(
30
comAtprotoRepoGetRecordOutputSchema(appBskyFeedPostSchema),
31
),
32
})
33
.safeParse(data);
34
35
+
if (!success) {
36
+
throw new Error(error.message);
37
+
}
38
39
return parseData.records;
40
};
+1
-1
src/lib/types/lexicons/app/bsky/feed/post.ts
···
103
})
104
.describe("Record containing a Bluesky post.");
105
106
-
export type Post = z.infer<typeof appBskyFeedPostSchema>;
···
103
})
104
.describe("Record containing a Bluesky post.");
105
106
+
export type AppBskyFeedPost = z.infer<typeof appBskyFeedPostSchema>;