Remade likes, added replies, added post and reply specific pages
This commit is contained in:
parent
ac57b9a09b
commit
e993eabeeb
|
@ -10,7 +10,7 @@ export function Button({
|
|||
}) {
|
||||
return (
|
||||
<button
|
||||
className="px-6 py-2 rounded-lg border w-fit hover:bg-ctp-crust"
|
||||
className="px-6 py-2 rounded-lg border w-fit bg-ctp-crust/50 hover:bg-ctp-crust"
|
||||
type={type}
|
||||
>
|
||||
<Text>{children}</Text>
|
||||
|
|
|
@ -72,100 +72,119 @@ export function Post({
|
|||
userId,
|
||||
post,
|
||||
topTitle,
|
||||
rootPostId,
|
||||
}: {
|
||||
post: PostWithRelations;
|
||||
userId?: number;
|
||||
topTitle?: ReactNode;
|
||||
rootPostId?: number;
|
||||
}) {
|
||||
const fetcher = useFetcher();
|
||||
const liked = post.likes.filter((like) => like.id === userId).length > 0;
|
||||
const liked = post.likes.filter((like) => like.userId === userId).length > 0;
|
||||
const reposted =
|
||||
post.reposts.filter((repost) => repost.userId === userId).length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={post.id}
|
||||
className="flex relative flex-col gap-4 p-6 transition-all bg-ctp-mantle/20 hover:bg-ctp-base first:rounded-t-lg last:rounded-b-lg "
|
||||
>
|
||||
{topTitle ? <Text type="subtitle">{topTitle}</Text> : ""}
|
||||
{post.reposters?.length > 0 ? (
|
||||
<div className="flex gap-1 items-center">
|
||||
{post.reposters.map((user) => (
|
||||
<Poster
|
||||
style="compact"
|
||||
key={user.id}
|
||||
name={user.name}
|
||||
username={user.username}
|
||||
pfp={user.pfp}
|
||||
/>
|
||||
))}
|
||||
<Text type="subtitle">reposted this</Text>
|
||||
</div>
|
||||
<>
|
||||
{post?.parentReply ? (
|
||||
<Post post={post.parentReply} userId={userId} rootPostId={rootPostId} />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<Poster
|
||||
pfp={post.author.pfp}
|
||||
username={post.author.username}
|
||||
name={post.author.name}
|
||||
/>
|
||||
</div>
|
||||
<Link to={`/post/${post.id}`}>
|
||||
<div>
|
||||
<Text>{post.text}</Text>
|
||||
<div
|
||||
key={post.id}
|
||||
className="flex relative flex-col gap-4 p-6 transition-all bg-ctp-mantle/20 hover:bg-ctp-base first:rounded-t-lg last:rounded-b-lg "
|
||||
>
|
||||
{topTitle ? <Text type="subtitle">{topTitle}</Text> : ""}
|
||||
{post.reposters?.length > 0 ? (
|
||||
<div className="flex gap-2 items-center">
|
||||
{post.reposters.map((user) => (
|
||||
<Poster
|
||||
style="compact"
|
||||
key={user.id}
|
||||
name={user.name}
|
||||
username={user.username}
|
||||
pfp={user.pfp}
|
||||
/>
|
||||
))}
|
||||
<Text type="subtitle">reposted this</Text>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<Poster
|
||||
pfp={post.author.pfp}
|
||||
username={post.author.username}
|
||||
name={post.author.name}
|
||||
/>
|
||||
</div>
|
||||
<Link to={`/${rootPostId ? "reply" : "post"}/${post.id}`}>
|
||||
<div>
|
||||
<Text>{post.text}</Text>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="flex gap-2 text-sm">
|
||||
<Text>{new Date(post.createdAt).toLocaleDateString()}</Text>
|
||||
<fetcher.Form action={`/home`} method="POST">
|
||||
<input
|
||||
type="hidden"
|
||||
name="intent"
|
||||
value={liked ? "unlike" : "like"}
|
||||
/>
|
||||
<input type="hidden" name="postId" value={post.id} />
|
||||
<button type="submit">
|
||||
<Text type="link">
|
||||
{liked ? "Unlike" : "Like"} ({post.likes.length})
|
||||
</Text>
|
||||
</button>
|
||||
</fetcher.Form>
|
||||
<fetcher.Form action={`/home`} method="POST">
|
||||
<input
|
||||
type="hidden"
|
||||
name="intent"
|
||||
value={reposted ? "unrepost" : "repost"}
|
||||
/>
|
||||
<input type="hidden" name="postId" value={post.id} />
|
||||
<button type="submit">
|
||||
<Text type="link">
|
||||
{reposted ? "Unrepost" : "Repost"} ({post.reposts.length})
|
||||
</Text>
|
||||
</button>
|
||||
</fetcher.Form>
|
||||
<details className="space-y-2">
|
||||
<summary className="flex cursor-pointer">
|
||||
<Text type="link">
|
||||
Reply ({post._count.replies}
|
||||
{post._count.childReplies})
|
||||
</Text>
|
||||
</summary>
|
||||
<Card className="absolute max-w-xs z-100">
|
||||
<SubTitle>Reply to {post.author.username}</SubTitle>
|
||||
<fetcher.Form
|
||||
action={`/home`}
|
||||
className="flex flex-col gap-3"
|
||||
method="POST"
|
||||
>
|
||||
<input type="hidden" name="intent" value={"reply"} />
|
||||
{rootPostId ? (
|
||||
<>
|
||||
<input type="hidden" name="postId" value={rootPostId} />
|
||||
<input type="hidden" name="replyId" value={post.id} />
|
||||
</>
|
||||
) : (
|
||||
<input type="hidden" name="postId" value={post.id} />
|
||||
)}
|
||||
<FormLabel>
|
||||
<Text>Reply body</Text>
|
||||
<TextArea name="reply" />
|
||||
</FormLabel>
|
||||
<Button type="submit">Post</Button>
|
||||
</fetcher.Form>
|
||||
</Card>
|
||||
</details>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="flex gap-2 text-sm">
|
||||
<Text>{new Date(post.createdAt).toLocaleDateString()}</Text>
|
||||
<fetcher.Form action={`/home`} method="POST">
|
||||
<input
|
||||
type="hidden"
|
||||
name="intent"
|
||||
value={liked ? "unlike" : "like"}
|
||||
/>
|
||||
<input type="hidden" name="postId" value={post.id} />
|
||||
<button type="submit">
|
||||
<Text type="link">
|
||||
{liked ? "Unlike" : "Like"} ({post.likes.length})
|
||||
</Text>
|
||||
</button>
|
||||
</fetcher.Form>
|
||||
<fetcher.Form action={`/home`} method="POST">
|
||||
<input
|
||||
type="hidden"
|
||||
name="intent"
|
||||
value={reposted ? "unrepost" : "repost"}
|
||||
/>
|
||||
<input type="hidden" name="postId" value={post.id} />
|
||||
<button type="submit">
|
||||
<Text type="link">
|
||||
{reposted ? "Unrepost" : "Repost"} ({post.reposts.length})
|
||||
</Text>
|
||||
</button>
|
||||
</fetcher.Form>
|
||||
<details className="space-y-2">
|
||||
<summary className="flex cursor-pointer">
|
||||
<Text type="link">Reply</Text>
|
||||
</summary>
|
||||
<Card className="absolute max-w-xs z-100">
|
||||
<SubTitle>Reply to {post.author.username}</SubTitle>
|
||||
<fetcher.Form
|
||||
action={`/home`}
|
||||
className="flex flex-col gap-3"
|
||||
method="POST"
|
||||
>
|
||||
<input type="hidden" name="intent" value={"reply"} />
|
||||
<input type="hidden" name="postId" value={post.id} />
|
||||
<FormLabel>
|
||||
<Text>Reply body</Text>
|
||||
<TextArea name="reply" />
|
||||
</FormLabel>
|
||||
<Button type="submit">Post</Button>
|
||||
</fetcher.Form>
|
||||
</Card>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,16 +22,10 @@ export async function likePost(postId: number, id: number) {
|
|||
return errors;
|
||||
}
|
||||
|
||||
const like = await prisma.user.update({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
const like = await prisma.like.create({
|
||||
data: {
|
||||
likes: {
|
||||
connect: {
|
||||
id: post?.id,
|
||||
},
|
||||
},
|
||||
postId: postId,
|
||||
userId: id,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -50,27 +44,31 @@ export async function unLikePost(postId: number, id: number) {
|
|||
};
|
||||
|
||||
const post = await getPostById(postId);
|
||||
|
||||
if (!post) {
|
||||
errors.postId = "No post with that id";
|
||||
}
|
||||
|
||||
const like = await prisma.like.findFirst({
|
||||
where: {
|
||||
userId: id,
|
||||
postId: postId,
|
||||
},
|
||||
});
|
||||
if (!like) {
|
||||
errors.like = "Repost doesn't exist";
|
||||
}
|
||||
|
||||
if (Object.values(errors).some(Boolean)) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
const like = await prisma.user.update({
|
||||
const likeQ = await prisma.like.delete({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
data: {
|
||||
likes: {
|
||||
disconnect: [{ id: postId }],
|
||||
},
|
||||
id: like.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!like) {
|
||||
if (!likeQ) {
|
||||
errors.like = "Something went wrong unliking the post";
|
||||
return errors;
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ export async function getPostById(id: number) {
|
|||
return post;
|
||||
}
|
||||
|
||||
function validatePostBody(body: string) {
|
||||
export function validatePostBody(body: string) {
|
||||
if (!body) {
|
||||
return "Post body can't be empty";
|
||||
}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import { prisma } from "~/utils/prisma.server";
|
||||
import { getPostById, validatePostBody } from "./post.server";
|
||||
import type { Reply } from "@prisma/client";
|
||||
|
||||
async function getReplyById(id: number) {
|
||||
const reply = await prisma.reply.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
export async function reply(
|
||||
body: Reply["text"],
|
||||
postId: Reply["postId"],
|
||||
replyId: Reply["parentReplyId"] = null,
|
||||
userId: Reply["userId"]
|
||||
) {
|
||||
const errors: {
|
||||
body: string | undefined;
|
||||
post: string | undefined;
|
||||
reply: string | undefined;
|
||||
} = {
|
||||
body: validatePostBody(body),
|
||||
post: undefined,
|
||||
reply: undefined,
|
||||
};
|
||||
|
||||
if (replyId && !(await getReplyById(replyId))) {
|
||||
errors.reply = "Reply not found";
|
||||
}
|
||||
|
||||
if (!(await getPostById(postId))) {
|
||||
errors.post = "Post not found";
|
||||
}
|
||||
|
||||
if (Object.values(errors).some(Boolean)) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
const post = await prisma.reply.create({
|
||||
data: {
|
||||
text: body,
|
||||
userId: userId,
|
||||
postId: postId,
|
||||
parentReplyId: replyId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!post) {
|
||||
errors.reply = "Something went wrong creating the post";
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
|
@ -116,7 +116,7 @@ export default function App() {
|
|||
<Links />
|
||||
</head>
|
||||
<body className="min-h-screen bg-ctp-base text-ctp-text">
|
||||
<div className="container flex flex-col gap-20 m-8 mx-auto xl:flex-row">
|
||||
<div className="container flex flex-col gap-20 m-8 lg:px-40 mx-auto xl:flex-row">
|
||||
<div className="flex mx-auto justify-center flex-grow w-80">
|
||||
<nav className="flex w-full flex-col gap-5">
|
||||
{!data?.id ? (
|
||||
|
|
|
@ -22,6 +22,7 @@ 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 async function action({ request }: ActionFunctionArgs) {
|
||||
const session = await getSession(request.headers.get("Cookie"));
|
||||
|
@ -85,6 +86,21 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||
|
||||
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;
|
||||
|
@ -110,7 +126,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||
where: { id: userId },
|
||||
select: { following: { select: { id: true } } },
|
||||
});
|
||||
const followingArr = [...following.following.map((user) => user.id)];
|
||||
const followingArr = [...following.following.map((user) => user.id), userId];
|
||||
|
||||
const feed = await prisma.post.findMany({
|
||||
where: {
|
||||
|
@ -157,7 +173,24 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||
},
|
||||
likes: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
pfp: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
replies: {
|
||||
where: {
|
||||
parentReplyId: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -200,10 +233,7 @@ export default function Index() {
|
|||
</Form>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Title>Twitter clone</Title>
|
||||
<SubTitle>Made with Remix</SubTitle>
|
||||
</>
|
||||
""
|
||||
)}
|
||||
</Card>
|
||||
<SubTitle>Feed</SubTitle>
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
import type { ActionFunction } from "@remix-run/node";
|
||||
import {
|
||||
json,
|
||||
type LoaderFunction,
|
||||
type LoaderFunctionArgs,
|
||||
} from "@remix-run/node";
|
||||
import {
|
||||
Form,
|
||||
useActionData,
|
||||
useLoaderData,
|
||||
useRouteLoaderData,
|
||||
} from "@remix-run/react";
|
||||
import { Card } from "~/components/Card";
|
||||
import { SubTitle, Text, Title } from "~/components/Typography";
|
||||
import type { PostWithRelations } from "~/utils/prisma.server";
|
||||
import { prisma } from "~/utils/prisma.server";
|
||||
import { Post } from "~/components/Post";
|
||||
import type { RootLoaderTypes } from "~/root";
|
||||
import { Button } from "~/components/Button";
|
||||
import { FormLabel, TextArea } from "~/components/Form";
|
||||
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
const post = await prisma.post.findUnique({
|
||||
where: {
|
||||
id: Number(params.id),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
text: true,
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
likes: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
reposts: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
replies: {
|
||||
where: {
|
||||
parentReplyId: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
text: true,
|
||||
createdAt: true,
|
||||
likes: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
},
|
||||
},
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
reposts: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
},
|
||||
},
|
||||
post: {
|
||||
select: {
|
||||
id: true,
|
||||
text: true,
|
||||
createdAt: true,
|
||||
likes: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
reposts: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
replies: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
childReplies: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
likes: {
|
||||
_count: "desc",
|
||||
},
|
||||
},
|
||||
},
|
||||
createdAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
replies: {
|
||||
where: {
|
||||
parentReplyId: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!post) {
|
||||
throw Error("Post not found");
|
||||
}
|
||||
|
||||
return json(post);
|
||||
}
|
||||
|
||||
export default function PostRoute() {
|
||||
const rootData = useRouteLoaderData<RootLoaderTypes>("root");
|
||||
const data: PostWithRelations = useLoaderData<LoaderFunction>();
|
||||
const errors = useActionData<ActionFunction>();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<Title>Post</Title>
|
||||
<Card className="!p-0 !gap-0 divide-y">
|
||||
<div className="bg-ctp-pink/5">
|
||||
<Post key={data.id} post={data} userId={rootData?.id} />
|
||||
</div>
|
||||
{rootData?.id ? (
|
||||
<div className="p-6 !bg-ctp-sky/5">
|
||||
<SubTitle>New reply</SubTitle>
|
||||
<Form action="/home" className="flex flex-col gap-3" method="POST">
|
||||
<input type="hidden" name="intent" value={"reply"} />
|
||||
<input type="hidden" name="postId" value={data.id} />
|
||||
<FormLabel>
|
||||
<Text>Reply body</Text>
|
||||
<TextArea name="reply" />
|
||||
<Text type="error">{errors?.body ? errors.body : ""}</Text>
|
||||
</FormLabel>
|
||||
<Button type="submit">Post</Button>
|
||||
</Form>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Title>Twitter clone</Title>
|
||||
<SubTitle>Made with Remix</SubTitle>
|
||||
</>
|
||||
)}
|
||||
{data.replies.map((reply) => (
|
||||
<div key={reply.id}>
|
||||
<Post post={reply} userId={rootData?.id} rootPostId={data.id} />
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,309 @@
|
|||
import type { ActionFunction } from "@remix-run/node";
|
||||
import {
|
||||
json,
|
||||
type LoaderFunction,
|
||||
type LoaderFunctionArgs,
|
||||
} from "@remix-run/node";
|
||||
import {
|
||||
Form,
|
||||
useActionData,
|
||||
useLoaderData,
|
||||
useRouteLoaderData,
|
||||
} from "@remix-run/react";
|
||||
import { Card } from "~/components/Card";
|
||||
import { SubTitle, Text, Title } from "~/components/Typography";
|
||||
import type { PostWithRelations } from "~/utils/prisma.server";
|
||||
import { prisma } from "~/utils/prisma.server";
|
||||
import { Post } from "~/components/Post";
|
||||
import type { RootLoaderTypes } from "~/root";
|
||||
import { Button } from "~/components/Button";
|
||||
import { FormLabel, TextArea } from "~/components/Form";
|
||||
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
const reply = await prisma.reply.findUnique({
|
||||
where: {
|
||||
id: Number(params.id),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
text: true,
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
likes: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
reposts: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
parentReply: {
|
||||
select: {
|
||||
id: true,
|
||||
text: true,
|
||||
createdAt: true,
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
likes: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
reposts: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
parentReply: {
|
||||
select: {
|
||||
id: true,
|
||||
text: true,
|
||||
createdAt: true,
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
likes: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
reposts: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
childReplies: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
childReplies: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
post: {
|
||||
select: {
|
||||
id: true,
|
||||
text: true,
|
||||
createdAt: true,
|
||||
likes: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
},
|
||||
},
|
||||
reposts: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
},
|
||||
},
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
replies: {
|
||||
where: {
|
||||
parentReplyId: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
childReplies: {
|
||||
select: {
|
||||
id: true,
|
||||
text: true,
|
||||
createdAt: true,
|
||||
likes: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
},
|
||||
},
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
reposts: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
},
|
||||
},
|
||||
post: {
|
||||
select: {
|
||||
id: true,
|
||||
text: true,
|
||||
createdAt: true,
|
||||
likes: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
reposts: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
replies: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
childReplies: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
likes: {
|
||||
_count: "desc",
|
||||
},
|
||||
},
|
||||
},
|
||||
createdAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
childReplies: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!reply) {
|
||||
throw Error("Reply not found");
|
||||
}
|
||||
|
||||
return json(reply);
|
||||
}
|
||||
|
||||
export default function ReplyRoute() {
|
||||
const rootData = useRouteLoaderData<RootLoaderTypes>("root");
|
||||
const data: PostWithRelations = useLoaderData<LoaderFunction>();
|
||||
const errors = useActionData<ActionFunction>();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<Title>Post</Title>
|
||||
<Card className="!p-0 !gap-0 divide-y">
|
||||
<div className="text-sm">
|
||||
<Post key={data.id} post={data.post} userId={rootData?.id} />
|
||||
</div>
|
||||
<div className="text-xl bg-ctp-pink/5">
|
||||
<Post
|
||||
key={data.id}
|
||||
post={data}
|
||||
userId={rootData?.id}
|
||||
rootPostId={data.post.id}
|
||||
/>
|
||||
</div>
|
||||
{rootData?.id ? (
|
||||
<div className="p-6 !bg-ctp-sky/5">
|
||||
<SubTitle>New reply</SubTitle>
|
||||
<Form action="/home" className="flex flex-col gap-3" method="POST">
|
||||
<input type="hidden" name="intent" value={"reply"} />
|
||||
<input type="hidden" name="postId" value={data.post.id} />
|
||||
<input type="hidden" name="replyId" value={data.id} />
|
||||
<FormLabel>
|
||||
<Text>Reply body</Text>
|
||||
<TextArea name="reply" />
|
||||
<Text type="error">{errors?.body ? errors.body : ""}</Text>
|
||||
</FormLabel>
|
||||
<Button type="submit">Post</Button>
|
||||
</Form>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Title>Twitter clone</Title>
|
||||
<SubTitle>Made with Remix</SubTitle>
|
||||
</>
|
||||
)}
|
||||
{data.childReplies.map((reply) => (
|
||||
<div key={reply.id}>
|
||||
<Post
|
||||
post={reply}
|
||||
userId={rootData?.id}
|
||||
rootPostId={data.post.id}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -5,8 +5,8 @@ import type {
|
|||
} from "@remix-run/node";
|
||||
import {
|
||||
Form,
|
||||
Link,
|
||||
useActionData,
|
||||
Link,
|
||||
useLoaderData,
|
||||
useRouteError,
|
||||
useRouteLoaderData,
|
||||
|
@ -15,7 +15,7 @@ import { Button } from "~/components/Button";
|
|||
import { Card } from "~/components/Card";
|
||||
import { Post, Poster } from "~/components/Post";
|
||||
import { SubTitle, Text, Title } from "~/components/Typography";
|
||||
import { followUser, getUserByName, unFollowUser } from "~/models/user.server";
|
||||
import { followUser, unFollowUser } from "~/models/user.server";
|
||||
import type { RootLoaderTypes } from "~/root";
|
||||
import { prisma, type UserWithRelations } from "~/utils/prisma.server";
|
||||
import { getSession } from "~/utils/session.server";
|
||||
|
@ -60,7 +60,10 @@ export async function action({ request }: LoaderFunctionArgs) {
|
|||
return null;
|
||||
}
|
||||
|
||||
export async function loader({ params }: LoaderFunctionArgs) {
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const tab = url.searchParams.get("tab") || null;
|
||||
|
||||
const data = await prisma.user.findFirst({
|
||||
where: {
|
||||
username: params.username,
|
||||
|
@ -72,102 +75,210 @@ export async function loader({ params }: LoaderFunctionArgs) {
|
|||
pfp: true,
|
||||
desc: true,
|
||||
createdAt: true,
|
||||
posts: {
|
||||
select: {
|
||||
id: true,
|
||||
text: true,
|
||||
createdAt: true,
|
||||
likes: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
reposts: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
},
|
||||
likes: {
|
||||
select: {
|
||||
id: true,
|
||||
text: true,
|
||||
createdAt: true,
|
||||
likes: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
reposts: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
reposts: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
createdAt: true,
|
||||
user: true,
|
||||
post: {
|
||||
select: {
|
||||
id: true,
|
||||
text: true,
|
||||
createdAt: true,
|
||||
likes: {
|
||||
select: {
|
||||
id: true,
|
||||
posts:
|
||||
!tab || tab === "posts"
|
||||
? {
|
||||
select: {
|
||||
id: true,
|
||||
text: true,
|
||||
createdAt: true,
|
||||
likes: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
reposts: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
replies: {
|
||||
where: {
|
||||
parentReplyId: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
reposts: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
user: true,
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
}
|
||||
: false,
|
||||
likes:
|
||||
tab === "likes"
|
||||
? {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
createdAt: true,
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
post: {
|
||||
select: {
|
||||
id: true,
|
||||
text: true,
|
||||
createdAt: true,
|
||||
likes: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
reposts: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
replies: {
|
||||
where: {
|
||||
parentReplyId: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
}
|
||||
: false,
|
||||
reposts:
|
||||
tab === "reposts"
|
||||
? {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
createdAt: true,
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
post: {
|
||||
select: {
|
||||
id: true,
|
||||
text: true,
|
||||
createdAt: true,
|
||||
likes: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
reposts: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
postId: true,
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
author: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
pfp: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
replies: {
|
||||
where: {
|
||||
parentReplyId: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
}
|
||||
: false,
|
||||
followedBy: {
|
||||
select: {
|
||||
id: true,
|
||||
|
@ -192,7 +303,6 @@ export async function loader({ params }: LoaderFunctionArgs) {
|
|||
export default function UserProfile() {
|
||||
const rootData = useRouteLoaderData<RootLoaderTypes>("root");
|
||||
const data: UserWithRelations = useLoaderData<LoaderFunction>();
|
||||
console.log(data.reposts);
|
||||
const actionData = useActionData<ActionFunction>();
|
||||
const isFollowing =
|
||||
data.followedBy.filter((follow) => follow.id === rootData?.id).length > 0;
|
||||
|
@ -245,61 +355,81 @@ export default function UserProfile() {
|
|||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div className="flex flex-col gap-10 md:flex-row">
|
||||
<div className="flex flex-col flex-grow gap-5 w-full">
|
||||
<div className="flex gap-5">
|
||||
<Link to={`?tab=posts`}>
|
||||
<SubTitle>Posts</SubTitle>
|
||||
{data.posts.length > 0 ? (
|
||||
<Card className="!p-0 !gap-0 divide-y">
|
||||
{data.posts.map((post) => (
|
||||
<Post userId={rootData?.id} key={post.id} post={post} />
|
||||
))}
|
||||
</Card>
|
||||
) : (
|
||||
<Text type="subtitle">No posts yet</Text>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow gap-5 w-full">
|
||||
</Link>
|
||||
<Link to={`?tab=likes`}>
|
||||
<SubTitle>Likes</SubTitle>
|
||||
{data.likes.length > 0 ? (
|
||||
<Card className="!p-0 !gap-0 divide-y">
|
||||
{data.likes.map((post) => (
|
||||
<Post
|
||||
userId={rootData?.id}
|
||||
key={post.id}
|
||||
post={post}
|
||||
topTitle={
|
||||
<>
|
||||
<strong>{data.username}</strong> liked this
|
||||
</>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Card>
|
||||
) : (
|
||||
<Text type="subtitle">No likes yet</Text>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow gap-5 w-full">
|
||||
</Link>
|
||||
<Link to={`?tab=reposts`}>
|
||||
<SubTitle>Reposts</SubTitle>
|
||||
{data.reposts.length > 0 ? (
|
||||
<Card className="!p-0 !gap-0 divide-y">
|
||||
{data.reposts.map((repost) => (
|
||||
<Post
|
||||
userId={rootData?.id}
|
||||
key={repost.id}
|
||||
post={repost.post}
|
||||
topTitle={
|
||||
<>
|
||||
<strong>{data.username}</strong> reposted this
|
||||
</>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Card>
|
||||
) : (
|
||||
<Text type="subtitle">No reposts yet</Text>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-col gap-10 lg:flex-row">
|
||||
{data.posts ? (
|
||||
<div className="flex flex-col flex-grow gap-5 w-full">
|
||||
{data.posts.length > 0 ? (
|
||||
<Card className="!p-0 !gap-0 divide-y">
|
||||
{data.posts.map((post) => (
|
||||
<Post userId={rootData?.id} key={post.id} post={post} />
|
||||
))}
|
||||
</Card>
|
||||
) : (
|
||||
<Text type="subtitle">No posts yet</Text>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{data.likes ? (
|
||||
<div className="flex flex-col flex-grow gap-5 w-full">
|
||||
{data.likes.length > 0 ? (
|
||||
<Card className="!p-0 !gap-0 divide-y">
|
||||
{data.likes.map((like) => (
|
||||
<Post
|
||||
userId={rootData?.id}
|
||||
key={like.id}
|
||||
post={like.post}
|
||||
topTitle={
|
||||
<>
|
||||
<strong>{data.username}</strong> liked this
|
||||
</>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Card>
|
||||
) : (
|
||||
<Text type="subtitle">No likes yet</Text>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{data.reposts ? (
|
||||
<div className="flex flex-col flex-grow gap-5 w-full">
|
||||
{data.reposts.length > 0 ? (
|
||||
<Card className="!p-0 !gap-0 divide-y">
|
||||
{data.reposts.map((repost) => (
|
||||
<Post
|
||||
userId={rootData?.id}
|
||||
key={repost.id}
|
||||
post={repost.post}
|
||||
topTitle={
|
||||
<>
|
||||
<strong>{data.username}</strong> reposted this
|
||||
</>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Card>
|
||||
) : (
|
||||
<Text type="subtitle">No reposts yet</Text>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -17,11 +17,12 @@ model User {
|
|||
name String?
|
||||
desc String?
|
||||
pfp String
|
||||
posts Post[] @relation("UserPosts")
|
||||
likes Post[] @relation("UserLikes")
|
||||
posts Post[]
|
||||
likes Like[]
|
||||
reposts Repost[]
|
||||
following User[] @relation("UserFollows")
|
||||
followedBy User[] @relation("UserFollows")
|
||||
replies Reply[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
@ -30,21 +31,54 @@ model User {
|
|||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
text String
|
||||
author User @relation("UserPosts", fields: [userId], references: [id])
|
||||
author User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
likes User[] @relation("UserLikes")
|
||||
likes Like[]
|
||||
reposts Repost[]
|
||||
replies Reply[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Reply {
|
||||
id Int @id @default(autoincrement())
|
||||
text String
|
||||
author User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
post Post @relation(fields: [postId], references: [id])
|
||||
postId Int
|
||||
parentReply Reply? @relation("ReplyToReply", fields: [parentReplyId], references: [id])
|
||||
childReplies Reply[] @relation("ReplyToReply")
|
||||
parentReplyId Int?
|
||||
likes Like[]
|
||||
reposts Repost[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Like {
|
||||
id Int @id @default(autoincrement())
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
post Post? @relation(fields: [postId], references: [id])
|
||||
postId Int?
|
||||
reply Reply? @relation(fields: [replyId], references: [id])
|
||||
replyId Int?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Repost {
|
||||
id Int @id @default(autoincrement())
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
post Post @relation(fields: [postId], references: [id])
|
||||
postId Int
|
||||
id Int @id @default(autoincrement())
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId Int
|
||||
post Post? @relation(fields: [postId], references: [id])
|
||||
postId Int?
|
||||
reply Reply? @relation(fields: [replyId], references: [id])
|
||||
replyId Int?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
|
Loading…
Reference in New Issue