Skip to content

Commit

Permalink
feat: implement PostFeed and render posts
Browse files Browse the repository at this point in the history
- Added `PostFeed` component with infinite scrolling functionality
- Added `Posts` component for individual post rendering
- Implemented `EditorOutput` for rendering post content
- Add utility functions for time formatting
  • Loading branch information
gupta-soham committed Sep 27, 2024
1 parent d2ddded commit 5a32be2
Show file tree
Hide file tree
Showing 9 changed files with 574 additions and 159 deletions.
20 changes: 11 additions & 9 deletions app/sub/[subId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
import React from "react";
import MiniPostCreator from "@/components/pages/MiniPostCreator";

import PostFeed from "@/components/pages/PostFeed";
import { Infinite_Scrolling_Pagination_Results } from "@/config";
import { getAuthSession } from "@/lib/auth";
import { db } from "@/lib/db";
import { Infinite_Scrolling_Pagination_Results } from "@/config";
import { notFound } from "next/navigation";
import MiniPostCreator from "@/components/pages/MiniPostCreator";

interface PageProps {
params: {
subId: string;
};
}

export default async function page({
params,
}: PageProps): Promise<JSX.Element> {
export default async function page({params}: PageProps): Promise<JSX.Element> {
const { subId } = params;
const session = await getAuthSession();
const subgroup = await db.subgroup.findFirst({
where: {
name: subId,
},
where: { name: subId },
include: {
posts: {
include: {
Expand All @@ -28,6 +25,9 @@ export default async function page({
comments: true,
subgroup: true,
},
orderBy: {
createdAt: 'desc'
},
take: Infinite_Scrolling_Pagination_Results,
},
},
Expand All @@ -44,6 +44,8 @@ export default async function page({
</h1>

<MiniPostCreator session={session!} />

<PostFeed initialPosts={subgroup.posts} subgroupName={subgroup.name} />
</>
);
}
58 changes: 58 additions & 0 deletions components/EditorOutput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use client";

import dynamic from "next/dynamic";
import Image from "next/image";

const Output = dynamic(
async () => (await import("editorjs-react-renderer")).default,
{
ssr: false,
}
);

const renderers = {
image: CustomImageRenderer,
code: CustomCodeRenderer,
};

const style = {
paragraph: {
fontSize: "0.875rem",
lineHeight: "1.25rem",
},
};

interface EditorComponentProps {
content: any;
}

export default function EditorOutput({ content }: EditorComponentProps) {
return (
<Output
className="text-sm"
style={style}
data={content}
renderers={renderers}
/>
);
}

function CustomCodeRenderer({ data }: any) {
data;

return (
<pre className="bg-gray-800 rounded-md p-4">
<code className="text-gray-100 text-sm">{data.code}</code>
</pre>
);
}

function CustomImageRenderer({ data }: any) {
const src = data.file.url;

return (
<div className="relative w-full min-h-[15rem]">
<Image alt="image" className="object-contain" fill src={src} />
</div>
);
}
95 changes: 95 additions & 0 deletions components/pages/PostFeed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"use client";
import { Infinite_Scrolling_Pagination_Results } from "@/config";
import { ExtendedPost } from "@/types/db";
import { useIntersection } from "@mantine/hooks";
import { useInfiniteQuery } from "@tanstack/react-query";
import axios from "axios";
import { useSession } from "next-auth/react";
import { useEffect, useRef } from "react";
import Posts from "./Posts";

interface PostFeedTypes {
initialPosts: ExtendedPost[];
subgroupName?: string;
}
export default function PostFeed({
initialPosts,
subgroupName,
}: PostFeedTypes) {
const lastPostRef = useRef<HTMLElement>(null);
const { ref, entry } = useIntersection({
root: lastPostRef.current,
threshold: 1,
});

const { data: session } = useSession();

// Functionality for Infinite Scrolling (TanSatck Query)
const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ["infinite-query", subgroupName],
queryFn: async ({ pageParam = 1 }) => {
const query =
`/api/posts?limit=${Infinite_Scrolling_Pagination_Results}&page=${pageParam}` +
(subgroupName ? `&sub=${subgroupName}` : "");

const { data } = await axios.get(query);
return data as ExtendedPost[];
},
initialPageParam: 1,
getNextPageParam: (lastPage, allPages, lastPageParam) => {
if (lastPage.length < Infinite_Scrolling_Pagination_Results) {
return undefined;
}
return lastPageParam + 1;
},
initialData: { pages: [initialPosts], pageParams: [1] },
});

useEffect(() => {
if (entry?.isIntersecting) {
fetchNextPage(); // Load more posts when the last post comes into view
}
}, [entry, fetchNextPage]);

const posts = data.pages.flatMap((page) => page) ?? initialPosts;

return (
<ul className="flex flex-col col-span-2 space-y-6">
{posts.map((post, index) => {
const totalVotes = post.votes.reduce((acc, vote) => {
if (vote.type === "UP") return acc + 1;
if (vote.type === "DOWN") return acc - 1;
return acc;
}, 0);

const isVoted = post.votes.find(
(vote) => vote.userId === session?.user.id
);

if (index === posts.length - 1)
return (
<li key={post.id} ref={ref}>
<Posts
post={post}
commentAmt={post.comments.length}
subgroupName={post.subgroup.name}
totalVotes={totalVotes}
currentVote={isVoted}
/>
</li>
);
else
return (
<Posts
key={post.id}
post={post}
commentAmt={post.comments.length}
subgroupName={post.subgroup.name}
totalVotes={totalVotes}
currentVote={isVoted}
/>
);
})}
</ul>
);
}
163 changes: 67 additions & 96 deletions components/pages/Posts.tsx
Original file line number Diff line number Diff line change
@@ -1,107 +1,78 @@
import React from "react";
// Preview Component for each Post
import { formatTimeToNow } from "@/lib/utils";
import { Post, User, Vote } from "@prisma/client";
import { MessageSquare } from "lucide-react";
import Link from "next/link";
import { useRef } from "react";
import EditorOutput from "../EditorOutput";

import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Card, CardHeader, CardContent, CardFooter } from "@/components/ui/card";
import {
ArrowDown,
ArrowUp,
Filter,
MessageCircle,
MessageCircleReply,
MoveDiagonal,
} from "lucide-react";
import Image from "next/image";
import { Button } from "@/components/ui/button";
type PartialVote = Pick<Vote, "type">;

type PostType = {
content: string | JSX.Element;
timeAgo: string;
upvotes: number;
comments: number;
};
export function Posts() {
return (
<div className="bg-card rounded-lg border border-card-border p-4 space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-bold">Your Feed</h2>
<Button variant="ghost" size="icon">
<Filter className="w-5 h-5" />
<span className="sr-only">Filter</span>
</Button>
</div>
<div className="grid gap-4">
<Post
content="Just finished my midterms! Time to celebrate with some friends. 🎉"
timeAgo="2 hours ago"
upvotes={123}
comments={42}
/>
<Post
content={
<Image
src="/placeholder.svg"
width={400}
height={225}
alt="Image"
className="rounded-md object-cover aspect-video"
/>
}
timeAgo="1 day ago"
upvotes={456}
comments={78}
/>
<Post
content="Anyone else struggling with their final project? I could use some help!"
timeAgo="3 days ago"
upvotes={89}
comments={21}
/>
</div>
</div>
);
interface PostProps {
post: Post & {
author: User;
votes: Vote[];
};
totalVotes: number;
subgroupName: string;
currentVote?: PartialVote;
commentAmt: number;
}
export default function Posts({
post,
totalVotes: _totalVotes,
currentVote: _currentVote,
subgroupName,
commentAmt,
}: PostProps) {
const pRef = useRef<HTMLParagraphElement>(null);

function Post({ content, timeAgo, upvotes, comments }: PostType) {
return (
<Card>
<CardHeader className="flex gap-3 relative ">
<div className="flex items-center gap-3">
<Avatar className="w-8 h-8 rounded-full border overflow-hidden">
<AvatarImage src="/placeholder-user.jpg" alt="User" />
<AvatarFallback>UN</AvatarFallback>
</Avatar>
<div className="rounded-md bg-white dark:bg-black/30 shadow">
<div className="px-6 py-4 flex justify-between">
<div className="w-0 flex-1">
<div className="max-h-40 mt-1 text-xs text-gray-500">
{subgroupName ? (
<>
<a
className="underline text-zinc-900 dark:text-zinc-100 text-sm underline-offset-2"
href={`/sub/${subgroupName}`}
>
sub/{subgroupName}
</a>
<span className="px-1"></span>
</>
) : null}
<span>Posted by u/{post.author.username}</span>{" "}
{formatTimeToNow(new Date(post.createdAt))}
</div>
<a href={`/sub/${subgroupName}/post/${post.id}`}>
<h1 className="text-lg font-semibold py-2 leading-6 text-gray-900 dark:text-gray-100">
{post.title}
</h1>
</a>

<div>
<div className="font-medium">Anonymous User</div>
<div className="text-xs text-muted-foreground">{timeAgo}</div>
<div
className="relative text-sm max-h-40 w-full overflow-clip"
ref={pRef}
>
<EditorOutput content={post.content} />
{pRef.current?.clientHeight === 160 ? (
// blur bottom if content is too long
<div className="absolute bottom-0 left-0 h-24 w-full bg-gradient-to-t from-white dark:from-black to-transparent"></div>
) : null}
</div>
</div>
</div>

<Button variant="ghost" size="icon" className="absolute top-2 right-2">
<MoveDiagonal className="w-5 h-5" />
<span className="sr-only">More</span>
</Button>
</CardHeader>

<CardContent>
<div className="text-sm">{content}</div>
</CardContent>
<CardFooter className="flex items-center gap-4">
<Button variant="ghost" size="icon">
<ArrowUp className="w-5 h-5" />
<span className="sr-only">Upvote</span>
</Button>
<div className="text-sm text-muted-foreground">{upvotes} upvotes</div>
<Button variant="ghost" size="icon">
<ArrowDown className="w-5 h-5" />
<span className="sr-only">Downvote</span>
</Button>
<Button variant="ghost" size="icon">
<MessageCircle className="w-5 h-5" />
<span className="sr-only">Comment</span>
</Button>
<div className="text-sm text-muted-foreground">{comments} comments</div>
</CardFooter>
</Card>
<div className="bg-gray-50 dark:bg-zinc-900 z-20 text-sm px-4 py-4 sm:px-6">
<Link
href={`/sub/${subgroupName}/post/${post.id}`}
className="w-fit flex items-center gap-2 text-gray-900 dark:text-gray-100"
>
<MessageSquare className="h-4 w-4" /> {commentAmt} comments
</Link>
</div>
</div>
);
}
Loading

0 comments on commit 5a32be2

Please sign in to comment.