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