Skip to content

Commit 713ec36

Browse files
Adds "my feeds" on profile (#204)
* feat: update my feeds tab on profile page * feat: update tabs to be routes * fix: double scrollbar * comment unused files in overview tab * fix: revert rsbuild * fmt * fix: add routegen to prettier ignore * clean up, server side --------- Co-authored-by: Elliot Braem <elliot@ejlbraem.com>
1 parent 08f36a3 commit 713ec36

File tree

24 files changed

+1181
-791
lines changed

24 files changed

+1181
-791
lines changed

.prettierignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ node_modules
44
.turbo
55
.next
66
.docusaurus
7-
packages/shared-db/migrations
7+
packages/shared-db/migrations
8+
apps/app/src/routeTree.gen.ts

apps/api/src/routes/api/users.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { FeedService } from "@curatedotfun/core-services";
12
import {
23
ApiErrorResponseSchema,
34
CreateUserRequestSchema,
5+
FeedsWrappedResponseSchema,
46
UpdateUserRequestSchema,
57
UserDeletedWrappedResponseSchema,
68
UserNearAccountIdParamSchema,
@@ -333,4 +335,41 @@ usersRoutes.get(
333335
},
334336
);
335337

338+
usersRoutes.get(
339+
"/:nearAccountId/feeds",
340+
zValidator("param", UserNearAccountIdParamSchema),
341+
async (c) => {
342+
const { nearAccountId } = c.req.valid("param");
343+
const sp = c.var.sp;
344+
345+
try {
346+
const feedService: FeedService = sp.getFeedService();
347+
const feeds = await feedService.getFeedsByCreator(nearAccountId);
348+
349+
return c.json(
350+
FeedsWrappedResponseSchema.parse({
351+
statusCode: 200,
352+
success: true,
353+
data: feeds.map((feed) => ({
354+
...feed,
355+
config: feed.config,
356+
})),
357+
}),
358+
);
359+
} catch (error) {
360+
c.var.sp
361+
.getLogger()
362+
.error({ error }, `Error fetching feeds for ${nearAccountId}`);
363+
return c.json(
364+
ApiErrorResponseSchema.parse({
365+
statusCode: 500,
366+
success: false,
367+
error: { message: "Failed to fetch feeds" },
368+
}),
369+
500,
370+
);
371+
}
372+
},
373+
);
374+
336375
export { usersRoutes };

apps/app/src/components/UserMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export default function UserMenu({ className }: UserMenuProps) {
7676
) : (
7777
<ProfileImage size="small" />
7878
)}
79-
<p className="text-sm font-medium leading-6 hidden sm:block">
79+
<p className="text-sm font-medium leading-6">
8080
{getUserDisplayName()}
8181
</p>
8282
<ChevronDown

apps/app/src/components/profile/ProfileTabs.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import {
33
Activity,
44
ScanSearch,
55
// Award,
6-
// Newspaper,
6+
Newspaper,
77
// NotepadTextDashed
88
} from "lucide-react";
99
import { ProfileOverview } from "./overview";
1010
// import { ProfileContent } from "./content";
11-
// import { MyFeeds } from "./my-feeds";
11+
import { MyFeeds } from "./my-feeds";
1212
// import { ProfilePoints } from "./points";
1313
import { ProfileActivity } from "./activity";
1414

@@ -26,12 +26,12 @@ const TABS = [
2626
// icon: NotepadTextDashed,
2727
// component: ProfileContent,
2828
// },
29-
// {
30-
// value: "my-feeds",
31-
// label: "My Feeds",
32-
// icon: Newspaper,
33-
// component: MyFeeds,
34-
// },
29+
{
30+
value: "my-feeds",
31+
label: "My Feeds",
32+
icon: Newspaper,
33+
component: MyFeeds,
34+
},
3535
// {
3636
// value: "points",
3737
// label: "Points",
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { FeedResponse } from "@curatedotfun/types";
2+
import { useFeedStats } from "../../../lib/api/feeds";
3+
import { Card } from "./card";
4+
5+
interface FeedCardProps {
6+
feed: FeedResponse;
7+
}
8+
9+
export function FeedCard({ feed }: FeedCardProps) {
10+
const { contentCount, curatorCount } = useFeedStats(feed.id);
11+
12+
const isComplete = !!(
13+
feed.config &&
14+
feed.config.name &&
15+
feed.config.description &&
16+
feed.config.sources &&
17+
feed.config.sources.length > 0
18+
);
19+
20+
const tags: string[] = []; // TODO: Extract tags from feed sources or create a tags system
21+
const imageSrc = feed.config?.image || "/images/feed-image.png";
22+
23+
return (
24+
<Card
25+
id={feed.id}
26+
image={imageSrc}
27+
title={feed.name}
28+
tags={tags}
29+
description={feed.description || "No description available"}
30+
createdAt={new Date(feed.createdAt)}
31+
curators={curatorCount}
32+
contents={contentCount}
33+
isCompleted={isComplete}
34+
/>
35+
);
36+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Search } from "lucide-react";
2+
import { Input } from "../../ui/input";
3+
4+
interface SearchFormProps {
5+
searchTerm: string;
6+
onSearchChange: (value: string) => void;
7+
}
8+
9+
export function SearchForm({ searchTerm, onSearchChange }: SearchFormProps) {
10+
return (
11+
<form className="relative w-full md:w-fit">
12+
<Input
13+
placeholder="Search feeds..."
14+
value={searchTerm}
15+
onChange={(e) => onSearchChange(e.target.value)}
16+
className="ps-9 sm:min-w-[300px] w-full"
17+
/>
18+
<Search
19+
className="absolute left-2 top-1/2 -translate-y-1/2 text-black/50 size-5"
20+
strokeWidth={1.5}
21+
/>
22+
</form>
23+
);
24+
}
Lines changed: 54 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Info, Newspaper, Users } from "lucide-react";
22
import { Badge } from "../../ui/badge";
33
import { cn } from "../../../lib/utils";
4+
import { Link } from "@tanstack/react-router";
45

56
interface CardProps {
7+
id: string;
68
image: string;
79
title: string;
810
tags: string[];
@@ -14,6 +16,7 @@ interface CardProps {
1416
}
1517

1618
export function Card({
19+
id,
1720
image,
1821
title,
1922
tags,
@@ -24,58 +27,63 @@ export function Card({
2427
isCompleted,
2528
}: CardProps) {
2629
return (
27-
<div className="self-stretch flex flex-col">
28-
{!isCompleted && (
29-
<div className="flex items-center text-yellow-700 gap-4 px-4 py-2 rounded-t-lg border border-yellow-700 bg-yellow-50">
30-
<Info strokeWidth={1.5} size={24} />
31-
Setup incomplete
32-
</div>
33-
)}
34-
<div
35-
className={cn(
36-
"flex p-4 gap-[32px] flex-col justify-between flex-1 self-stretch rounded-lg border border-neutral-200 bg-white",
37-
!isCompleted && "border-yellow-700 rounded-t-none border-t-0",
30+
<Link to={"/feed/$feedId"} params={{ feedId: id }} className="no-underline">
31+
<div className="self-stretch flex flex-col">
32+
{!isCompleted && (
33+
<div className="flex items-center text-yellow-700 gap-4 px-4 py-2 rounded-t-lg border border-yellow-700 bg-yellow-50">
34+
<Info strokeWidth={1.5} size={24} />
35+
Setup incomplete
36+
</div>
3837
)}
39-
>
40-
<div className="space-y-3">
41-
<div className="flex gap-[14px] p-2.5 rounded-md bg-neutral-50">
42-
<img
43-
src={image}
44-
alt="Near Week"
45-
className="object-cover size-[68px] shrink-0"
46-
/>
47-
<div>
48-
<p className="font-londrina font-[900] text-black text-[24px]">
49-
{title}
50-
</p>
51-
<div className="flex items-center gap-2 flex-wrap">
52-
{tags.length > 0 &&
53-
tags.map((tag, index) => (
54-
<Badge key={index} className="text-xs">
55-
#{tag}
56-
</Badge>
57-
))}
38+
<div
39+
className={cn(
40+
"flex p-4 gap-[32px] flex-col justify-between flex-1 self-stretch rounded-lg border border-neutral-200 bg-white",
41+
!isCompleted && "border-yellow-700 rounded-t-none border-t-0",
42+
)}
43+
>
44+
<div className="space-y-3">
45+
<div className="flex gap-[14px] p-2.5 rounded-md bg-neutral-50">
46+
<img
47+
src={image}
48+
alt={title}
49+
className="object-cover size-[68px] shrink-0"
50+
onError={(e) => {
51+
e.currentTarget.src = "/images/default-feed.png";
52+
}}
53+
/>
54+
<div>
55+
<p className="font-londrina font-[900] text-black text-[24px]">
56+
{title}
57+
</p>
58+
<div className="flex items-center gap-2 flex-wrap">
59+
{tags.length > 0 &&
60+
tags.map((tag, index) => (
61+
<Badge key={index} className="text-xs">
62+
#{tag}
63+
</Badge>
64+
))}
65+
</div>
5866
</div>
5967
</div>
68+
<p>{description}</p>
69+
<p className="mt-0.5">Created {createdAt.toLocaleDateString()}</p>
6070
</div>
61-
<p>{description}</p>
62-
<p className="mt-0.5">Created {createdAt.toLocaleDateString()}</p>
63-
</div>
64-
<div className="flex gap-[14px] items-center justify-between p-2.5 rounded-md bg-neutral-50">
65-
<div className="flex items-center gap-2">
66-
<Users strokeWidth={1.5} size={24} />
67-
<p className="text-black text-base">
68-
<span className="font-bold">{curators}</span> Curators
69-
</p>
70-
</div>
71-
<div className="flex items-center gap-2">
72-
<Newspaper strokeWidth={1.5} size={24} />
73-
<p className="text-black text-base">
74-
<span className="font-bold">{contents}</span> Contents
75-
</p>
71+
<div className="flex gap-[14px] items-center justify-between p-2.5 rounded-md bg-neutral-50">
72+
<div className="flex items-center gap-2">
73+
<Users strokeWidth={1.5} size={24} />
74+
<p className="text-black text-base">
75+
<span className="font-bold">{curators}</span> Curators
76+
</p>
77+
</div>
78+
<div className="flex items-center gap-2">
79+
<Newspaper strokeWidth={1.5} size={24} />
80+
<p className="text-black text-base">
81+
<span className="font-bold">{contents}</span> Contents
82+
</p>
83+
</div>
7684
</div>
7785
</div>
7886
</div>
79-
</div>
87+
</Link>
8088
);
8189
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export const filterOptions = [
2+
{
3+
value: "all",
4+
label: "All",
5+
},
6+
{
7+
value: "completed",
8+
label: "Completed",
9+
},
10+
{
11+
value: "incomplete",
12+
label: "Setup incomplete",
13+
},
14+
] as const;
15+
16+
export type FilterValue = (typeof filterOptions)[number]["value"];

0 commit comments

Comments
 (0)