diff options
| author | Factiven <[email protected]> | 2023-09-13 00:45:53 +0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2023-09-13 00:45:53 +0700 |
| commit | 7327a69b55a20b99b14ee0803d6cf5f8b88c45ef (patch) | |
| tree | cbcca777593a8cc4b0282e7d85a6fc51ba517e25 /pages/en/schedule | |
| parent | Update issue templates (diff) | |
| download | moopa-7327a69b55a20b99b14ee0803d6cf5f8b88c45ef.tar.xz moopa-7327a69b55a20b99b14ee0803d6cf5f8b88c45ef.zip | |
Update v4 - Merge pre-push to main (#71)
* Create build-test.yml
* initial v4 commit
* update: github workflow
* update: push on branch
* Update .github/ISSUE_TEMPLATE/bug_report.md
* configuring next.config.js file
Diffstat (limited to 'pages/en/schedule')
| -rw-r--r-- | pages/en/schedule/index.js | 523 |
1 files changed, 523 insertions, 0 deletions
diff --git a/pages/en/schedule/index.js b/pages/en/schedule/index.js new file mode 100644 index 0000000..0a49037 --- /dev/null +++ b/pages/en/schedule/index.js @@ -0,0 +1,523 @@ +import Image from "next/image"; +import { useEffect, useRef, useState } from "react"; +import { NewNavbar } from "../../../components/anime/mobile/topSection"; +import Link from "next/link"; +import { CalendarIcon } from "@heroicons/react/24/solid"; +import { ClockIcon } from "@heroicons/react/24/outline"; +import Loading from "../../../components/shared/loading"; +import { timeStamptoAMPM, timeStamptoHour } from "../../../utils/getTimes"; +import { + filterFormattedSchedule, + filterScheduleByDay, + sortScheduleByDay, + transformSchedule, +} from "../../../utils/schedulesUtils"; + +import { scheduleQuery } from "../../../lib/graphql/query"; +import MobileNav from "../../../components/shared/MobileNav"; + +import { useSession } from "next-auth/react"; +import redis from "../../../lib/redis"; +import Head from "next/head"; + +const day = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", +]; + +const isAired = (timestamp) => { + const currentTime = new Date().getTime() / 1000; + return timestamp <= currentTime; +}; + +export async function getServerSideProps() { + const now = new Date(); + // Adjust for Japan timezone (add 9 hours) + const nowJapan = new Date(now.getTime() + 9 * 60 * 60 * 1000); + + // Calculate the time until midnight of the next day in Japan timezone + const midnightTomorrowJapan = new Date( + nowJapan.getFullYear(), + nowJapan.getMonth(), + nowJapan.getDate() + 1, + 0, + 0, + 0, + 0 + ); + const timeUntilMidnightJapan = Math.round( + (midnightTomorrowJapan - nowJapan) / 1000 + ); + + let cachedData; + + // Check if the data is already in Redis + if (redis) { + cachedData = await redis.get("new_schedule"); + } + + if (cachedData) { + const scheduleByDay = JSON.parse(cachedData); + + // const today = now.getDay(); + // const todaySchedule = day[today]; + + return { + props: { + schedule: scheduleByDay, + // today: todaySchedule, + }, + }; + } else { + now.setHours(0, 0, 0, 0); // Set the time to 00:00:00.000 + const dayInSeconds = 86400; // Number of seconds in a day + const yesterdayStart = Math.floor(now.getTime() / 1000) - dayInSeconds; + // Calculate weekStart from yesterday's 00:00:00 + const weekStart = yesterdayStart; + const weekEnd = weekStart + 604800; + + // const today = now.getDay(); + // const todaySchedule = day[today]; + + // const now = new Date(); + // const currentDayOfWeek = now.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday + + // // Calculate the number of seconds until the current Saturday at 00:00:00 + // const secondsUntilSaturday = (6 - currentDayOfWeek) * 24 * 60 * 60; + + // // Calculate weekStart as the current time minus secondsUntilSaturday + // const weekStart = Math.floor(now.getTime() / 1000) - secondsUntilSaturday; + + // // Calculate weekEnd as one week from weekStart + // const weekEnd = weekStart + 604800; // One week in seconds + + let page = 1; + const airingSchedules = []; + + while (true) { + const res = await fetch("https://graphql.anilist.co", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ + query: scheduleQuery, + variables: { + weekStart, + weekEnd, + page, + }, + }), + }); + + const json = await res.json(); + const schedules = json.data.Page.airingSchedules; + + if (schedules.length === 0) { + break; // No more data to fetch + } + + airingSchedules.push(...schedules); + page++; + } + + const timestampToDay = (timestamp) => { + const options = { weekday: "long" }; + return new Date(timestamp * 1000).toLocaleDateString(undefined, options); + }; + + const scheduleByDay = {}; + airingSchedules.forEach((schedule) => { + const day = timestampToDay(schedule.airingAt); + if (!scheduleByDay[day]) { + scheduleByDay[day] = []; + } + scheduleByDay[day].push(schedule); + }); + + if (redis) { + await redis.set( + "new_schedule", + JSON.stringify(scheduleByDay), + "EX", + timeUntilMidnightJapan + ); + } + + return { + props: { + schedule: scheduleByDay, + // today: todaySchedule, + }, + }; + } + // setSchedule(scheduleByDay); +} + +export default function Schedule({ schedule }) { + const { data: session } = useSession(); + + // const [schedule, setSchedule] = useState({}); + const [filterDay, setFilterDay] = useState("All"); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setLoading(true); + async function setDay() { + const now = new Date(); + const today = day[now.getDay()]; + setFilterDay(today); + setLoading(false); + } + setDay(); + }, []); + // Sort the schedule object by day, placing today's schedule first + const sortedSchedule = sortScheduleByDay(schedule); + const formattedSchedule = transformSchedule(schedule); + + // State to keep track of the next airing anime + const [nextAiringAnime, setNextAiringAnime] = useState(null); + // const [nextAiringBanner, setNextAiringBanner] = useState(null); + + // State to keep track of the currently airing anime + const [currentlyAiringAnime, setCurrentlyAiringAnime] = useState(null); + + const [layout, setLayout] = useState(1); + + // Effect to update the next and currently airing anime + useEffect(() => { + const now = new Date().getTime() / 1000; // Current time in seconds + let nextAiring = null; + let currentlyAiring = null; + + for (const [, schedules] of Object.entries(sortedSchedule)) { + for (const s of schedules) { + if (s.airingAt > now) { + if (!nextAiring) { + nextAiring = s.id; + // setNextAiringBanner(s.media.bannerImage); + } + } else if (s.airingAt + 1440 > now) { + currentlyAiring = s.id; + } + } + if (nextAiring && currentlyAiring) break; + } + + setNextAiringAnime(nextAiring); + setCurrentlyAiringAnime(currentlyAiring); + }, [sortedSchedule]); + + const scrollContainerRef = useRef(null); + + useEffect(() => { + // Scroll to center the active button when it changes + if (scrollContainerRef.current) { + const activeButton = + scrollContainerRef.current.querySelector(".text-action"); + if (activeButton) { + const containerWidth = scrollContainerRef.current.clientWidth; + const buttonLeft = activeButton.offsetLeft; + const buttonWidth = activeButton.clientWidth; + const scrollLeft = buttonLeft - containerWidth / 2 + buttonWidth / 2; + scrollContainerRef.current.scrollLeft = scrollLeft; + } + } + }, [filterDay]); + + return ( + <> + <Head> + <title>Moopa - Schedule</title> + {/* write a meta with good seo for this page */} + <meta + name="description" + content="Moopa is a website where you can find all the information about your favorite anime and manga." + /> + <meta + name="keywords" + content="anime, manga, moopa, anilist, information, schedule, airing, next, currently, airing, anime, manga" + /> + <meta name="robots" content="index, follow" /> + <meta name="author" content="Moopa Team" /> + <meta name="url" content="https://moopa.live/en/schedule" /> + <meta name="og:title" property="og:title" content="Moopa - Schedule" /> + <meta + name="og:description" + property="og:description" + content="Moopa is a website where you can find all the information about your favorite anime and manga." + /> + <meta property="og:type" content="website" /> + <meta property="og:url" content="https://moopa.live/en/schedule" /> + <meta + property="og:image" + content="https://beta.moopa.live/preview.png" + /> + <meta + property="og:image:alt" + content="Moopa is a website where you can find all the information about your favorite anime and manga." + /> + <meta property="og:locale" content="en_US" /> + <meta property="og:site_name" content="Moopa" /> + <meta name="twitter:card" content="summary_large_image" /> + {/* <meta name="twitter:site" content="@moopa_anime" /> + <meta name="twitter:creator" content="@moopa_anime" /> */} + <meta + name="twitter:image" + content="https://beta.moopa.live/preview.png" + /> + <meta + name="twitter:image:alt" + content="Moopa is a website where you can find all the information about your favorite anime and manga." + /> + <meta name="twitter:title" content="Moopa - Schedule" /> + <meta + name="twitter:description" + content="Moopa is a website where you can find all the information about your favorite anime and manga." + /> + <link rel="canonical" href="https://moopa.live/en/schedule" /> + </Head> + <MobileNav sessions={session} hideProfile={true} /> + <div className="w-screen"> + <NewNavbar scrollP={10} session={session} toTop={true} /> + <span className="absolute z-20 top-0 left-0 w-screen h-[190px] lg:h-[250px] bg-secondary overflow-hidden"> + <div className="absolute top-40 lg:top-36 w-full h-full bg-primary rounded-t-3xl xl:rounded-t-[50px]" /> + </span> + <div className="flex flex-col mx-auto my-10 w-full mt-16 lg:mt-24 max-w-screen-2xl gap-5 md:gap-10 z-30"> + <div className="flex flex-col lg:flex-row gap-2 justify-between z-20 px-3"> + <ul + ref={scrollContainerRef} + className="flex overflow-x-scroll cust-scroll items-center gap-5 font-karla text-2xl font-semibold" + > + <button + type="button" + onClick={() => setFilterDay("All")} + className={`hover:text-action transition-all duration-200 ease-out cursor-pointer ${ + filterDay === "All" ? "text-action" : "" + }`} + > + All + </button> + {day.map((i) => ( + <button + key={i} + // id={`same_${i}`} + type="button" + onClick={() => { + setLoading(true); + setFilterDay(i); + setLoading(false); + }} + className={`py-2 lg:py-0 outline-none hover:text-action transition-all duration-200 ease-out cursor-pointer ${ + filterDay === i ? "text-action" : "" + }`} + > + {i} + </button> + ))} + </ul> + <div className="flex gap-3"> + <ClockIcon + className={`w-6 h-6 cursor-pointer ${ + layout === 1 ? "text-action" : "text-white" + }`} + onClick={() => setLayout(1)} + /> + <CalendarIcon + className={`w-6 h-6 cursor-pointer ${ + layout === 2 ? "text-action" : "text-white" + }`} + onClick={() => setLayout(2)} + /> + </div> + </div> + + {layout === 1 ? ( + !loading ? ( + Object.entries( + filterFormattedSchedule(formattedSchedule, filterDay) + ).map(([day, timeSlots], index) => ( + <div + key={`section_${day}`} + // id={`same_${day}`} + className="flex flex-col gap-5 z-50 px-3" + > + <h2 className="font-bold font-outfit text-white text-2xl z-[250]"> + {day} + </h2> + {Object.entries(timeSlots).map(([time, animeList]) => ( + <div + key={time} + // id={`same_${time}`} + className="relative space-y-2" + > + <div className="ml-4 flex items-center gap-2"> + <h3 className="text-lg text-gray-200 font-semibold"> + {timeStamptoAMPM(time)} + </h3> + {/* {!isAired(time) && <p>Airing Next</p>} */} + <p + className={`absolute left-0 h-1.5 w-1.5 rounded-full ${ + isAired(time) ? "bg-action" : "bg-gray-600" // Add a class for currently airing anime + }`} + ></p> + </div> + <div className="w-full grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5 md:gap-7 grid-flow-row relative"> + {animeList.map((s, index) => { + const m = s.media; + return ( + <> + <Link + key={m.id} + // id={`same_${m.id}`} + href={`/en/${m.type.toLowerCase()}/${m.id}`} + className={`flex bg-secondary rounded group cursor-pointer ml-4 z-50`} + > + <Image + src={m.coverImage.extraLarge} + alt="image" + width="0" + height="0" + className="w-[50px] h-[65px] object-cover shrink-0" + /> + <div className="flex flex-col justify-center font-karla p-2"> + <h1 className="font-semibold line-clamp-1 text-sm group-hover:text-action transition-all duration-200 ease-out"> + {m.title.romaji} + </h1> + <p className="text-gray-400 group-hover:text-action/80 transition-all duration-200 ease-out"> + Ep {s.episode} {timeStamptoHour(s.airingAt)} + </p> + </div> + </Link> + <p + key={`p_${s.id}_${index}`} + className={`absolute translate-x-full top-1/2 -translate-y-1/2 h-full w-0.5 ${ + isAired(time) ? "bg-action" : "bg-gray-600" // Add a class for currently airing anime + }`} + ></p> + </> + ); + })} + </div> + </div> + ))} + </div> + )) + ) : ( + <div className="z-[500] pt-10 lg:pt-0"> + <Loading /> + </div> + ) + ) : !loading ? ( + Object.entries(filterScheduleByDay(sortedSchedule, filterDay)).map( + ([day, schedules]) => ( + <div + key={`section2_${day}`} + // id={`same_${day}`} + className="flex flex-col gap-5 px-3 z-50" + > + <h2 + // id={day} + className="font-bold font-outfit text-white text-2xl" + > + {day} + </h2> + <div className="w-full grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-5 md:gap-7 grid-flow-row"> + {schedules.map((s) => { + const m = s.media; + + return ( + <Link + key={m.id} + // id={`same_${m.id}`} + href={`/en/${m.type?.toLowerCase()}/${m.id}`} + className={`flex bg-secondary rounded group cursor-pointer relative ${ + s.id === nextAiringAnime + ? "ring-1 ring-sky-500" + : "" // Add a class for next airing anime + } ${ + s.id === currentlyAiringAnime + ? "ring-1 ring-action" + : "" // Add a class for currently airing anime + }`} + > + {/* <p className={``}> */} + <p className="absolute flex top-0 right-0 -mt-1 -mr-1 justify-center items-center"> + <span + className={`relative flex justify-center h-3 w-3 tooltip-container ${ + s.id === nextAiringAnime ? "" : "hidden" // Add a className for next airing anime + }`} + > + {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span> */} + <span className="relative inline-flex rounded-full h-3 w-3 bg-sky-500"></span> + <span className="tooltip">Next Airing</span> + </span> + </p> + <p className="absolute flex top-0 right-0 -mt-1 -mr-1 justify-center items-center"> + <span + className={`relative flex justify-center h-3 w-3 tooltip-container ${ + s.id === currentlyAiringAnime ? "" : "hidden" // Add a className for currently airing anime + }`} + > + <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-400 opacity-75"></span> + <span className="relative inline-flex rounded-full h-3 w-3 bg-orange-500"></span> + <span className="tooltip">Airing Now</span> + </span> + </p> + {/* <span + className={`${ + s.id === nextAiringAnime + ? "bg-orange-700 text-sm px-3 py-1 rounded-full font-bold text-white" + : "" + } mx-auto`} + > + Airing Next + </span> */} + {/* </p> */} + {/* {s.media?.bannerImage && ( + <Image + src={s.media?.bannerImage} + alt="banner" + width="0" + height="0" + className="absolute pointer-events-none top-0 opacity-0 group-hover:opacity-10 transition-all duration-500 ease-linear -z-10 left-0 rounded-l w-full h-[250px] object-cover" + /> + )} */} + <Image + src={m.coverImage.extraLarge} + alt="image" + width="0" + height="0" + className="w-[50px] h-[65px] object-cover shrink-0" + /> + <div className="flex flex-col justify-center font-karla p-2"> + <h1 className="font-semibold line-clamp-1 text-sm group-hover:text-action transition-all duration-200 ease-out"> + {m.title.romaji} + </h1> + <p className="text-gray-400 group-hover:text-action/80 transition-all duration-200 ease-out"> + Ep {s.episode} {timeStamptoHour(s.airingAt)} + </p> + </div> + </Link> + ); + })} + </div> + </div> + ) + ) + ) : ( + <div className="z-[500] pt-10 lg:pt-0"> + <Loading /> + </div> + )} + </div> + </div> + </> + ); +} |