diff options
| author | Factiven <[email protected]> | 2023-05-16 22:40:02 +0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2023-05-16 22:40:02 +0700 |
| commit | 9a5754fdba9d778f820fe89b44d1e21ca9f0bb4d (patch) | |
| tree | 8bd574163e760216bc91f7b3c164232b6982efe8 /components | |
| parent | Update v3.5.6 (diff) | |
| download | moopa-9a5754fdba9d778f820fe89b44d1e21ca9f0bb4d.tar.xz moopa-9a5754fdba9d778f820fe89b44d1e21ca9f0bb4d.zip | |
Update v3.5.7 (#12)
* Merge request (#11)
* Update v3.5.5
> Now Skip button will hide if player is not in focused state.
> Added some options to player.
> Manga images should be displayed now.
* Update videoPlayer.js
* Revamp hero section #1
* UI Improvement
> Updating main page
> Updated Genres selection using params method
> Added search bar v1.0 on main page ( [ctrl + space] to access search bar )
* update meta
* Update [...id].js
* Update [...id].js
> Back to ssr I guess
* update episode selector
* Update [...info].js
* Update UI
> Added On-Going section for AniList user
* Update content.js
* added dynamic og
* Update og.jsx
* Update og
* Update og.jsx
* update og and id fallback
> Added fallback for anime info if it's not found
* Update v3.5.7
> Added On-Going section for AniList user
> Added Genre section
> Added dynamic Open Graph when sharing anime
> Added Episode Selector above information
Diffstat (limited to 'components')
| -rw-r--r-- | components/hero/content.js | 182 | ||||
| -rw-r--r-- | components/hero/genres.js | 69 | ||||
| -rw-r--r-- | components/searchBar.js | 144 | ||||
| -rw-r--r-- | components/videoPlayer.js | 2 |
4 files changed, 380 insertions, 17 deletions
diff --git a/components/hero/content.js b/components/hero/content.js index 7e2d9ab..24ee942 100644 --- a/components/hero/content.js +++ b/components/hero/content.js @@ -1,9 +1,54 @@ import Link from "next/link"; -import React, { useState } from "react"; +import React, { useState, useRef, useEffect } from "react"; import Image from "next/image"; -import { MdChevronLeft, MdChevronRight } from "react-icons/md"; +import { MdChevronRight } from "react-icons/md"; +import { + ChevronRightIcon, + ArrowRightCircleIcon, +} from "@heroicons/react/24/outline"; + +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 [scrollLefts, setScrollLefts] = useState(null); + const containerRef = useRef(null); + + const [isDragging, setIsDragging] = useState(false); + const [clicked, setClicked] = useState(false); + + useEffect(() => { + const click = localStorage.getItem("clicked"); + if (click) { + setClicked(JSON.parse(click)); + } + }, []); + + const handleMouseDown = (e) => { + setIsDragging(true); + setStartX(e.pageX - containerRef.current.offsetLeft); + setScrollLefts(containerRef.current.scrollLeft); + }; + + 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(); + } + }; -export default function Content({ ids, section, data }) { const [scrollLeft, setScrollLeft] = useState(false); const [scrollRight, setScrollRight] = useState(true); @@ -24,27 +69,59 @@ export default function Content({ ids, section, data }) { setScrollRight(scrollRight); }; - // console.log({ left: scrollLeft, right: 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> - <h1 className="px-5 font-karla text-[20px] font-bold">{section}</h1> + <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"> - <MdChevronLeft + <div onClick={slideLeft} - size={35} - className={`mb-5 cursor-pointer hover:text-action absolute left-0 bg-gradient-to-r from-[#141519] z-40 h-full hover:opacity-100 ${ - scrollLeft ? "visible" : "hidden" + 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 scroll-smooth whitespace-nowrap overflow-y-hidden scrollbar-hide lg:gap-8 gap-5 p-10 z-30 " + 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} > - {filteredData?.map((anime) => { + {slicedData?.map((anime) => { + const progress = og?.find((i) => i.mediaId === anime.id); return ( <div key={anime.id} @@ -52,8 +129,39 @@ export default function Content({ ids, section, data }) { > <Link href={`/anime/${anime.id}`} - className="hover:scale-105 group relative duration-300 ease-in-out" + 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> + )} + <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={ @@ -72,17 +180,27 @@ export default function Content({ ids, section, data }) { anime.coverImage?.large || "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png" } - className="z-20 h-[192px] w-[135px] lg:h-[265px] lg:w-[185px] object-cover rounded-md" + 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={`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 ${ + 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" }`} /> @@ -90,3 +208,37 @@ export default function Content({ ids, section, data }) { </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/hero/genres.js b/components/hero/genres.js new file mode 100644 index 0000000..1c8a475 --- /dev/null +++ b/components/hero/genres.js @@ -0,0 +1,69 @@ +import Image from "next/image"; +import { ChevronRightIcon } from "@heroicons/react/24/outline"; +import Link from "next/link"; + +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() { + 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-[300px] 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={`/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-[300px] right-0" /> + </div> + </div> + ); +} diff --git a/components/searchBar.js b/components/searchBar.js new file mode 100644 index 0000000..35e9b45 --- /dev/null +++ b/components/searchBar.js @@ -0,0 +1,144 @@ +import { useState, useEffect, useRef } from "react"; +import { motion as m, AnimatePresence } from "framer-motion"; +import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { useAniList } from "../lib/useAnilist"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; + +const SearchBar = () => { + const [isOpen, setIsOpen] = useState(false); + const searchBoxRef = useRef(null); + + const router = useRouter(); + + const { aniAdvanceSearch } = useAniList(); + const [data, setData] = useState(null); + const [query, setQuery] = useState(""); + + useEffect(() => { + if (isOpen) { + searchBoxRef.current.querySelector("input").focus(); + } + const handleKeyDown = (e) => { + if (e.ctrlKey && e.code === "Space") { + setIsOpen((prev) => !prev); + setData(null); + setQuery(""); + } + }; + + document.addEventListener("keydown", handleKeyDown); + + const handleClick = (e) => { + if (searchBoxRef.current && !searchBoxRef.current.contains(e.target)) { + setIsOpen(false); + } + }; + document.addEventListener("click", handleClick); + + return () => { + document.removeEventListener("keydown", handleKeyDown); + document.removeEventListener("click", handleClick); + }; + }, [isOpen]); + + async function search() { + const data = await aniAdvanceSearch({ + search: query, + type: "ANIME", + perPage: 10, + }); + setData(data); + } + + useEffect(() => { + if (query) { + search(); + } + }, [query]); + + function handleSubmit(e) { + e.preventDefault(); + if (data?.media.length) { + router.push(`/anime/${data?.media[0].id}`); + } + } + + return ( + <AnimatePresence> + {isOpen && ( + <m.div + initial={{ opacity: 0, y: -100 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -100 }} + className="fixed top-0 w-screen flex justify-center z-50" + > + <div + ref={searchBoxRef} + className={` bg-[#1c1c1fef] text-white p-4 ${ + isOpen ? "flex" : "hidden" + } flex-col w-[80%] backdrop-blur-sm rounded-b-lg`} + > + <form onSubmit={handleSubmit}> + <input + type="text" + className="w-full rounded-lg px-4 py-2 mb-2 bg-[#474747]" + placeholder="Search..." + onChange={(e) => setQuery(e.target.value)} + /> + </form> + <div className="flex flex-col gap-2 p-2 font-karla"> + {data?.media.map((i) => ( + <Link + key={i.id} + href={i.type === "ANIME" ? `/anime/${i.id}` : `/`} + className="flex hover:bg-[#3e3e3e] rounded-md" + > + <Image + src={i.coverImage.extraLarge} + alt="search results" + width={500} + height={500} + className="object-cover w-14 h-14 rounded-md" + /> + <div className="flex items-center justify-between w-full px-5"> + <div> + <h1>{i.title.userPreferred}</h1> + <h5 className="text-sm font-light text-[#878787] flex gap-2"> + {i.status + ?.toLowerCase() + .replace(/^\w/, (c) => c.toUpperCase())}{" "} + {i.status && i.season && <>·</>}{" "} + {i.season + ?.toLowerCase() + .replace(/^\w/, (c) => c.toUpperCase())}{" "} + {(i.status || i.season) && i.episodes && <>·</>}{" "} + {i.episodes || 0} Episodes + </h5> + </div> + <div className="text-sm text-[#b5b5b5] "> + <h1> + {i.type + ?.toLowerCase() + .replace(/^\w/, (c) => c.toUpperCase())} + </h1> + </div> + </div> + </Link> + ))} + </div> + {query && ( + <button className="flex items-center gap-2 justify-center"> + <MagnifyingGlassIcon className="h-5 w-5" /> + <Link href={`/search/${query}`}>More Results...</Link> + </button> + )} + </div> + </m.div> + )} + </AnimatePresence> + ); +}; + +export default SearchBar; diff --git a/components/videoPlayer.js b/components/videoPlayer.js index c441acc..8594645 100644 --- a/components/videoPlayer.js +++ b/components/videoPlayer.js @@ -16,7 +16,6 @@ export default function VideoPlayer({ }) { const [url, setUrl] = useState(); const [source, setSource] = useState([]); - const [loading, setLoading] = useState(true); const { markProgress } = useAniList(session); useEffect(() => { @@ -52,7 +51,6 @@ export default function VideoPlayer({ setUrl(defUrl); setSource(source); - setLoading(false); } catch (error) { console.error(error); } |