From 9a5754fdba9d778f820fe89b44d1e21ca9f0bb4d Mon Sep 17 00:00:00 2001 From: Factiven Date: Tue, 16 May 2023 22:40:02 +0700 Subject: 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 --- components/hero/content.js | 182 +++++++++++++++++++++++++++++++++++++++++---- components/hero/genres.js | 69 +++++++++++++++++ components/searchBar.js | 144 +++++++++++++++++++++++++++++++++++ components/videoPlayer.js | 2 - 4 files changed, 380 insertions(+), 17 deletions(-) create mode 100644 components/hero/genres.js create mode 100644 components/searchBar.js (limited to 'components') 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 (
-

{section}

+
+

{section}

+ +
- + > + +
- {filteredData?.map((anime) => { + {slicedData?.map((anime) => { + const progress = og?.find((i) => i.mediaId === anime.id); return (
+ {ids === "onGoing" && ( +
+
+

+ {anime.title.romaji || anime.title.english} +

+ {checkProgress(progress) && + !clicked?.hasOwnProperty(anime.id) && ( + + )} + {checkProgress(progress) && ( +
handleAlert(anime.id)} + className="group-hover:visible invisible absolute top-0 bg-black bg-opacity-20 w-full h-full z-20 text-center" + > +

+ {checkProgress(progress)} +

+
+ )} +
+

Episode {anime.nextAiringEpisode.episode} in

+

+ {convertSecondsToTime( + anime?.nextAiringEpisode?.timeUntilAiring + )} +

+
+
+
+ )}
); })} + {filteredData.length >= 10 && section !== "Recommendations" && ( +
+
+

+ More on {section} +

+ +
+
+ )}
@@ -90,3 +208,37 @@ export default function Content({ ids, section, data }) {
); } + +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 ( +
+
+

Top Genres

+ +
+
+
+
+
+ {g.map((a, index) => ( + +
+

+ {a.name} +

+
+ genres images + + ))} +
+
+
+
+
+ ); +} 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 ( + + {isOpen && ( + +
+
+ setQuery(e.target.value)} + /> +
+
+ {data?.media.map((i) => ( + + search results +
+
+

{i.title.userPreferred}

+
+ {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 +
+
+
+

+ {i.type + ?.toLowerCase() + .replace(/^\w/, (c) => c.toUpperCase())} +

+
+
+ + ))} +
+ {query && ( + + )} +
+
+ )} +
+ ); +}; + +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); } -- cgit v1.2.3