twitter-clone/app/routes/home.tsx

282 lines
6.7 KiB
TypeScript

import { prisma, type PostWithRelations } from "~/utils/prisma.server";
import type {
ActionFunction,
ActionFunctionArgs,
LoaderFunction,
LoaderFunctionArgs,
MetaFunction,
} from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import {
Form,
Link,
useActionData,
useLoaderData,
useRouteLoaderData,
} from "@remix-run/react";
import { Button } from "~/components/Button";
import { Card } from "~/components/Card";
import { FormLabel, TextArea } from "~/components/Form";
import { Post } from "~/components/Post";
import { SubTitle, Text, Title } from "~/components/Typography";
import { like, unLike } from "~/models/like.server";
import { createPost } from "~/models/post.server";
import { commitSession, getSession } from "~/utils/session.server";
import type { RootLoaderTypes } from "~/root";
import { repost, unRepost } from "~/models/repost.server";
import { reply } from "~/models/reply.server";
export const meta: MetaFunction<typeof loader> = () => {
return [{ title: `Home | Twitter Clone` }];
};
export async function action({ request }: ActionFunctionArgs) {
const session = await getSession(request.headers.get("Cookie"));
if (!session.has("userId")) {
return redirect("/");
}
const userId = session.get("userId");
const formData = await request
.formData()
.then((data) => Object.fromEntries(data));
switch (formData.intent) {
case "newpost":
const postErrors = await createPost(String(formData.post), userId);
if (Object.values(postErrors).some(Boolean)) {
return postErrors;
}
session.flash("globalMessage", "Post created!");
break;
case "like":
const likeErrors = await like(
Number(formData.postId),
userId,
Number(formData.replyId)
);
if (Object.values(likeErrors).some(Boolean)) {
return likeErrors;
}
session.flash("globalMessage", "Post liked!");
break;
case "unlike":
const unLikeErrors = await unLike(
Number(formData.postId),
userId,
Number(formData.replyId)
);
if (Object.values(unLikeErrors).some(Boolean)) {
return unLikeErrors;
}
session.flash("globalMessage", "Post un-liked!");
break;
case "repost":
const repostErrors = await repost(
Number(formData.postId),
userId,
Number(formData.replyId)
);
if (Object.values(repostErrors).some(Boolean)) {
return repostErrors;
}
session.flash("globalMessage", "Post reposted!");
break;
case "unrepost":
const unrepostErrors = await unRepost(
Number(formData.postId),
userId,
Number(formData.replyId)
);
if (Object.values(unrepostErrors).some(Boolean)) {
return unrepostErrors;
}
session.flash("globalMessage", "Post un-reposted!");
break;
case "reply":
const replyErrors = await reply(
String(formData.reply),
Number(formData.postId),
Number(formData.replyId),
userId
);
if (Object.values(replyErrors).some(Boolean)) {
return replyErrors;
}
session.flash("globalMessage", "Replied to post!");
break;
default:
return;
}
return json(
{},
{
headers: {
"Set-Cookie": await commitSession(session),
},
}
);
}
export async function loader({ request }: LoaderFunctionArgs) {
const session = await getSession(request.headers.get("Cookie"));
if (!session.has("userId")) {
return redirect("/");
}
const userId = session.get("userId");
const following = await prisma.user.findFirst({
where: { id: userId },
select: { following: { select: { id: true } } },
});
const followingArr = [...following.following.map((user) => user.id)];
const feed = await prisma.post.findMany({
where: {
OR: [
{
userId: {
in: followingArr,
},
},
{
reposts: {
some: {
userId: {
in: followingArr,
},
},
},
},
],
},
select: {
id: true,
text: true,
createdAt: true,
author: {
select: {
username: true,
name: true,
pfp: true,
},
},
reposts: {
select: {
userId: true,
user: {
select: {
id: true,
username: true,
pfp: true,
name: true,
},
},
},
},
likes: {
select: {
userId: true,
user: {
select: {
id: true,
username: true,
pfp: true,
name: true,
},
},
},
},
_count: {
select: {
replies: {
where: {
parentReplyId: null,
},
},
},
},
},
orderBy: {
createdAt: "desc",
},
});
feed.forEach((post) => {
const filter = post.reposts.filter((user) =>
followingArr.includes(user.userId)
);
const reposters = filter.map((repost) => repost.user);
post.reposters = reposters;
});
return feed;
}
export default function Index() {
const rootData = useRouteLoaderData<RootLoaderTypes>("root");
const data: PostWithRelations[] = useLoaderData<LoaderFunction>();
const errors = useActionData<ActionFunction>();
return (
<div className="flex flex-col gap-5">
<Title>Home</Title>
<Card className="!gap-0">
{rootData?.id ? (
<>
<SubTitle>New post</SubTitle>
<Form method="POST">
<input type="hidden" name="intent" value={"newpost"} />
<div className="flex flex-col gap-3">
<FormLabel>
<Text>Post body</Text>
<TextArea name="post" />
<Text type="error">{errors?.body ? errors.body : ""}</Text>
</FormLabel>
<Button type="submit">Post</Button>
</div>
</Form>
</>
) : (
""
)}
</Card>
<SubTitle>Feed</SubTitle>
{data.length > 0 ? (
<Card className="!p-0 !gap-0 divide-y">
{data.map((post) => (
<Post userId={rootData?.id} key={post.id} post={post} />
))}
</Card>
) : (
<Text className="flex gap-1" type="error">
Feed is empty,{" "}
<Link to={`/users`}>
<Text type="link">follow someone</Text>
</Link>{" "}
so their recent posts will show up here!
</Text>
)}
</div>
);
}