aboutsummaryrefslogtreecommitdiff
path: root/components/admin
diff options
context:
space:
mode:
authorFactiven <[email protected]>2023-09-26 23:35:35 +0700
committerFactiven <[email protected]>2023-09-26 23:35:35 +0700
commit20b8a7267827e3a07c1eef668c3b9c22fda43765 (patch)
tree2fec9006dfac5737d8b227bf5ccce73880800cc2 /components/admin
parentUpdate release.md (diff)
downloadmoopa-20b8a7267827e3a07c1eef668c3b9c22fda43765.tar.xz
moopa-20b8a7267827e3a07c1eef668c3b9c22fda43765.zip
Update v4.1.2v4.1.2
Diffstat (limited to 'components/admin')
-rw-r--r--components/admin/dashboard/index.js134
-rw-r--r--components/admin/layout.js75
-rw-r--r--components/admin/meta/AppendMeta.js252
3 files changed, 461 insertions, 0 deletions
diff --git a/components/admin/dashboard/index.js b/components/admin/dashboard/index.js
new file mode 100644
index 0000000..64a1d6f
--- /dev/null
+++ b/components/admin/dashboard/index.js
@@ -0,0 +1,134 @@
+import React, { useState } from "react";
+
+export default function AdminDashboard({
+ animeCount,
+ infoCount,
+ metaCount,
+ report,
+}) {
+ const [message, setMessage] = useState("");
+ const [selectedTime, setSelectedTime] = useState("");
+ const [unixTimestamp, setUnixTimestamp] = useState(null);
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+
+ if (selectedTime) {
+ const unixTime = Math.floor(new Date(selectedTime).getTime() / 1000);
+ setUnixTimestamp(unixTime);
+ }
+ };
+ return (
+ <div className="flex flex-col gap-5 px-5 py-10 h-full">
+ <div className="flex flex-col gap-2">
+ <p className="font-semibold">Stats</p>
+ <div className="grid grid-cols-3 gap-5">
+ <div className="flex-center flex-col bg-secondary rounded p-5">
+ <p className="font-karla text-4xl">{animeCount}</p>
+ <p className="font-karla text-xl">Anime</p>
+ </div>
+ <div className="flex-center flex-col bg-secondary rounded p-5">
+ <p className="font-karla text-4xl">{infoCount}</p>
+ <p className="font-karla text-xl">detail info</p>
+ </div>
+ <div className="flex-center flex-col bg-secondary rounded p-5">
+ <p className="font-karla text-4xl">{metaCount}</p>
+ <p className="font-karla text-xl">Metadata</p>
+ </div>
+ </div>
+ </div>
+ <div className="grid grid-cols-2 gap-5 h-full">
+ <div className="flex flex-col gap-2">
+ <p className="font-semibold">Broadcast</p>
+ <div className="flex flex-col justify-between bg-secondary rounded p-5 h-full">
+ <form onSubmit={handleSubmit}>
+ <div className="mb-4">
+ <label
+ htmlFor="message"
+ className="block text-txt font-light mb-2"
+ >
+ Message
+ </label>
+ <input
+ type="text"
+ id="message"
+ value={message}
+ onChange={(e) => setMessage(e.target.value)}
+ required
+ className="w-full px-3 py-2 border rounded-md focus:outline-none text-black"
+ />
+ </div>
+ <div className="mb-4">
+ <label
+ htmlFor="selectedTime"
+ className="block text-txt font-light mb-2"
+ >
+ Select Time
+ </label>
+ <input
+ type="datetime-local"
+ id="selectedTime"
+ value={selectedTime}
+ onChange={(e) => setSelectedTime(e.target.value)}
+ required
+ className="w-full px-3 py-2 border rounded-md focus:outline-none text-black"
+ />
+ </div>
+ <button
+ type="submit"
+ className="bg-image text-white py-2 px-4 rounded-md hover:bg-opacity-80 transition duration-300"
+ >
+ Submit
+ </button>
+ </form>
+ {unixTimestamp && (
+ <p>
+ Unix Timestamp: <strong>{unixTimestamp}</strong>
+ </p>
+ )}
+ </div>
+ </div>
+ <div className="flex flex-col gap-2">
+ <p className="font-semibold">Recent Reports</p>
+ <div className="bg-secondary rounded p-5 h-full">
+ <div className="rounded overflow-hidden w-full h-full">
+ {report?.map((i, index) => (
+ <div
+ key={index}
+ className="odd:bg-primary/80 even:bg-primary/40 p-2 flex justify-between items-center"
+ >
+ {i.desc}{" "}
+ {i.severity === "Low" && (
+ <span className="relative w-5 h-5 flex-center shrink-0">
+ {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */}
+ <span className="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>
+ </span>
+ )}
+ {i.severity === "Medium" && (
+ <span className="relative w-5 h-5 flex-center shrink-0">
+ {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */}
+ <span className="relative inline-flex rounded-full h-3 w-3 bg-amber-500"></span>
+ </span>
+ )}
+ {i.severity === "High" && (
+ <span className="relative w-5 h-5 flex-center shrink-0">
+ {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */}
+ <span className="relative animate-pulse inline-flex rounded-full h-3 w-3 bg-rose-500"></span>
+ </span>
+ )}
+ {i.severity === "Critical" && (
+ <span className="relative w-5 h-5 flex-center shrink-0">
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-900 opacity-75"></span>
+ <span className="relative inline-flex rounded-full h-3 w-3 bg-red-900"></span>
+ </span>
+ )}
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="w-full h-full">a</div>
+ </div>
+ );
+}
diff --git a/components/admin/layout.js b/components/admin/layout.js
new file mode 100644
index 0000000..3209dcf
--- /dev/null
+++ b/components/admin/layout.js
@@ -0,0 +1,75 @@
+import {
+ CloudArrowUpIcon,
+ Cog6ToothIcon,
+ HomeIcon,
+ UserIcon,
+} from "@heroicons/react/24/outline";
+import Link from "next/link";
+import { useRouter } from "next/router";
+import React from "react";
+
+const Navigation = [
+ {
+ name: "Dashboard",
+ page: 1,
+ icon: <HomeIcon />,
+ current: false,
+ },
+ {
+ name: "Metadata",
+ page: 2,
+ icon: <CloudArrowUpIcon />,
+ current: false,
+ },
+ {
+ name: "Users",
+ page: 3,
+ icon: <UserIcon />,
+ current: false,
+ },
+ {
+ name: "Settings",
+ page: 4,
+ icon: <Cog6ToothIcon />,
+ current: false,
+ },
+];
+
+export default function AdminLayout({ children, page, setPage }) {
+ return (
+ <div className="relative w-screen h-screen">
+ <div className="absolute flex flex-col gap-5 top-0 left-0 py-2 bg-secondary w-[14rem] h-full">
+ <div className="flex flex-col px-3">
+ <p className="text-sm font-light text-action font-outfit">moopa</p>
+ <h1 className="text-2xl font-bold text-white">
+ Admin <br />
+ Dashboard
+ </h1>
+ </div>
+ <div className="flex flex-col px-1">
+ {Navigation.map((item, index) => (
+ <button
+ key={item.name}
+ onClick={() => {
+ setPage(item.page);
+ }}
+ className={`flex items-center gap-2 p-2 group ${
+ page == item.page ? "bg-image/50" : "text-txt"
+ } hover:bg-image rounded transition-colors duration-200 ease-in-out`}
+ >
+ <div
+ className={`w-5 h-5 ${
+ page == item.page ? "text-action" : "text-txt"
+ } group-hover:text-action`}
+ >
+ {item.icon}
+ </div>
+ <p>{item.name}</p>
+ </button>
+ ))}
+ </div>
+ </div>
+ <div className="ml-[14rem] overflow-x-hidden h-full">{children}</div>
+ </div>
+ );
+}
diff --git a/components/admin/meta/AppendMeta.js b/components/admin/meta/AppendMeta.js
new file mode 100644
index 0000000..1707ed2
--- /dev/null
+++ b/components/admin/meta/AppendMeta.js
@@ -0,0 +1,252 @@
+import Loading from "@/components/shared/loading";
+import Image from "next/image";
+import { useState } from "react";
+import { toast } from "react-toastify";
+
+// Define a function to convert the data
+function convertData(episodes) {
+ const convertedData = episodes.map((episode) => ({
+ episode: episode.episode,
+ title: episode?.title,
+ description: episode?.description || null,
+ img: episode?.img?.hd || episode?.img?.mobile || null, // Use hd if available, otherwise use mobile
+ }));
+
+ return convertedData;
+}
+
+export default function AppendMeta({ api }) {
+ const [id, setId] = useState();
+ const [resultData, setResultData] = useState(null);
+
+ const [query, setQuery] = useState("");
+ const [tmdbId, setTmdbId] = useState();
+ const [hasilQuery, setHasilQuery] = useState([]);
+ const [season, setSeason] = useState();
+
+ const [override, setOverride] = useState();
+
+ const [loading, setLoading] = useState(false);
+
+ const handleSearch = async () => {
+ try {
+ setLoading(true);
+ setResultData(null);
+ const res = await fetch(`${api}/meta/tmdb/${query}`);
+ const json = await res.json();
+ const data = json.results.filter((i) => i.type === "TV Series");
+ setHasilQuery(data);
+ setLoading(false);
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ const handleDetail = async () => {
+ try {
+ setLoading(true);
+ const res = await fetch(`${api}/meta/tmdb/info/${tmdbId}?type=TV%20Series
+`);
+ const json = await res.json();
+ const data = json.seasons;
+ setHasilQuery(data);
+ setLoading(false);
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ const handleStore = async () => {
+ try {
+ setLoading(true);
+ if (!resultData && !id) {
+ console.log("No data to store");
+ setLoading(false);
+ return;
+ }
+ const data = await fetch("/api/v2/admin/meta", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ id: id,
+ data: resultData,
+ }),
+ });
+ if (data.status === 200) {
+ const json = await data.json();
+ toast.success(json.message);
+ setLoading(false);
+ }
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ const handleOverride = async () => {
+ setResultData(JSON.parse(override));
+ };
+
+ return (
+ <>
+ <div className="container mx-auto p-4 scrol">
+ <h1 className="text-3xl font-semibold mb-4">Append Data Page</h1>
+ <div>
+ <div className="space-y-3 mb-4">
+ <label>Search Anime:</label>
+ <input
+ type="text"
+ className="w-full px-3 py-2 border rounded-md text-black"
+ value={query}
+ onChange={(e) => setQuery(e.target.value)}
+ />
+ <button
+ type="button"
+ className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
+ onClick={handleSearch}
+ >
+ Find Anime{" "}
+ </button>
+ </div>
+ <div className="space-y-3 mb-4">
+ <label>Get Episodes:</label>
+ <input
+ type="number"
+ placeholder="TMDB ID"
+ className="w-full px-3 py-2 border rounded-md text-black"
+ value={tmdbId}
+ onChange={(e) => setTmdbId(e.target.value)}
+ />
+ <button
+ type="button"
+ className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
+ onClick={handleDetail}
+ >
+ Get Details
+ </button>
+ </div>
+
+ <div className="space-y-3 mb-4">
+ <label>Override Result:</label>
+ <textarea
+ rows="5"
+ className="w-full px-3 py-2 border rounded-md text-black"
+ value={override}
+ onChange={(e) => setOverride(e.target.value)}
+ />
+ <button
+ type="button"
+ className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
+ onClick={handleOverride}
+ >
+ Override{" "}
+ </button>
+ </div>
+
+ <div className="space-y-3 mb-4">
+ <label className="block text-sm font-medium text-gray-300">
+ Anime ID:
+ </label>
+ <input
+ type="number"
+ placeholder="AniList ID"
+ className="w-full px-3 py-2 border rounded-md text-black"
+ value={id}
+ onChange={(e) => setId(e.target.value)}
+ />
+ </div>
+ <div className="mb-4">
+ <button
+ className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
+ onClick={handleStore}
+ >
+ Store Data {season && `Season ${season}`}
+ </button>
+ </div>
+
+ {!loading && hasilQuery?.some((i) => i?.season) && (
+ <div className="border rounded-md p-4 mt-4">
+ <h2 className="text-lg font-semibold mb-2">
+ Which season do you want to format?
+ </h2>
+ <div className="w-full flex gap-2">
+ {hasilQuery?.map((season, index) => (
+ <button
+ type="button"
+ className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
+ key={index}
+ onClick={() => {
+ setLoading(true);
+ const data = hasilQuery[index].episodes;
+ const convertedData = convertData(data);
+ setSeason(index + 1);
+ setResultData(convertedData);
+ console.log(convertedData);
+ setLoading(false);
+ }}
+ >
+ <p>{season.season} </p>
+ </button>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {!loading && resultData && (
+ <div className="border rounded-md p-4 mt-4">
+ <h2 className="text-lg font-semibold mb-2">Season {season}</h2>
+ <pre>{JSON.stringify(resultData, null, 2)}</pre>
+ </div>
+ )}
+ {!loading && hasilQuery && (
+ <div className="border rounded-md p-4 mt-4">
+ {/* <h2 className="text-lg font-semibold mb-2">
+ Result Data,{" "}
+ {hasilQuery.length > 0 && `${hasilQuery.length} Seasons`}:
+ </h2> */}
+ <div className="flex flex-wrap gap-10">
+ {!hasilQuery.every((i) => i?.episodes) &&
+ hasilQuery?.map((i, index) => (
+ <div
+ key={i.id}
+ className="flex flex-col items-center gap-2"
+ >
+ <p className="font-karla font-semibold">
+ {i.releaseDate}
+ </p>
+ <Image
+ src={i.image}
+ width={500}
+ height={500}
+ className="w-[160px] h-[210px] object-cover"
+ />
+ <button
+ className="bg-blue-500 text-white w-[160px] py-1 rounded-md hover:bg-blue-600 text-sm"
+ onClick={() => {
+ setTmdbId(i.id);
+ }}
+ >
+ <p className="line-clamp-1 px-1">{i.title}</p>
+ </button>
+ </div>
+ ))}
+ </div>
+ <pre>{JSON.stringify(hasilQuery, null, 2)}</pre>
+ </div>
+ )}
+
+ {loading && <Loading />}
+ </div>
+ <div>
+ {/* {resultData && (
+ <div className="border rounded-md p-4 mt-4">
+ <h2 className="text-lg font-semibold mb-2">Result Data:</h2>
+ <pre>{JSON.stringify(resultData, null, 2)}</pre>
+ </div>
+ )} */}
+ </div>
+ </div>
+ </>
+ );
+}