Add like and repost functionality to replies, show them properly in the user pages, sort classnames. Basic functionality pretty much done now

This commit is contained in:
Joonas 2023-11-21 11:19:51 +02:00
parent 07df91cb5d
commit 486002b0bc
10 changed files with 647 additions and 155 deletions

View File

@ -135,7 +135,7 @@ export function Post({
key={post.id}
className={`flex relative flex-col ${
enlarge ? "text-xl" : ""
} gap-4 transition-all bg-ctp-mantle/20 hover:bg-ctp-base first:rounded-t-lg last:rounded-b-lg`}
} gap-4 transition-all hover:bg-ctp-base first:rounded-t-lg last:rounded-b-lg`}
>
<div
className={` ${!child && reply ? `border-y py-4` : ""} ${
@ -155,7 +155,7 @@ export function Post({
</div>
<div className="flex relative flex-col gap-2 w-full">
<div className="absolute right-0">
<Text className="subtitle text-[12px] text-ctp-subtext1/25">
<Text className="subtitle text-[12px] text-ctp-subtext1/30">
{new Date(post.createdAt).toLocaleDateString("en-fi", {
year: "numeric",
month: "long",
@ -225,7 +225,11 @@ export function Post({
name="intent"
value={liked ? "unlike" : "like"}
/>
<input type="hidden" name="postId" value={post.id} />
<input
type="hidden"
name={rootPostId ? "replyId" : "postId"}
value={post.id}
/>
<button type="submit">
<Text type="link">
{liked ? "Unlike" : "Like"} ({post.likes.length})
@ -238,7 +242,11 @@ export function Post({
name="intent"
value={reposted ? "unrepost" : "repost"}
/>
<input type="hidden" name="postId" value={post.id} />
<input
type="hidden"
name={rootPostId ? "replyId" : "postId"}
value={post.id}
/>
<button type="submit">
<Text type="link">
{reposted ? "Unrepost" : "Repost"} ({post.reposts.length})

View File

@ -1,86 +1,179 @@
import { prisma } from "~/utils/prisma.server";
import { getPostById } from "./post.server";
import { getReplyById, reply } from "./reply.server";
interface PostErrors {
postId: string | undefined;
like: string | undefined;
}
export async function likePost(postId: number, id: number) {
const errors: PostErrors = {
postId: undefined,
like: undefined,
};
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 = "You have already liked that post";
}
if (Object.values(errors).some(Boolean)) {
return errors;
}
const likeQ = await prisma.like.create({
data: {
postId: postId,
userId: id,
},
});
if (!likeQ) {
errors.like = "Something went wrong liking the post";
return errors;
}
return errors;
interface ReplyErrors {
replyId: string | undefined;
reply: string | undefined;
}
export async function unLikePost(postId: number, id: number) {
const errors: PostErrors = {
postId: undefined,
like: undefined,
};
export async function like(
postId: number,
id: number,
replyId: number | undefined
) {
if (replyId) {
const errors: ReplyErrors = {
replyId: undefined,
reply: undefined,
};
const post = await getPostById(postId);
if (!post) {
errors.postId = "No post with that id";
}
const post = await getReplyById(replyId);
if (!post) {
errors.replyId = "No reply with that id";
}
const like = await prisma.like.findFirst({
where: {
userId: id,
postId: postId,
},
});
if (!like) {
errors.like = "Like doesn't exist";
}
const reply = await prisma.like.findFirst({
where: {
userId: id,
replyId: replyId,
},
});
if (reply) {
errors.reply = "You have already liked that reply";
}
if (Object.values(errors).some(Boolean)) {
return errors;
}
const replyQ = await prisma.like.create({
data: {
replyId: replyId,
userId: id,
},
});
if (!replyQ) {
errors.reply = "Something went wrong liking the post";
return errors;
}
return errors;
} else {
const errors: PostErrors = {
postId: undefined,
like: undefined,
};
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 = "You have already liked that post";
}
if (Object.values(errors).some(Boolean)) {
return errors;
}
const likeQ = await prisma.like.create({
data: {
postId: postId,
userId: id,
},
});
if (!likeQ) {
errors.like = "Something went wrong liking the post";
return errors;
}
return errors;
}
}
export async function unLike(
postId: number,
id: number,
replyId: number | undefined
) {
if (replyId) {
const errors: ReplyErrors = {
replyId: undefined,
reply: undefined,
};
const reply = await getReplyById(replyId);
if (!reply) {
errors.replyId = "No post with that id";
}
const like = await prisma.like.findFirst({
where: {
userId: id,
replyId: replyId,
},
});
if (!like) {
errors.reply = "Like doesn't exist";
}
if (Object.values(errors).some(Boolean)) {
return errors;
}
const replyQ = await prisma.like.delete({
where: {
id: like.id,
},
});
if (!replyQ) {
errors.reply = "Something went wrong unliking the post";
return errors;
}
return errors;
} else {
const errors: PostErrors = {
postId: undefined,
like: undefined,
};
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 = "Like doesn't exist";
}
if (Object.values(errors).some(Boolean)) {
return errors;
}
const likeQ = await prisma.like.delete({
where: {
id: like.id,
},
});
if (!likeQ) {
errors.like = "Something went wrong unliking the post";
return errors;
}
if (Object.values(errors).some(Boolean)) {
return errors;
}
const likeQ = await prisma.like.delete({
where: {
id: like.id,
},
});
if (!likeQ) {
errors.like = "Something went wrong unliking the post";
return errors;
}
return errors;
}

View File

@ -2,7 +2,7 @@ import { prisma } from "~/utils/prisma.server";
import { getPostById, validatePostBody } from "./post.server";
import type { Reply } from "@prisma/client";
async function getReplyById(id: number) {
export async function getReplyById(id: number) {
const reply = await prisma.reply.findUnique({
where: {
id: id,

View File

@ -1,76 +1,159 @@
import { prisma } from "~/utils/prisma.server";
import { getPostById } from "./post.server";
import { getReplyById } from "./reply.server";
export async function repost(postId: number, id: number) {
const errors = {};
export async function repost(
postId: number,
id: number,
replyId: number | undefined
) {
if (replyId) {
const errors = {};
const post = await getPostById(postId);
const reply = await getReplyById(replyId);
if (!post) {
errors.repost = "Post doesn't exist";
}
if (!reply) {
errors.repost = "Post doesn't exist";
}
const repost = await prisma.repost.findFirst({
where: {
postId: postId,
userId: id,
},
});
const repost = await prisma.repost.findFirst({
where: {
replyId: replyId,
userId: id,
},
});
if (repost) {
errors.repost = "You have already reposted that post";
}
if (repost) {
errors.repost = "You have already reposted that reply";
}
if (Object.values(errors).some(Boolean)) {
return errors;
}
const repostQ = await prisma.repost.create({
data: {
userId: id,
replyId: replyId,
},
});
if (!repostQ) {
errors.repost = "Something went wrong";
}
return errors;
} else {
const errors = {};
const post = await getPostById(postId);
if (!post) {
errors.repost = "Post doesn't exist";
}
const repost = await prisma.repost.findFirst({
where: {
postId: postId,
userId: id,
},
});
if (repost) {
errors.repost = "You have already reposted that post";
}
if (Object.values(errors).some(Boolean)) {
return errors;
}
const repostQ = await prisma.repost.create({
data: {
userId: id,
postId: postId,
},
});
if (!repostQ) {
errors.repost = "Something went wrong";
}
if (Object.values(errors).some(Boolean)) {
return errors;
}
const repostQ = await prisma.repost.create({
data: {
userId: id,
postId: postId,
},
});
if (!repostQ) {
errors.repost = "Something went wrong";
}
return errors;
}
export async function unRepost(postId: number, id: number) {
const errors = {};
export async function unRepost(
postId: number,
id: number,
replyId: number | undefined
) {
if (replyId) {
const errors = {};
const post = await getPostById(postId);
if (!post) {
errors.repost = "Post doesn't exist";
}
const reply = await getReplyById(replyId);
if (!reply) {
errors.repost = "Reply doesn't exist";
}
const repost = await prisma.repost.findFirst({
where: {
userId: id,
postId: postId,
},
});
const repost = await prisma.repost.findFirst({
where: {
userId: id,
replyId: replyId,
},
});
if (!repost) {
errors.repost = "Repost doesn't exist";
}
if (!repost) {
errors.repost = "Repost doesn't exist";
}
if (Object.values(errors).some(Boolean)) {
return errors;
}
const repostQ = await prisma.repost.delete({
where: {
id: repost.id,
},
});
if (!repostQ) {
errors.repost = "Something went wrong";
}
return errors;
} else {
const errors = {};
const post = await getPostById(postId);
if (!post) {
errors.repost = "Post doesn't exist";
}
const repost = await prisma.repost.findFirst({
where: {
userId: id,
postId: postId,
},
});
if (!repost) {
errors.repost = "Repost doesn't exist";
}
if (Object.values(errors).some(Boolean)) {
return errors;
}
const repostQ = await prisma.repost.delete({
where: {
id: repost.id,
},
});
if (!repostQ) {
errors.repost = "Something went wrong";
}
if (Object.values(errors).some(Boolean)) {
return errors;
}
const repostQ = await prisma.repost.delete({
where: {
id: repost.id,
},
});
if (!repostQ) {
errors.repost = "Something went wrong";
}
return errors;
}

View File

@ -19,7 +19,7 @@ import { Card } from "~/components/Card";
import { FormLabel, TextArea } from "~/components/Form";
import { Post } from "~/components/Post";
import { SubTitle, Text, Title } from "~/components/Typography";
import { likePost, unLikePost } from "~/models/like.server";
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";
@ -54,7 +54,11 @@ export async function action({ request }: ActionFunctionArgs) {
break;
case "like":
const likeErrors = await likePost(Number(formData.postId), userId);
const likeErrors = await like(
Number(formData.postId),
userId,
Number(formData.replyId)
);
if (Object.values(likeErrors).some(Boolean)) {
return likeErrors;
@ -64,7 +68,11 @@ export async function action({ request }: ActionFunctionArgs) {
break;
case "unlike":
const unLikeErrors = await unLikePost(Number(formData.postId), userId);
const unLikeErrors = await unLike(
Number(formData.postId),
userId,
Number(formData.replyId)
);
if (Object.values(unLikeErrors).some(Boolean)) {
return unLikeErrors;
@ -74,7 +82,11 @@ export async function action({ request }: ActionFunctionArgs) {
break;
case "repost":
const repostErrors = await repost(Number(formData.postId), userId);
const repostErrors = await repost(
Number(formData.postId),
userId,
Number(formData.replyId)
);
if (Object.values(repostErrors).some(Boolean)) {
return repostErrors;
@ -84,7 +96,11 @@ export async function action({ request }: ActionFunctionArgs) {
break;
case "unrepost":
const unrepostErrors = await unRepost(Number(formData.postId), userId);
const unrepostErrors = await unRepost(
Number(formData.postId),
userId,
Number(formData.replyId)
);
if (Object.values(unrepostErrors).some(Boolean)) {
return unrepostErrors;

View File

@ -1,12 +1,12 @@
import type { ActionFunction } from "@remix-run/node";
import {
json,
type LoaderFunction,
type LoaderFunctionArgs,
type ActionFunction,
} from "@remix-run/node";
import {
Form,
useActionData,
useFetcher,
useLoaderData,
useRouteLoaderData,
} from "@remix-run/react";
@ -162,6 +162,7 @@ export default function PostRoute() {
const rootData = useRouteLoaderData<RootLoaderTypes>("root");
const data: PostWithRelations = useLoaderData<LoaderFunction>();
const errors = useActionData<ActionFunction>();
const fetcher = useFetcher();
return (
<div className="flex flex-col gap-5">
@ -178,7 +179,11 @@ export default function PostRoute() {
{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">
<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={data.id} />
<FormLabel>
@ -187,7 +192,7 @@ export default function PostRoute() {
<Text type="error">{errors?.body ? errors.body : ""}</Text>
</FormLabel>
<Button type="submit">Post</Button>
</Form>
</fetcher.Form>
</div>
) : (
""

View File

@ -8,6 +8,7 @@ import {
import {
Form,
useActionData,
useFetcher,
useLoaderData,
useRouteLoaderData,
} from "@remix-run/react";
@ -346,6 +347,7 @@ export default function ReplyRoute() {
const rootData = useRouteLoaderData<RootLoaderTypes>("root");
const data: PostWithRelations = useLoaderData<LoaderFunction>();
const errors = useActionData<ActionFunction>();
const fetcher = useFetcher();
return (
<div className="flex flex-col gap-5">
@ -397,7 +399,11 @@ export default function ReplyRoute() {
{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">
<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={data.post.id} />
<input type="hidden" name="replyId" value={data.id} />
@ -407,13 +413,10 @@ export default function ReplyRoute() {
<Text type="error">{errors?.body ? errors.body : ""}</Text>
</FormLabel>
<Button type="submit">Post</Button>
</Form>
</fetcher.Form>
</div>
) : (
<>
<Title>Twitter clone</Title>
<SubTitle>Made with Remix</SubTitle>
</>
""
)}
{data.childReplies.map((reply) => (
<div key={reply.id}>
@ -425,12 +428,21 @@ export default function ReplyRoute() {
topTitle={
<>
Replying to
<Poster
style="compact"
username={data.parentReply.author.username}
name={data.parentReply.author.name}
pfp={data.parentReply.author.pfp}
/>
{data?.parentReply?.author ? (
<Poster
style="compact"
username={data.parentReply.author.username}
name={data.parentReply.author.name}
pfp={data.parentReply.author.pfp}
/>
) : (
<Poster
style="compact"
username={data.post.author.username}
name={data.post.author.name}
pfp={data.post.author.pfp}
/>
)}
</>
}
/>

View File

@ -205,6 +205,66 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
},
},
},
reply: {
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: {
author: {
select: {
id: true,
username: true,
name: true,
pfp: true,
},
},
},
},
post: {
select: {
author: {
select: {
id: true,
username: true,
name: true,
pfp: true,
},
},
},
},
_count: {
select: {
childReplies: {
where: {
parentReplyId: null,
},
},
},
},
},
},
},
orderBy: {
createdAt: "desc",
@ -278,6 +338,66 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
},
},
},
reply: {
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: {
author: {
select: {
id: true,
username: true,
name: true,
pfp: true,
},
},
},
},
post: {
select: {
author: {
select: {
id: true,
username: true,
name: true,
pfp: true,
},
},
},
},
_count: {
select: {
childReplies: {
where: {
parentReplyId: null,
},
},
},
},
},
},
},
orderBy: {
createdAt: "desc",
@ -311,6 +431,7 @@ export default function UserProfile() {
const actionData = useActionData<ActionFunction>();
const isFollowing =
data.followedBy.filter((follow) => follow.id === rootData?.id).length > 0;
console.log(data);
return (
<div className="flex flex-col gap-5">
@ -395,10 +516,35 @@ export default function UserProfile() {
<Post
userId={rootData?.id}
key={like.id}
post={like.post}
rootPostId={like?.reply?.post?.id}
post={like.reply ? like.reply : like.post}
topTitle={
<>
<strong>{data.username}</strong> liked this
{like.reply ? (
<>
, Replying to{" "}
{like.reply.parentReply ? (
<Poster
style="compact"
username={
like.reply.parentReply.author.username
}
name={like.reply.parentReply.author.name}
pfp={like.reply.parentReply.author.pfp}
/>
) : (
<Poster
style="compact"
username={like.reply.post.author.username}
name={like.reply.post.author.name}
pfp={like.reply.post.author.pfp}
/>
)}
</>
) : (
""
)}
</>
}
/>
@ -419,10 +565,35 @@ export default function UserProfile() {
<Post
userId={rootData?.id}
key={repost.id}
post={repost.post}
rootPostId={repost?.reply?.post?.id}
post={repost.reply ? repost.reply : repost.post}
topTitle={
<>
<strong>{data.username}</strong> reposted this
{repost.reply ? (
<>
, Replying to{" "}
{repost.reply.parentReply ? (
<Poster
style="compact"
username={
repost.reply.parentReply.author.username
}
name={repost.reply.parentReply.author.name}
pfp={repost.reply.parentReply.author.pfp}
/>
) : (
<Poster
style="compact"
username={repost.reply.post.author.username}
name={repost.reply.post.author.name}
pfp={repost.reply.post.author.pfp}
/>
)}
</>
) : (
""
)}
</>
}
/>

View File

@ -0,0 +1,49 @@
import type { LoaderFunction, LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData, useParams } from "@remix-run/react";
import { Card } from "~/components/Card";
import { Poster } from "~/components/Post";
import { Title } from "~/components/Typography";
import { prisma } from "~/utils/prisma.server";
export async function loader({ params }: LoaderFunctionArgs) {
const followers = await prisma.user.findMany({
where: {
following: {
some: {
username: params.username,
},
},
},
select: {
id: true,
username: true,
name: true,
pfp: true,
},
});
return followers;
}
export default function UserFollowers() {
const data = useLoaderData<LoaderFunction>();
const params = useParams();
return (
<div className="flex flex-col gap-5">
<Title>
User <em>{params.username}</em> followers
</Title>
<Card>
{data.map((follower) => (
<Poster
key={follower.id}
username={follower.username}
name={follower.name}
pfp={follower.pfp}
/>
))}
</Card>
</div>
);
}

View File

@ -0,0 +1,55 @@
import type { LoaderFunction, LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData, useParams } from "@remix-run/react";
import { Card } from "~/components/Card";
import { Poster } from "~/components/Post";
import { Title } from "~/components/Typography";
import { prisma } from "~/utils/prisma.server";
export async function loader({ params }: LoaderFunctionArgs) {
const followed = await prisma.user.findMany({
where: {
followedBy: {
some: {
username: params.username,
},
},
},
select: {
id: true,
username: true,
name: true,
pfp: true,
},
});
console.log(followed);
if (!followed) {
throw Error("Not found");
}
return followed;
}
export default function UserFollowed() {
const data = useLoaderData<LoaderFunction>();
const params = useParams();
return (
<div className="flex flex-col gap-5">
<Title>
User <em>{params.username}</em> follows
</Title>
<Card>
{data.map((follower) => (
<Poster
key={follower.id}
username={follower.username}
name={follower.name}
pfp={follower.pfp}
/>
))}
</Card>
</div>
);
}