Compare commits

..

No commits in common. "d980d11be049fa88b5adc95ae32591399d04fcc5" and "2d6fead452ea9ee6294fb13dafc466365344d0b0" have entirely different histories.

7 changed files with 437 additions and 717 deletions

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import LikeButton from "@/components/posts/LikeButton"; import LikeButton from "@/components/posts/LikeButton";
import { getCookie, hasCookie } from "@/helpers/cookie"; import { getCookie } from "@/helpers/cookie";
import { PostType } from "@/types/PostType"; import { PostType } from "@/types/PostType";
import { TagType } from "@/types/TagType"; import { TagType } from "@/types/TagType";
import { UserType } from "@/types/UserType"; import { UserType } from "@/types/UserType";
@ -36,9 +36,6 @@ import {
import { redirect, useParams } from "next/navigation"; import { redirect, useParams } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import Editor from "@/components/editor";
import sanitizeHtml from "sanitize-html";
import CommentCard from "@/components/posts/CommentCard";
export default function PostPage() { export default function PostPage() {
const [post, setPost] = useState<PostType>(); const [post, setPost] = useState<PostType>();
@ -46,8 +43,6 @@ export default function PostPage() {
const [reduceMotion, setReduceMotion] = useState<boolean>(false); const [reduceMotion, setReduceMotion] = useState<boolean>(false);
const [user, setUser] = useState<UserType>(); const [user, setUser] = useState<UserType>();
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [content, setContent] = useState("");
const [waitingPost, setWaitingPost] = useState(false);
useEffect(() => { useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
@ -115,177 +110,210 @@ export default function PostPage() {
/> />
</div> </div>
) : ( ) : (
<> <Card className="bg-opacity-60 !duration-250 !ease-linear !transition-all">
<Card className="bg-opacity-60 !duration-250 !ease-linear !transition-all"> <CardBody className="p-5">
<CardBody className="p-5"> <div>
<div> {post && (
{post && ( <div>
<div> <Link href={`/p/${post.slug}`}>
<Link href={`/p/${post.slug}`}> <p className="text-2xl">{post.title}</p>
<p className="text-2xl">{post.title}</p> </Link>
</Link>
<div className="flex items-center gap-3 text-xs text-default-500 pt-1"> <div className="flex items-center gap-3 text-xs text-default-500 pt-1">
<p>By</p> <p>By</p>
<Link <Link
href={`/u/${post.author.slug}`} href={`/u/${post.author.slug}`}
className="flex items-center gap-2" className="flex items-center gap-2"
> >
<Avatar <Avatar
size="sm" size="sm"
className="w-6 h-6" className="w-6 h-6"
src={post.author.profilePicture} src={post.author.profilePicture}
classNames={{ classNames={{
base: "bg-transparent", base: "bg-transparent",
}} }}
/>
<p>{post.author.name}</p>
</Link>
<p>
{formatDistance(new Date(post.createdAt), new Date(), {
addSuffix: true,
})}
</p>
</div>
<Spacer y={4} />
<div
className="prose dark:prose-invert !duration-250 !ease-linear !transition-all"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
<Spacer y={4} />
{post.tags.filter((tag) => tag.name != "D2Jam").length >
0 ? (
<div className="flex gap-1">
{post.tags
.filter((tag) => tag.name != "D2Jam")
.map((tag: TagType) => (
<Link
href="/"
key={tag.id}
className={`transition-all transform duration-500 ease-in-out ${
!reduceMotion ? "hover:scale-110" : ""
}`}
>
<Chip
radius="sm"
size="sm"
className="!duration-250 !ease-linear !transition-all"
variant="faded"
avatar={
tag.icon && (
<Avatar
src={tag.icon}
classNames={{ base: "bg-transparent" }}
/>
)
}
>
{tag.name}
</Chip>
</Link>
))}
</div>
) : (
<></>
)}
{post.tags.length > 0 && <Spacer y={4} />}
<div className="flex gap-3">
<LikeButton
likes={post.likes.length}
liked={post.hasLiked}
parentId={post.id}
/> />
<Link href="#create-comment"> <p>{post.author.name}</p>
<Button size="sm" variant="bordered"> </Link>
<MessageCircle size={16} /> {post.comments.length} <p>
</Button> {formatDistance(new Date(post.createdAt), new Date(), {
</Link> addSuffix: true,
<Dropdown backdrop="opaque"> })}
<DropdownTrigger> </p>
<Button size="sm" variant="bordered" isIconOnly> </div>
<MoreVertical size={16} />
</Button> <Spacer y={4} />
</DropdownTrigger>
<DropdownMenu className="text-[#333] dark:text-white"> <div
<DropdownSection className="prose dark:prose-invert !duration-250 !ease-linear !transition-all"
showDivider={user?.mod} dangerouslySetInnerHTML={{ __html: post.content }}
title="Actions" />
<Spacer y={4} />
{post.tags.filter((tag) => tag.name != "D2Jam").length > 0 ? (
<div className="flex gap-1">
{post.tags
.filter((tag) => tag.name != "D2Jam")
.map((tag: TagType) => (
<Link
href="/"
key={tag.id}
className={`transition-all transform duration-500 ease-in-out ${
!reduceMotion ? "hover:scale-110" : ""
}`}
> >
<Chip
radius="sm"
size="sm"
className="!duration-250 !ease-linear !transition-all"
variant="faded"
avatar={
tag.icon && (
<Avatar
src={tag.icon}
classNames={{ base: "bg-transparent" }}
/>
)
}
>
{tag.name}
</Chip>
</Link>
))}
</div>
) : (
<></>
)}
{post.tags.length > 0 && <Spacer y={4} />}
<div className="flex gap-3">
<LikeButton post={post} />
<Button
size="sm"
variant="bordered"
onPress={() => {
toast.warning("Comment functionality coming soon");
}}
>
<MessageCircle size={16} /> {0}
</Button>
<Dropdown backdrop="opaque">
<DropdownTrigger>
<Button size="sm" variant="bordered" isIconOnly>
<MoreVertical size={16} />
</Button>
</DropdownTrigger>
<DropdownMenu className="text-[#333] dark:text-white">
<DropdownSection
showDivider={user?.mod}
title="Actions"
>
<DropdownItem
key="report"
startContent={<Flag />}
description="Report this post to moderators to handle"
onPress={() => {
toast.warning("Report functionality coming soon");
}}
>
Create Report
</DropdownItem>
{user?.slug == post.author.slug ? (
<DropdownItem <DropdownItem
key="report" key="delete"
startContent={<Flag />} startContent={<Trash />}
description="Report this post to moderators to handle" description="Delete your post"
onPress={() => { onPress={async () => {
toast.warning( const response = await fetch(
"Report functionality coming soon" process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/post"
: "http://localhost:3005/api/v1/post",
{
body: JSON.stringify({
postId: post.id,
username: getCookie("user"),
}),
method: "DELETE",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${getCookie(
"token"
)}`,
},
credentials: "include",
}
); );
if (response.ok) {
toast.success("Deleted post");
redirect("/");
} else {
toast.error("Error while deleting post");
}
}} }}
> >
Create Report Delete
</DropdownItem> </DropdownItem>
{user?.slug == post.author.slug ? ( ) : (
<DropdownItem <></>
key="delete" )}
startContent={<Trash />} </DropdownSection>
description="Delete your post" {user?.mod ? (
onPress={async () => { <DropdownSection title="Mod Zone">
const response = await fetch( <DropdownItem
process.env.NEXT_PUBLIC_MODE === "PROD" key="remove"
? "https://d2jam.com/api/v1/post" startContent={<X />}
: "http://localhost:3005/api/v1/post", description="Remove this post"
{ onPress={async () => {
body: JSON.stringify({ const response = await fetch(
postId: post.id, process.env.NEXT_PUBLIC_MODE === "PROD"
username: getCookie("user"), ? "https://d2jam.com/api/v1/post"
}), : "http://localhost:3005/api/v1/post",
method: "DELETE", {
headers: { body: JSON.stringify({
"Content-Type": "application/json", postId: post.id,
authorization: `Bearer ${getCookie( username: getCookie("user"),
"token" }),
)}`, method: "DELETE",
}, headers: {
credentials: "include", "Content-Type": "application/json",
} authorization: `Bearer ${getCookie(
); "token"
)}`,
if (response.ok) { },
toast.success("Deleted post"); credentials: "include",
redirect("/");
} else {
toast.error("Error while deleting post");
} }
}} );
>
Delete if (response.ok) {
</DropdownItem> toast.success("Removed post");
) : ( redirect("/");
<></> } else {
)} toast.error("Error while removing post");
</DropdownSection> }
{user?.mod ? ( }}
<DropdownSection title="Mod Zone"> >
Remove
</DropdownItem>
{post.sticky ? (
<DropdownItem <DropdownItem
key="remove" key="unsticky"
startContent={<X />} startContent={<StarOff />}
description="Remove this post" description="Unsticky post"
onPress={async () => { onPress={async () => {
const response = await fetch( const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD" process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/post" ? "https://d2jam.com/api/v1/post/sticky"
: "http://localhost:3005/api/v1/post", : "http://localhost:3005/api/v1/post/sticky",
{ {
body: JSON.stringify({ body: JSON.stringify({
postId: post.id, postId: post.id,
sticky: false,
username: getCookie("user"), username: getCookie("user"),
}), }),
method: "DELETE", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
authorization: `Bearer ${getCookie( authorization: `Bearer ${getCookie(
@ -297,342 +325,234 @@ export default function PostPage() {
); );
if (response.ok) { if (response.ok) {
toast.success("Removed post"); toast.success("Unsticked post");
redirect("/"); redirect("/");
} else { } else {
toast.error("Error while removing post"); toast.error("Error while removing post");
} }
}} }}
> >
Remove Unsticky
</DropdownItem> </DropdownItem>
{post.sticky ? ( ) : (
<DropdownItem <DropdownItem
key="unsticky" key="sticky"
startContent={<StarOff />} startContent={<Star />}
description="Unsticky post" description="Sticky post"
onPress={async () => { onPress={async () => {
const response = await fetch( const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD" process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/post/sticky" ? "https://d2jam.com/api/v1/post/sticky"
: "http://localhost:3005/api/v1/post/sticky", : "http://localhost:3005/api/v1/post/sticky",
{ {
body: JSON.stringify({ body: JSON.stringify({
postId: post.id, postId: post.id,
sticky: false, sticky: true,
username: getCookie("user"), username: getCookie("user"),
}), }),
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
authorization: `Bearer ${getCookie( authorization: `Bearer ${getCookie(
"token" "token"
)}`, )}`,
}, },
credentials: "include", credentials: "include",
}
);
if (response.ok) {
toast.success("Unsticked post");
redirect("/");
} else {
toast.error("Error while removing post");
} }
}} );
>
Unsticky
</DropdownItem>
) : (
<DropdownItem
key="sticky"
startContent={<Star />}
description="Sticky post"
onPress={async () => {
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/post/sticky"
: "http://localhost:3005/api/v1/post/sticky",
{
body: JSON.stringify({
postId: post.id,
sticky: true,
username: getCookie("user"),
}),
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${getCookie(
"token"
)}`,
},
credentials: "include",
}
);
if (response.ok) { if (response.ok) {
toast.success("Unsticked post"); toast.success("Unsticked post");
redirect("/"); redirect("/");
} else { } else {
toast.error("Error while removing post"); toast.error("Error while removing post");
}
}}
>
Sticky
</DropdownItem>
)}
{user?.admin && !post.author.mod ? (
<DropdownItem
key="promote-mod"
startContent={<Shield />}
description="Promote user to Mod"
onPress={async () => {
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/mod"
: "http://localhost:3005/api/v1/mod",
{
body: JSON.stringify({
targetname: post.author.slug,
mod: true,
username: getCookie("user"),
}),
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${getCookie(
"token"
)}`,
},
credentials: "include",
} }
}} );
>
Sticky
</DropdownItem>
)}
{user?.admin && !post.author.mod ? (
<DropdownItem
key="promote-mod"
startContent={<Shield />}
description="Promote user to Mod"
onPress={async () => {
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/mod"
: "http://localhost:3005/api/v1/mod",
{
body: JSON.stringify({
targetname: post.author.slug,
mod: true,
username: getCookie("user"),
}),
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${getCookie(
"token"
)}`,
},
credentials: "include",
}
);
if (response.ok) { if (response.ok) {
toast.success("Promoted User to Mod"); toast.success("Promoted User to Mod");
window.location.reload(); window.location.reload();
} else { } else {
toast.error( toast.error(
"Error while promoting user to Mod" "Error while promoting user to Mod"
);
}
}}
>
Appoint as mod
</DropdownItem>
) : (
<></>
)}
{user?.admin &&
post.author.mod &&
!post.author.admin ? (
<DropdownItem
key="demote-mod"
startContent={<ShieldX />}
description="Demote user from Mod"
onPress={async () => {
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/mod"
: "http://localhost:3005/api/v1/mod",
{
body: JSON.stringify({
targetname: post.author.slug,
username: getCookie("user"),
}),
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${getCookie(
"token"
)}`,
},
credentials: "include",
}
); );
}
if (response.ok) { }}
toast.success("Demoted User"); >
window.location.reload(); Appoint as mod
} else { </DropdownItem>
toast.error("Error while demoting user"); ) : (
<></>
)}
{user?.admin &&
post.author.mod &&
!post.author.admin ? (
<DropdownItem
key="demote-mod"
startContent={<ShieldX />}
description="Demote user from Mod"
onPress={async () => {
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/mod"
: "http://localhost:3005/api/v1/mod",
{
body: JSON.stringify({
targetname: post.author.slug,
username: getCookie("user"),
}),
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${getCookie(
"token"
)}`,
},
credentials: "include",
} }
}} );
>
Remove as mod if (response.ok) {
</DropdownItem> toast.success("Demoted User");
) : ( window.location.reload();
<></> } else {
)} toast.error("Error while demoting user");
{user?.admin && !post.author.admin ? ( }
<DropdownItem }}
key="promote-admin" >
startContent={<ShieldAlert />} Remove as mod
description="Promote user to Admin" </DropdownItem>
onPress={async () => { ) : (
const response = await fetch( <></>
process.env.NEXT_PUBLIC_MODE === "PROD" )}
? "https://d2jam.com/api/v1/mod" {user?.admin && !post.author.admin ? (
: "http://localhost:3005/api/v1/mod", <DropdownItem
{ key="promote-admin"
body: JSON.stringify({ startContent={<ShieldAlert />}
targetname: post.author.slug, description="Promote user to Admin"
admin: true, onPress={async () => {
username: getCookie("user"), const response = await fetch(
}), process.env.NEXT_PUBLIC_MODE === "PROD"
method: "POST", ? "https://d2jam.com/api/v1/mod"
headers: { : "http://localhost:3005/api/v1/mod",
"Content-Type": "application/json", {
authorization: `Bearer ${getCookie( body: JSON.stringify({
"token" targetname: post.author.slug,
)}`, admin: true,
}, username: getCookie("user"),
credentials: "include", }),
} method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${getCookie(
"token"
)}`,
},
credentials: "include",
}
);
if (response.ok) {
toast.success("Promoted User to Admin");
window.location.reload();
} else {
toast.error(
"Error while promoting user to Admin"
); );
}
if (response.ok) { }}
toast.success("Promoted User to Admin"); >
window.location.reload(); Appoint as admin
} else { </DropdownItem>
toast.error( ) : (
"Error while promoting user to Admin" <></>
); )}
{user?.admin &&
post.author.admin &&
post.author.id !== user.id ? (
<DropdownItem
key="demote-admin"
startContent={<ShieldX />}
description="Demote user to mod"
onPress={async () => {
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/mod"
: "http://localhost:3005/api/v1/mod",
{
body: JSON.stringify({
targetname: post.author.slug,
mod: true,
username: getCookie("user"),
}),
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${getCookie(
"token"
)}`,
},
credentials: "include",
} }
}} );
>
Appoint as admin if (response.ok) {
</DropdownItem> toast.success("Demoted User to Mod");
) : ( window.location.reload();
<></> } else {
)} toast.error(
{user?.admin && "Error while demoting user to mod"
post.author.admin &&
post.author.id !== user.id ? (
<DropdownItem
key="demote-admin"
startContent={<ShieldX />}
description="Demote user to mod"
onPress={async () => {
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/mod"
: "http://localhost:3005/api/v1/mod",
{
body: JSON.stringify({
targetname: post.author.slug,
mod: true,
username: getCookie("user"),
}),
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${getCookie(
"token"
)}`,
},
credentials: "include",
}
); );
}
if (response.ok) { }}
toast.success("Demoted User to Mod"); >
window.location.reload(); Remove as admin
} else { </DropdownItem>
toast.error( ) : (
"Error while demoting user to mod" <></>
); )}
} </DropdownSection>
}} ) : (
> <></>
Remove as admin )}
</DropdownItem> </DropdownMenu>
) : ( </Dropdown>
<></>
)}
</DropdownSection>
) : (
<></>
)}
</DropdownMenu>
</Dropdown>
</div>
</div> </div>
)} </div>
</div> )}
</CardBody> </div>
</Card> </CardBody>
<div id="create-comment" /> </Card>
<Spacer y={10} />
<Editor content={content} setContent={setContent} />
<Spacer />
<Button
color="primary"
onPress={async () => {
if (!content) {
toast.error("Please enter valid content");
return;
}
if (!hasCookie("token")) {
toast.error("You are not logged in");
return;
}
const sanitizedHtml = sanitizeHtml(content);
setWaitingPost(true);
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/comment"
: "http://localhost:3005/api/v1/comment",
{
body: JSON.stringify({
content: sanitizedHtml,
username: getCookie("user"),
postId: post?.id,
}),
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${getCookie("token")}`,
},
credentials: "include",
}
);
if (response.status == 401) {
toast.error("Invalid User");
setWaitingPost(false);
return;
}
if (response.ok) {
toast.success("Successfully created comment");
setWaitingPost(false);
window.location.reload();
} else {
toast.error("An error occured");
setWaitingPost(false);
}
}}
>
{waitingPost ? (
<LoaderCircle className="animate-spin" size={16} />
) : (
<p>Create Comment</p>
)}
</Button>
<Spacer y={10} />
<div className="flex flex-col gap-3">
{post?.comments.map((comment) => (
<div key={comment.id}>
<CommentCard comment={comment} />
</div>
))}
</div>
</>
)} )}
</> </>
); );

View file

@ -1,162 +0,0 @@
import { CommentType } from "@/types/CommentType";
import { Avatar, Button, Card, CardBody, Spacer } from "@nextui-org/react";
import { formatDistance } from "date-fns";
import { LoaderCircle, Reply } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import Editor from "../editor";
import { toast } from "react-toastify";
import { getCookie, hasCookie } from "@/helpers/cookie";
import sanitizeHtml from "sanitize-html";
import LikeButton from "./LikeButton";
export default function CommentCard({ comment }: { comment: CommentType }) {
const [creatingReply, setCreatingReply] = useState<boolean>(false);
const [content, setContent] = useState("");
const [waitingPost, setWaitingPost] = useState(false);
return (
<Card className="bg-opacity-60 !duration-250 !ease-linear !transition-all">
<CardBody className="p-5">
<div>
<div className="flex items-center gap-3 text-xs text-default-500 pt-1">
<p>By</p>
<Link
href={`/u/${comment.author.slug}`}
className="flex items-center gap-2"
>
<Avatar
size="sm"
className="w-6 h-6"
src={comment.author.profilePicture}
classNames={{
base: "bg-transparent",
}}
/>
<p>{comment.author.name}</p>
</Link>
<p>
{formatDistance(new Date(comment.createdAt), new Date(), {
addSuffix: true,
})}
</p>
</div>
<Spacer y={4} />
<div
className="prose dark:prose-invert !duration-250 !ease-linear !transition-all"
dangerouslySetInnerHTML={{ __html: comment.content }}
/>
<Spacer y={4} />
<div className="flex gap-3">
<LikeButton
likes={comment.likes.length}
liked={comment.hasLiked}
parentId={comment.id}
isComment
/>
<Button
size="sm"
variant="bordered"
onPress={() => {
setCreatingReply(!creatingReply);
}}
>
<Reply size={16} />
</Button>
</div>
<Spacer y={4} />
{creatingReply && (
<>
<Editor content={content} setContent={setContent} />
<div id="create-comment" />
<Spacer />
<Button
color="primary"
onPress={async () => {
if (!content) {
toast.error("Please enter valid content");
return;
}
if (!hasCookie("token")) {
toast.error("You are not logged in");
return;
}
const sanitizedHtml = sanitizeHtml(content);
setWaitingPost(true);
const response = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? "https://d2jam.com/api/v1/comment"
: "http://localhost:3005/api/v1/comment",
{
body: JSON.stringify({
content: sanitizedHtml,
username: getCookie("user"),
commentId: comment?.id,
}),
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${getCookie("token")}`,
},
credentials: "include",
}
);
if (response.status == 401) {
toast.error("Invalid User");
setWaitingPost(false);
return;
}
if (response.ok) {
toast.success("Successfully created comment");
setWaitingPost(false);
window.location.reload();
} else {
toast.error("An error occured");
setWaitingPost(false);
}
}}
>
{waitingPost ? (
<LoaderCircle className="animate-spin" size={16} />
) : (
<p>Create Reply</p>
)}
</Button>
<Spacer y={4} />
</>
)}
{comment.children.length > 0 &&
(comment.children[0].author ? (
<div className="flex flex-col gap-3">
{comment.children.map((comment) => (
<CommentCard key={comment.id} comment={comment} />
))}
</div>
) : (
<Button
variant="bordered"
onPress={() => {
toast.warning("Feature coming soon");
}}
>
Load replies
</Button>
))}
</div>
</CardBody>
</Card>
);
}

View file

@ -1,6 +1,7 @@
"use client"; "use client";
import { Button } from "@nextui-org/react"; import { Button } from "@nextui-org/react";
import { PostType } from "@/types/PostType";
import { Heart } from "lucide-react"; import { Heart } from "lucide-react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { getCookie } from "@/helpers/cookie"; import { getCookie } from "@/helpers/cookie";
@ -8,22 +9,11 @@ import { redirect } from "next/navigation";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
export default function LikeButton({ export default function LikeButton({ post }: { post: PostType }) {
likes, const [likes, setLikes] = useState<number>(post.likes.length);
liked, const [liked, setLiked] = useState<boolean>(false);
parentId,
isComment = false,
}: {
likes: number;
liked: boolean;
parentId: number;
isComment?: boolean;
}) {
const { theme } = useTheme(); const { theme } = useTheme();
const [reduceMotion, setReduceMotion] = useState<boolean>(false); const [reduceMotion, setReduceMotion] = useState<boolean>(false);
const [likeEffect, setLikeEffect] = useState<boolean>(false);
const [updatedLikes, setUpdatedLikes] = useState<number>(likes);
const [updatedLiked, setUpdatedLiked] = useState<boolean>(liked);
useEffect(() => { useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
@ -44,8 +34,8 @@ export default function LikeButton({
size="sm" size="sm"
variant="bordered" variant="bordered"
style={{ style={{
color: updatedLiked ? (theme == "dark" ? "#5ed4f7" : "#05b7eb") : "", color: post.hasLiked ? (theme == "dark" ? "#5ed4f7" : "#05b7eb") : "",
borderColor: updatedLiked borderColor: post.hasLiked
? theme == "dark" ? theme == "dark"
? "#5ed4f744" ? "#5ed4f744"
: "#05b7eb44" : "#05b7eb44"
@ -69,28 +59,27 @@ export default function LikeButton({
credentials: "include", credentials: "include",
body: JSON.stringify({ body: JSON.stringify({
username: getCookie("user"), username: getCookie("user"),
postId: !isComment ? parentId : 0, postId: post.id,
commentId: isComment ? parentId : 0,
}), }),
} }
); );
if (!updatedLiked) { post.hasLiked = !post.hasLiked;
setLikeEffect(true);
setTimeout(() => setLikeEffect(false), 1000);
setUpdatedLikes(updatedLikes + 1);
} else {
setLikeEffect(false);
setUpdatedLikes(updatedLikes - 1);
}
setUpdatedLiked(!updatedLiked); if (post.hasLiked) {
setLiked(true);
setTimeout(() => setLiked(false), 1000);
setLikes(likes + 1);
} else {
setLiked(false);
setLikes(likes - 1);
}
if (!response.ok) { if (!response.ok) {
if (response.status == 401) { if (response.status == 401) {
redirect("/login"); redirect("/login");
} else { } else {
setUpdatedLiked(!updatedLiked); post.hasLiked = !post.hasLiked;
toast.error("An error occurred"); toast.error("An error occurred");
return; return;
} }
@ -102,7 +91,7 @@ export default function LikeButton({
<Heart <Heart
size={16} size={16}
className={ className={
likeEffect && !reduceMotion liked && !reduceMotion
? "animate-ping absolute top-0 left-0" ? "animate-ping absolute top-0 left-0"
: "absolute top-0 left-0" : "absolute top-0 left-0"
} }
@ -113,7 +102,7 @@ export default function LikeButton({
zIndex: "10", zIndex: "10",
}} }}
/> />
<p>{updatedLikes}</p> <p>{likes}</p>
</div> </div>
</Button> </Button>
); );

View file

@ -197,16 +197,16 @@ export default function PostCard({
{post.tags.length > 0 && <Spacer y={4} />} {post.tags.length > 0 && <Spacer y={4} />}
<div className="flex gap-3"> <div className="flex gap-3">
<LikeButton <LikeButton post={post} />
likes={post.likes.length} <Button
liked={post.hasLiked} size="sm"
parentId={post.id} variant="bordered"
/> onPress={() => {
<Link href={`/p/${post.slug}#create-comment`}> toast.warning("Comment functionality coming soon");
<Button size="sm" variant="bordered"> }}
<MessageCircle size={16} /> {post.comments.length} >
</Button> <MessageCircle size={16} /> {0}
</Link> </Button>
<Dropdown backdrop="opaque"> <Dropdown backdrop="opaque">
<DropdownTrigger> <DropdownTrigger>
<Button size="sm" variant="bordered" isIconOnly> <Button size="sm" variant="bordered" isIconOnly>

View file

@ -23,23 +23,19 @@ export default function Timers() {
fetchCurrentJamPhase(); fetchCurrentJamPhase();
}, []); }, []);
if (activeJamResponse && activeJamResponse.jam) {
const startTimeUTC = new Date(
activeJamResponse.jam.startTime if(activeJamResponse && activeJamResponse.jam)
).toISOString(); {
const startTimeUTC = new Date(activeJamResponse.jam.startTime).toISOString();
console.log(startTimeUTC); console.log(startTimeUTC);
if ( if (activeJamResponse.phase == "Suggestion" || activeJamResponse.phase == "Survival" || activeJamResponse.phase == "Voting") {
activeJamResponse.phase == "Suggestion" ||
activeJamResponse.phase == "Survival" ||
activeJamResponse.phase == "Voting" ||
activeJamResponse.phase == "Upcoming Jam"
) {
return ( return (
<div className="text-[#333] dark:text-white transition-color duration-250"> <div className="text-[#333] dark:text-white transition-color duration-250">
<Timer <Timer
name="Jam starts in" name="Jam starts in"
targetDate={new Date(activeJamResponse.jam.startTime)} targetDate={new Date(activeJamResponse.jam.startTime) }
/> />
<Spacer y={8} /> <Spacer y={8} />
<p>Site under construction</p> <p>Site under construction</p>
@ -50,12 +46,7 @@ export default function Timers() {
<div className="text-[#333] dark:text-white transition-color duration-250"> <div className="text-[#333] dark:text-white transition-color duration-250">
<Timer <Timer
name="Jam ends in" name="Jam ends in"
targetDate={ targetDate={ new Date(new Date(activeJamResponse.jam.startTime).getTime() + (activeJamResponse.jam.jammingHours * 60 * 60 * 1000))}
new Date(
new Date(activeJamResponse.jam.startTime).getTime() +
activeJamResponse.jam.jammingHours * 60 * 60 * 1000
)
}
/> />
<Spacer y={8} /> <Spacer y={8} />
<p>Site under construction</p> <p>Site under construction</p>
@ -66,13 +57,7 @@ export default function Timers() {
<div className="text-[#333] dark:text-white transition-color duration-250"> <div className="text-[#333] dark:text-white transition-color duration-250">
<Timer <Timer
name="Rating ends in" name="Rating ends in"
targetDate={ targetDate={new Date(new Date(activeJamResponse.jam.startTime).getTime() + (activeJamResponse.jam.jammingHours * 60 * 60 * 1000) + (activeJamResponse.jam.ratingHours * 60 * 60 * 1000))}
new Date(
new Date(activeJamResponse.jam.startTime).getTime() +
activeJamResponse.jam.jammingHours * 60 * 60 * 1000 +
activeJamResponse.jam.ratingHours * 60 * 60 * 1000
)
}
/> />
<Spacer y={8} /> <Spacer y={8} />
<p>Site under construction</p> <p>Site under construction</p>
@ -88,4 +73,5 @@ export default function Timers() {
); );
} }
} }
} }

View file

@ -1,11 +0,0 @@
import { UserType } from "./UserType";
export interface CommentType {
id: number;
content: string;
children: CommentType[];
author: UserType;
createdAt: Date;
likes: [];
hasLiked: boolean;
}

View file

@ -1,4 +1,3 @@
import { CommentType } from "./CommentType";
import { TagType } from "./TagType"; import { TagType } from "./TagType";
import { UserType } from "./UserType"; import { UserType } from "./UserType";
@ -10,7 +9,6 @@ export interface PostType {
content: string; content: string;
author: UserType; author: UserType;
createdAt: Date; createdAt: Date;
comments: CommentType[];
tags: TagType[]; tags: TagType[];
likes: []; likes: [];
hasLiked: boolean; hasLiked: boolean;