diff options
Diffstat (limited to 'components/home/content.tsx')
| -rw-r--r-- | components/home/content.tsx | 611 |
1 files changed, 611 insertions, 0 deletions
diff --git a/components/home/content.tsx b/components/home/content.tsx new file mode 100644 index 0000000..b193381 --- /dev/null +++ b/components/home/content.tsx @@ -0,0 +1,611 @@ +import Link from "next/link"; +import React, { useState, useRef, useEffect, Fragment } from "react"; +import { useDraggable } from "react-use-draggable-scroll"; +import Image from "next/image"; +import { MdChevronRight } from "react-icons/md"; +import { + ChevronRightIcon, + ArrowRightCircleIcon, +} from "@heroicons/react/24/outline"; + +import { ChevronLeftIcon } from "@heroicons/react/20/solid"; +import { ExclamationCircleIcon, PlayIcon } from "@heroicons/react/24/solid"; +import { useRouter } from "next/router"; +import HistoryOptions from "./content/historyOptions"; +import { toast } from "sonner"; +import { truncateImgUrl } from "@/utils/imageUtils"; + +type ContentProps = { + ids: string; + section: string; + data?: any; + userData?: UserDataTypes[]; + og?: any; + userName?: string; + setRemoved?: any; + type?: string; +}; + +type UserDataTypes = { + id: string; + aniId?: string; + title?: string; + aniTitle?: string; + image?: string; + episode?: number; + timeWatched?: number; + duration?: number; + provider?: string; + nextId?: string; + nextNumber?: number; + dub?: boolean; + createdDate: string; + userProfileId: string; + watchId: string; +}; + +interface SlicedDataTypes { + id: string | number; + slug?: string; + nextAiringEpisode?: any; + currentEpisode?: number; + idMal: number; + status: string; + title: Title; + bannerImage: string; + coverImage: CoverImage | string; + image?: string; + episodeNumber?: number; + description: string; +} + +interface Title { + romaji: string; + english: string; + native: string; +} + +interface CoverImage { + extraLarge: string; + large: string; + medium: string; + color?: string; +} + +export default function Content({ + ids, + section, + data, + userData, + og, + userName, + setRemoved, + type = "anime", +}: ContentProps) { + const ref = useRef<HTMLElement>(null!); + const { events } = useDraggable(ref); + + const router = useRouter(); + + const [clicked, setClicked] = useState(false); + + useEffect(() => { + const click = localStorage.getItem("clicked"); + + if (click) { + setClicked(JSON.parse(click)); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const [scrollLeft, setScrollLeft] = useState(false); + const [scrollRight, setScrollRight] = useState(true); + + const slideLeft = () => { + if (ref.current) { + ref.current.classList.add("scroll-smooth"); + var slider = document.getElementById(ids); + if (slider?.scrollLeft) { + slider.scrollLeft = slider.scrollLeft - 500; + } + ref.current.classList.remove("scroll-smooth"); + } + }; + const slideRight = () => { + if (ref.current) { + ref.current.classList.add("scroll-smooth"); + var slider = document.getElementById(ids); + if (slider?.scrollLeft) { + slider.scrollLeft = slider.scrollLeft + 500; + } + ref.current.classList.remove("scroll-smooth"); + } + }; + + const handleScroll = (e: any) => { + const scrollLeft = e.target.scrollLeft > 31; + const scrollRight = + e.target.scrollLeft < e.target.scrollWidth - e.target.clientWidth; + setScrollLeft(scrollLeft); + setScrollRight(scrollRight); + }; + + function handleAlert(e: string) { + if (localStorage.getItem("clicked")) { + const existingDataString = localStorage.getItem("clicked"); + const existingData = existingDataString + ? JSON.parse(existingDataString) + : {}; + + existingData[e] = true; + + const updatedDataString = JSON.stringify(existingData); + + localStorage.setItem("clicked", updatedDataString); + } else { + const newData = { + [e]: true, + }; + + const newDataString = JSON.stringify(newData); + + localStorage.setItem("clicked", newDataString); + } + } + + const array = data; + let filteredData = array?.filter((item: any) => item !== null); + const slicedData: SlicedDataTypes[] = + filteredData?.length > 15 ? filteredData?.slice(0, 15) : filteredData; + + const goToPage = () => { + if (section === "Recently Watched") { + router.push(`/en/anime/recently-watched`); + } + if (section === "New Episodes") { + router.push(`/en/anime/recent`); + } + if (section === "Trending Now") { + router.push(`/en/anime/trending`); + } + if (section === "Popular Anime") { + router.push(`/en/anime/popular`); + } + if (section === "Your Plan") { + router.push(`/en/profile/${userName}/#planning`); + } + if (section === "On-Going Anime" || section === "Your Watch List") { + router.push(`/en/profile/${userName}/#current`); + } + }; + + const removeItem = async (id: string, aniId: string) => { + if (userName) { + // remove from database + const res = await fetch(`/api/user/update/episode`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: userName, + id, + aniId, + }), + }); + const data = await res.json(); + + if (id) { + // remove from local storage + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings") || "{}") || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + } + if (aniId) { + const currentData = + JSON.parse(localStorage.getItem("artplayer_settings") || "{}") || {}; + + const updatedData: { [key: string]: any } = {}; + + for (const key in currentData) { + const item = currentData[key]; + if (item.aniId !== aniId) { + updatedData[key] = item; + } + } + + localStorage.setItem("artplayer_settings", JSON.stringify(updatedData)); + } + + // update client + setRemoved(id || aniId); + + if (data?.message === "Episode deleted") { + toast.success("Episode removed from history"); + } + } else { + if (id) { + // remove from local storage + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings") || "{}") || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + setRemoved(id); + } + if (aniId) { + const currentData = + JSON.parse(localStorage.getItem("artplayer_settings") || "{}") || {}; + + // Create a new object to store the updated data + const updatedData: { [key: string]: any } = {}; + + // Iterate through the current data and copy items with different aniId to the updated object + for (const key in currentData) { + const item = currentData[key]; + if (item.aniId !== aniId) { + updatedData[key] = item; + } + } + + // Update localStorage with the filtered data + localStorage.setItem("artplayer_settings", JSON.stringify(updatedData)); + setRemoved(aniId); + } + } + }; + + return ( + <div> + <div + className={`flex items-center justify-between lg:justify-normal lg:gap-3 px-5 z-40 ${ + section === "Recommendations" ? "" : "cursor-pointer" + }`} + onClick={goToPage} + > + <h1 className="font-karla text-[20px] font-bold">{section}</h1> + <ChevronRightIcon className="w-5 h-5" /> + </div> + <div className="relative flex items-center lg:gap-2"> + <div + onClick={slideLeft} + className={`flex items-center mb-5 cursor-pointer hover:text-action absolute left-0 bg-gradient-to-r from-[#141519] z-40 h-full hover:opacity-100 ${ + scrollLeft ? "lg:visible" : "invisible" + }`} + > + <ChevronLeftIcon className="w-7 h-7 stroke-2" /> + </div> + <div + id={ids} + className="flex h-full w-full select-none overflow-x-scroll overflow-y-hidden scrollbar-hide lg:gap-8 gap-4 lg:p-10 py-8 px-5 z-30" + onScroll={handleScroll} + {...events} + ref={ref as React.RefObject<HTMLDivElement>} + > + {ids !== "recentlyWatched" + ? slicedData?.map((anime) => { + const progress = og?.find((i: any) => i.mediaId === anime.id); + + let image; + if (typeof anime.coverImage === "string") { + image = truncateImgUrl(anime.coverImage); + } else if (anime.coverImage) { + image = anime.coverImage.extraLarge || anime.coverImage.large; + } + + if (!image && anime.image) { + image = anime.image; + } + + return ( + <div + key={anime.id} + className="flex flex-col gap-3 shrink-0 cursor-pointer" + > + <Link + href={ + ids === "listManga" + ? `/en/manga/${anime.id}` + : ids === "recentAdded" + ? anime?.slug + ? `/en/anime/watch/${ + anime.id + }/gogoanime?id=${encodeURIComponent( + anime?.slug + )}&num=${anime.currentEpisode}` + : `/en/${type}/${anime.id}` + : `/en/${type}/${anime.id}` + } + className="hover:scale-105 hover:shadow-lg duration-300 ease-out group relative" + title={anime.title.romaji} + > + {ids === "onGoing" && ( + <div className="h-[190px] lg:h-[265px] w-[135px] lg:w-[185px] bg-gradient-to-b from-transparent to-black/90 absolute z-40 rounded-md whitespace-normal font-karla group"> + <div className="flex flex-col items-center h-full justify-end text-center pb-5"> + <h1 className="line-clamp-1 w-[70%] text-[10px]"> + {anime.title.romaji || anime.title.english} + </h1> + {checkProgress(progress) && + !clicked?.hasOwnProperty(anime.id) && ( + <ExclamationCircleIcon className="w-7 h-7 absolute z-40 text-white -top-3 -right-3" /> + )} + {checkProgress(progress) && ( + <div + onClick={() => handleAlert(String(anime.id))} + className="group-hover:visible invisible absolute top-0 bg-black bg-opacity-20 w-full h-full z-20 text-center" + > + <h1 className="text-[12px] lg:text-sm pt-28 lg:pt-44 font-bold opacity-100"> + {checkProgress(progress)} + </h1> + </div> + )} + {anime.nextAiringEpisode && ( + <div className="flex gap-1 text-[13px] lg:text-base"> + <h1> + Episode {anime.nextAiringEpisode.episode} in + </h1> + <h1 className="font-bold"> + {convertSecondsToTime( + anime?.nextAiringEpisode?.timeUntilAiring + )} + </h1> + </div> + )} + </div> + </div> + )} + <div className="h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] rounded-md z-30"> + {ids === "recentAdded" && ( + <div className="absolute bg-gradient-to-b from-black/30 to-transparent from-5% to-30% top-0 z-30 w-full h-full rounded" /> + )} + {image && ( + <Image + draggable={false} + src={image} + alt={ + anime.title.romaji || + anime.title.english || + "coverImage" + } + width={500} + height={300} + className="z-20 h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] object-cover rounded-md brightness-90" + /> + )} + </div> + {ids === "recentAdded" && ( + <Fragment> + <Image + src="/svg/episode-badge.svg" + alt="episode-badge" + width={200} + height={100} + className="w-24 lg:w-32 absolute top-1 -right-[12px] lg:-right-[17px] z-40" + /> + <p className="absolute z-40 text-center w-[86px] lg:w-[110px] top-1 -right-2 lg:top-[5.5px] lg:-right-2 font-karla text-sm lg:text-base"> + Episode{" "} + <span className="text-white"> + {anime?.currentEpisode || anime?.episodeNumber} + </span> + </p> + </Fragment> + )} + </Link> + {ids !== "onGoing" && ( + <Link + href={ + ids === "listManga" + ? `/en/manga/${anime.id}` + : `/en/${type.toLowerCase()}/${anime.id}` + } + className="w-[135px] lg:w-[185px] line-clamp-2" + title={anime.title.romaji} + > + <h1 className="font-karla font-semibold xl:text-base text-[15px]"> + {anime.status === "RELEASING" || + ids === "recentAdded" ? ( + <span className="dots bg-green-500" /> + ) : anime.status === "NOT_YET_RELEASED" ? ( + <span className="dots bg-red-500" /> + ) : null} + {anime.title.romaji} + </h1> + </Link> + )} + </div> + ); + }) + : userData + ?.filter((i) => i.title && i.title !== null) + ?.slice(0, 10) + .map((i) => { + const time = i.timeWatched; + const duration = i.duration; + let prog = time && duration ? (time / duration) * 100 : 0; + if (prog > 90) prog = 100; + + return ( + <div + key={i.watchId} + className="flex flex-col gap-2 shrink-0 cursor-pointer relative group/item" + > + <div className="absolute flex flex-col gap-1 z-40 top-1 right-1 transition-all duration-200 ease-out opacity-0 group-hover/item:opacity-100 scale-90 group-hover/item:scale-100 group-hover/item:visible invisible "> + <HistoryOptions + remove={removeItem} + watchId={i.watchId} + aniId={i.aniId} + /> + {i?.nextId && ( + <button + type="button" + className="flex flex-col items-center group/next relative" + onClick={() => { + router.push( + `/en/anime/watch/${i.aniId}/${ + i.provider + }?id=${encodeURIComponent( + i?.nextId || "" + )}&num=${i?.nextNumber}${ + i?.dub ? `&dub=${i?.dub}` : "" + }` + ); + }} + > + <ChevronRightIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full hover:text-action scale-100 hover:scale-105 transition-all duration-200 ease-out" /> + <span className="absolute font-karla bg-secondary shadow-black shadow-2xl py-1 px-2 whitespace-nowrap text-white text-sm rounded-md right-7 -bottom-[2px] z-40 duration-300 transition-all ease-out group-hover/next:visible group-hover/next:scale-100 group-hover/next:translate-x-0 group-hover/next:opacity-100 opacity-0 translate-x-10 scale-50 invisible"> + Play Next Episode + </span> + </button> + )} + </div> + <Link + className="relative w-[320px] aspect-video rounded-md overflow-hidden group" + href={`/en/anime/watch/${i.aniId}/${ + i.provider + }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}${ + i?.dub ? `&dub=${i?.dub}` : "" + }`} + > + <div className="w-full h-full bg-gradient-to-t from-black/70 from-20% to-transparent group-hover:to-black/40 transition-all duration-300 ease-out absolute z-30" /> + <div className="absolute bottom-3 left-0 mx-2 text-white flex gap-2 items-center w-[80%] z-30"> + <PlayIcon className="w-5 h-5 shrink-0" /> + <h1 + className="font-semibold font-karla line-clamp-1" + title={i?.title || i?.aniTitle} + > + {i?.title === i.aniTitle + ? `Episode ${i.episode}` + : i?.title || i?.aniTitle} + </h1> + </div> + <span + className={`absolute bottom-0 left-0 h-[2px] bg-red-600 z-30`} + style={{ + width: `${prog}%`, + }} + /> + + {i?.image && ( + <Image + src={i?.image} + width={320} + height={180} + alt="Episode Thumbnail" + className="w-full object-cover group-hover:scale-[1.02] duration-300 ease-out z-10" + /> + )} + </Link> + + <Link + className="flex flex-col font-karla w-full" + href={`/en/anime/watch/${i.aniId}/${ + i.provider + }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}`} + > + {/* <h1 className="font-semibold">{i.title}</h1> */} + <p className="flex items-center gap-1 text-sm text-gray-400 w-[320px]"> + <span + className="text-white" + style={{ + display: "inline-block", + maxWidth: "220px", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }} + title={i.aniTitle} + > + {i.aniTitle} + </span>{" "} + | Episode {i.episode} + </p> + </Link> + </div> + ); + })} + {userData && + userData?.filter((i) => i.aniId !== null)?.length >= 10 && + section !== "Recommendations" && ( + <div + key={section} + className="flex flex-col cursor-pointer" + onClick={goToPage} + > + <div className="w-[320px] aspect-video overflow-hidden object-cover rounded-md border-secondary border-2 flex flex-col gap-2 items-center text-center justify-center text-[#6a6a6a] hover:text-[#9f9f9f] hover:border-[#757575] transition-colors duration-200"> + <h1 className="whitespace-pre-wrap text-sm"> + More on {section} + </h1> + <ArrowRightCircleIcon className="w-5 h-5" /> + </div> + </div> + )} + {filteredData?.length >= 10 && section !== "Recommendations" && ( + <div + key={section} + className="flex cursor-pointer" + onClick={goToPage} + > + <div className="h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] object-cover rounded-md border-secondary border-2 flex flex-col gap-2 items-center text-center justify-center text-[#6a6a6a] hover:text-[#9f9f9f] hover:border-[#757575] transition-colors duration-200"> + <h1 className="whitespace-pre-wrap text-sm"> + More on {section} + </h1> + <ArrowRightCircleIcon className="w-5 h-5" /> + </div> + </div> + )} + </div> + <MdChevronRight + onClick={slideRight} + size={30} + className={`hidden md:block mb-5 cursor-pointer hover:text-action absolute right-0 bg-gradient-to-l from-[#141519] z-40 h-full hover:opacity-100 hover:bg-gradient-to-l ${ + scrollRight ? "visible" : "hidden" + }`} + /> + </div> + </div> + ); +} + +function convertSecondsToTime(sec: number) { + let days = Math.floor(sec / (3600 * 24)); + let hours = Math.floor((sec % (3600 * 24)) / 3600); + let minutes = Math.floor((sec % 3600) / 60); + + let time = ""; + + if (days > 0) { + time += `${days}d `; + time += `${hours}h`; + } else { + time += `${hours}h `; + time += `${minutes}m`; + } + + return time.trim(); +} + +function checkProgress(entry: { progress: any; media: any }) { + const { progress, media } = entry; + const { episodes, nextAiringEpisode } = media; + + if (nextAiringEpisode !== null) { + const { episode } = nextAiringEpisode; + + if (episode - progress > 1) { + const missedEpisodes = episode - progress - 1; + return `${missedEpisodes} episode${missedEpisodes > 1 ? "s" : ""} behind`; + } + } + + return; +} |