Jamjar/src/components/posts/index.tsx
2025-02-09 02:39:02 +02:00

446 lines
14 KiB
TypeScript

"use client";
import { ReactNode, useEffect, useState } from "react";
import PostCard from "./PostCard";
import { PostType } from "@/types/PostType";
import {
Avatar,
Button,
Chip,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownTrigger,
Popover,
PopoverContent,
PopoverTrigger,
} from "@nextui-org/react";
import { PostSort } from "@/types/PostSort";
import { PostStyle } from "@/types/PostStyle";
import { getCookie } from "@/helpers/cookie";
import { UserType } from "@/types/UserType";
import {
Calendar,
Calendar1,
CalendarArrowDown,
CalendarCog,
CalendarDays,
CalendarFold,
CalendarRange,
Check,
Clock1,
Clock2,
Clock3,
Clock4,
ClockArrowDown,
ClockArrowUp,
LoaderCircle,
Sparkles,
Trophy,
X,
} from "lucide-react";
import { PostTime } from "@/types/PostTimes";
import { TagType } from "@/types/TagType";
import { useTheme } from "next-themes";
import StickyPostCard from "./StickyPostCard";
import { getTags } from "@/requests/tag";
import { getSelf } from "@/requests/user";
import { getPosts } from "@/requests/post";
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");
const [user, setUser] = useState<UserType>();
const [loading, setLoading] = useState<boolean>(true);
const [tags, setTags] = useState<{
[category: string]: { tags: TagType[]; priority: number };
}>();
const [tagRules, setTagRules] = useState<{ [key: number]: number }>();
const [reduceMotion, setReduceMotion] = useState<boolean>(false);
const { theme } = useTheme();
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduceMotion(mediaQuery.matches);
const handleChange = (event: MediaQueryListEvent) => {
setReduceMotion(event.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => {
mediaQuery.removeEventListener("change", handleChange);
};
}, []);
useEffect(() => {
const loadUserAndPosts = async () => {
setLoading(true);
const tagResponse = await getTags();
if (tagResponse.ok) {
const tagObject: {
[category: string]: { tags: TagType[]; priority: number };
} = {};
for (const tag of await tagResponse.json()) {
if (tag.name == "D2Jam") {
continue;
}
if (tag.category) {
if (tag.category.name in tagObject) {
tagObject[tag.category.name].tags.push(tag);
} else {
tagObject[tag.category.name] = {
tags: [tag],
priority: tag.category.priority,
};
}
}
}
setTags(tagObject);
}
// Fetch the user
const userResponse = await getSelf();
const userData = userResponse.ok ? await userResponse.json() : undefined;
setUser(userData);
// Fetch posts (with userSlug if user is available)
const postsResponse = await getPosts(sort, time, false, tagRules, userData?.slug);
setPosts(await postsResponse.json());
// Sticky posts
// Fetch posts (with userSlug if user is available)
const stickyPostsResponse = await getPosts(sort, time, true, tagRules, userData?.slug);
setStickyPosts(await stickyPostsResponse.json());
setLoading(false);
};
loadUserAndPosts();
}, [sort, time, tagRules]);
const sorts: Record<
PostSort,
{ name: string; icon: ReactNode; description: string }
> = {
top: {
name: "Top",
icon: <Trophy />,
description: "Shows the most liked posts first",
},
newest: {
name: "Newest",
icon: <ClockArrowUp />,
description: "Shows the newest posts first",
},
oldest: {
name: "Oldest",
icon: <ClockArrowDown />,
description: "Shows the oldest posts first",
},
};
const times: Record<
PostTime,
{ name: string; icon: ReactNode; description: string }
> = {
hour: {
name: "Hour",
icon: <Clock1 />,
description: "Shows posts from the last hour",
},
three_hours: {
name: "Three Hours",
icon: <Clock2 />,
description: "Shows posts from the last three hours",
},
six_hours: {
name: "Six Hours",
icon: <Clock3 />,
description: "Shows posts from the last six hours",
},
twelve_hours: {
name: "Twelve Hours",
icon: <Clock4 />,
description: "Shows posts from the last twelve hours",
},
day: {
name: "Day",
icon: <Calendar />,
description: "Shows posts from the last day",
},
week: {
name: "Week",
icon: <CalendarDays />,
description: "Shows posts from the last week",
},
month: {
name: "Month",
icon: <CalendarRange />,
description: "Shows posts from the last month",
},
three_months: {
name: "Three Months",
icon: <CalendarFold />,
description: "Shows posts from the last three months",
},
six_months: {
name: "Six Months",
icon: <CalendarCog />,
description: "Shows posts from the last six months",
},
nine_months: {
name: "Nine Months",
icon: <CalendarArrowDown />,
description: "Shows posts from the last nine months",
},
year: {
name: "Year",
icon: <Calendar1 />,
description: "Shows posts from the last year",
},
all: {
name: "All Time",
icon: <Sparkles />,
description: "Shows all posts",
},
};
return (
<div>
{loading ? (
<div className="flex justify-center p-6">
<LoaderCircle
className="animate-spin text-[#333] dark:text-[#999]"
size={24}
/>
</div>
) : (
stickyPosts &&
stickyPosts.length > 0 && (
<div className="flex flex-col gap-3 p-4">
{stickyPosts.map((post) => (
<StickyPostCard key={post.id} post={post} />
))}
</div>
)
)}
<div className="flex justify-between p-4 pb-0">
<div className="flex gap-2">
<Dropdown backdrop="opaque">
<DropdownTrigger>
<Button
size="sm"
className="text-xs bg-white dark:bg-[#252525] !duration-250 !ease-linear !transition-all text-[#333] dark:text-white"
variant="faded"
>
{sorts[sort]?.name}
</Button>
</DropdownTrigger>
<DropdownMenu
onAction={(key) => {
setSort(key as PostSort);
}}
className="text-[#333] dark:text-white"
>
{Object.entries(sorts).map(([key, sort]) => (
<DropdownItem
key={key}
startContent={sort.icon}
description={sort.description}
>
{sort.name}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
<Dropdown backdrop="opaque">
<DropdownTrigger>
<Button
size="sm"
className="text-xs bg-white dark:bg-[#252525] !duration-250 !ease-linear !transition-all text-[#333] dark:text-white"
variant="faded"
>
{times[time]?.name}
</Button>
</DropdownTrigger>
<DropdownMenu
onAction={(key) => {
setTime(key as PostTime);
}}
className="text-[#333] dark:text-white"
>
{Object.entries(times).map(([key, sort]) => (
<DropdownItem
key={key}
startContent={sort.icon}
description={sort.description}
>
{sort.name}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
<Popover placement="bottom" showArrow backdrop="opaque">
<PopoverTrigger>
<Button
size="sm"
className="text-xs bg-white dark:bg-[#252525] !duration-250 !ease-linear !transition-all text-[#333] dark:text-white"
variant="faded"
>
{tagRules && Object.keys(tagRules).length > 0
? "Custom Tags"
: "All Tags"}
</Button>
</PopoverTrigger>
<PopoverContent>
<div className="p-4 max-w-[800px] max-h-[400px] overflow-y-scroll">
<p className="text-2xl">Tag Filtering</p>
{tags && Object.keys(tags).length > 0 ? (
Object.keys(tags)
.sort(
(tag1, tag2) => tags[tag2].priority - tags[tag1].priority
)
.map((category: string) => (
<div key={category} className="w-full">
<p>{category}</p>
<div className="flex gap-1 flex-wrap p-4 w-full">
{tags[category].tags.map((tag) => (
<Chip
size="sm"
variant="faded"
avatar={
tag.icon && (
<Avatar
src={tag.icon}
classNames={{ base: "bg-transparent" }}
/>
)
}
key={tag.id}
onClick={() => {
if (!tagRules) {
setTagRules({ [tag.id]: 1 });
} else {
if (tag.id in tagRules) {
if (tagRules[tag.id] === 1) {
setTagRules({
...tagRules,
[tag.id]: -1,
});
} else {
const updatedRules = { ...tagRules };
delete updatedRules[tag.id];
setTagRules(updatedRules);
}
} else {
setTagRules({ ...tagRules, [tag.id]: 1 });
}
}
}}
className={`transition-all transform duration-500 ease-in-out cursor-pointer ${
!reduceMotion ? "hover:scale-110" : ""
}`}
style={{
color:
tagRules && tag.id in tagRules
? tagRules[tag.id] === 1
? theme == "dark"
? "#5ed4f7"
: "#05b7eb"
: theme == "dark"
? "#f78e5e"
: "#eb2b05"
: "",
borderColor:
tagRules && tag.id in tagRules
? tagRules[tag.id] === 1
? theme == "dark"
? "#5ed4f7"
: "#05b7eb"
: theme == "dark"
? "#f78e5e"
: "#eb2b05"
: "",
}}
endContent={
tagRules &&
tag.id in tagRules &&
(tagRules[tag.id] === 1 ? (
<Check size={16} />
) : (
<X size={16} />
))
}
>
{tag.name}
</Chip>
))}
</div>
</div>
))
) : (
<p>No tags could be found</p>
)}
</div>
</PopoverContent>
</Popover>
</div>
<div>
<Dropdown backdrop="opaque">
<DropdownTrigger>
<Button
size="sm"
className="text-xs bg-white dark:bg-[#252525] !duration-250 !ease-linear !transition-all text-[#333] dark:text-white"
variant="faded"
>
{style.charAt(0).toUpperCase() + style.slice(1)}
</Button>
</DropdownTrigger>
<DropdownMenu
onAction={(key) => {
setStyle(key as PostStyle);
}}
className="text-[#333] dark:text-white"
>
<DropdownItem key="cozy">Cozy</DropdownItem>
<DropdownItem key="compact">Compact</DropdownItem>
<DropdownItem key="ultra">Ultra Compact</DropdownItem>
<DropdownItem key="adaptive">Adaptive</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
</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">
{posts && posts.length > 0 ? (
posts.map((post) => (
<PostCard key={post.id} post={post} style={style} user={user} />
))
) : (
<p className="text-center text-[#333] dark:text-white transition-color duration-250 ease-linear">
No posts match your filters
</p>
)}
</div>
)}
</div>
);
return <div></div>;
}