diff --git a/README.md b/README.md index 3e30a8f5..a28a821c 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Project of animes and mangas website, utilizing the AniList, Consumet and Aniwat - [x] `Comment`: Write what you thougth of that episode or just tell something that every should know about. - [x] `Log In`: You can log in with Google, GitHub or Anonymously (with some restrictions). - [x] `Keep Watching`: Continue the episode from where you stop last time. +- [x] `Be Notified`: When a New Episode is Released, you get a notification on the website. - [x] `Bookmark your favourite animes e mangas`: Save for later your animes and mangas. - [x] `Bookmark the episodes you watched`: And keep watching from there later - [x] `News Feed`: Keep up with the latest news of animes, mangas and the industry. @@ -21,7 +22,6 @@ Project of animes and mangas website, utilizing the AniList, Consumet and Aniwat ## :pushpin: Under Development - [ ] `Add Reply to Comments` -- [ ] `Be notified when a new episode/chapter is released` ## :heavy_check_mark: Tecnologies Used diff --git a/app/components/AddToPlaylistButton/component.module.css b/app/components/AddToPlaylistButton/component.module.css index d577c7b5..332cab2f 100644 --- a/app/components/AddToPlaylistButton/component.module.css +++ b/app/components/AddToPlaylistButton/component.module.css @@ -8,7 +8,7 @@ font-weight: 600; - border: 1px solid transparent; + border: 1px solid var(--white-100); } diff --git a/app/components/AddToPlaylistButton/index.tsx b/app/components/AddToPlaylistButton/index.tsx index bf1133af..a18fae81 100644 --- a/app/components/AddToPlaylistButton/index.tsx +++ b/app/components/AddToPlaylistButton/index.tsx @@ -3,7 +3,13 @@ import React, { useEffect, useState } from 'react' import styles from "./component.module.css" import LoadingSvg from "@/public/assets/ripple-1s-200px.svg" import LoadingsssSvg from "@/public/assets/bookmark-check-fill.svg" -import { getFirestore, doc, updateDoc, arrayUnion, arrayRemove, getDoc, FieldPath, setDoc, DocumentSnapshot, DocumentData } from 'firebase/firestore'; +import { + getFirestore, doc, + updateDoc, arrayUnion, + arrayRemove, getDoc, + FieldPath, setDoc, + DocumentSnapshot, DocumentData +} from 'firebase/firestore'; import { initFirebase } from '@/app/firebaseApp' import { getAuth } from 'firebase/auth' import { ApiDefaultResult } from '@/app/ts/interfaces/apiAnilistDataInterface' @@ -36,52 +42,28 @@ function AddToPlaylistButton({ data, customText }: { data: ApiDefaultResult, cus setIsLoading(true) - if (!wasAddedToPlaylist) { - - await updateDoc(doc(db, 'users', user.uid), - { - bookmarks: arrayUnion({ - id: data.id, - title: { - romaji: data.title.romaji - }, - format: data.format, - description: data.description, - coverImage: { - extraLarge: data.coverImage.extraLarge, - large: data.coverImage.large - } - }) - } as unknown as FieldPath, - { merge: true } - ) + const bookmarkData = { + id: data.id, + title: { + romaji: data.title.romaji + }, + format: data.format, + description: data.description, + coverImage: { + extraLarge: data.coverImage.extraLarge, + large: data.coverImage.large + } + } - setWasAddedToPlaylist(true) + await updateDoc(doc(db, 'users', user.uid), + { + bookmarks: !wasAddedToPlaylist ? arrayUnion(...[bookmarkData]) : arrayRemove(...[bookmarkData]) - } - else { - - await updateDoc(doc(db, 'users', user.uid), - { - bookmarks: arrayRemove({ - id: data.id, - title: { - romaji: data.title.romaji - }, - format: data.format, - description: data.description, - coverImage: { - extraLarge: data.coverImage.extraLarge, - large: data.coverImage.large - } - }) - } as unknown as FieldPath, - { merge: true } - ) - - setWasAddedToPlaylist(false) + } as unknown as FieldPath, + { merge: true } + ) - } + !wasAddedToPlaylist ? setWasAddedToPlaylist(true) : setWasAddedToPlaylist(false) setIsLoading(false) } diff --git a/app/components/NavButtons/index.tsx b/app/components/NavButtons/index.tsx index ba81497b..e0cff0ff 100644 --- a/app/components/NavButtons/index.tsx +++ b/app/components/NavButtons/index.tsx @@ -40,10 +40,9 @@ function NavButtons(props: PropsType) { return (
- {props.options.map((item, key: number) => ( - <> + {props.options.map((item) => ( +
diff --git a/app/components/UserLoginModal/index.tsx b/app/components/UserLoginModal/index.tsx index b471e921..7db2e22b 100644 --- a/app/components/UserLoginModal/index.tsx +++ b/app/components/UserLoginModal/index.tsx @@ -78,6 +78,7 @@ function UserModal({ onClick, auth, }: ModalTypes) { await setDoc(doc(collection(db, "users"), user.uid), { bookmarks: [], keepWatching: [], + notifications: [], comments: {}, episodesWatchedBySource: {}, videoSource: "gogoanime", diff --git a/app/layout/header/components/Notifications/component.module.css b/app/layout/header/components/Notifications/component.module.css new file mode 100644 index 00000000..b618ab02 --- /dev/null +++ b/app/layout/header/components/Notifications/component.module.css @@ -0,0 +1,203 @@ +#notification_container { + + position: relative; + height: 100%; + + display: flex; + align-items: center; + +} + +#notification_btn { + + margin: auto; + display: flex; + + padding: 10px; + + border-radius: 50%; + + background-color: transparent; + border: 0; + +} + +#notification_btn:hover, +#notification_btn[data-active=true] { + + background-color: var(--white-25); + +} + +#notification_btn svg { + + transform: scale(1.3); + +} + +span#notifications_badge { + + font-size: var(--font-size--small-2); + + position: absolute; + top: 20%; + right: -5%; + + padding: 3px 6px; + + border-radius: 50%; + background: var(--brand-color); + color: var(--white-100); + +} + +/* NOTIFICATIONS RESULTS */ +#results_container { + + box-shadow: 4px 4px 12px 0px var(--black-100); + + transform: translate(25%, 0%) !important; + + width: calc(96vw - 32px); + max-width: 400px; + + position: absolute; + right: 0; + top: 60px; + + background-color: var(--background); + border: 2px solid var(--brand-color); + border-radius: 8px; + + padding: 16px; + +} + +@media(min-width: 580px) { + + #results_container { + min-width: 290px; + transform: translate(-75%, 0%) !important; + left: 0; + } + +} + +@media(min-width: 1780px) { + + #results_container { + transform: translate(-50%, 0%) !important; + left: 50%; + } + +} + +#results_container>h4 { + + color: var(--white-100); + font-size: var(--font-size--h5); + + margin-bottom: 24px; + +} + +#results_container>ul { + + max-height: 500px; + overflow: auto; + + display: flex; + flex-direction: column; + gap: 8px 0; + +} + +#results_container>ul::-webkit-scrollbar { + width: 4px; +} + +#results_container>ul::-webkit-scrollbar-track { + box-shadow: inset 0 0 8px var(--white-05); + border-radius: 10px; +} + +#results_container>ul::-webkit-scrollbar-thumb { + background: var(--white-50); + border-radius: 6px; +} + +/* NOTIFICATION ITEM */ +.notification_item_container { + + background: var(--black-75); + border-radius: 8px; + + color: var(--white-100); + + display: grid; + grid-template-columns: minmax(70px, 100px) 1fr; + gap: 0 16px; + +} + +.notification_item_container .img_container { + + position: relative; + height: inherit; + + aspect-ratio: 46 / 65; + +} + +.notification_item_container .notification_item_info { + + margin: 6px 8px; + +} + +.notification_item_container h5 { + + + font-weight: 500; + + font-size: var(--font-size--p); + +} + +.notification_item_container small { + + font-weight: 300; + + margin-top: 4px; + + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + overflow: hidden; +} + +.notification_item_container p { + + margin: 16px 0; + + font-size: var(--font-size--small-1); + +} + +.notification_item_container a { + + font-weight: 500; + + text-align: center; + + border-radius: 6px; + + background: var(--brand-color); + + width: 100%; + + padding: 8px 16px; + + font-size: var(--font-size--small-1); + +} \ No newline at end of file diff --git a/app/layout/header/components/Notifications/index.tsx b/app/layout/header/components/Notifications/index.tsx new file mode 100644 index 00000000..868f23b5 --- /dev/null +++ b/app/layout/header/components/Notifications/index.tsx @@ -0,0 +1,208 @@ +"use client" +import React, { useEffect, useState } from 'react' +import BellFillSvg from '@/public/assets/bell-fill.svg' +import BellSvg from '@/public/assets/bell.svg' +import { getAuth } from 'firebase/auth' +import { useAuthState } from 'react-firebase-hooks/auth' +import { doc, getDoc, getFirestore } from 'firebase/firestore' +import { initFirebase } from '@/app/firebaseApp' +import styles from "./component.module.css" +import { AnimatePresence, motion } from 'framer-motion' +import Image from 'next/image' +import Link from 'next/link' +import { convertFromUnix } from '@/app/lib/formatDateUnix' + +function NotificationsComponent() { + + const db = getFirestore(initFirebase()) + const auth = getAuth() + const [user] = useAuthState(auth) + + const [notifications, setNotifications] = useState([]) + const [hasNewNotifications, setHasNewNotifications] = useState(false) + const [menuOpen, setMenuOpen] = useState(false) + + // check if notifications already is setted on local storage + async function checkNotificationsStored() { + + if (!user) return + + let notificationsStored: NotificationFirebase[] = [] + + if (localStorage.getItem('notifications') == undefined) { + + notificationsStored = await getDoc(doc(db, "users", user!.uid)).then( + (res) => res.data()?.notifications || [] + ) + + localStorage.setItem('notificationsVisualized', "true") + localStorage.setItem('notifications', JSON.stringify(notificationsStored)) + localStorage.setItem('notificationsLastUpdate', `${(new Date().getTime() / 1000).toFixed(0)}`) + + } + else { + + const dateNow = Number((new Date().getTime() / 1000).toFixed(0)) + const dateLastUpdate = Number(Number(localStorage.getItem('notificationsLastUpdate')) + 1800) || 0 + + // compare last Update with additional 30 minutes with the current time. If TRUE, fetchs Notifications again + if (dateNow >= dateLastUpdate) { + + notificationsStored = await getDoc(doc(db, "users", user!.uid)).then( + (res) => res.data()?.notifications || [] + ).then( + (res) => res.sort((a: NotificationFirebase, b: NotificationFirebase) => a.title.romaji.localeCompare(b.title.romaji)) + ) + + localStorage.setItem('notificationsVisualized', "true") + localStorage.setItem('notifications', JSON.stringify(notificationsStored)) + localStorage.setItem('notificationsLastUpdate', `${(new Date().getTime() / 1000).toFixed(0)}`) + + } + + if (notificationsStored.length == 0) notificationsStored = JSON.parse(localStorage.getItem('notifications')!) || [] + + } + + showNotificationsByConditions(notificationsStored) + + } + + // check if notifications should be shown to the user, COMPARING the Curr Date with the Date of Release + function showNotificationsByConditions(notificationsData: NotificationFirebase[]) { + + if (notificationsData.length == 0) return + + const currDateInUnix = parseInt((new Date().getTime() / 1000).toFixed(0)) + + let notificationsToBeShown: NotificationFirebase[] = [] + + notificationsData.map((item) => { + currDateInUnix >= item.nextReleaseDate ? notificationsToBeShown.push(item) : undefined + }) + + if (notificationsToBeShown.length > 0) { + localStorage.setItem('notificationsVisualized', "false") + setHasNewNotifications(true) + } + + setNotifications(notificationsToBeShown) + + } + + function openNotificationsMenu() { + + setMenuOpen(!menuOpen) + + localStorage.setItem('notificationsVisualized', "true") + + setHasNewNotifications(false) + + } + + useEffect(() => { + + if (localStorage.getItem('notificationsVisualized')) { + if (localStorage.getItem('notificationsVisualized') == "true") setHasNewNotifications(false) + } + else { + localStorage.setItem('notificationsVisualized', "true") + } + + checkNotificationsStored() + + }, [user]) + + if (user) { + return ( + + + + + {hasNewNotifications && ( + {notifications.length} + )} + + + {menuOpen && ( + + +

Latest Notifications

+ + {notifications.length == 0 ? + ( +
+

No New Notifications

+
+ ) : ( +
    + {notifications.map((item, key) => ( + +
  • + +
    + {item.title.romaji} +
    + +
    + +
    Episode {item.episodeNumber} Released!
    + + {item.title.romaji} + + {item.lastEpisode && ( +

    Watch the Season Finale!

    + )} + +

    Released on {convertFromUnix(item.nextReleaseDate)}

    + + SEE MORE + +
    + +
  • + + ))} +
+ ) + } +
+ )} +
+
+
+ ) + } +} + +export default NotificationsComponent \ No newline at end of file diff --git a/app/layout/header/components/SearchContainer/component.module.css b/app/layout/header/components/SearchContainer/component.module.css index 28847d3f..3570f75f 100644 --- a/app/layout/header/components/SearchContainer/component.module.css +++ b/app/layout/header/components/SearchContainer/component.module.css @@ -2,7 +2,7 @@ display: flex; } -@media(min-width: 550px) { +@media(min-width: 769px) { #search_container #form_mobile_search { display: none; @@ -188,7 +188,7 @@ background: linear-gradient(var(--background) 70%, var(--background)); } -@media(min-width: 550px) { +@media(min-width: 769px) { #search_results_container { top: 60px; @@ -216,6 +216,7 @@ display: block; margin-top: 16px; + margin-bottom: 16px; margin-right: 16px; margin-left: auto; @@ -237,8 +238,6 @@ #search_results_container>ul { - padding: 16px 0; - height: calc(100vh - 156px); overflow: auto; @@ -266,14 +265,19 @@ #search_container button#btn_open_search_form_mobile { + transition: all ease-in-out 100ms; + + padding: 10px; + background-color: transparent; border: 0; + border-radius: 50%; fill: var(--white-100); } -@media(min-width: 550px) { +@media(min-width: 769px) { #search_container button#btn_open_search_form_mobile { display: none; @@ -281,7 +285,13 @@ } } -@media(max-width: 549px) { +#search_container button#btn_open_search_form_mobile[data-active=true] { + + background-color: var(--white-25); + +} + +@media(max-width: 768px) { #search_container #form_search { display: none; @@ -293,4 +303,6 @@ display: flex; + transform: scale(1.3); + } \ No newline at end of file diff --git a/app/layout/header/components/SearchContainer/index.tsx b/app/layout/header/components/SearchContainer/index.tsx index 5ddf41fe..470e2e57 100644 --- a/app/layout/header/components/SearchContainer/index.tsx +++ b/app/layout/header/components/SearchContainer/index.tsx @@ -93,6 +93,7 @@ function SearchContainer() { id={styles.btn_open_search_form_mobile} onClick={() => toggleSearchBarMobile(!isMobileSearchBarOpen)} aria-controls={styles.input_search_bar} + data-active={isMobileSearchBarOpen} aria-label={isMobileSearchBarOpen ? 'Click to Hide Search Bar' : 'Click to Show Search Bar'} className={styles.heading_btn} > @@ -160,25 +161,35 @@ function SearchContainer() { {/* SEARCH RESULTS */} - {searchResults != null && ( -
- - - -
    - {searchResults.map((item: ApiDefaultResult, key: number) => ( - toggleSearchBarMobile(false)} - /> - ))} -
+ + {searchResults != null && ( + -
- )} + + +
    + {searchResults.map((item: ApiDefaultResult, key: number) => ( + toggleSearchBarMobile(false)} + /> + ))} +
+ + + )} + ) } diff --git a/app/layout/header/components/UserSettingsModal/index.tsx b/app/layout/header/components/UserSettingsModal/index.tsx index 170ee3da..00f6ace8 100644 --- a/app/layout/header/components/UserSettingsModal/index.tsx +++ b/app/layout/header/components/UserSettingsModal/index.tsx @@ -172,7 +172,7 @@ function UserSettingsModal({ onClick, auth, newUser }: SettingsTypes) { await updateProfile(user, { photoURL: newImgProfileSelected || user.photoURL, - displayName: form.username.value || user.displayName + displayName: user.isAnonymous ? user.displayName : form.username.value || user.displayName }) } diff --git a/app/layout/header/components/UserSideMenu/component.module.css b/app/layout/header/components/UserSideMenu/component.module.css index 022fe285..bc4b9f65 100644 --- a/app/layout/header/components/UserSideMenu/component.module.css +++ b/app/layout/header/components/UserSideMenu/component.module.css @@ -7,7 +7,7 @@ gap: 0 4px; background-color: var(--background); - padding: 8px; + height: 32px; border: none; @@ -72,7 +72,7 @@ } -#user_container #user_btn.active svg{ +#user_container #user_btn.active svg { fill: var(--brand-color); diff --git a/app/layout/header/headerComponent.module.css b/app/layout/header/headerComponent.module.css index b4896fe2..905af0e1 100644 --- a/app/layout/header/headerComponent.module.css +++ b/app/layout/header/headerComponent.module.css @@ -89,9 +89,10 @@ header#background button svg { #user_and_search_container { position: static; + gap: 8px; } -@media(min-width: 576px) { +@media(min-width: 768px) { #user_and_search_container { position: relative; } @@ -101,8 +102,6 @@ header#background button svg { width: 100%; - /* margin: auto 0; */ - padding-left: 64px; } diff --git a/app/layout/header/index.tsx b/app/layout/header/index.tsx index 20076215..2e5dd9ae 100644 --- a/app/layout/header/index.tsx +++ b/app/layout/header/index.tsx @@ -9,6 +9,7 @@ import UserSideMenu from './components/UserSideMenu' import NavListMenu from './components/NavListMenu' import SearchContainer from './components/SearchContainer' import NewsNavListHover from './components/NewsNavListHover' +import NotificationsComponent from './components/Notifications' function Header() { @@ -55,11 +56,14 @@ function Header() { -
+
{/* SEARCH MOBILE AND DESKTOP, SEARCH RESULTS CONTAINER INSIDE*/} + {/* NOTIFICATIONS */} + + {/* USER MENU -- RIGHT SIDE OF SCREEN */} diff --git a/app/lib/fetchAnimeOnApi.ts b/app/lib/fetchAnimeOnApi.ts index b7fc4ee7..5f53f442 100644 --- a/app/lib/fetchAnimeOnApi.ts +++ b/app/lib/fetchAnimeOnApi.ts @@ -10,13 +10,15 @@ export async function fetchWithGoGoAnime(textSearch: string, only?: "episodes") const regexMediaTitle = regexOnlyAlphabetic(checkApiMisspellingMedias(textSearch)).toLowerCase() - let mediaSearched = await gogoanime.getInfoFromThisMedia(textSearch, "anime") as MediaInfo + let mediaSearched = await gogoanime.getInfoFromThisMedia(regexMediaTitle, "anime") as MediaInfo let searchResultsForMedia let filterBestResult + // if no results were found by matching the id, search the title if (!mediaSearched) { searchResultsForMedia = await gogoanime.searchMedia(regexMediaTitle, "anime") as MediaSearchResult[] + // filter to closest title name to the query filterBestResult = searchResultsForMedia.filter(item => regexOnlyAlphabetic(item.title).toLowerCase().indexOf(regexMediaTitle) !== -1 ) @@ -26,6 +28,7 @@ export async function fetchWithGoGoAnime(textSearch: string, only?: "episodes") if (!mediaSearched) return null + // if only EPISODES is requested if (only == "episodes") { let episodes: any[] = [] @@ -46,35 +49,63 @@ export async function fetchWithGoGoAnime(textSearch: string, only?: "episodes") } -export async function fetchWithAniWatch(textSearch: string, only?: "episodes", format?: string, mediaTotalEpisodes?: number) { +export async function fetchWithAniWatch(textSearch: string, only?: "episodes" | "search_list", format?: string, mediaTotalEpisodes?: number) { const regexMediaTitle = regexOnlyAlphabetic(checkApiMisspellingMedias(textSearch)).toLowerCase() let searchResultsForMedia = await aniwatch.searchMedia(regexMediaTitle).then((res) => res!.animes) as MediaInfoAniwatch[] + // filter the same format if (format) { searchResultsForMedia = searchResultsForMedia.filter(item => item.type.toLowerCase() == format.toLowerCase()) } - let closestResult: MediaInfoAniwatch | undefined + let closestResult: MediaInfoAniwatch[] | undefined + // filter to item which has the same media name + closestResult = searchResultsForMedia.filter( + (item) => regexOnlyAlphabetic(item.name).toLowerCase() == regexMediaTitle + ) + + // if is only SEARCH LIST is requested + if (only == "search_list") { + if (closestResult.length != 0) { + return closestResult + } + } + else { + closestResult = closestResult.length == 0 ? searchResultsForMedia : closestResult + } + + // filter which has the same ammount of episodes if (mediaTotalEpisodes) { - closestResult = searchResultsForMedia.find( + const filter = closestResult.filter( (item) => item.episodes.sub == mediaTotalEpisodes ) + + if (filter.length != 0) closestResult = filter + } + + // if is only SEARCH LIST is requested + if (only == "search_list") { + + return closestResult.length > 0 ? closestResult : searchResultsForMedia + } + // filter closest title result if (!closestResult) { - closestResult = searchResultsForMedia.find((item) => + closestResult = searchResultsForMedia.filter((item) => regexOnlyAlphabetic(item.name).toLowerCase().includes(regexMediaTitle) || searchResultsForMedia[0] ) } + // if only EPISODES is requested if (only == "episodes") { if (!closestResult) return null - const res = await aniwatch.getEpisodes(closestResult.id) as EpisodesFetchedAnimeWatch + const res = await aniwatch.getEpisodes(closestResult[0].id) as EpisodesFetchedAnimeWatch return res?.episodes?.length == 0 ? null : res.episodes } diff --git a/app/media/[id]/components/AddToNotifications/component.module.css b/app/media/[id]/components/AddToNotifications/component.module.css new file mode 100644 index 00000000..e6035281 --- /dev/null +++ b/app/media/[id]/components/AddToNotifications/component.module.css @@ -0,0 +1,43 @@ +#container { + + transition: all ease-in-out 200ms; + + height: inherit; + + text-align: center; + + font-weight: 600; + + border: 1px solid var(--white-100) !important; + + background-color: transparent !important; + +} + +#container svg { + + transform: scale(1.4); + +} + +#container[data-added=false] svg { + + fill: var(--white-100); + +} + +#container[disabled] { + + background-color: var(--white-100); + +} + +#container[data-added=true] { + + gap: 0 8px; + + background-color: var(--white-100) !important; + color: var(--black-100) !important; + + border-color: var(--black-100) !important; +} \ No newline at end of file diff --git a/app/media/[id]/components/AddToNotifications/index.tsx b/app/media/[id]/components/AddToNotifications/index.tsx new file mode 100644 index 00000000..5ebb7fdd --- /dev/null +++ b/app/media/[id]/components/AddToNotifications/index.tsx @@ -0,0 +1,150 @@ +"use client" +import React, { useEffect, useState } from 'react' +import styles from "./component.module.css" +import LoadingSvg from "@/public/assets/ripple-1s-200px.svg" +import BellFillSvg from "@/public/assets/bell-fill.svg" +import BellSvg from "@/public/assets/bell-slash.svg" +import { + getFirestore, doc, + updateDoc, arrayUnion, + arrayRemove, getDoc, + FieldPath, setDoc, + DocumentSnapshot, DocumentData +} from 'firebase/firestore'; +import { initFirebase } from '@/app/firebaseApp' +import { getAuth } from 'firebase/auth' +import { ApiDefaultResult } from '@/app/ts/interfaces/apiAnilistDataInterface' +import { useAuthState } from 'react-firebase-hooks/auth' +import UserModal from '@/app/components/UserLoginModal'; +import { AnimatePresence, motion } from 'framer-motion'; + +function AddToNotificationsList({ data }: { data: ApiDefaultResult }) { + + const [isLoading, setIsLoading] = useState(false) + const [wasAddedToNotifications, setWasAddedToNotifications] = useState(false) + + const [isUserModalOpen, setIsUserModalOpen] = useState(false) + + const auth = getAuth() + + const [user, loading] = useAuthState(auth) + + const db = getFirestore(initFirebase()); + + // WHEN BUTTON IS CLICKED, RUN FUNCTION TO ADD OR REMOVE MEDIA FROM FIRESTORE + async function addThisMedia() { + + if (!user) { + + // opens user login modal + setIsUserModalOpen(true) + return + } + + setIsLoading(true) + + const notificationData = { + mediaId: data.id, + title: { + romaji: data.title.romaji + }, + isComplete: data.status, + nextReleaseDate: data.nextAiringEpisode?.airingAt, + episodeNumber: data.nextAiringEpisode?.episode, + lastEpisode: data.nextAiringEpisode?.episode == data.episodes, + coverImage: { + extraLarge: data.coverImage.extraLarge, + large: data.coverImage.large + } + } + + await updateDoc(doc(db, 'users', user.uid), + { + notifications: !wasAddedToNotifications ? arrayUnion(...[notificationData]) : arrayRemove(...[notificationData]) + + } as unknown as FieldPath, + { merge: true } + ) + + !wasAddedToNotifications ? setWasAddedToNotifications(true) : setWasAddedToNotifications(false) + + setIsLoading(false) + } + + // IF MEDIA ID MATCHS ANY RESULT ON DB, IT SETS THIS COMPONENT BUTTON AS ACTIVE + async function isMediaOnDB() { + + if (!user) return setWasAddedToNotifications(false) + + let userDoc: DocumentSnapshot = await getDoc(doc(db, 'users', user.uid)) + + // IF USER HAS NO DOC ON FIRESTORE, IT CREATES ONE + if (userDoc.exists() == false) { + + userDoc = await setDoc(doc(db, 'users', user.uid), {}) as unknown as DocumentSnapshot + + return + } + + const isMediaIdOnDoc = userDoc.get("notifications")?.find((item: NotificationFirebase) => item.mediaId == data.id) + + if (isMediaIdOnDoc) { + setWasAddedToNotifications(true) + } + } + + useEffect(() => { + + if (!user || loading) { + return + } else { + setIsUserModalOpen(false) + isMediaOnDB() + } + + }, [user]) + + if (data.nextAiringEpisode?.airingAt) { + return ( + <> + + {isUserModalOpen && ( + setIsUserModalOpen(false)} + auth={auth} + /> + )} + + + addThisMedia()} + disabled={isLoading} + data-added={wasAddedToNotifications} + title={wasAddedToNotifications ? + `Remove ${data.title && data.title?.romaji} From Notifications` + : + `Get Notified When ${data.title && data.title?.romaji} Get a New Episode` + } + > + {isLoading ? + + : + (wasAddedToNotifications ? + + : + + ) + } + + + + ) + } +} + +export default AddToNotificationsList \ No newline at end of file diff --git a/app/media/[id]/components/AnimeEpisodesContainer/index.tsx b/app/media/[id]/components/AnimeEpisodesContainer/index.tsx index 54b4e4ed..1b6eb29e 100644 --- a/app/media/[id]/components/AnimeEpisodesContainer/index.tsx +++ b/app/media/[id]/components/AnimeEpisodesContainer/index.tsx @@ -120,10 +120,10 @@ function EpisodesContainer(props: EpisodesContainerTypes) { break - // get data from GOGOANIME as default - case "gogoanime": + case "gogoanime": // get data from GOGOANIME as default setEpisodeSource(chooseSource) + mediaEpisodes = await fetchWithGoGoAnime(query, "episodes") as MediaEpisodes[] if (mediaEpisodes == null) { @@ -142,16 +142,13 @@ function EpisodesContainer(props: EpisodesContainerTypes) { break - // get data from ANIWATCH - case "aniwatch": + case "aniwatch": // get data from ANIWATCH setEpisodeSource(chooseSource) - const searchResultsForMedia = await aniwatch.searchMedia(regexOnlyAlphabetic(query)) as MediaInfoFetchedAnimeWatch + const searchResultsForMedia = await fetchWithAniWatch(query, "search_list", props.mediaFormat, dataImdbMapped.length) as MediaInfoAniwatch[] - setMediaResultsInfoArray(searchResultsForMedia.animes.filter(item => item.type.toLowerCase() == props.mediaFormat.toLowerCase())) - - setEpisodeSource(chooseSource) + setMediaResultsInfoArray(searchResultsForMedia) mediaEpisodes = await fetchWithAniWatch(query, "episodes", props.mediaFormat, dataImdbMapped.length) as EpisodesFetchedAnimeWatch["episodes"] @@ -164,8 +161,7 @@ function EpisodesContainer(props: EpisodesContainerTypes) { break - // get data from VIDSRC - default: + case "vidsrc": // get data from VIDSRC // VIDSRC has no episodes INFO // so it will use IMDB info data, and redirect it to a url with episode and season number. @@ -190,6 +186,12 @@ function EpisodesContainer(props: EpisodesContainerTypes) { break + default: + + console.log("need to work on it") + + break + } } diff --git a/app/media/[id]/page.module.css b/app/media/[id]/page.module.css index caa001ec..dbab27f8 100644 --- a/app/media/[id]/page.module.css +++ b/app/media/[id]/page.module.css @@ -110,7 +110,15 @@ h2.heading_style { } -#media_title_container #add_playlist_container>button { +#media_title_container #btns_actions_container { + + display: flex; + flex-direction: row; + gap: 8px; + +} + +#media_title_container #btns_actions_container>button { box-shadow: 3px 3px 4px var(--black-75); @@ -127,7 +135,7 @@ h2.heading_style { } -#media_title_container #add_playlist_container>button[data-added=true] { +#media_title_container #btns_actions_container>button[data-added=true] { background-color: var(--brand-color); @@ -162,12 +170,11 @@ h2.heading_style { #media_title_container #genres_and_type_container { gap: 16px; - /* flex-wrap: wrap; */ justify-content: space-between; } -#media_title_container #genres_and_type_container>div:not(#add_playlist_container) { +#media_title_container #genres_and_type_container>div:not(#btns_actions_container) { margin: auto 0; diff --git a/app/media/[id]/page.tsx b/app/media/[id]/page.tsx index 91c17e81..688737a1 100644 --- a/app/media/[id]/page.tsx +++ b/app/media/[id]/page.tsx @@ -25,6 +25,7 @@ import { convertFromUnix } from '@/app/lib/formatDateUnix' import CommentSectionContainer from '../../components/CommentSectionContainer' import { getMediaInfo } from '@/api/imdb' import { ImdbEpisode, ImdbMediaInfo } from '@/app/ts/interfaces/apiImdbInterface' +import AddToNotificationsList from './components/AddToNotifications' export const revalidate = 43200 // revalidate cached data every 12 hours @@ -133,13 +134,17 @@ async function MediaPage({ params }: { params: { id: number } }) { )}
-
+
+ + + , ] } /> +
@@ -175,7 +180,15 @@ async function MediaPage({ params }: { params: { id: number } }) {

STATUS

-

{mediaData.status == "NOT_YET_RELEASED" ? "TO BE RELEASED" : mediaData.status || "Not Available"}

+

+ { + mediaData.status == "NOT_YET_RELEASED" ? "TO BE RELEASED" + : + mediaData.status == "FINISHED" ? "COMPLETE" + : + mediaData.status || "Not Available" + } +

)} diff --git a/app/ts/interfaces/apiAnilistDataInterface.d.ts b/app/ts/interfaces/apiAnilistDataInterface.d.ts index 191e6a39..f619cae4 100644 --- a/app/ts/interfaces/apiAnilistDataInterface.d.ts +++ b/app/ts/interfaces/apiAnilistDataInterface.d.ts @@ -13,6 +13,11 @@ export interface ApiDefaultResult { month: number, day: number, }, + nextAiringEpisode: { + airingAt: number, + episode: number + }, + status: string | null, description: string, episodes: number, duration: number, @@ -73,14 +78,9 @@ export interface EpisodesType { export interface ApiMediaResults extends ApiDefaultResult { - nextAiringEpisode: { - airingAt: number, - episode: number - }, hashtag: string, favourites: number, source: string, - status: string | null, duration: number | null, volumes: number | null, chapters: number | null, diff --git a/app/ts/interfaces/firestoreDataInterface.d.ts b/app/ts/interfaces/firestoreDataInterface.d.ts index 724b2c67..936c49af 100644 --- a/app/ts/interfaces/firestoreDataInterface.d.ts +++ b/app/ts/interfaces/firestoreDataInterface.d.ts @@ -49,4 +49,21 @@ interface KeepWatchingItem { }, id: number +} + +interface NotificationFirebase { + + mediaId: number, + title: { + romaji: string + }, + isComplete: boolean, + nextReleaseDate: number, + lastEpisode: boolean, + episodeNumber: number, + coverImage: { + extraLarge: string, + large: string + } + } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 48a6bac4..ed8d6a3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aniproject", - "version": "2.5.7", + "version": "2.5.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aniproject", - "version": "2.5.7", + "version": "2.5.8", "dependencies": { "@vidstack/react": "^1.10.9", "axios": "^1.6.6", diff --git a/package.json b/package.json index 94b0c258..af066a44 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aniproject", - "version": "2.5.7", + "version": "2.5.8", "private": true, "scripts": { "dev": "next dev", diff --git a/public/assets/bell-fill.svg b/public/assets/bell-fill.svg new file mode 100644 index 00000000..a537c3a0 --- /dev/null +++ b/public/assets/bell-fill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/assets/bell-slash.svg b/public/assets/bell-slash.svg new file mode 100644 index 00000000..7817e2b4 --- /dev/null +++ b/public/assets/bell-slash.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/assets/bell.svg b/public/assets/bell.svg new file mode 100644 index 00000000..a71eba30 --- /dev/null +++ b/public/assets/bell.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file