Skip to content

Commit 3e9aa04

Browse files
committed
feat: implement post feed system with service, schema, and UI components
1 parent f8731b1 commit 3e9aa04

4 files changed

Lines changed: 82 additions & 11 deletions

File tree

apps/api/src/modules/posts/post.schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const createPostSchema = z.object({
1616
export const getPostsSchema = z.object({
1717
cursor: z.string().optional(),
1818
limit: z.number().default(10),
19+
feedType: z.enum(["Newest", "Trending", "Following"]).optional(),
1920
});
2021

2122
//get posts by Id

apps/api/src/modules/posts/post.service.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,36 @@ export async function createPost(
5252
export async function getPosts(
5353
prisma: PrismaClient,
5454
userId: string | undefined,
55-
input: { cursor?: string | null; limit: number }
55+
input: { cursor?: string | null; limit: number; feedType?: "Newest" | "Trending" | "Following" }
5656
) {
57+
let orderBy: any = { createdAt: "desc" };
58+
let where: any = {};
59+
60+
if (input.feedType === "Trending") {
61+
orderBy = [
62+
{ likes: { _count: "desc" } },
63+
{ createdAt: "desc" }
64+
];
65+
} else if (input.feedType === "Following") {
66+
if (!userId) {
67+
return { posts: [], nextCursor: null };
68+
}
69+
where = {
70+
author: {
71+
followers: {
72+
some: {
73+
followerId: userId
74+
}
75+
}
76+
}
77+
};
78+
}
79+
5780
const posts = await prisma.post.findMany({
81+
where,
5882
take: input.limit + 1,
5983
cursor: input.cursor ? { id: input.cursor } : undefined,
60-
orderBy: { createdAt: "desc" },
84+
orderBy,
6185
include: {
6286
author: { select: AUTHOR_SELECT },
6387
likes: true,

apps/web/app/feed/components/feedbox/FeedBox.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function FeedBox({ session }: FeedBoxProps) {
1818
const pathname = usePathname();
1919
const params = useParams();
2020
const searchParams = useSearchParams();
21-
const [activeTab, setActiveTab] = useState("For you");
21+
const [activeTab, setActiveTab] = useState("Newest");
2222

2323
// Check if we're on a post detail page
2424
const isPostDetail = pathname?.startsWith("/feed/post/");
@@ -49,7 +49,7 @@ export function FeedBox({ session }: FeedBoxProps) {
4949
<CreatePostBox session={session} />
5050

5151
{/* Post feed */}
52-
<PostList session={session} />
52+
<PostList session={session} activeTab={activeTab} />
5353
</>
5454
)}
5555
</>

apps/web/app/feed/components/feedbox/PostList.tsx

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,78 @@ import { ArticleCard } from "../article/ArticleCard";
55
import { trpc } from "@/utils/trpc";
66
import { useMemo } from "react";
77
import type { Session } from "next-auth";
8+
import Link from "next/link";
89

910
interface PostListProps {
1011
session?: Session | null;
12+
activeTab?: string;
1113
}
1214

13-
export function PostList({ session }: PostListProps = {}) {
14-
const { data: postsData, isLoading: postsLoading } = trpc.posts.getPosts.useQuery({ limit: 20 });
15-
const { data: articlesData, isLoading: articlesLoading } = trpc.articles.getArticles.useQuery({ limit: 20 });
15+
export function PostList({ session, activeTab = "Newest" }: PostListProps = {}) {
16+
// Only fetch if they're not a guest trying to view Following, or if they are just viewing other tabs
17+
const isGuestOnFollowing = !session && activeTab === "Following";
18+
19+
const { data: postsData, isLoading: postsLoading } = trpc.posts.getPosts.useQuery(
20+
{ limit: 20, feedType: activeTab as any },
21+
{ enabled: !isGuestOnFollowing }
22+
);
23+
24+
const { data: articlesData, isLoading: articlesLoading } = trpc.articles.getArticles.useQuery(
25+
{ limit: 20 },
26+
{ enabled: activeTab !== "Following" } // Articles don't have a following concept yet, so skip them on Following tab to avoid mixing
27+
);
1628

1729
const feedItems = useMemo(() => {
30+
if (isGuestOnFollowing) return [];
31+
1832
const posts = (postsData?.posts || []).map((post: any) => ({ ...post, _feedType: "post" }));
1933

34+
// If we're on Following tab, we decided to skip articles above (so it'll be undefined/empty)
2035
const rawArticles = Array.isArray(articlesData)
2136
? articlesData
2237
: (articlesData as any)?.articles || (articlesData as any)?.items || [];
2338

24-
const articles = rawArticles.map((article: any) => ({ ...article, _feedType: "article" }));
39+
const articles = activeTab === "Following"
40+
? [] // Don't mix articles into following feed for now
41+
: rawArticles.map((article: any) => ({ ...article, _feedType: "article" }));
2542

2643
const combined = [...posts, ...articles].sort((a, b) => {
2744
const dateA = new Date(a.createdAt || a.date || Date.now()).getTime();
2845
const dateB = new Date(b.createdAt || b.date || Date.now()).getTime();
46+
47+
// If Trending, we don't want to override the backend sorting with strict date sorting for posts.
48+
// But since we have articles mixed in, we might need to.
49+
// Actually, if it's trending, we should trust the backend order for posts, and maybe just append articles?
50+
// For simplicity, if it's Trending, let's just stick to the backend post order, but we have mixed items.
51+
// Since `getPosts` sorted by trending, let's keep the posts order and interleave articles by date?
52+
// Actually, if activeTab === "Trending", maybe we shouldn't sort them by date and lose the likes sort.
53+
// Let's just sort by date if it's Newest or Following. If Trending, we might want a different strategy.
54+
// But we don't have "likes" on articles easily comparable. Let's just sort by date for Newest/Following.
55+
if (activeTab === "Trending") {
56+
// Keep original order as much as possible. Since articles are sorted by date, and posts by likes.
57+
// This might be tricky. Let's just return 0 to keep relative order, and put articles at the end.
58+
if (a._feedType === b._feedType) return 0;
59+
return a._feedType === "post" ? -1 : 1;
60+
}
61+
2962
return dateB - dateA;
3063
});
3164

3265
return combined;
33-
}, [postsData, articlesData]);
66+
}, [postsData, articlesData, isGuestOnFollowing, activeTab]);
67+
68+
if (isGuestOnFollowing) {
69+
return (
70+
<div className="text-neutral-500 p-8 text-center text-sm flex flex-col items-center gap-2">
71+
<p>Sign in to see posts from developers you follow.</p>
72+
<Link href="/signin" className="px-4 py-2 bg-white text-black rounded-full font-medium hover:bg-neutral-200 transition-colors mt-2">
73+
Sign In
74+
</Link>
75+
</div>
76+
);
77+
}
3478

35-
if (postsLoading && articlesLoading) {
79+
if (postsLoading && (articlesLoading || activeTab === "Following")) {
3680
return <div className="text-neutral-500 p-8 text-center text-sm">Loading feed...</div>;
3781
}
3882

@@ -71,7 +115,9 @@ export function PostList({ session }: PostListProps = {}) {
71115

72116
{feedItems.length === 0 && !postsLoading && !articlesLoading && (
73117
<div className="text-neutral-500 p-8 text-center text-sm">
74-
No posts yet. Be the first to share something!
118+
{activeTab === "Following"
119+
? "You aren't following anyone who has posted yet."
120+
: "No posts yet. Be the first to share something!"}
75121
</div>
76122
)}
77123
</div>

0 commit comments

Comments
 (0)