Skip to content

Commit

Permalink
feat: Add post fetching functionality with infinite scrolling
Browse files Browse the repository at this point in the history
- 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
gupta-soham committed Nov 18, 2024
1 parent cb7501f commit e4b91a8
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 173 deletions.
82 changes: 82 additions & 0 deletions app/api/posts/route.ts
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 });
}
}
122 changes: 60 additions & 62 deletions app/sub/[subId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// import ToFeedButton from '@/components/ToFeedButton'
import SubscribeLeaveToggle from "@/components/SubscribeLeaveToggle";
import { buttonVariants } from "@/components/ui/button";
import { getAuthSession } from "@/lib/auth";
Expand Down Expand Up @@ -66,71 +65,70 @@ export default async function layout({
});

return (
<div className="sm:container max-w-7xl mx-auto h-full pt-12">
<div>
{/* <ToFeedButton /> */}

<div className="grid grid-cols-1 md:grid-cols-3 gap-y-4 md:gap-x-4 py-6">
<ul className="flex flex-col col-span-2 space-y-6">{children}</ul>

{/* Info Sidebar */}
<div className="hidden overflow-hidden h-fit rounded-lg border border-gray-200 dark:border-gray-700 order-first md:order-last sm:block">
<div className="px-6 py-4 dark:bg-gray-800">
<p className="font-semibold py-3 dark:text-white">
About sub/{subgroup.name}
</p>
<div className="sm:container max-w-7xl mx-auto px-4 py-6 lg:py-8">
{/* <ToFeedButton /> */}

<div className="grid grid-cols-1 md:grid-cols-3 gap-y-4 md:gap-x-4">
<div className="col-span-2">
<ul className="space-y-6">{children}</ul>
</div>

{/* Info Sidebar */}
<aside className="order-first md:order-last">
<div className="rounded-lg border bg-card shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b">
<h2 className="font-semibold">About sub/{subgroup.name}</h2>
</div>
<dl className="divide-y divide-gray-100 dark:divide-gray-700 px-6 py-4 text-sm leading-6 bg-gray-200 dark:bg-black/10">
<div className="flex justify-between gap-x-4 py-3">
<dt className="text-gray-500 dark:text-gray-400">Created</dt>
<dd className="text-gray-700 dark:text-gray-300">
<time dateTime={subgroup.createdAt.toDateString()}>
{format(subgroup.createdAt, "d MMMM, yyyy")}
</time>
</dd>
</div>
<div className="flex justify-between gap-x-4 py-3">
<dt className="text-gray-500 dark:text-gray-400">Members</dt>
<dd className="flex items-start gap-x-2">
<div className="text-gray-900 dark:text-gray-100">
{memberCount}
</div>
</dd>
</div>
{subgroup.creatorId === session?.user?.id ? (
<div className="flex justify-between gap-x-4 py-3">
<dt className="text-gray-500 dark:text-gray-400">
You created this community
</dt>

<div className="px-6 py-4 bg-muted/10 space-y-4">
<dl className="space-y-4 text-sm">
<div className="flex justify-between gap-x-4">
<dt className="text-muted-foreground">Created</dt>
<dd className="text-foreground">
<time dateTime={subgroup.createdAt.toDateString()}>
{format(subgroup.createdAt, "d MMMM, yyyy")}
</time>
</dd>
</div>
) : (
<div className="flex justify-between gap-x-4 py-3">
<dt className="text-gray-500 dark:text-gray-400">
created by u/@{creator?.username}
</dt>

<div className="flex justify-between gap-x-4">
<dt className="text-muted-foreground">Members</dt>
<dd className="text-foreground">{memberCount}</dd>
</div>
)}

{subgroup.creatorId !== session?.user?.id ? (
<SubscribeLeaveToggle
isSubscribed={isSubscribed}
subgroupId={subgroup.id}
subgroupName={subgroup.name}
/>
) : null}
<Link
className={buttonVariants({
variant: "outline",
className:
"w-full mb-6 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600",
})}
href={`/sub/${subId}/submit`}
>
Create Post
</Link>
</dl>

{subgroup.creatorId === session?.user?.id ? (
<div className="text-muted-foreground">
You created this community
</div>
) : (
<div className="text-muted-foreground">
created by u/{creator?.username}
</div>
)}
</dl>

<div className="space-y-3">
{subgroup.creatorId !== session?.user?.id && (
<SubscribeLeaveToggle
isSubscribed={isSubscribed}
subgroupId={subgroup.id}
subgroupName={subgroup.name}
/>
)}

<Link
className={buttonVariants({
variant: "outline",
className: "w-full",
})}
href={`/sub/${subId}/submit`}
>
Create Post
</Link>
</div>
</div>
</div>
</div>
</aside>
</div>
</div>
);
Expand Down
112 changes: 112 additions & 0 deletions app/sub/[subId]/post/[postId]/page.tsx
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;
39 changes: 39 additions & 0 deletions components/pages/CustomFeed.tsx
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} />;
}
Loading

0 comments on commit e4b91a8

Please sign in to comment.