-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add post fetching functionality with infinite scrolling
- Added API route `/api/posts/route.ts` for fetching posts - Created `CustomFeed` and `GeneralFeed` components to cater authenticated users - Implemented server-side post rendering with Redis caching - Added dynamic post page with vote server component and EditorOutput
- Loading branch information
1 parent
cb7501f
commit e4b91a8
Showing
7 changed files
with
321 additions
and
173 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { getAuthSession } from "@/lib/auth"; | ||
import { db } from "@/lib/db"; | ||
import { z } from "zod"; | ||
|
||
export async function GET(req: Request) { | ||
const url = new URL(req.url); | ||
|
||
const session = await getAuthSession(); | ||
|
||
let followedCommunitiesIds: string[] = []; | ||
|
||
if (session) { | ||
const followedCommunities = await db.subscription.findMany({ | ||
where: { | ||
userId: session.user.id, | ||
}, | ||
include: { | ||
subgroup: true, | ||
}, | ||
}); | ||
|
||
followedCommunitiesIds = followedCommunities.map((sub) => sub.subgroup.id); | ||
} | ||
|
||
// Fetch posts | ||
try { | ||
const { limit, page, subgroupName } = z | ||
.object({ | ||
limit: z.string(), | ||
page: z.string(), | ||
subgroupName: z.string().nullish().optional(), // for specific subgroup | ||
}) | ||
.parse({ | ||
subgroupName: url.searchParams.get("sub"), | ||
limit: url.searchParams.get("limit"), | ||
page: url.searchParams.get("page"), | ||
}); | ||
|
||
// Filter posts by subgroup or followed communities | ||
let whereClause = {}; | ||
|
||
if (subgroupName) { | ||
whereClause = { | ||
subgroup: { | ||
name: subgroupName, | ||
}, | ||
}; | ||
} else if (session) { | ||
whereClause = { | ||
subgroup: { | ||
id: { | ||
in: followedCommunitiesIds, | ||
}, | ||
}, | ||
}; | ||
} | ||
|
||
// Infinite scrolling pagination | ||
const posts = await db.post.findMany({ | ||
take: parseInt(limit), | ||
skip: (parseInt(page) - 1) * parseInt(limit), // skip should start from 0 for page 1 | ||
orderBy: { | ||
createdAt: "desc", | ||
}, | ||
include: { | ||
subgroup: true, | ||
votes: true, | ||
author: true, | ||
comments: true, | ||
}, | ||
where: whereClause, | ||
}); | ||
|
||
return new Response(JSON.stringify(posts)); | ||
} catch (error) { | ||
if (error instanceof z.ZodError) { | ||
return new Response("Invalid data passed", { status: 422 }); // Unprocessable entity | ||
} | ||
|
||
return new Response("Could not fetch posts", { status: 500 }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import EditorOutput from "@/components/EditorOutput"; | ||
import PostVoteServer from "@/components/pages/PostVoteServer"; | ||
import { buttonVariants } from "@/components/ui/button"; | ||
import { db } from "@/lib/db"; | ||
import { redis } from "@/lib/redis"; | ||
import { formatTimeToNow } from "@/lib/utils"; | ||
import { CachedPost } from "@/types/redis"; | ||
import { Post, User, Vote } from "@prisma/client"; | ||
import { ArrowBigDown, ArrowBigUp, Loader2 } from "lucide-react"; | ||
import { notFound } from "next/navigation"; | ||
import { Suspense } from "react"; | ||
|
||
interface SubgroupPostPageProps { | ||
params: { | ||
postId: string; | ||
subId: string; | ||
}; | ||
} | ||
|
||
export const dynamic = "force-dynamic"; | ||
export const fetchCache = "force-no-store"; | ||
|
||
const SubgroupPostPage = async ({ params }: SubgroupPostPageProps) => { | ||
const cachedPost = (await redis.hgetall( | ||
`post:${params.postId}` | ||
)) as CachedPost; | ||
|
||
let post: (Post & { votes: Vote[]; author: User }) | null = null; | ||
|
||
if (!cachedPost) { | ||
post = await db.post.findFirst({ | ||
where: { | ||
id: params.postId, | ||
}, | ||
include: { | ||
votes: true, | ||
author: true, | ||
}, | ||
}); | ||
} | ||
|
||
if (!post && !cachedPost) return notFound(); | ||
|
||
return ( | ||
<div> | ||
<div className="h-full flex flex-col sm:flex-row items-center sm:items-start justify-between"> | ||
<Suspense fallback={<PostSkeleton />}> | ||
<PostVoteServer | ||
postId={post?.id ?? cachedPost.id} | ||
getData={async () => { | ||
return await db.post.findUnique({ | ||
where: { | ||
id: params.postId, | ||
}, | ||
include: { | ||
votes: true, | ||
}, | ||
}); | ||
}} | ||
/> | ||
</Suspense> | ||
|
||
<div className="sm:w-0 w-full flex-1 p-4 rounded-sm border-2"> | ||
<p className="max-h-40 mt-1 truncate text-xs text-gray-500"> | ||
<span> | ||
Posted by u/{post?.author.username ?? cachedPost.authorUsername} | ||
</span> | ||
<span className="mx-2">•</span> | ||
<span> | ||
{formatTimeToNow( | ||
new Date(post?.createdAt ?? cachedPost.createdAt) | ||
)} | ||
</span> | ||
</p> | ||
<h1 className="text-xl font-semibold py-2 leading-6 text-primary"> | ||
{post?.title ?? cachedPost.title} | ||
</h1> | ||
|
||
<EditorOutput content={post?.content ?? cachedPost.content} /> | ||
<Suspense | ||
fallback={ | ||
<Loader2 className="h-5 w-5 animate-spin text-zinc-500" /> | ||
} | ||
></Suspense> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
function PostSkeleton() { | ||
return ( | ||
<div className="flex items-center flex-col pr-6 w-20"> | ||
{/* upvote */} | ||
<div className={buttonVariants({ variant: "ghost" })}> | ||
<ArrowBigUp className="h-5 w-5 text-zinc-700" /> | ||
</div> | ||
|
||
{/* score */} | ||
<div className="text-center py-2 font-medium text-sm text-zinc-900"> | ||
<Loader2 className="h-3 w-3 animate-spin" /> | ||
</div> | ||
|
||
{/* downvote */} | ||
<div className={buttonVariants({ variant: "ghost" })}> | ||
<ArrowBigDown className="h-5 w-5 text-zinc-700" /> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
export default SubgroupPostPage; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { Infinite_Scrolling_Pagination_Results } from "@/config"; | ||
import { db } from "@/lib/db"; | ||
import PostFeed from "./PostFeed"; | ||
import { getAuthSession } from "@/lib/auth"; | ||
|
||
export default async function CustomFeed() { | ||
const session = await getAuthSession(); | ||
|
||
const followedCommunitiesPosts = await db.subscription.findMany({ | ||
where: { | ||
userId: session?.user.id, | ||
}, | ||
include: { | ||
subgroup: true, | ||
}, | ||
}); | ||
|
||
const posts = await db.post.findMany({ | ||
where: { | ||
subgroup: { | ||
name: { | ||
in: followedCommunitiesPosts.map(({ subgroup }) => subgroup.id), | ||
}, | ||
}, | ||
}, | ||
orderBy: { | ||
createdAt: "desc", | ||
}, | ||
include: { | ||
votes: true, | ||
author: true, | ||
comments: true, | ||
subgroup: true, | ||
}, | ||
take: Infinite_Scrolling_Pagination_Results, | ||
}); | ||
|
||
return <PostFeed initialPosts={posts} />; | ||
} |
Oops, something went wrong.