Add stickied posts

This commit is contained in:
Ategon 2025-01-22 22:08:09 -05:00
parent 94a3d31e8d
commit 42c730e073
6 changed files with 307 additions and 5 deletions

View file

@ -2,7 +2,14 @@
import Editor from "@/components/editor";
import { getCookie, hasCookie } from "@/helpers/cookie";
import { Avatar, Button, Form, Input, Spacer } from "@nextui-org/react";
import {
Avatar,
Button,
Checkbox,
Form,
Input,
Spacer,
} from "@nextui-org/react";
import { LoaderCircle } from "lucide-react";
import { redirect } from "next/navigation";
import { ReactNode, useEffect, useState } from "react";
@ -10,6 +17,7 @@ import { toast } from "react-toastify";
import sanitizeHtml from "sanitize-html";
import Select, { MultiValue, StylesConfig } from "react-select";
import { useTheme } from "next-themes";
import { UserType } from "@/types/UserType";
export default function CreatePostPage() {
const [title, setTitle] = useState("");
@ -31,6 +39,8 @@ export default function CreatePostPage() {
}[]
>();
const { theme } = useTheme();
const [user, setUser] = useState<UserType>();
const [sticky, setSticky] = useState(false);
useEffect(() => {
setMounted(true);
@ -46,7 +56,8 @@ export default function CreatePostPage() {
}
);
const user = await response.json();
const localuser = await response.json();
setUser(localuser);
const tagResponse = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
@ -63,7 +74,7 @@ export default function CreatePostPage() {
}[] = [];
for (const tag of await tagResponse.json()) {
if (tag.modOnly && !user.mod) {
if (tag.modOnly && !localuser.mod) {
continue;
}
newoptions.push({
@ -90,7 +101,6 @@ export default function CreatePostPage() {
}
setOptions(newoptions);
setSelectedTags(newoptions.filter((tag) => tag.isFixed));
}
};
load();
@ -211,8 +221,14 @@ export default function CreatePostPage() {
body: JSON.stringify({
title: title,
content: sanitizedHtml,
sticky,
username: getCookie("user"),
tags,
tags: [
...tags,
...(options
? options.filter((tag) => tag.isFixed).map((tag) => tag.id)
: []),
],
}),
method: "POST",
headers: {
@ -268,6 +284,15 @@ export default function CreatePostPage() {
/>
)}
{user && user.mod && (
<div>
<Spacer />
<Checkbox isSelected={sticky} onValueChange={setSticky}>
Sticky
</Checkbox>
</div>
)}
<Spacer />
<div className="flex gap-2">

View file

@ -28,6 +28,8 @@ import {
Shield,
ShieldAlert,
ShieldX,
Star,
StarOff,
Trash,
X,
} from "lucide-react";
@ -295,6 +297,81 @@ export default function PostPage() {
>
Remove
</DropdownItem>
{post.sticky ? (
<DropdownItem
key="unsticky"
startContent={<StarOff />}
description="Unsticky 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: false,
username: getCookie("user"),
}),
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${getCookie(
"token"
)}`,
},
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) {
toast.success("Unsticked post");
redirect("/");
} else {
toast.error("Error while removing post");
}
}}
>
Sticky
</DropdownItem>
)}
{user?.admin && !post.author.mod ? (
<DropdownItem
key="promote-mod"

View file

@ -25,6 +25,8 @@ import {
Shield,
ShieldAlert,
ShieldX,
Star,
StarOff,
Trash,
X,
} from "lucide-react";
@ -296,6 +298,81 @@ export default function PostCard({
>
Remove
</DropdownItem>
{post.sticky ? (
<DropdownItem
key="unsticky"
startContent={<StarOff />}
description="Unsticky 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: false,
username: getCookie("user"),
}),
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${getCookie(
"token"
)}`,
},
credentials: "include",
}
);
if (response.ok) {
toast.success("Unsticked post");
window.location.reload();
} 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) {
toast.success("Stickied post");
window.location.reload();
} else {
toast.error("Error while removing post");
}
}}
>
Sticky
</DropdownItem>
)}
{user?.admin && !post.author.mod ? (
<DropdownItem
key="promote-mod"

View file

@ -0,0 +1,54 @@
"use client";
import { Avatar, Card, CardBody } from "@nextui-org/react";
import { formatDistance } from "date-fns";
import Link from "next/link";
import { PostType } from "@/types/PostType";
import { Megaphone, NotebookText } from "lucide-react";
export default function StickyPostCard({ post }: { post: PostType }) {
return (
<Card className="bg-opacity-60 !duration-250 !ease-linear !transition-all flex">
<CardBody className="p-5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex gap-4 items-center text-blue-600 dark:text-blue-400 transition-all duration-250 ease-linear">
{post.tags.filter((tag) => tag.name === "Changelog").length >
0 ? (
<NotebookText />
) : (
<Megaphone />
)}
<Link href={`/p/${post.slug}`}>
<p>{post.title}</p>
</Link>
</div>
<div className="flex items-center gap-3 text-xs text-default-500 pt-1">
<p>By</p>
<Link
href={`/u/${post.author.slug}`}
className="flex items-center gap-2"
>
<Avatar
size="sm"
className="w-6 h-6"
src={post.author.profilePicture}
classNames={{
base: "bg-transparent",
}}
/>
<p>{post.author.name}</p>
</Link>
<p>
{formatDistance(new Date(post.createdAt), new Date(), {
addSuffix: true,
})}
</p>
</div>
</div>
</div>
</CardBody>
</Card>
);
}

View file

@ -42,9 +42,11 @@ import {
import { PostTime } from "@/types/PostTimes";
import { TagType } from "@/types/TagType";
import { useTheme } from "next-themes";
import StickyPostCard from "./StickyPostCard";
export default function Posts() {
const [posts, setPosts] = useState<PostType[]>();
const [stickyPosts, setStickyPosts] = useState<PostType[]>();
const [sort, setSort] = useState<PostSort>("newest");
const [time, setTime] = useState<PostTime>("all");
const [style, setStyle] = useState<PostStyle>("cozy");
@ -144,6 +146,31 @@ export default function Posts() {
}`
);
setPosts(await postsResponse.json());
// Sticky posts
// Fetch posts with userSlug if user is available
const stickyPostsResponse = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? `https://d2jam.com/api/v1/posts?sort=${sort}&user=${
userData.slug
}&time=${time}&tags=${
tagRules
? Object.entries(tagRules)
.map((key) => `${key}`)
.join("_")
: ""
}&sticky=true`
: `http://localhost:3005/api/v1/posts?sort=${sort}&user=${
userData.slug
}&time=${time}&tags=${
tagRules
? Object.entries(tagRules)
.map((key) => `${key}`)
.join("_")
: ""
}&sticky=true`
);
setStickyPosts(await stickyPostsResponse.json());
setLoading(false);
} else {
setUser(undefined);
@ -167,6 +194,26 @@ export default function Posts() {
}`
);
setPosts(await postsResponse.json());
// Fetch posts without userSlug if user is not available
const stickyPostsResponse = await fetch(
process.env.NEXT_PUBLIC_MODE === "PROD"
? `https://d2jam.com/api/v1/posts?sort=${sort}&time=${time}&tags=${
tagRules
? Object.entries(tagRules)
.map((key, value) => `${key}-${value}`)
.join("_")
: ""
}&sticky=true`
: `http://localhost:3005/api/v1/posts?sort=${sort}&time=${time}&tags=${
tagRules
? Object.entries(tagRules)
.map((key, value) => `${key}-${value}`)
.join("_")
: ""
}&sticky=true`
);
setStickyPosts(await stickyPostsResponse.json());
setLoading(false);
}
};
@ -263,6 +310,27 @@ export default function Posts() {
return (
<div>
{loading ? (
<div className="flex justify-center p-6">
<LoaderCircle
className="animate-spin text-[#333] dark:text-[#999]"
size={24}
/>
</div>
) : (
<div className="flex flex-col gap-3 p-4">
{stickyPosts && stickyPosts.length > 0 ? (
stickyPosts.map((post) => (
<StickyPostCard key={post.id} post={post} />
))
) : (
<p className="text-center text-[#333] dark:text-white transition-color duration-250 ease-linear">
No posts match your filters
</p>
)}
</div>
)}
<div className="flex justify-between p-4 pb-0">
<div className="flex gap-2">
<Dropdown backdrop="opaque">

View file

@ -5,6 +5,7 @@ export interface PostType {
id: number;
slug: string;
title: string;
sticky: boolean;
content: string;
author: UserType;
createdAt: Date;