"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"; 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 fetch( process.env.NEXT_PUBLIC_MODE === "PROD" ? `https://d2jam.com/api/v1/tags` : `http://localhost:3005/api/v1/tags` ); 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 fetch( process.env.NEXT_PUBLIC_MODE === "PROD" ? `https://d2jam.com/api/v1/self?username=${getCookie("user")}` : `http://localhost:3005/api/v1/self?username=${getCookie("user")}`, { headers: { authorization: `Bearer ${getCookie("token")}` }, credentials: "include", } ); if (userResponse.ok) { const userData = await userResponse.json(); setUser(userData); // Fetch posts with userSlug if user is available const postsResponse = 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("_") : "" }` : `http://localhost:3005/api/v1/posts?sort=${sort}&user=${ userData.slug }&time=${time}&tags=${ tagRules ? Object.entries(tagRules) .map((key) => `${key}`) .join("_") : "" }` ); 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); // Fetch posts without userSlug if user is not available const postsResponse = 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) => `${key}`) .join("_") : "" }` : `http://localhost:3005/api/v1/posts?sort=${sort}&time=${time}&tags=${ tagRules ? Object.entries(tagRules) .map((key) => `${key}`) .join("_") : "" }` ); 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) => `${key}`) .join("_") : "" }&sticky=true` : `http://localhost:3005/api/v1/posts?sort=${sort}&time=${time}&tags=${ tagRules ? Object.entries(tagRules) .map((key) => `${key}`) .join("_") : "" }&sticky=true` ); 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>; }