diff options
| author | Factiven <[email protected]> | 2023-07-16 22:35:39 +0700 |
|---|---|---|
| committer | Factiven <[email protected]> | 2023-07-16 22:35:39 +0700 |
| commit | 1eee181e219dfd993d396ac3169e7aad3dd285eb (patch) | |
| tree | 23fe54e9c3f8810f3ac9ab6b29070b4f0d4b9d20 /components/home | |
| parent | removed console.log (diff) | |
| download | moopa-1eee181e219dfd993d396ac3169e7aad3dd285eb.tar.xz moopa-1eee181e219dfd993d396ac3169e7aad3dd285eb.zip | |
Update v3.6.4
- Added Manga page with a working tracker for AniList user
- Added schedule component to home page
- Added disqus comment section so you can fight on each other (not recommended)
- Added /id and /en route for english and indonesian subs (id route still work in progress)
Diffstat (limited to 'components/home')
| -rw-r--r-- | components/home/content.js | 267 | ||||
| -rw-r--r-- | components/home/genres.js | 87 | ||||
| -rw-r--r-- | components/home/schedule.js | 216 | ||||
| -rw-r--r-- | components/home/staticNav.js | 112 |
4 files changed, 682 insertions, 0 deletions
diff --git a/components/home/content.js b/components/home/content.js new file mode 100644 index 0000000..d67483d --- /dev/null +++ b/components/home/content.js @@ -0,0 +1,267 @@ +import Link from "next/link"; +import React, { useState, useRef, useEffect } from "react"; +import Image from "next/image"; +import { MdChevronRight } from "react-icons/md"; +import { + ChevronRightIcon, + ArrowRightCircleIcon, +} from "@heroicons/react/24/outline"; + +import { parseCookies } from "nookies"; + +import { ChevronLeftIcon } from "@heroicons/react/20/solid"; +import { ExclamationCircleIcon } from "@heroicons/react/24/solid"; + +export default function Content({ ids, section, data, og }) { + const [startX, setStartX] = useState(null); + const containerRef = useRef(null); + const [cookie, setCookie] = useState(null); + + const [isDragging, setIsDragging] = useState(false); + const [clicked, setClicked] = useState(false); + + const [lang, setLang] = useState("en"); + + useEffect(() => { + const click = localStorage.getItem("clicked"); + + if (click) { + setClicked(JSON.parse(click)); + } + + let lang = null; + if (!cookie) { + const cookie = parseCookies(); + lang = cookie.lang || null; + setCookie(cookie); + } + if (lang === "en" || lang === null) { + setLang("en"); + } else if (lang === "id") { + setLang("id"); + } + }, []); + + const handleMouseDown = (e) => { + setIsDragging(true); + setStartX(e.pageX - containerRef.current.offsetLeft); + }; + + const handleMouseUp = () => { + setIsDragging(false); + }; + + const handleMouseMove = (e) => { + if (!isDragging) return; + e.preventDefault(); + const x = e.pageX - containerRef.current.offsetLeft; + const walk = (x - startX) * 3; + containerRef.current.scrollLeft = scrollLeft - walk; + }; + + const handleClick = (e) => { + if (isDragging) { + e.preventDefault(); + } + }; + + const [scrollLeft, setScrollLeft] = useState(false); + const [scrollRight, setScrollRight] = useState(true); + + const slideLeft = () => { + var slider = document.getElementById(ids); + slider.scrollLeft = slider.scrollLeft - 500; + }; + const slideRight = () => { + var slider = document.getElementById(ids); + slider.scrollLeft = slider.scrollLeft + 500; + }; + + const handleScroll = (e) => { + const scrollLeft = e.target.scrollLeft > 31; + const scrollRight = + e.target.scrollLeft < e.target.scrollWidth - e.target.clientWidth; + setScrollLeft(scrollLeft); + setScrollRight(scrollRight); + }; + + function handleAlert(e) { + if (localStorage.getItem("clicked")) { + const existingDataString = localStorage.getItem("clicked"); + const existingData = 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) => item !== null); + const slicedData = + filteredData?.length > 15 ? filteredData?.slice(0, 15) : filteredData; + + return ( + <div> + <div className="flex items-center justify-between lg:justify-normal lg:gap-3 px-5"> + <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="scroll flex h-full w-full items-center select-none overflow-x-scroll whitespace-nowrap overflow-y-hidden scrollbar-hide lg:gap-8 gap-3 lg:p-10 py-8 px-5 z-30 scroll-smooth" + onScroll={handleScroll} + onMouseDown={handleMouseDown} + onMouseUp={handleMouseUp} + onMouseMove={handleMouseMove} + onClick={handleClick} + ref={containerRef} + > + {slicedData?.map((anime) => { + const progress = og?.find((i) => i.mediaId === anime.id); + + return ( + <div + key={anime.id} + className="flex shrink-0 cursor-pointer items-center" + > + <Link + href={`/${lang}/anime/${anime.id}`} + className="hover:scale-105 hover:shadow-lg group relative duration-300 ease-out" + > + {ids === "onGoing" && ( + <div className="h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] bg-gradient-to-b from-transparent to-black 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 -top-3 -right-3" /> + )} + {checkProgress(progress) && ( + <div + onClick={() => handleAlert(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> + )} + <Image + draggable={false} + src={ + anime.image || + anime.coverImage?.extraLarge || + anime.coverImage?.large || + "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png" + } + alt={ + anime.title.romaji || anime.title.english || "coverImage" + } + width={209} + height={300} + placeholder="blur" + blurDataURL={ + anime.image || + anime.coverImage?.extraLarge || + anime.coverImage?.large || + "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png" + } + className="z-20 h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] object-cover rounded-md brightness-90" + /> + </Link> + </div> + ); + })} + {filteredData.length >= 10 && section !== "Recommendations" && ( + <div key={section} className="flex "> + <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]"> + <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) { + 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) { + 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; +} diff --git a/components/home/genres.js b/components/home/genres.js new file mode 100644 index 0000000..a126c14 --- /dev/null +++ b/components/home/genres.js @@ -0,0 +1,87 @@ +import Image from "next/image"; +import { ChevronRightIcon } from "@heroicons/react/24/outline"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { parseCookies } from "nookies"; + +const g = [ + { + name: "Action", + img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx20958-HuFJyr54Mmir.jpg", + }, + { + name: "Comedy", + img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx21202-TfzXuWQf2oLQ.png", + }, + { + name: "Horror", + img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx127230-FlochcFsyoF4.png", + }, + { + name: "Romance", + img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx124080-h8EPH92nyRfS.jpg", + }, + { + name: "Music", + img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx130003-5Y8rYzg982sq.png", + }, + { + name: "Sports", + img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx20464-eW7ZDBOcn74a.png", + }, +]; + +export default function Genres() { + const [lang, setLang] = useState("en"); + const [cookie, setCookie] = useState(null); + + useEffect(() => { + let lang = null; + if (!cookie) { + const cookie = parseCookies(); + lang = cookie.lang || null; + setCookie(cookie); + } + if (lang === "en" || lang === null) { + setLang("en"); + } else if (lang === "id") { + setLang("id"); + } + }, []); + return ( + <div className="antialiased"> + <div className="flex items-center justify-between lg:justify-normal lg:gap-3 px-5"> + <h1 className="font-karla text-[20px] font-bold">Top Genres</h1> + <ChevronRightIcon className="w-5 h-5" /> + </div> + <div className="flex xl:justify-center items-center relative"> + <div className="bg-gradient-to-r from-primary to-transparent z-40 absolute w-7 h-[200px] left-0" /> + <div className="flex lg:gap-8 gap-3 lg:p-10 py-8 px-5 z-30 overflow-y-hidden overflow-x-scroll snap-x snap-proximity scrollbar-none relative"> + <div className="flex lg:gap-10 gap-3"> + {g.map((a, index) => ( + <Link + href={`${lang}/search/anime/?genres=${a.name}`} + key={index} + className="relative hover:shadow-lg hover:scale-105 duration-200 cursor-pointer ease-out h-[190px] w-[135px] lg:h-[265px] lg:w-[230px] rounded-md shrink-0" + > + <div className="bg-gradient-to-b from-transparent to-[#0c0d10] h-[190px] w-[135px] lg:h-[265px] lg:w-[230px] rounded-md absolute flex justify-center items-end"> + <h1 className="pb-7 lg:text-xl font-karla font-semibold"> + {a.name} + </h1> + </div> + <Image + src={a.img} + alt="genres images" + width={1000} + height={1000} + className="object-cover shrink-0 h-[190px] w-[135px] lg:h-[265px] lg:w-[230px] rounded-md" + /> + </Link> + ))} + </div> + </div> + <div className="bg-gradient-to-l from-primary to-transparent z-40 absolute w-7 h-[200px] right-0" /> + </div> + </div> + ); +} diff --git a/components/home/schedule.js b/components/home/schedule.js new file mode 100644 index 0000000..35414d2 --- /dev/null +++ b/components/home/schedule.js @@ -0,0 +1,216 @@ +import { Textfit } from "react-textfit"; +import Image from "next/image"; +import { useEffect, useRef, useState } from "react"; +import { convertUnixToTime } from "../../utils/getTimes"; +import { PlayIcon } from "@heroicons/react/20/solid"; +import { BackwardIcon, ForwardIcon } from "@heroicons/react/24/solid"; +import Link from "next/link"; + +export default function Schedule({ data, scheduleData, time }) { + let now = new Date(); + let currentDay = + now.toLocaleString("default", { weekday: "long" }).toLowerCase() + + "Schedule"; + currentDay = currentDay.replace("Schedule", ""); + + const [activeSection, setActiveSection] = useState(currentDay); + + const scrollRef = useRef(null); + + useEffect(() => { + const index = Object.keys(scheduleData).indexOf(activeSection + "Schedule"); + if (scrollRef.current) { + scrollRef.current.scrollLeft = scrollRef.current.clientWidth * index; + } + }, [activeSection, scheduleData]); + + const handleScroll = (e) => { + const { scrollLeft, clientWidth } = e.target; + const index = Math.floor(scrollLeft / clientWidth); + let day = Object.keys(scheduleData)[index]; + day = day.replace("Schedule", ""); + setActiveSection(day); + }; + + // buttons to scroll horizontally + const scrollLeft = () => { + if (scrollRef.current.scrollLeft === 0) { + scrollRef.current.scrollLeft = scrollRef.current.scrollWidth; + } else { + scrollRef.current.scrollLeft -= scrollRef.current.offsetWidth; + } + }; + + const scrollRight = () => { + const difference = + scrollRef.current.scrollWidth - + scrollRef.current.offsetWidth - + scrollRef.current.scrollLeft; + if (difference < 5) { + // adjust the threshold as needed + scrollRef.current.scrollLeft = 0; + } else { + scrollRef.current.scrollLeft += scrollRef.current.offsetWidth; + } + }; + + return ( + <div className="flex flex-col gap-5 px-4 lg:px-0"> + <h1 className="font-bold font-karla text-[20px] lg:px-5"> + Don't miss out! + </h1> + <div className="rounded mb-5 shadow-md shadow-black"> + <div className="overflow-hidden w-full h-[96px] lg:h-[10rem] rounded relative"> + <div className="absolute flex flex-col justify-center pl-5 lg:pl-16 rounded z-20 bg-gradient-to-r from-30% from-[#0c0c0c] to-transparent w-full h-full"> + <h1 className="text-xs lg:text-lg">Coming Up Next!</h1> + <Textfit + mode="single" + min={16} + max={40} + className="w-1/2 lg:w-2/5 hidden lg:block font-medium font-karla leading-[2.9rem] text-white line-clamp-1" + > + <Link + href={`/en/anime/${data.id}`} + className="hover:underline underline-offset-4 decoration-2" + > + {data.title.romaji || data.title.english || data.title.native} + </Link> + </Textfit> + <h1 className="w-1/2 lg:hidden font-medium font-karla leading-9 text-white line-clamp-1"> + {data.title.romaji || data.title.english || data.title.native} + </h1> + </div> + {data.bannerImage ? ( + <Image + src={data.bannerImage || data.coverImage.large} + width={500} + height={500} + alt="banner next anime" + className="absolute z-10 top-0 right-0 w-3/4 h-full object-cover brightness-[30%]" + /> + ) : ( + <Image + src={data.coverImage.large} + width={500} + height={500} + sizes="100vw" + alt="banner next anime" + className="absolute z-10 top-0 right-0 h-full object-contain object-right brightness-[90%]" + /> + )} + <div + className={`absolute flex justify-end items-center pr-5 gap-5 md:gap-10 z-20 w-1/2 h-full right-0 ${ + data.bannerImage ? "md:pr-16" : "md:pr-48" + }`} + > + {/* Countdown Timer */} + <div className="flex items-center gap-2 md:gap-5 font-bold font-karla text-sm md:text-xl"> + {/* Countdown Timer */} + <div className="flex flex-col items-center"> + <span className="text-action/80">{time.days}</span> + <span className="text-sm lg:text-base font-medium">Days</span> + </div> + <span></span> + <div className="flex flex-col items-center"> + <span className="text-action/80">{time.hours}</span> + <span className="text-sm lg:text-base font-medium">Hours</span> + </div> + <span></span> + <div className="flex flex-col items-center"> + <span className="text-action/80">{time.minutes}</span> + <span className="text-sm lg:text-base font-medium">Mins</span> + </div> + <span></span> + <div className="flex flex-col items-center"> + <span className="text-action/80">{time.seconds}</span> + <span className="text-sm lg:text-base font-medium">Secs</span> + </div> + </div> + </div> + </div> + <div className="w-full bg-tersier rounded-b overflow-hidden"> + <div + ref={scrollRef} + className="flex overflow-x-scroll snap snap-x snap-proximity scrollbar-hide" + onScroll={handleScroll} + > + {Object.entries(scheduleData).map(([section, data], index) => { + const uniqueArray = data.reduce((accumulator, current) => { + if (!accumulator.find((item) => item.id === current.id)) { + accumulator.push(current); + } + return accumulator; + }, []); + + return ( + <div + key={index} + className="snap-start flex-shrink-0 h-[240px] overflow-y-scroll scrollbar-thin scrollbar-thumb-secondary scrollbar-thumb-rounded w-full" + style={{ scrollbarGutter: "stable" }} + > + <div className="flex flex-col gap-2 px-2 pt-2"> + {uniqueArray.map((i, index) => { + const currentTime = Date.now(); + const hasAired = i.airingAt < currentTime; + + return ( + <Link + key={`${i.id}-${index}`} + href={`/en/anime/${i.id}`} + className={`${ + hasAired ? "opacity-40" : "" + } h-full w-full flex items-center p-2 flex-shrink-0 hover:bg-secondary cursor-pointer`} + > + <div className="shrink-0"> + <Image + src={i.coverImage} + alt="coverSchedule" + width={300} + height={300} + className="w-10 h-10 object-cover rounded" + /> + </div> + <div className="flex items-center justify-between w-full"> + <div className="font-karla px-2"> + <h1 className="font-semibold text-sm line-clamp-1"> + {i.title.romaji} + </h1> + <p className="font-semibold text-xs text-gray-400"> + {convertUnixToTime(i.airingAt)} - Episode{" "} + {i.airingEpisode} + </p> + </div> + <div> + <PlayIcon className="w-6 h-6 text-gray-300" /> + </div> + </div> + </Link> + ); + })} + </div> + </div> + ); + })} + </div> + <div className="flex items-center bg-tersier justify-between font-karla p-2 border-t border-secondary/40"> + <button + type="button" + className="bg-secondary px-2 py-1 rounded" + onClick={scrollLeft} + > + <BackwardIcon className="w-5 h-5" /> + </button> + <div className="font-bold uppercase">{activeSection}</div> + <button + type="button" + className="bg-secondary px-2 py-1 rounded" + onClick={scrollRight} + > + <ForwardIcon className="w-5 h-5" /> + </button> + </div> + </div> + </div> + </div> + ); +} diff --git a/components/home/staticNav.js b/components/home/staticNav.js new file mode 100644 index 0000000..93f7b26 --- /dev/null +++ b/components/home/staticNav.js @@ -0,0 +1,112 @@ +import { signIn, useSession } from "next-auth/react"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import { getCurrentSeason } from "../../utils/getTimes"; +import Link from "next/link"; +import { parseCookies } from "nookies"; + +export default function Navigasi() { + const { data: sessions, status } = useSession(); + const [year, setYear] = useState(new Date().getFullYear()); + const [season, setSeason] = useState(getCurrentSeason()); + + const [lang, setLang] = useState("en"); + const [cookie, setCookies] = useState(null); + + const router = useRouter(); + + useEffect(() => { + let lang = null; + if (!cookie) { + const cookie = parseCookies(); + lang = cookie.lang || null; + setCookies(cookie); + } + if (lang === "en" || lang === null) { + setLang("en"); + } else if (lang === "id") { + setLang("id"); + } + }, []); + + const handleFormSubmission = (inputValue) => { + router.push(`/${lang}/search/${encodeURIComponent(inputValue)}`); + }; + + const handleKeyDown = async (event) => { + if (event.key === "Enter") { + event.preventDefault(); + const inputValue = event.target.value; + handleFormSubmission(inputValue); + } + }; + return ( + <> + {/* NAVBAR PC */} + <div className="flex items-center justify-center"> + <div className="flex w-full items-center justify-between px-5 lg:mx-[94px]"> + <div className="flex items-center lg:gap-16 lg:pt-7"> + <Link + href="/en/" + className=" font-outfit lg:text-[40px] text-[30px] font-bold text-[#FF7F57]" + > + moopa + </Link> + <ul className="hidden items-center gap-10 pt-2 font-outfit text-[14px] lg:flex"> + <li> + <Link + href={`/en/search/anime?season=${season}&seasonYear=${year}`} + > + This Season + </Link> + </li> + <li> + <Link href="/en/search/manga">Manga</Link> + </li> + <li> + <Link href="/en/search/anime">Anime</Link> + </li> + + {status === "loading" ? ( + <li>Loading...</li> + ) : ( + <> + {!sessions && ( + <li> + <button + onClick={() => signIn("AniListProvider")} + className="ring-1 ring-action font-karla font-bold px-2 py-1 rounded-md" + > + Sign in + </button> + </li> + )} + {sessions && ( + <li className="text-center"> + <Link href={`/en/profile/${sessions?.user.name}`}> + My List + </Link> + </li> + )} + </> + )} + </ul> + </div> + <div className="relative flex lg:scale-75 scale-[65%] items-center mb-7 lg:mb-1"> + <div className="search-box "> + <input + className="search-text" + type="text" + placeholder="Search Anime" + onKeyDown={handleKeyDown} + /> + <div className="search-btn"> + <i className="fas fa-search"></i> + </div> + </div> + </div> + </div> + </div> + </> + ); +} |