aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.env.example5
-rw-r--r--.eslintrc.json5
-rw-r--r--README.md1
-rw-r--r--components/anime/episode.js1
-rw-r--r--components/anime/infoDetails.js5
-rw-r--r--components/anime/viewMode/listMode.js18
-rw-r--r--components/anime/viewMode/thumbnailDetail.js4
-rw-r--r--components/anime/viewMode/thumbnailOnly.js4
-rw-r--r--components/anime/watch/primary/details.js18
-rw-r--r--components/anime/watch/primarySide.js20
-rw-r--r--components/anime/watch/secondarySide.js4
-rw-r--r--components/home/content.js126
-rw-r--r--components/home/schedule.js4
-rw-r--r--components/manga/info/topSection.js2
-rw-r--r--components/manga/rightBar.js1
-rw-r--r--components/videoPlayer.js23
-rw-r--r--lib/anilist/useAnilist.js200
-rw-r--r--next.config.js2
-rw-r--r--package-lock.json4
-rw-r--r--package.json2
-rw-r--r--pages/api/consumet/episode/[id].js5
-rw-r--r--pages/api/user/profile.js16
-rw-r--r--pages/api/user/update/episode.js30
-rw-r--r--pages/en/anime/[...id].js6
-rw-r--r--pages/en/anime/recently-watched.js98
-rw-r--r--pages/en/anime/watch/[...info].js6
-rw-r--r--pages/en/dmca.js5
-rw-r--r--pages/en/index.js36
-rw-r--r--pages/en/manga/read/[...params].js1
-rw-r--r--pages/index.js21
-rw-r--r--prisma/migrations/20230810051657_ondelete_cascade/migration.sql5
-rw-r--r--prisma/schema.prisma2
-rw-r--r--prisma/user.js48
-rw-r--r--public/preview.pngbin0 -> 871269 bytes
34 files changed, 528 insertions, 200 deletions
diff --git a/.env.example b/.env.example
index a79878a..2f85f93 100644
--- a/.env.example
+++ b/.env.example
@@ -11,4 +11,7 @@ NEXTAUTH_URL="for development use http://localhost:3000/ and for production use
PROXY_URI="I recommend you to use this cors-anywhere as a proxy https://github.com/Rob--W/cors-anywhere follow the instruction on how to use it there. Skip this if you only use gogoanime as a source"
API_URI="host your own API from this repo https://github.com/consumet/api.consumet.org. Don't put / at the end of the url."
API_KEY="this API key is used for schedules and manga page. get the key from https://anify.tv/discord"
-DISQUS_SHORTNAME='put your disqus shortname here. (optional)' \ No newline at end of file
+DISQUS_SHORTNAME='put your disqus shortname here. (optional)'
+
+## Prisma
+DATABASE_URL="Your postgresql connection url" \ No newline at end of file
diff --git a/.eslintrc.json b/.eslintrc.json
index dfa8f73..dbda85f 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,4 +1,7 @@
{
"extends": "next/core-web-vitals",
- "rules": { "react/no-unescaped-entities": 0 }
+ "rules": {
+ "react/no-unescaped-entities": 0,
+ "react/no-unknown-property": ["error", { "ignore": ["css"] }]
+ }
}
diff --git a/README.md b/README.md
index f21ccfc..4f964a5 100644
--- a/README.md
+++ b/README.md
@@ -95,6 +95,7 @@ npm install
3. Generate Prisma :
```bash
+npx prisma migrate dev
npx prisma generate
```
diff --git a/components/anime/episode.js b/components/anime/episode.js
index c889c25..5d3451b 100644
--- a/components/anime/episode.js
+++ b/components/anime/episode.js
@@ -246,6 +246,7 @@ export default function AnimeEpisode({ info, progress }) {
info={info}
episode={episode}
index={index}
+ artStorage={artStorage}
providerId={providerId}
progress={progress}
dub={isDub}
diff --git a/components/anime/infoDetails.js b/components/anime/infoDetails.js
index 0cf233c..814e49b 100644
--- a/components/anime/infoDetails.js
+++ b/components/anime/infoDetails.js
@@ -45,7 +45,10 @@ export default function DesktopDetails({
<div className="hidden lg:flex w-full flex-col gap-5 h-[250px]">
<div className="flex flex-col gap-2">
- <h1 className=" font-inter font-bold text-[36px] text-white line-clamp-1">
+ <h1
+ className="title font-inter font-bold text-[36px] text-white line-clamp-1"
+ title={info?.title?.romaji || info?.title?.english}
+ >
{info ? (
info?.title?.romaji || info?.title?.english
) : (
diff --git a/components/anime/viewMode/listMode.js b/components/anime/viewMode/listMode.js
index 2016262..f3bcf05 100644
--- a/components/anime/viewMode/listMode.js
+++ b/components/anime/viewMode/listMode.js
@@ -4,10 +4,16 @@ export default function ListMode({
info,
episode,
index,
+ artStorage,
providerId,
progress,
dub,
}) {
+ const time = artStorage?.[episode?.id]?.timeWatched;
+ const duration = artStorage?.[episode?.id]?.duration;
+ let prog = (time / duration) * 100;
+ if (prog > 90) prog = 100;
+
return (
<div key={episode.number} className="flex flex-col gap-3 px-2">
<Link
@@ -15,7 +21,11 @@ export default function ListMode({
episode.id
)}&num=${episode.number}${dub ? `&dub=${dub}` : ""}`}
className={`text-start text-sm lg:text-lg ${
- progress && episode.number <= progress
+ progress
+ ? progress && episode.number <= progress
+ ? "text-[#5f5f5f]"
+ : "text-white"
+ : prog === 100
? "text-[#5f5f5f]"
: "text-white"
}`}
@@ -24,7 +34,11 @@ export default function ListMode({
{episode.title && (
<p
className={`text-xs lg:text-sm ${
- progress && episode.number <= progress
+ progress
+ ? progress && episode.number <= progress
+ ? "text-[#5f5f5f]"
+ : "text-[#b1b1b1]"
+ : prog === 100
? "text-[#5f5f5f]"
: "text-[#b1b1b1]"
} italic`}
diff --git a/components/anime/viewMode/thumbnailDetail.js b/components/anime/viewMode/thumbnailDetail.js
index a085bc7..6efeb77 100644
--- a/components/anime/viewMode/thumbnailDetail.js
+++ b/components/anime/viewMode/thumbnailDetail.js
@@ -10,7 +10,7 @@ export default function ThumbnailDetail({
progress,
dub,
}) {
- const time = artStorage?.[epi?.id]?.time;
+ const time = artStorage?.[epi?.id]?.timeWatched;
const duration = artStorage?.[epi?.id]?.duration;
let prog = (time / duration) * 100;
if (prog > 90) prog = 100;
@@ -33,7 +33,7 @@ export default function ThumbnailDetail({
className="object-cover z-30 rounded-lg h-[110px] lg:h-[160px] brightness-[65%]"
/>
<span
- className={`absolute bottom-0 left-0 h-[3px] bg-red-700`}
+ className={`absolute bottom-0 left-0 h-[2px] bg-red-700`}
style={{
width:
progress && artStorage && epi?.number <= progress
diff --git a/components/anime/viewMode/thumbnailOnly.js b/components/anime/viewMode/thumbnailOnly.js
index 6063dfc..99f02bd 100644
--- a/components/anime/viewMode/thumbnailOnly.js
+++ b/components/anime/viewMode/thumbnailOnly.js
@@ -9,7 +9,7 @@ export default function ThumbnailOnly({
progress,
dub,
}) {
- const time = artStorage?.[episode?.id]?.time;
+ const time = artStorage?.[episode?.id]?.timeWatched;
const duration = artStorage?.[episode?.id]?.duration;
let prog = (time / duration) * 100;
if (prog > 90) prog = 100;
@@ -25,7 +25,7 @@ export default function ThumbnailOnly({
Episode {episode?.number}
</span>
<span
- className={`absolute bottom-7 left-0 h-1 bg-red-600`}
+ className={`absolute bottom-7 left-0 h-[2px] bg-red-600`}
style={{
width:
progress && artStorage && episode?.number <= progress
diff --git a/components/anime/watch/primary/details.js b/components/anime/watch/primary/details.js
index 94c3360..f092879 100644
--- a/components/anime/watch/primary/details.js
+++ b/components/anime/watch/primary/details.js
@@ -8,6 +8,7 @@ export default function Details({
info,
session,
epiNumber,
+ description,
id,
onList,
setOnList,
@@ -48,7 +49,10 @@ export default function Details({
<Skeleton height={240} />
)}
</div>
- <div className="grid w-full pl-5 gap-3 h-[240px]">
+ <div
+ className="grid w-full pl-5 gap-3 h-[240px]"
+ data-episode={info?.episodes || "0"}
+ >
<div className="grid grid-cols-2 gap-1 items-center">
<h2 className="text-sm font-light font-roboto text-[#878787]">
Studios
@@ -93,11 +97,15 @@ export default function Details({
<div className="grid grid-flow-dense grid-cols-2 gap-2 h-full w-full">
{info ? (
<>
- <div className="line-clamp-3">{info.title?.romaji || ""}</div>
- <div className="line-clamp-3">
+ <div className="title-rm line-clamp-3">
+ {info.title?.romaji || ""}
+ </div>
+ <div className="title-en line-clamp-3">
{info.title?.english || ""}
</div>
- <div className="line-clamp-3">{info.title?.native || ""}</div>
+ <div className="title-nt line-clamp-3">
+ {info.title?.native || ""}
+ </div>
</>
) : (
<Skeleton width={200} height={50} />
@@ -120,7 +128,7 @@ export default function Details({
<div className={`bg-secondary rounded-md mt-3 mx-3`}>
{info && (
<p
- dangerouslySetInnerHTML={{ __html: info?.description }}
+ dangerouslySetInnerHTML={{ __html: description }}
className={`p-5 text-sm font-light font-roboto text-[#e4e4e4] `}
/>
)}
diff --git a/components/anime/watch/primarySide.js b/components/anime/watch/primarySide.js
index c601795..b032fd6 100644
--- a/components/anime/watch/primarySide.js
+++ b/components/anime/watch/primarySide.js
@@ -27,6 +27,7 @@ export default function PrimarySide({
setOnList,
episodeList,
timeWatched,
+ dub,
}) {
const [episodeData, setEpisodeData] = useState();
const [open, setOpen] = useState(false);
@@ -148,6 +149,7 @@ export default function PrimarySide({
aniTitle={info.title?.romaji || info.title?.english}
track={navigation}
timeWatched={timeWatched}
+ dub={dub}
/>
)
) : (
@@ -162,13 +164,14 @@ export default function PrimarySide({
<Link
href={`/en/anime/${info.id}`}
className="hover:underline"
+ title={navigation?.playing?.title || info.title?.romaji}
>
{navigation?.playing?.title || info.title?.romaji}
</Link>
</h1>
- <h1 className="text-sm font-karla font-light">
+ <h3 className="text-sm font-karla font-light">
Episode {epiNumber}
- </h1>
+ </h3>
</div>
<div className="flex gap-4 items-center justify-end">
<div className="relative">
@@ -180,7 +183,11 @@ export default function PrimarySide({
(episode) => episode.number === parseInt(e.target.value)
);
router.push(
- `/en/anime/watch/${info.id}/${providerId}?id=${selectedEpisode.id}&num=${selectedEpisode.number}`
+ `/en/anime/watch/${info.id}/${providerId}?id=${
+ selectedEpisode.id
+ }&num=${selectedEpisode.number}${
+ dub ? `&dub=${dub}` : ""
+ }`
);
}}
>
@@ -199,7 +206,11 @@ export default function PrimarySide({
}relative group`}
onClick={() => {
router.push(
- `/en/anime/watch/${info.id}/${providerId}?id=${navigation?.next.id}&num=${navigation?.next.number}`
+ `/en/anime/watch/${info.id}/${providerId}?id=${
+ navigation?.next.id
+ }&num=${navigation?.next.number}${
+ dub ? `&dub=${dub}` : ""
+ }`
);
}}
>
@@ -229,6 +240,7 @@ export default function PrimarySide({
<Details
info={info}
session={session}
+ description={navigation?.playing?.description || info?.description}
epiNumber={epiNumber}
id={watchId}
onList={onList}
diff --git a/components/anime/watch/secondarySide.js b/components/anime/watch/secondarySide.js
index e3f0224..5d9b8f9 100644
--- a/components/anime/watch/secondarySide.js
+++ b/components/anime/watch/secondarySide.js
@@ -18,7 +18,7 @@ export default function SecondarySide({
{episode && episode.length > 0 ? (
episode.some((item) => item.title && item.description) > 0 ? (
episode.map((item) => {
- const time = artStorage?.[item.id]?.time;
+ const time = artStorage?.[item.id]?.timeWatched;
const duration = artStorage?.[item.id]?.duration;
let prog = (time / duration) * 100;
if (prog > 90) prog = 100;
@@ -50,7 +50,7 @@ export default function SecondarySide({
}`}
/>
<span
- className={`absolute bottom-0 left-0 h-[3px] bg-red-700`}
+ className={`absolute bottom-0 left-0 h-[2px] bg-red-700`}
style={{
width:
progress && artStorage && item?.number <= progress
diff --git a/components/home/content.js b/components/home/content.js
index f13c7a8..70f0e3f 100644
--- a/components/home/content.js
+++ b/components/home/content.js
@@ -5,6 +5,7 @@ import { MdChevronRight } from "react-icons/md";
import {
ChevronRightIcon,
ArrowRightCircleIcon,
+ XMarkIcon,
} from "@heroicons/react/24/outline";
import { parseCookies } from "nookies";
@@ -12,6 +13,7 @@ import { parseCookies } from "nookies";
import { ChevronLeftIcon } from "@heroicons/react/20/solid";
import { ExclamationCircleIcon, PlayIcon } from "@heroicons/react/24/solid";
import { useRouter } from "next/router";
+import { toast } from "react-toastify";
export default function Content({
ids,
@@ -20,6 +22,7 @@ export default function Content({
userData,
og,
userName,
+ setRemoved,
}) {
const router = useRouter();
@@ -139,10 +142,64 @@ export default function Content({
}
};
+ const removeItem = async (id) => {
+ if (userName) {
+ // remove from database
+ const res = await fetch(`/api/user/update/episode`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: userName,
+ id: id,
+ }),
+ });
+ const data = await res.json();
+
+ // remove from local storage
+ const artplayerSettings =
+ JSON.parse(localStorage.getItem("artplayer_settings")) || {};
+ if (artplayerSettings[id]) {
+ delete artplayerSettings[id];
+ localStorage.setItem(
+ "artplayer_settings",
+ JSON.stringify(artplayerSettings)
+ );
+ }
+
+ // update client
+ setRemoved(id);
+
+ if (data?.message === "Episode deleted") {
+ toast.success("Episode removed from history", {
+ position: "bottom-right",
+ autoClose: 5000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ draggable: true,
+ theme: "dark",
+ });
+ }
+ } else {
+ const artplayerSettings =
+ JSON.parse(localStorage.getItem("artplayer_settings")) || {};
+ if (artplayerSettings[id]) {
+ delete artplayerSettings[id];
+ localStorage.setItem(
+ "artplayer_settings",
+ JSON.stringify(artplayerSettings)
+ );
+ }
+
+ setRemoved(id);
+ }
+ };
+
return (
<div>
<div
- className={`flex items-center justify-between lg:justify-normal lg:gap-3 px-5 ${
+ className={`flex items-center justify-between lg:justify-normal lg:gap-3 px-5 z-40 ${
section === "Recommendations" ? "" : "cursor-pointer"
}`}
onClick={goToPage}
@@ -169,7 +226,6 @@ export default function Content({
onClick={handleClick}
ref={containerRef}
>
-
{ids !== "recentlyWatched"
? slicedData?.map((anime) => {
const progress = og?.find((i) => i.mediaId === anime.id);
@@ -273,14 +329,27 @@ export default function Content({
if (prog > 90) prog = 100;
return (
- <Link
+ <div
key={i.watchId}
- className="flex flex-col gap-2 shrink-0 cursor-pointer"
- href={`/en/anime/watch/${i.aniId}/${
- i.provider
- }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}`}
+ className="flex flex-col gap-2 shrink-0 cursor-pointer relative group/item"
>
- <div className="relative w-[320px] aspect-video rounded-md overflow-hidden group">
+ <div className="absolute z-40 top-1 right-1 group-hover/item:visible invisible hover:text-action">
+ <div
+ className="flex flex-col items-center group/delete"
+ onClick={() => removeItem(i.watchId)}
+ >
+ <XMarkIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full" />
+ <span className="absolute font-karla bg-secondary shadow-black shadow-2xl py-1 px-2 whitespace-nowrap text-white text-sm rounded-md right-7 -bottom-[2px] z-40 duration-300 transition-all ease-out group-hover/delete:visible group-hover/delete:scale-100 group-hover/delete:translate-x-0 group-hover/delete:opacity-100 opacity-0 translate-x-10 scale-50 invisible">
+ Remove from history
+ </span>
+ </div>
+ </div>
+ <Link
+ className="relative w-[320px] aspect-video rounded-md overflow-hidden group"
+ href={`/en/anime/watch/${i.aniId}/${
+ i.provider
+ }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}`}
+ >
<div className="w-full h-full bg-gradient-to-t from-black/70 from-20% to-transparent group-hover:to-black/40 transition-all duration-300 ease-out absolute z-30" />
<div className="absolute bottom-3 left-0 mx-2 text-white flex gap-2 items-center w-[80%] z-30">
<PlayIcon className="w-5 h-5 shrink-0" />
@@ -299,6 +368,7 @@ export default function Content({
width: `${prog}%`,
}}
/>
+
{i?.image && (
<Image
src={i?.image}
@@ -308,9 +378,14 @@ export default function Content({
className="w-fit group-hover:scale-[1.02] duration-300 ease-out z-10"
/>
)}
- </div>
+ </Link>
- <div className="flex flex-col font-karla w-full">
+ <Link
+ className="flex flex-col font-karla w-full"
+ href={`/en/anime/watch/${i.aniId}/${
+ i.provider
+ }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}`}
+ >
{/* <h1 className="font-semibold">{i.title}</h1> */}
<p className="flex items-center gap-1 text-sm text-gray-400 w-[320px]">
<span
@@ -328,24 +403,25 @@ export default function Content({
</span>{" "}
| Episode {i.episode}
</p>
- </div>
- </Link>
+ </Link>
+ </div>
);
})}
- {userData?.length >= 10 && section !== "Recommendations" && (
- <div
- key={section}
- className="flex cursor-pointer"
- onClick={goToPage}
- >
- <div className="w-[320px] aspect-video overflow-hidden object-cover rounded-md border-secondary border-2 flex flex-col gap-2 items-center text-center justify-center text-[#6a6a6a] hover:text-[#9f9f9f] hover:border-[#757575] transition-colors duration-200">
- <h1 className="whitespace-pre-wrap text-sm">
- More on {section}
- </h1>
- <ArrowRightCircleIcon className="w-5 h-5" />
+ {userData?.filter((i) => i.aniId !== null)?.length >= 10 &&
+ section !== "Recommendations" && (
+ <div
+ key={section}
+ className="flex cursor-pointer"
+ onClick={goToPage}
+ >
+ <div className="w-[320px] aspect-video overflow-hidden object-cover rounded-md border-secondary border-2 flex flex-col gap-2 items-center text-center justify-center text-[#6a6a6a] hover:text-[#9f9f9f] hover:border-[#757575] transition-colors duration-200">
+ <h1 className="whitespace-pre-wrap text-sm">
+ More on {section}
+ </h1>
+ <ArrowRightCircleIcon className="w-5 h-5" />
+ </div>
</div>
- </div>
- )}
+ )}
{filteredData?.length >= 10 && section !== "Recommendations" && (
<div
key={section}
diff --git a/components/home/schedule.js b/components/home/schedule.js
index 73c63f0..4043c5e 100644
--- a/components/home/schedule.js
+++ b/components/home/schedule.js
@@ -117,10 +117,10 @@ export default function Schedule({ data, scheduleData, time }) {
>
<div className="flex flex-col gap-2 px-2 pt-2">
{scheduleData[days[currentPage]]
- .filter((show, index, self) => {
+ ?.filter((show, index, self) => {
return index === self.findIndex((s) => s.id === show.id);
})
- .map((i, index) => {
+ ?.map((i, index) => {
const currentTime = Date.now();
const hasAired = i.airingAt < currentTime;
diff --git a/components/manga/info/topSection.js b/components/manga/info/topSection.js
index 14dc5e5..40b5a37 100644
--- a/components/manga/info/topSection.js
+++ b/components/manga/info/topSection.js
@@ -66,7 +66,7 @@ export default function TopSection({ info, firstEp, setCookie }) {
</div>
<div className="w-full flex flex-col justify-start z-40">
<div className="md:h-1/2 py-2 md:py-5 flex flex-col md:gap-2 justify-end">
- <h1 className="text-xl md:text-2xl xl:text-3xl text-white font-semibold font-karla line-clamp-1 text-start">
+ <h1 className="title text-xl md:text-2xl xl:text-3xl text-white font-semibold font-karla line-clamp-1 text-start">
{info.title?.romaji || info.title?.english || info.title?.native}
</h1>
<span className="flex flex-wrap text-xs lg:text-sm md:text-[#747478]">
diff --git a/components/manga/rightBar.js b/components/manga/rightBar.js
index 6d37e4a..18c5e55 100644
--- a/components/manga/rightBar.js
+++ b/components/manga/rightBar.js
@@ -151,6 +151,7 @@ export default function RightBar({
Chapter Progress
</label>
<input
+ id="chapter-progress"
type="number"
placeholder="0"
min={0}
diff --git a/components/videoPlayer.js b/components/videoPlayer.js
index 46129ab..dcde703 100644
--- a/components/videoPlayer.js
+++ b/components/videoPlayer.js
@@ -35,6 +35,7 @@ export default function VideoPlayer({
track,
aniTitle,
timeWatched,
+ dub,
}) {
const [url, setUrl] = useState("");
const [source, setSource] = useState([]);
@@ -226,10 +227,7 @@ export default function VideoPlayer({
watchId: id,
title: track?.playing?.title || aniTitle,
aniTitle: aniTitle,
- image:
- track?.playing?.image ||
- info?.bannerImage ||
- info?.coverImage?.extraLarge,
+ image: track?.playing?.image || info?.coverImage?.extraLarge,
number: Number(progress),
duration: art.duration,
timeWatched: art.currentTime,
@@ -260,10 +258,7 @@ export default function VideoPlayer({
watchId: id,
title: track?.playing?.title || aniTitle,
aniTitle: aniTitle,
- image:
- track?.playing?.image ||
- info?.bannerImage ||
- info?.coverImage?.extraLarge,
+ image: track?.playing?.image || info?.coverImage?.extraLarge,
episode: Number(progress),
duration: art.duration,
timeWatched: art.currentTime,
@@ -285,6 +280,12 @@ export default function VideoPlayer({
});
});
+ art.on("resize", () => {
+ art.subtitle.style({
+ fontSize: art.height * 0.05 + "px",
+ });
+ });
+
art.on("video:timeupdate", async () => {
if (!session) return;
@@ -313,7 +314,9 @@ export default function VideoPlayer({
router.push(
`/en/anime/watch/${aniId}/${provider}?id=${encodeURIComponent(
track?.next?.id
- )}&num=${track?.next?.number}`
+ )}&num=${track?.next?.number}${
+ dub ? `&dub=${dub}` : ""
+ }`
);
}
},
@@ -332,7 +335,7 @@ export default function VideoPlayer({
router.push(
`/en/anime/watch/${aniId}/${provider}?id=${encodeURIComponent(
track?.next?.id
- )}&num=${track?.next?.number}`
+ )}&num=${track?.next?.number}${dub ? `&dub=${dub}` : ""}`
);
}
}, 7000);
diff --git a/lib/anilist/useAnilist.js b/lib/anilist/useAnilist.js
index bedb4a5..72e11ca 100644
--- a/lib/anilist/useAnilist.js
+++ b/lib/anilist/useAnilist.js
@@ -1,14 +1,18 @@
import { useState, useEffect } from "react";
import { toast } from "react-toastify";
-function useMedia(username, accessToken, status) {
+export const useAniList = (session, stats) => {
const [media, setMedia] = 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 }),
});
@@ -18,68 +22,47 @@ function useMedia(username, accessToken, status) {
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
+ query ($username: String, $status: MediaListStatus) {
+ MediaListCollection(userName: $username, type: ANIME, status: $status) {
+ lists {
status
- progress
- score
- media {
+ name
+ entries {
id
+ mediaId
status
- nextAiringEpisode {
+ progress
+ score
+ media {
+ id
+ status
+ nextAiringEpisode {
timeUntilAiring
episode
- }
- title {
- english
- romaji
- }
- episodes
- coverImage {
- large
+ }
+ title {
+ english
+ romaji
+ }
+ episodes
+ coverImage {
+ large
+ }
}
}
}
}
}
- }
- `;
+ `;
fetchGraphQL(queryMedia, { username, status: status?.stats }).then((data) =>
setMedia(data.data.MediaListCollection.lists)
);
}, [username, accessToken, status?.stats]);
- return media;
-}
-
-export function useAniList(session, stats) {
- const accessToken = session?.user?.token;
- const username = session?.user?.name;
- const status = stats || null;
- const media = useMedia(username, accessToken, status);
-
- 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();
- };
-
- const markComplete = (mediaId) => {
+ const markComplete = async (mediaId) => {
if (!accessToken) return;
const completeQuery = `
- mutation($mediaId: Int ) {
+ mutation($mediaId: Int) {
SaveMediaListEntry(mediaId: $mediaId, status: COMPLETED) {
id
mediaId
@@ -87,14 +70,13 @@ export function useAniList(session, stats) {
}
}
`;
- fetchGraphQL(completeQuery, { mediaId }).then((data) =>
- console.log({ Complete: data })
- );
+ const data = await fetchGraphQL(completeQuery, { mediaId });
+ console.log({ Complete: data });
};
- const markPlanning = (mediaId) => {
+ const markPlanning = async (mediaId) => {
if (!accessToken) return;
- const completeQuery = `
+ const planningQuery = `
mutation($mediaId: Int ) {
SaveMediaListEntry(mediaId: $mediaId, status: PLANNING) {
id
@@ -103,40 +85,98 @@ export function useAniList(session, stats) {
}
}
`;
- fetchGraphQL(completeQuery, { mediaId }).then((data) =>
- console.log({ added_to_list: data })
- );
+ const data = await fetchGraphQL(planningQuery, { mediaId });
+ console.log({ added_to_list: data });
};
- const markProgress = (mediaId, progress, stats, volumeProgress) => {
- if (!accessToken) return;
- const progressWatched = `
- mutation($mediaId: Int, $progress: Int, $status: MediaListStatus, $progressVolumes: Int) {
- SaveMediaListEntry(mediaId: $mediaId, progress: $progress, status: $status, progressVolumes: $progressVolumes) {
+ const getUserLists = async (id) => {
+ const getLists = `
+ query ($id: Int) {
+ Media(id: $id) {
+ mediaListEntry {
+ customLists
+ }
id
- mediaId
- progress
- status
+ type
+ title {
+ romaji
+ english
+ native
+ }
}
}
+ `;
+ const data = await fetchGraphQL(getLists, { id });
+ 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) {
+ id
+ mediaId
+ progress
+ status
+ }
+ }
`;
- fetchGraphQL(progressWatched, {
- mediaId,
- progress,
- status: stats,
- progressVolumes: volumeProgress,
- }).then(() => {
- console.log(`Progress Updated: ${progress}`);
- toast.success(`Progress Updated: ${progress}`, {
- position: "bottom-right",
- autoClose: 5000,
- hideProgressBar: false,
- closeOnClick: true,
- draggable: true,
- theme: "dark",
- });
- });
+
+ 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);
+ }
+
+ 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")) {
+ 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",
+ });
+ }
+ }
};
- return { media, markComplete, markProgress, markPlanning };
-}
+ return { media, markComplete, markProgress, markPlanning, getUserLists };
+};
diff --git a/next.config.js b/next.config.js
index fcf654b..f7da518 100644
--- a/next.config.js
+++ b/next.config.js
@@ -18,7 +18,7 @@ module.exports = withPWA({
},
],
},
- distDir: process.env.BUILD_DIR || ".next",
+ // distDir: process.env.BUILD_DIR || ".next",
trailingSlash: true,
output: "standalone",
// async headers() {
diff --git a/package-lock.json b/package-lock.json
index a1ff279..cecee3a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "moopa",
- "version": "3.9.1",
+ "version": "3.9.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "moopa",
- "version": "3.9.1",
+ "version": "3.9.3",
"dependencies": {
"@apollo/client": "^3.7.3",
"@headlessui/react": "^1.7.15",
diff --git a/package.json b/package.json
index b5ddad8..76d9adf 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "moopa",
- "version": "3.9.1",
+ "version": "3.9.3",
"private": true,
"founder": "Factiven",
"scripts": {
diff --git a/pages/api/consumet/episode/[id].js b/pages/api/consumet/episode/[id].js
index e6f40ce..6e7f318 100644
--- a/pages/api/consumet/episode/[id].js
+++ b/pages/api/consumet/episode/[id].js
@@ -1,4 +1,3 @@
-import axios from "axios";
import cacheData from "memory-cache";
const API_URL = process.env.API_URI;
@@ -9,7 +8,7 @@ export default async function handler(req, res) {
const dub = req.query.dub || false;
const refresh = req.query.refresh || false;
- const providers = ["enime", "gogoanime"];
+ const providers = ["enime", "gogoanime", "zoro"];
const datas = [];
const cached = cacheData.get(id + dub);
@@ -59,7 +58,7 @@ export default async function handler(req, res) {
if (datas.length === 0) {
return res.status(404).json({ message: "Anime not found" });
} else {
- cacheData.put(id + dub, { data: datas }, 1000 * 60 * 60 * 10);
+ cacheData.put(id + dub, { data: datas }, 1000 * 60 * 60 * 10);
res.status(200).json({ data: datas });
}
}
diff --git a/pages/api/user/profile.js b/pages/api/user/profile.js
index dd22bd8..e20aaca 100644
--- a/pages/api/user/profile.js
+++ b/pages/api/user/profile.js
@@ -43,13 +43,21 @@ export default async function handler(req, res) {
}
case "DELETE": {
const { name } = req.body;
- const user = await deleteUser(name);
- if (!user) {
- return res.status(404).json({ message: "User not found" });
+ // return res.status(200).json({ name });
+ if (session.user.name !== name) {
+ return res.status(401).json({ message: "Unauthorized" });
} else {
- return res.status(200).json(user);
+ const user = await deleteUser(name);
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ } else {
+ return res.status(200).json(user);
+ }
}
}
+ default: {
+ return res.status(405).json({ message: "Method not allowed" });
+ }
}
} catch (error) {
console.log(error);
diff --git a/pages/api/user/update/episode.js b/pages/api/user/update/episode.js
index 7974446..52c9494 100644
--- a/pages/api/user/update/episode.js
+++ b/pages/api/user/update/episode.js
@@ -3,6 +3,7 @@ import { authOptions } from "../../auth/[...nextauth]";
import {
createList,
+ deleteEpisode,
getEpisode,
updateUserEpisode,
} from "../../../../prisma/user";
@@ -16,13 +17,17 @@ export default async function handler(req, res) {
case "POST": {
const { name, id } = JSON.parse(req.body);
- const episode = await createList(name, id);
- if (!episode) {
- return res
- .status(200)
- .json({ message: "Episode is already created" });
+ if (session.user.name !== name) {
+ return res.status(401).json({ message: "Unauthorized" });
} else {
- return res.status(201).json(episode);
+ const episode = await createList(name, id);
+ if (!episode) {
+ return res
+ .status(200)
+ .json({ message: "Episode is already created" });
+ } else {
+ return res.status(201).json(episode);
+ }
}
}
case "PUT": {
@@ -68,6 +73,19 @@ export default async function handler(req, res) {
return res.status(200).json(episode);
}
}
+ case "DELETE": {
+ const { name, id } = req.body;
+ if (session.user.name !== name) {
+ return res.status(401).json({ message: "Unauthorized" });
+ } else {
+ const episode = await deleteEpisode(name, id);
+ if (!episode) {
+ return res.status(404).json({ message: "Episode not found" });
+ } else {
+ return res.status(200).json({ message: "Episode deleted" });
+ }
+ }
+ }
}
} catch (error) {
console.log(error);
diff --git a/pages/en/anime/[...id].js b/pages/en/anime/[...id].js
index 5e4aed8..534aa17 100644
--- a/pages/en/anime/[...id].js
+++ b/pages/en/anime/[...id].js
@@ -125,14 +125,14 @@ export default function Info({ info, color }) {
}&image=${info.bannerImage || info.coverImage.extraLarge}`}
/>
</Head>
- <ToastContainer pauseOnFocusLoss={false} />
+ <ToastContainer pauseOnHover={false} />
<Modal open={open} onClose={() => handleClose()}>
<div>
{!session && (
<div className="flex-center flex-col gap-5 px-10 py-5 bg-secondary rounded-md">
- <h1 className="text-md font-extrabold font-karla">
+ <div className="text-md font-extrabold font-karla">
Edit your list
- </h1>
+ </div>
<button
className="flex items-center bg-[#363642] rounded-md text-white p-1"
onClick={() => signIn("AniListProvider")}
diff --git a/pages/en/anime/recently-watched.js b/pages/en/anime/recently-watched.js
index 0a7fbae..1cc713a 100644
--- a/pages/en/anime/recently-watched.js
+++ b/pages/en/anime/recently-watched.js
@@ -7,10 +7,13 @@ import Footer from "../../../components/footer";
import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
import MobileNav from "../../../components/home/mobileNav";
+import { ToastContainer, toast } from "react-toastify";
+import { XMarkIcon } from "@heroicons/react/24/outline";
export default function PopularAnime({ sessions }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
+ const [remove, setRemoved] = useState();
useEffect(() => {
setLoading(true);
@@ -46,11 +49,66 @@ export default function PopularAnime({ sessions }) {
}
};
fetchData();
- }, []);
+ }, [remove]);
+
+ const removeItem = async (id) => {
+ if (sessions?.user?.name) {
+ // remove from database
+ const res = await fetch(`/api/user/update/episode`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: sessions?.user?.name,
+ id: id,
+ }),
+ });
+ const data = await res.json();
+
+ // remove from local storage
+ const artplayerSettings =
+ JSON.parse(localStorage.getItem("artplayer_settings")) || {};
+ if (artplayerSettings[id]) {
+ delete artplayerSettings[id];
+ localStorage.setItem(
+ "artplayer_settings",
+ JSON.stringify(artplayerSettings)
+ );
+ }
+
+ // update client
+ setRemoved(id);
+
+ if (data?.message === "Episode deleted") {
+ toast.success("Episode removed from history", {
+ position: "bottom-right",
+ autoClose: 5000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ draggable: true,
+ theme: "dark",
+ });
+ }
+ } else {
+ const artplayerSettings =
+ JSON.parse(localStorage.getItem("artplayer_settings")) || {};
+ if (artplayerSettings[id]) {
+ delete artplayerSettings[id];
+ localStorage.setItem(
+ "artplayer_settings",
+ JSON.stringify(artplayerSettings)
+ );
+ }
+
+ setRemoved(id);
+ }
+ };
return (
<>
<MobileNav sessions={sessions} />
+ <ToastContainer pauseOnHover={false} />
<div className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10">
<div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed left-0 px-3">
<Link href="/en" className="flex gap-2 items-center font-karla">
@@ -68,14 +126,27 @@ export default function PopularAnime({ sessions }) {
if (prog > 90) prog = 100;
return (
- <Link
+ <div
key={i.watchId}
- className="flex flex-col gap-2 shrink-0 cursor-pointer"
- href={`/en/anime/watch/${i.aniId}/${
- i.provider
- }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}`}
+ className="flex flex-col gap-2 shrink-0 cursor-pointer relative group/item"
>
- <div className="relative md:w-[320px] aspect-video rounded-md overflow-hidden group">
+ <div className="absolute z-40 top-1 right-1 group-hover/item:visible invisible hover:text-action">
+ <div
+ className="flex flex-col items-center group/delete"
+ onClick={() => removeItem(i.watchId)}
+ >
+ <XMarkIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full" />
+ <span className="absolute font-karla bg-secondary shadow-black shadow-2xl py-1 px-2 whitespace-nowrap text-white text-sm rounded-md right-7 -bottom-[2px] z-40 duration-300 transition-all ease-out group-hover/delete:visible group-hover/delete:scale-100 group-hover/delete:translate-x-0 group-hover/delete:opacity-100 opacity-0 translate-x-10 scale-50 invisible">
+ Remove from history
+ </span>
+ </div>
+ </div>
+ <Link
+ className="relative md:w-[320px] aspect-video rounded-md overflow-hidden group"
+ href={`/en/anime/watch/${i.aniId}/${
+ i.provider
+ }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}`}
+ >
<div className="w-full h-full bg-gradient-to-t from-black/70 from-20% to-transparent group-hover:to-black/40 transition-all duration-300 ease-out absolute z-30" />
<div className="absolute bottom-3 left-0 mx-2 text-white flex gap-2 items-center w-[80%] z-30">
<PlayIcon className="w-5 h-5 shrink-0" />
@@ -101,8 +172,13 @@ export default function PopularAnime({ sessions }) {
className="w-fit group-hover:scale-[1.02] duration-300 ease-out z-10"
/>
)}
- </div>
- <div className="flex flex-col font-karla w-full">
+ </Link>
+ <Link
+ className="flex flex-col font-karla w-full"
+ href={`/en/anime/watch/${i.aniId}/${
+ i.provider
+ }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}`}
+ >
{/* <h1 className="font-semibold">{i.title}</h1> */}
<p className="flex items-center gap-1 text-sm text-gray-400 md:w-[320px]">
<span
@@ -119,8 +195,8 @@ export default function PopularAnime({ sessions }) {
</span>{" "}
| Episode {i.episode}
</p>
- </div>
- </Link>
+ </Link>
+ </div>
);
})}
diff --git a/pages/en/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js
index e013c6b..c17d9c5 100644
--- a/pages/en/anime/watch/[...info].js
+++ b/pages/en/anime/watch/[...info].js
@@ -172,8 +172,6 @@ export default function Info({
};
}, [sessions?.user?.name, epiNumber, dub]);
- // console.log(proxy);
-
return (
<>
<Head>
@@ -199,6 +197,7 @@ export default function Info({
setLoading={setLoading}
loading={loading}
timeWatched={userData?.timeWatched}
+ dub={dub}
/>
<SecondarySide
info={info}
@@ -230,8 +229,7 @@ export async function getServerSideProps(context) {
const proxy = process.env.PROXY_URI;
const disqus = process.env.DISQUS_SHORTNAME;
- const aniId = query.info[0];
- const provider = query.info[1];
+ const [aniId, provider] = query.info;
const watchId = query.id;
const epiNumber = query.num;
const dub = query.dub;
diff --git a/pages/en/dmca.js b/pages/en/dmca.js
index 8dad7d7..fd93811 100644
--- a/pages/en/dmca.js
+++ b/pages/en/dmca.js
@@ -14,10 +14,7 @@ export default function DMCA() {
property rights of others and complying with the Digital
Millennium Copyright Act (DMCA)."
/>
- <meta
- property="og:image"
- content="https://cdn.discordapp.com/attachments/1068758633464201268/1081591948705546330/logo.png"
- />
+ <meta property="og:image" content="/icon-512x512.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/c.svg" />
</Head>
diff --git a/pages/en/index.js b/pages/en/index.js
index 159d257..73b4e94 100644
--- a/pages/en/index.js
+++ b/pages/en/index.js
@@ -9,7 +9,6 @@ import Content from "../../components/home/content";
import { motion } from "framer-motion";
import { signOut } from "next-auth/react";
-import { useAniList } from "../../lib/anilist/useAnilist";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../api/auth/[...nextauth]";
import SearchBar from "../../components/searchBar";
@@ -25,6 +24,7 @@ import { createUser } from "../../prisma/user";
import { checkAdBlock } from "adblock-checker";
import { ToastContainer, toast } from "react-toastify";
+import { useAniList } from "../../lib/anilist/useAnilist";
export async function getServerSideProps(context) {
const session = await getServerSession(context.req, context.res, authOptions);
@@ -35,7 +35,6 @@ export async function getServerSideProps(context) {
}
} catch (error) {
console.error(error);
- // Handle the error here
}
const trendingDetail = await aniListData({
@@ -108,8 +107,14 @@ export default function Home({ detail, populars, sessions, upComing }) {
useEffect(() => {
const getSchedule = async () => {
- const { data } = await axios.get(`/api/anify/schedule`);
- setSchedules(data);
+ const res = await fetch(`/api/anify/schedule`);
+ const data = await res.json();
+
+ if (!res.ok) {
+ setSchedules(null);
+ } else {
+ setSchedules(data);
+ }
};
getSchedule();
}, []);
@@ -120,12 +125,16 @@ export default function Home({ detail, populars, sessions, upComing }) {
function getRelease() {
let releasingAnime = [];
let progress = [];
+ let seenIds = new Set(); // Create a Set to store the IDs of seen anime
release.map((list) => {
list.entries.map((entry) => {
- if (entry.media.status === "RELEASING") {
+ if (
+ entry.media.status === "RELEASING" &&
+ !seenIds.has(entry.media.id)
+ ) {
releasingAnime.push(entry.media);
+ seenIds.add(entry.media.id); // Add the ID to the Set
}
-
progress.push(entry);
});
});
@@ -139,8 +148,7 @@ export default function Home({ detail, populars, sessions, upComing }) {
const [planned, setPlanned] = useState(null);
const [greeting, setGreeting] = useState("");
const [user, setUser] = useState(null);
-
- // console.log({ user });
+ const [removed, setRemoved] = useState();
const [prog, setProg] = useState(null);
@@ -194,7 +202,7 @@ export default function Home({ detail, populars, sessions, upComing }) {
// const data = await res.json();
}
userData();
- }, [sessions?.user?.name]);
+ }, [sessions?.user?.name, removed]);
useEffect(() => {
const time = new Date().getHours();
@@ -251,7 +259,11 @@ export default function Home({ detail, populars, sessions, upComing }) {
/>
<meta
name="twitter:image"
- content="https://cdn.discordapp.com/attachments/1084446049986420786/1093300833422168094/image.png"
+ content="https://beta.moopa.live/preview.png"
+ />
+ <meta
+ name="description"
+ content="Discover your new favorite anime or manga title! Moopa offers a vast library of high-quality content, accessible on multiple devices and without any interruptions. Start using Moopa today!"
/>
<link rel="icon" href="/c.svg" />
</Head>
@@ -262,7 +274,7 @@ export default function Home({ detail, populars, sessions, upComing }) {
<Navigasi />
<SearchBar />
<ToastContainer
- pauseOnFocusLoss={false}
+ pauseOnHover={false}
style={{
width: "400px",
}}
@@ -350,6 +362,8 @@ export default function Home({ detail, populars, sessions, upComing }) {
ids="recentlyWatched"
section="Recently Watched"
userData={user}
+ userName={sessions?.user?.name}
+ setRemoved={setRemoved}
/>
</motion.div>
)}
diff --git a/pages/en/manga/read/[...params].js b/pages/en/manga/read/[...params].js
index e608d16..301b646 100644
--- a/pages/en/manga/read/[...params].js
+++ b/pages/en/manga/read/[...params].js
@@ -115,6 +115,7 @@ export default function Read({ data, currentId, sessions }) {
}`
: "Getting Info..."}
</title>
+ <meta id="CoverImage" data-manga-cover={info?.coverImage} />
</Head>
<div className="w-screen flex justify-evenly relative">
<ToastContainer pauseOnFocusLoss={false} />
diff --git a/pages/index.js b/pages/index.js
index 6f020fb..56b2c1f 100644
--- a/pages/index.js
+++ b/pages/index.js
@@ -1,7 +1,26 @@
+import Head from "next/head";
import { parseCookies } from "nookies";
export default function Home() {
- return <></>;
+ return (
+ <>
+ <Head>
+ <meta
+ name="twitter:title"
+ content="Moopa - Free Anime and Manga Streaming"
+ />
+ <meta
+ name="twitter:description"
+ content="Discover your new favorite anime or manga title! Moopa offers a vast library of high-quality content, accessible on multiple devices and without any interruptions. Start using Moopa today!"
+ />
+ <meta name="twitter:image" content="/preview.png" />
+ <meta
+ name="description"
+ content="Discover your new favorite anime or manga title! Moopa offers a vast library of high-quality content, accessible on multiple devices and without any interruptions. Start using Moopa today!"
+ />
+ </Head>
+ </>
+ );
}
export async function getServerSideProps(context) {
diff --git a/prisma/migrations/20230810051657_ondelete_cascade/migration.sql b/prisma/migrations/20230810051657_ondelete_cascade/migration.sql
new file mode 100644
index 0000000..a521884
--- /dev/null
+++ b/prisma/migrations/20230810051657_ondelete_cascade/migration.sql
@@ -0,0 +1,5 @@
+-- DropForeignKey
+ALTER TABLE "WatchListEpisode" DROP CONSTRAINT "WatchListEpisode_userProfileId_fkey";
+
+-- AddForeignKey
+ALTER TABLE "WatchListEpisode" ADD CONSTRAINT "WatchListEpisode_userProfileId_fkey" FOREIGN KEY ("userProfileId") REFERENCES "UserProfile"("name") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index f336e54..072415b 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -25,7 +25,7 @@ model WatchListEpisode {
duration Int?
provider String?
createdDate DateTime? @default(now())
- userProfile UserProfile @relation(fields: [userProfileId], references: [name])
+ userProfile UserProfile @relation(fields: [userProfileId], references: [name], onDelete: Cascade)
userProfileId String
watchId String
}
diff --git a/prisma/user.js b/prisma/user.js
index 8c436a5..dd61078 100644
--- a/prisma/user.js
+++ b/prisma/user.js
@@ -1,9 +1,9 @@
-// import { PrismaClient } from "@prisma/client";
+import { Prisma } from "@prisma/client";
// const prisma = new PrismaClient();
import { prisma } from "../lib/prisma";
-export const createUser = async (name, setting) => {
+export const createUser = async (name) => {
try {
const checkUser = await prisma.userProfile.findUnique({
where: {
@@ -22,6 +22,15 @@ export const createUser = async (name, setting) => {
return null;
}
} catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ if (error.code === "P2002") {
+ console.log(
+ "There is a unique constraint violation, a new user cannot be created with this name"
+ );
+ }
+ } else if (error instanceof Prisma.PrismaClientUnknownRequestError) {
+ console.log("An unknown Prisma error occurred:", error.message);
+ }
console.error(error);
throw new Error("Error creating user");
}
@@ -218,19 +227,38 @@ export const updateUserEpisode = async ({
}
};
-export const updateTimeWatched = async (id, timeWatched) => {
+export const deleteEpisode = async (name, id) => {
try {
- const user = await prisma.watchListEpisode.update({
+ const user = await prisma.watchListEpisode.deleteMany({
where: {
- id: id,
- },
- data: {
- timeWatched: timeWatched,
+ watchId: id,
+ userProfileId: name,
},
});
- return user;
+ if (user) {
+ return user;
+ } else {
+ return { message: "Episode not found" };
+ }
} catch (error) {
console.error(error);
- throw new Error("Error updating time watched");
+ throw new Error("Error deleting episode");
}
};
+
+// export const updateTimeWatched = async (id, timeWatched) => {
+// try {
+// const user = await prisma.watchListEpisode.update({
+// where: {
+// id: id,
+// },
+// data: {
+// timeWatched: timeWatched,
+// },
+// });
+// return user;
+// } catch (error) {
+// console.error(error);
+// throw new Error("Error updating time watched");
+// }
+// };
diff --git a/public/preview.png b/public/preview.png
new file mode 100644
index 0000000..b5fd49f
--- /dev/null
+++ b/public/preview.png
Binary files differ