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 /lib | |
| 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 'lib')
| -rw-r--r-- | lib/Artplayer.js | 19 | ||||
| -rw-r--r-- | lib/anify/info.js | 13 | ||||
| -rw-r--r-- | lib/anify/page.js | 13 | ||||
| -rw-r--r-- | lib/anilist/AniList.js | 2 | ||||
| -rw-r--r-- | lib/anilist/aniAdvanceSearch.js | 92 | ||||
| -rw-r--r-- | lib/anilist/getMedia.js | 90 | ||||
| -rw-r--r-- | lib/anilist/getUpcomingAnime.js | 52 | ||||
| -rw-r--r-- | lib/anilist/useAnilist.js | 250 | ||||
| -rw-r--r-- | lib/graphql/query.js | 304 | ||||
| -rw-r--r-- | lib/hooks/isOpenState.js | 17 | ||||
| -rw-r--r-- | lib/hooks/useDebounce.js | 17 | ||||
| -rw-r--r-- | lib/redis.js | 13 |
12 files changed, 710 insertions, 172 deletions
diff --git a/lib/Artplayer.js b/lib/Artplayer.js index 96afe2b..48da24d 100644 --- a/lib/Artplayer.js +++ b/lib/Artplayer.js @@ -14,10 +14,9 @@ export default function Player({ id, track, // socket - socket, - isPlay, - watchdata, - room, + // isPlay, + // watchdata, + // room, autoplay, setautoplay, ...rest @@ -59,18 +58,20 @@ export default function Player({ theme: "#f97316", controls: [ { + index: 10, name: "fast-rewind", - position: "right", - html: '<svg class="hi-solid hi-rewind inline-block w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M8.445 14.832A1 1 0 0010 14v-2.798l5.445 3.63A1 1 0 0017 14V6a1 1 0 00-1.555-.832L10 8.798V6a1 1 0 00-1.555-.832l-6 4a1 1 0 000 1.664l6 4z"/></svg>', + position: "left", + html: '<svg class="hi-solid hi-rewind inline-block w-7 h-7" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M8.445 14.832A1 1 0 0010 14v-2.798l5.445 3.63A1 1 0 0017 14V6a1 1 0 00-1.555-.832L10 8.798V6a1 1 0 00-1.555-.832l-6 4a1 1 0 000 1.664l6 4z"/></svg>', tooltip: "Backward 5s", click: function () { art.backward = 5; }, }, { + index: 11, name: "fast-forward", - position: "right", - html: '<svg class="hi-solid hi-fast-forward inline-block w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M4.555 5.168A1 1 0 003 6v8a1 1 0 001.555.832L10 11.202V14a1 1 0 001.555.832l6-4a1 1 0 000-1.664l-6-4A1 1 0 0010 6v2.798l-5.445-3.63z"/></svg>', + position: "left", + html: '<svg class="hi-solid hi-fast-forward inline-block w-7 h-7" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M4.555 5.168A1 1 0 003 6v8a1 1 0 001.555.832L10 11.202V14a1 1 0 001.555.832l6-4a1 1 0 000-1.664l-6-4A1 1 0 0010 6v2.798l-5.445-3.63z"/></svg>', tooltip: "Forward 5s", click: function () { art.forward = 5; @@ -79,7 +80,7 @@ export default function Player({ ], settings: [ { - html: "Autoplay", + html: "Autoplay Next", // icon: '<img width="22" heigth="22" src="/assets/img/state.svg">', tooltip: "ON/OFF", switch: localStorage.getItem("autoplay") === "true" ? true : false, diff --git a/lib/anify/info.js b/lib/anify/info.js index 8978664..e7d4025 100644 --- a/lib/anify/info.js +++ b/lib/anify/info.js @@ -1,5 +1,5 @@ import axios from "axios"; -import cacheData from "memory-cache"; +import redis from "../redis"; export async function fetchInfo(id, key) { try { @@ -15,13 +15,18 @@ export async function fetchInfo(id, key) { export default async function getAnifyInfo(id, key) { try { - const cached = cacheData.get(id); + let cached; + if (redis) { + cached = await redis.get(id); + } if (cached) { - return cached; + return JSON.parse(cached); } else { const data = await fetchInfo(id, key); if (data) { - cacheData.put(id, data, 1000 * 60 * 10); + if (redis) { + await redis.set(id, JSON.stringify(data), "EX", 60 * 10); + } return data; } else { return { message: "Schedule not found" }; diff --git a/lib/anify/page.js b/lib/anify/page.js index 6361230..b2b1207 100644 --- a/lib/anify/page.js +++ b/lib/anify/page.js @@ -1,4 +1,4 @@ -import cacheData from "memory-cache"; +import redis from "../redis"; // Function to fetch new data async function fetchData(id, providerId, chapterId, key) { @@ -21,13 +21,18 @@ export default async function getAnifyPage( key ) { try { - const cached = cacheData.get(chapterId); + let cached; + if (redis) { + cached = await redis.get(chapterId); + } if (cached) { - return cached; + return JSON.parse(cached); } else { const data = await fetchData(mediaId, providerId, chapterId, key); if (!data.error) { - cacheData.put(chapterId, data, 1000 * 60 * 10); + if (redis) { + await redis.set(chapterId, JSON.stringify(data), "EX", 60 * 10); + } return data; } else { return { message: "Manga/Novel not found :(" }; diff --git a/lib/anilist/AniList.js b/lib/anilist/AniList.js index f5fe19c..b8d6ed3 100644 --- a/lib/anilist/AniList.js +++ b/lib/anilist/AniList.js @@ -29,8 +29,10 @@ export async function aniListData({ sort, page = 1 }) { romaji english } + bannerImage coverImage { extraLarge + color } description } diff --git a/lib/anilist/aniAdvanceSearch.js b/lib/anilist/aniAdvanceSearch.js index 263ca9d..02a5c53 100644 --- a/lib/anilist/aniAdvanceSearch.js +++ b/lib/anilist/aniAdvanceSearch.js @@ -1,63 +1,53 @@ -const advance = ` - query ($search: String, $type: MediaType, $status: MediaStatus, $season: MediaSeason, $seasonYear: Int, $genres: [String], $tags: [String], $sort: [MediaSort], $page: Int, $perPage: Int) { - Page (page: $page, perPage: $perPage) { - pageInfo { - total - currentPage - lastPage - hasNextPage - } - media (search: $search, type: $type, status: $status, season: $season, seasonYear: $seasonYear, genre_in: $genres, tag_in: $tags, sort: $sort, isAdult: false) { - id - title { - userPreferred - } - type - episodes - chapters - status - format - season - seasonYear - coverImage { - extraLarge - color - } - averageScore - isAdult - } +import { advanceSearchQuery } from "../graphql/query"; + +export async function aniAdvanceSearch({ + search, + type, + genres, + page, + sort, + format, + season, + seasonYear, + perPage, +}) { + const categorizedGenres = genres?.reduce((result, item) => { + const existingEntry = result[item.type]; + + if (existingEntry) { + existingEntry.push(item.value); + } else { + result[item.type] = [item.value]; } - } -`; -export async function aniAdvanceSearch(options = {}) { - const { - search = null, - type = "ANIME", - seasonYear = NaN, - season = undefined, - genres = null, - page = 1, - perPage = null, - sort = "POPULARITY_DESC", - } = options; - // console.log(page); + return result; + }, {}); + const response = await fetch("https://graphql.anilist.co/", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ - query: advance, + query: advanceSearchQuery, variables: { - search: search, - type: type, - seasonYear: seasonYear, - season: season, - genres: genres, - perPage: perPage, - sort: sort, - page: page, + ...(search && { + search: search, + ...(!sort && { sort: "SEARCH_MATCH" }), + }), + ...(type && { type: type }), + ...(seasonYear && { seasonYear: seasonYear }), + ...(season && { + season: season, + ...(!seasonYear && { seasonYear: new Date().getFullYear() }), + }), + ...(categorizedGenres && { ...categorizedGenres }), + ...(format && { format: format }), + // ...(genres && { genres: genres }), + // ...(tags && { tags: tags }), + ...(perPage && { perPage: perPage }), + ...(sort && { sort: sort }), + ...(page && { page: page }), }, }), }); diff --git a/lib/anilist/getMedia.js b/lib/anilist/getMedia.js new file mode 100644 index 0000000..c4628ab --- /dev/null +++ b/lib/anilist/getMedia.js @@ -0,0 +1,90 @@ +import { useEffect, useState } from "react"; + +export default function GetMedia(session, stats) { + const [media, setMedia] = useState([]); + const [recommendations, setRecommendations] = useState([]); + const accessToken = session?.user?.token; + const username = session?.user?.name; + const status = stats || null; + + const fetchGraphQL = async (query, variables) => { + const response = await fetch("https://graphql.anilist.co/", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: accessToken ? `Bearer ${accessToken}` : undefined, + }, + body: JSON.stringify({ query, variables }), + }); + return response.json(); + }; + + useEffect(() => { + if (!username || !accessToken) return; + const queryMedia = ` + query ($username: String, $status: MediaListStatus, $sort: [RecommendationSort]) { + Page(page: 1, perPage: 10) { + recommendations(sort: $sort, onList: true) { + mediaRecommendation { + id + title { + userPreferred + } + description + format + type + status(version: 2) + bannerImage + isAdult + coverImage { + extraLarge + } + } + } + } + MediaListCollection(userName: $username, type: ANIME, status: $status, sort: UPDATED_TIME_DESC) { + lists { + status + name + entries { + id + mediaId + status + progress + score + media { + id + status + nextAiringEpisode { + timeUntilAiring + episode + } + title { + english + romaji + } + episodes + coverImage { + large + } + } + } + } + } +} + + `; + fetchGraphQL(queryMedia, { + username, + status: status?.stats, + sort: "ID_DESC", + }).then((data) => { + setMedia(data.data.MediaListCollection.lists); + setRecommendations( + data.data.Page.recommendations.map((i) => i.mediaRecommendation) + ); + }); + }, [username, accessToken, status?.stats]); + + return { media, recommendations }; +} diff --git a/lib/anilist/getUpcomingAnime.js b/lib/anilist/getUpcomingAnime.js index fc848fd..2ab9315 100644 --- a/lib/anilist/getUpcomingAnime.js +++ b/lib/anilist/getUpcomingAnime.js @@ -19,23 +19,39 @@ const getUpcomingAnime = async () => { } const query = ` - query ($season: MediaSeason, $seasonYear: Int) { - Page(page: 1, perPage: 20) { - media(season: $season, seasonYear: $seasonYear, sort: POPULARITY_DESC, type: ANIME) { + query ($season: MediaSeason, $year: Int, $format: MediaFormat, $excludeFormat: MediaFormat, $status: MediaStatus, $minEpisodes: Int, $page: Int) { + Page(page: $page) { + pageInfo { + hasNextPage + total + } + media(season: $season, seasonYear: $year, format: $format, format_not: $excludeFormat, status: $status, episodes_greater: $minEpisodes, isAdult: false, type: ANIME, sort: TITLE_ROMAJI) { id - coverImage{ - large - } - bannerImage + idMal title { - english romaji native + english + } + startDate { + year + month + day + } + status + season + format + description + bannerImage + coverImage { + extraLarge + color } - nextAiringEpisode { - episode - airingAt - timeUntilAiring + airingSchedule(notYetAired: true, perPage: 1) { + nodes { + episode + airingAt + } } } } @@ -43,8 +59,9 @@ const getUpcomingAnime = async () => { `; const variables = { - season: currentSeason, - seasonYear: currentYear, + season: "FALL", + year: currentYear, + format: "TV", }; let response = await fetch("https://graphql.anilist.co", { @@ -63,13 +80,14 @@ const getUpcomingAnime = async () => { let currentSeasonAnime = json.data.Page.media; let nextAiringAnime = currentSeasonAnime.filter( - (anime) => - anime.nextAiringEpisode !== null && anime.nextAiringEpisode.episode === 1 + (anime) => anime.airingSchedule.nodes?.[0]?.episode === 1 ); if (nextAiringAnime.length >= 1) { nextAiringAnime.sort( - (a, b) => a.nextAiringEpisode.airingAt - b.nextAiringEpisode.airingAt + (a, b) => + a.airingSchedule.nodes?.[0].airingAt - + b.airingSchedule.nodes?.[0].airingAt ); return nextAiringAnime; // return all upcoming anime, not just the first two } diff --git a/lib/anilist/useAnilist.js b/lib/anilist/useAnilist.js index 72e11ca..17ab11b 100644 --- a/lib/anilist/useAnilist.js +++ b/lib/anilist/useAnilist.js @@ -1,63 +1,107 @@ -import { useState, useEffect } from "react"; import { toast } from "react-toastify"; -export const useAniList = (session, stats) => { - const [media, setMedia] = useState([]); +export const useAniList = (session) => { const accessToken = session?.user?.token; - const username = session?.user?.name; - const status = stats || null; const fetchGraphQL = async (query, variables) => { const response = await fetch("https://graphql.anilist.co/", { method: "POST", headers: { "Content-Type": "application/json", - Authorization: accessToken ? `Bearer ${accessToken}` : undefined, + ...(accessToken && { Authorization: `Bearer ${accessToken}` }), }, body: JSON.stringify({ query, variables }), }); return response.json(); }; - useEffect(() => { - if (!username || !accessToken) return; - const queryMedia = ` - query ($username: String, $status: MediaListStatus) { - MediaListCollection(userName: $username, type: ANIME, status: $status) { - lists { - status - name - entries { - id - mediaId - status - progress - score - media { - id - status - nextAiringEpisode { - timeUntilAiring - episode - } - title { - english - romaji - } - episodes - coverImage { - large - } - } - } - } - } + const quickSearch = async ({ search, type, isAdult = false }) => { + if (!search || search === " ") return; + const searchQuery = ` + query ($type: MediaType, $search: String, $isAdult: Boolean) { + Page(perPage: 8) { + pageInfo { + total + hasNextPage + } + results: media(type: $type, isAdult: $isAdult, search: $search) { + id + title { + userPreferred + } + coverImage { + medium } + type + format + bannerImage + isLicensed + genres + startDate { + year + } + } + } +} `; - fetchGraphQL(queryMedia, { username, status: status?.stats }).then((data) => - setMedia(data.data.MediaListCollection.lists) - ); - }, [username, accessToken, status?.stats]); + const data = await fetchGraphQL(searchQuery, { search, type, isAdult }); + return data; + }; + + const multiSearch = async (search) => { + if (!search || search === " ") return; + const searchQuery = ` + query ($search: String, $isAdult: Boolean) { + anime: Page(perPage: 8) { + pageInfo { + total + hasNextPage + } + results: media(type: ANIME, isAdult: $isAdult, search: $search) { + id + title { + userPreferred + } + coverImage { + medium + } + type + format + bannerImage + isLicensed + genres + startDate { + year + } + } + } + manga: Page(perPage: 8) { + pageInfo { + total + hasNextPage + } + results: media(type: MANGA, isAdult: $isAdult, search: $search) { + id + title { + userPreferred + } + coverImage { + medium + } + type + format + bannerImage + isLicensed + startDate { + year + } + } + } +} +`; + const data = await fetchGraphQL(searchQuery, { search }); + return data; + }; const markComplete = async (mediaId) => { if (!accessToken) return; @@ -94,7 +138,10 @@ export const useAniList = (session, stats) => { query ($id: Int) { Media(id: $id) { mediaListEntry { + progress + status customLists + repeat } id type @@ -103,6 +150,11 @@ export const useAniList = (session, stats) => { english native } + format + episodes + nextAiringEpisode { + episode + } } } `; @@ -110,23 +162,11 @@ export const useAniList = (session, stats) => { return data; }; - const customLists = async (lists) => { - const setList = ` - mutation($lists: [String]){ - UpdateUser(animeListOptions: { customLists: $lists }){ - id - } - } - `; - const data = await fetchGraphQL(setList, { lists }); - return data; - }; - const markProgress = async (mediaId, progress, stats, volumeProgress) => { if (!accessToken) return; const progressWatched = ` - mutation($mediaId: Int, $progress: Int, $status: MediaListStatus, $progressVolumes: Int, $lists: [String]) { - SaveMediaListEntry(mediaId: $mediaId, progress: $progress, status: $status, progressVolumes: $progressVolumes, customLists: $lists) { + mutation($mediaId: Int, $progress: Int, $status: MediaListStatus, $progressVolumes: Int, $lists: [String], $repeat: Int) { + SaveMediaListEntry(mediaId: $mediaId, progress: $progress, status: $status, progressVolumes: $progressVolumes, customLists: $lists, repeat: $repeat) { id mediaId progress @@ -137,46 +177,82 @@ export const useAniList = (session, stats) => { const user = await getUserLists(mediaId); const media = user?.data?.Media; - if (media) { - let checkList = media?.mediaListEntry?.customLists - ? Object.entries(media?.mediaListEntry?.customLists).map( - ([key, value]) => key - ) || [] - : []; - if (!checkList?.includes("Watched using Moopa")) { - checkList.push("Watched using Moopa"); - await customLists(checkList); + if (media && media.type !== "MANGA") { + let customList; + + if (session.user.name) { + const res = await fetch( + `/api/user/profile?name=${session.user.name}` + ).then((res) => res.json()); + customList = res?.setting === null ? true : res?.setting?.CustomLists; } - let lists = media?.mediaListEntry?.customLists - ? Object.entries(media?.mediaListEntry?.customLists) + let lists = media.mediaListEntry?.customLists + ? Object.entries(media.mediaListEntry?.customLists) .filter(([key, value]) => value === true) .map(([key, value]) => key) || [] : []; - if (!lists?.includes("Watched using Moopa")) { + + if (customList === true && !lists?.includes("Watched using Moopa")) { lists.push("Watched using Moopa"); } - if (lists.length > 0) { - await fetchGraphQL(progressWatched, { - mediaId, - progress, - status: stats, - progressVolumes: volumeProgress, - lists, - }); - console.log(`Progress Updated: ${progress}`); - toast.success(`Progress Updated: ${progress}`, { - position: "bottom-right", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - draggable: true, - theme: "dark", - }); + + const singleEpisode = + (!media.episodes || + (media.format === "MOVIE" && media.episodes === 1)) && + 1; + const videoEpisode = Number(progress) || singleEpisode; + const mediaEpisode = + media.nextAiringEpisode?.episode || media.episodes || singleEpisode; + const status = + media.mediaListEntry?.status === "REPEATING" ? "REPEATING" : "CURRENT"; + + let variables = { + mediaId, + progress, + status, + progressVolumes: volumeProgress, + lists, + }; + + if (videoEpisode === mediaEpisode) { + variables.status = "COMPLETED"; + if (media.mediaListEntry?.status === "REPEATING") + variables.repeat = media.mediaListEntry.repeat + 1; } + + // if (lists.length > 0) { + await fetchGraphQL(progressWatched, variables); + console.log(`Progress Updated: ${progress}`, status); + // } + } else if (media && media.type === "MANGA") { + let variables = { + mediaId, + progress, + status: stats, + progressVolumes: volumeProgress, + }; + + await fetchGraphQL(progressWatched, variables); + console.log(`Progress Updated: ${progress}`, status); + toast.success(`Progress Updated: ${progress}`, { + position: "bottom-right", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: true, + draggable: true, + theme: "dark", + }); } }; - return { media, markComplete, markProgress, markPlanning, getUserLists }; + return { + markComplete, + markProgress, + markPlanning, + getUserLists, + multiSearch, + quickSearch, + }; }; diff --git a/lib/graphql/query.js b/lib/graphql/query.js new file mode 100644 index 0000000..297edb2 --- /dev/null +++ b/lib/graphql/query.js @@ -0,0 +1,304 @@ +const scheduleQuery = ` +query ($weekStart: Int, $weekEnd: Int, $page: Int) { + Page(page: $page) { + pageInfo { + hasNextPage + total + } + airingSchedules(airingAt_greater: $weekStart, airingAt_lesser: $weekEnd) { + id + episode + airingAt + media { + id + idMal + title { + romaji + native + english + } + startDate { + year + month + day + } + endDate { + year + month + day + } + type + status + season + format + genres + synonyms + duration + popularity + episodes + source(version: 2) + countryOfOrigin + hashtag + averageScore + siteUrl + description + bannerImage + isAdult + coverImage { + extraLarge + color + } + trailer { + id + site + thumbnail + } + externalLinks { + site + url + } + rankings { + rank + type + season + allTime + } + studios(isMain: true) { + nodes { + id + name + siteUrl + } + } + relations { + edges { + relationType(version: 2) + node { + id + title { + romaji + native + english + } + siteUrl + } + } + } + } + } + } +} +`; + +const advanceSearchQuery = ` +query ($page: Int = 1, $id: Int, $type: MediaType, $isAdult: Boolean = false, $search: String, $format: [MediaFormat], $status: MediaStatus, $countryOfOrigin: CountryCode, $source: MediaSource, $season: MediaSeason, $seasonYear: Int, $year: String, $onList: Boolean, $yearLesser: FuzzyDateInt, $yearGreater: FuzzyDateInt, $episodeLesser: Int, $episodeGreater: Int, $durationLesser: Int, $durationGreater: Int, $chapterLesser: Int, $chapterGreater: Int, $volumeLesser: Int, $volumeGreater: Int, $licensedBy: [Int], $isLicensed: Boolean, $genres: [String], $excludedGenres: [String], $tags: [String], $excludedTags: [String], $minimumTagRank: Int, $sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]) { + Page(page: $page, perPage: 20) { + pageInfo { + total + perPage + currentPage + lastPage + hasNextPage + } + media(id: $id, type: $type, season: $season, format_in: $format, status: $status, countryOfOrigin: $countryOfOrigin, source: $source, search: $search, onList: $onList, seasonYear: $seasonYear, startDate_like: $year, startDate_lesser: $yearLesser, startDate_greater: $yearGreater, episodes_lesser: $episodeLesser, episodes_greater: $episodeGreater, duration_lesser: $durationLesser, duration_greater: $durationGreater, chapters_lesser: $chapterLesser, chapters_greater: $chapterGreater, volumes_lesser: $volumeLesser, volumes_greater: $volumeGreater, licensedById_in: $licensedBy, isLicensed: $isLicensed, genre_in: $genres, genre_not_in: $excludedGenres, tag_in: $tags, tag_not_in: $excludedTags, minimumTagRank: $minimumTagRank, sort: $sort, isAdult: $isAdult) { + id + title { + userPreferred + } + coverImage { + extraLarge + large + color + } + startDate { + year + month + day + } + endDate { + year + month + day + } + bannerImage + season + seasonYear + description + type + format + status(version: 2) + episodes + duration + chapters + volumes + genres + isAdult + averageScore + popularity + nextAiringEpisode { + airingAt + timeUntilAiring + episode + } + mediaListEntry { + id + status + } + studios(isMain: true) { + edges { + isMain + node { + id + name + } + } + } + } + } +}`; + +const currentUserQuery = ` +query { + Viewer { + id + name + avatar { + large + medium + } + bannerImage + mediaListOptions { + animeList { + customLists + } + } + } + }`; + +const mediaInfoQuery = ` + query ($id: Int) { + Media(id: $id) { + id + type + format + title { + romaji + english + native + } + coverImage { + extraLarge + large + color + } + bannerImage + description + episodes + nextAiringEpisode { + episode + airingAt + timeUntilAiring + } + averageScore + popularity + status + genres + season + seasonYear + duration + relations { + edges { + id + relationType(version: 2) + node { + id + title { + userPreferred + } + format + type + status(version: 2) + bannerImage + coverImage { + extraLarge + color + } + } + } + } + recommendations { + nodes { + mediaRecommendation { + id + title { + romaji + } + coverImage { + extraLarge + large + } + } + } + } + } +}`; + +const mediaUserQuery = ` +query ($username: String, $status: MediaListStatus) { + MediaListCollection(userName: $username, type: ANIME, status: $status, sort: UPDATED_TIME_DESC) { + user { + id + name + about (asHtml: true) + createdAt + avatar { + large + } + statistics { + anime { + count + episodesWatched + meanScore + minutesWatched + } + } + bannerImage + mediaListOptions { + animeList { + sectionOrder + } + } + } + lists { + status + name + entries { + id + mediaId + status + progress + score + media { + id + status + title { + english + romaji + } + episodes + coverImage { + large + } + } + } + } + } + }`; + +export { + scheduleQuery, + advanceSearchQuery, + currentUserQuery, + mediaInfoQuery, + mediaUserQuery, +}; diff --git a/lib/hooks/isOpenState.js b/lib/hooks/isOpenState.js new file mode 100644 index 0000000..6aade61 --- /dev/null +++ b/lib/hooks/isOpenState.js @@ -0,0 +1,17 @@ +import React, { createContext, useContext, useState } from "react"; + +const SearchContext = createContext(); + +export const SearchProvider = ({ children }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + <SearchContext.Provider value={{ isOpen, setIsOpen }}> + {children} + </SearchContext.Provider> + ); +}; + +export function useSearch() { + return useContext(SearchContext); +} diff --git a/lib/hooks/useDebounce.js b/lib/hooks/useDebounce.js new file mode 100644 index 0000000..e3a1631 --- /dev/null +++ b/lib/hooks/useDebounce.js @@ -0,0 +1,17 @@ +import { useEffect, useState } from "react"; + +export default function useDebounce(value, delay) { + const [debounceValue, setDebounceValue] = useState(value); + + useEffect(() => { + const timeoutId = setTimeout(() => { + setDebounceValue(value); + }, delay); + + return () => { + clearTimeout(timeoutId); + }; + }, [value, delay]); + + return debounceValue; +} diff --git a/lib/redis.js b/lib/redis.js new file mode 100644 index 0000000..ed8b8c5 --- /dev/null +++ b/lib/redis.js @@ -0,0 +1,13 @@ +import { Redis } from "ioredis"; + +const REDIS_URL = process.env.REDIS_URL; + +let redis; + +if (REDIS_URL) { + redis = new Redis(REDIS_URL); +} else { + console.warn("REDIS_URL is not defined. Redis caching will be disabled."); +} + +export default redis; |