aboutsummaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
authorFactiven <[email protected]>2023-05-16 22:40:02 +0700
committerGitHub <[email protected]>2023-05-16 22:40:02 +0700
commit9a5754fdba9d778f820fe89b44d1e21ca9f0bb4d (patch)
tree8bd574163e760216bc91f7b3c164232b6982efe8 /components
parentUpdate v3.5.6 (diff)
downloadmoopa-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.js182
-rw-r--r--components/hero/genres.js69
-rw-r--r--components/searchBar.js144
-rw-r--r--components/videoPlayer.js2
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 && <>&#183;</>}{" "}
+ {i.season
+ ?.toLowerCase()
+ .replace(/^\w/, (c) => c.toUpperCase())}{" "}
+ {(i.status || i.season) && i.episodes && <>&#183;</>}{" "}
+ {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);
}