From eb620c0b1dd365ab75b919040f7bd3744e1288cf Mon Sep 17 00:00:00 2001
From: Kentai Radiquum <kentai.waah@gmail.com>
Date: Wed, 17 Jul 2024 12:58:19 +0500
Subject: [PATCH] feat: add history api and page

---
 app/api/history/route.js                   | 19 +++++
 app/api/utils.js                           | 52 ++++++++++---
 app/components/ReleaseLink/ReleaseLink.jsx | 40 +++++++---
 app/history/page.js                        |  9 +++
 app/pages/History.jsx                      | 88 ++++++++++++++++++++++
 5 files changed, 186 insertions(+), 22 deletions(-)
 create mode 100644 app/api/history/route.js
 create mode 100644 app/history/page.js
 create mode 100644 app/pages/History.jsx

diff --git a/app/api/history/route.js b/app/api/history/route.js
new file mode 100644
index 0000000..75e650a
--- /dev/null
+++ b/app/api/history/route.js
@@ -0,0 +1,19 @@
+import { NextResponse } from "next/server";
+import { fetchDataViaGet } from "../utils";
+import { ENDPOINTS } from "../config";
+
+export async function GET(request) {
+  const page = parseInt(request.nextUrl.searchParams.get(["page"])) || 0;
+  const token = request.nextUrl.searchParams.get(["token"]) || null;
+  const sortName = request.nextUrl.searchParams.get(["sort"]) || "adding_descending";
+
+  if (!token || token == "null") {
+    return NextResponse.json({ message: "No token provided" }, { status: 403 });
+  }
+
+  let url = new URL(`${ENDPOINTS.user.history}/${page}`);
+  url.searchParams.set("token", token);
+
+  const response = await fetchDataViaGet(url.toString());
+  return NextResponse.json(response);
+}
diff --git a/app/api/utils.js b/app/api/utils.js
index 5c6b1ee..2d7ce12 100644
--- a/app/api/utils.js
+++ b/app/api/utils.js
@@ -44,14 +44,17 @@ export const fetchDataViaPost = async (url, body, API_V2) => {
 
 export const authorize = async (url, data) => {
   try {
-    const response = await fetch(`${url}?login=${data.login}&password=${data.password}`, {
-      method: "POST",
-      headers: {
-        "User-Agent": USER_AGENT,
-        Sign: "9aa5c7af74e8cd70c86f7f9587bde23d",
-        "Content-Type": "application/x-www-form-urlencoded",
-      },
-    });
+    const response = await fetch(
+      `${url}?login=${data.login}&password=${data.password}`,
+      {
+        method: "POST",
+        headers: {
+          "User-Agent": USER_AGENT,
+          Sign: "9aa5c7af74e8cd70c86f7f9587bde23d",
+          "Content-Type": "application/x-www-form-urlencoded",
+        },
+      }
+    );
     if (response.status !== 200) {
       throw new Error("Error authorizing user");
     }
@@ -74,9 +77,34 @@ export function removeJWT() {
 }
 
 export function numberDeclension(number, one, two, five) {
-  if (number > 10 && [11, 12, 13, 14].includes(number%100)) return five;
-  let last_num = number%10;
+  if (number > 10 && [11, 12, 13, 14].includes(number % 100)) return five;
+  let last_num = number % 10;
   if (last_num == 1) return one;
-  if ([2,3,4].includes(last_num)) return two;
-  if ([5,6,7,8,9, 0].includes(last_num)) return five;
+  if ([2, 3, 4].includes(last_num)) return two;
+  if ([5, 6, 7, 8, 9, 0].includes(last_num)) return five;
+}
+
+export function sinceUnixDate(unixInSeconds) {
+  const unix = Math.floor(unixInSeconds * 1000);
+  const date = new Date(unix);
+  const currentDate = new Date().valueOf();
+  const dateDifferenceSeconds = new Date(currentDate - unix) / 1000;
+
+  const minutes = Math.floor(dateDifferenceSeconds / 60)
+  const hours = Math.floor(dateDifferenceSeconds / 3600);
+  const days = Math.floor(dateDifferenceSeconds / 86400);
+
+  const minutesName = numberDeclension(minutes, "минута", "минуты", "минут");
+  const hoursName = numberDeclension(hours, "час", "часа", "часов");
+  const daysName = numberDeclension(days, "день", "дня", "дней");
+
+  if (dateDifferenceSeconds < 60) return "менее минуты назад";
+  if (dateDifferenceSeconds < 3600)
+    return `${minutes} ${minutesName} назад`;
+  if (dateDifferenceSeconds < 86400)
+    return `${hours} ${hoursName} назад`;
+  if (dateDifferenceSeconds < 2592000)
+    return `${days} ${daysName} назад`;
+
+  return date.toLocaleString("ru-RU").split(",")[0];
 }
diff --git a/app/components/ReleaseLink/ReleaseLink.jsx b/app/components/ReleaseLink/ReleaseLink.jsx
index 8f6c7f6..91b8517 100644
--- a/app/components/ReleaseLink/ReleaseLink.jsx
+++ b/app/components/ReleaseLink/ReleaseLink.jsx
@@ -1,4 +1,5 @@
 import Link from "next/link";
+import { sinceUnixDate } from "@/app/api/utils";
 
 export const ReleaseLink = (props) => {
   const grade = props.grade.toFixed(1);
@@ -25,7 +26,7 @@ export const ReleaseLink = (props) => {
             src={props.image}
             alt=""
           />
-          <div className="absolute flex flex-col items-start justify-start gap-1 left-2 top-2">
+          <div className="absolute flex flex-wrap items-start justify-start max-w-[45%] gap-0.5 sm:gap-1 left-2 top-2">
             <div className="flex gap-1 ">
               <div
                 className={`rounded-sm ${
@@ -40,15 +41,10 @@ export const ReleaseLink = (props) => {
                     : "bg-green-500"
                 }`}
               >
-                <p className="px-2 sm:px-4 py-0.5 sm:py-1 text-xs xl:text-base text-white">
+                <p className="px-2 sm:px-4 py-0.5 sm:py-1 text-xs sm:text-base text-white">
                   {grade}
                 </p>
               </div>
-              {props.is_favorite && (
-                <div className="flex items-center justify-center bg-pink-500 rounded-sm">
-                  <span className="w-6 h-full bg-white sm:w-8 sm:h-8 iconify mdi--heart"></span>
-                </div>
-              )}
             </div>
             {user_list && (
               <div className={`rounded-sm ${user_list.bg_color}`}>
@@ -58,16 +54,16 @@ export const ReleaseLink = (props) => {
               </div>
             )}
           </div>
-          <div className="absolute flex flex-col items-end gap-1 top-2 right-2">
+          <div className="absolute flex flex-wrap max-w-[45%] justify-end items-end gap-0.5 sm:gap-1 top-2 right-2">
             {props.status ? (
               <div className="bg-gray-500 rounded-sm">
-                <p className="px-2 sm:px-4 py-0.5 sm:py-1 text-xs xl:text-base text-white">
+                <p className="px-2 sm:px-4 py-0.5 sm:py-1 text-xs xl:text-base text-white text-center">
                   {props.status.name}
                 </p>
               </div>
             ) : (
               <div className="bg-gray-500 rounded-sm">
-                <p className="px-2 sm:px-4 py-0.5 sm:py-1 text-xs xl:text-base text-white">
+                <p className="px-2 sm:px-4 py-0.5 sm:py-1 text-xs xl:text-base text-white text-center">
                   {props.status_id == 1
                     ? "Завершено"
                     : props.status_id == 2
@@ -88,6 +84,30 @@ export const ReleaseLink = (props) => {
                 )}
               </div>
             </div>
+            {props.is_favorite && (
+              <div className="flex items-center justify-center bg-pink-500 rounded-sm">
+                <span className="w-3 px-4 py-2.5 text-white sm:px-4 sm:py-3 xl:px-6 xl:py-4 iconify mdi--heart"></span>
+              </div>
+            )}
+            {props.last_view_episode && (
+              <div className="bg-gray-500 rounded-sm">
+                <div className="px-2 sm:px-4 py-0.5 sm:py-1 text-xs xl:text-base text-white flex">
+                  {props.last_view_episode.name ? (
+                    <p>{`${props.last_view_episode.name}`} </p>
+                  ) : (
+                    <p>{`${props.last_view_episode.position + 1} серия`}</p>
+                  )}
+                </div>
+              </div>
+            )}
+            {"last_view_timestamp" in props &&
+              props.last_view_timestamp != 0 && (
+                <div className="bg-gray-500 rounded-sm">
+                  <div className="px-2 sm:px-4 py-0.5 sm:py-1 text-xs xl:text-base text-white flex">
+                    <p>{sinceUnixDate(props.last_view_timestamp)}</p>
+                  </div>
+                </div>
+              )}
           </div>
           <p className="absolute text-xs text-white xl:text-base lg:text-lg left-2 bottom-2 right-2">
             {props.title_ru}
diff --git a/app/history/page.js b/app/history/page.js
new file mode 100644
index 0000000..79e029d
--- /dev/null
+++ b/app/history/page.js
@@ -0,0 +1,9 @@
+export const metadata = {
+  title: "История",
+};
+
+import { HistoryPage } from "@/app/pages/History";
+
+export default function Index() {
+  return <HistoryPage />;
+}
diff --git a/app/pages/History.jsx b/app/pages/History.jsx
new file mode 100644
index 0000000..26367a0
--- /dev/null
+++ b/app/pages/History.jsx
@@ -0,0 +1,88 @@
+"use client";
+import useSWRInfinite from "swr/infinite";
+import { ReleaseSection } from "@/app/components/ReleaseSection/ReleaseSection";
+import { Spinner } from "@/app/components/Spinner/Spinner";
+import { useState, useEffect } from "react";
+import { useScrollPosition } from "@/app/hooks/useScrollPosition";
+import { useUserStore } from "../store/auth";
+
+const fetcher = async (url) => {
+  const res = await fetch(url);
+
+  if (!res.ok) {
+    const error = new Error("An error occurred while fetching the data.");
+    error.info = await res.json();
+    error.status = res.status;
+    throw error;
+  }
+
+  return res.json();
+};
+
+export function HistoryPage() {
+  const token = useUserStore((state) => state.token);
+  const [isLoadingEnd, setIsLoadingEnd] = useState(false);
+
+  const getKey = (pageIndex, previousPageData) => {
+    if (previousPageData && !previousPageData.content.length) return null;
+    return `/api/history?page=${pageIndex}&token=${token}`;
+  };
+
+  const { data, error, isLoading, size, setSize } = useSWRInfinite(
+    getKey,
+    fetcher,
+    { initialSize: 2 }
+  );
+
+  const [content, setContent] = useState(null);
+  useEffect(() => {
+    if (data) {
+      let allReleases = [];
+      for (let i = 0; i < data.length; i++) {
+        allReleases.push(...data[i].content);
+      }
+      setContent(allReleases);
+      setIsLoadingEnd(true);
+    }
+  }, [data]);
+
+  const scrollPosition = useScrollPosition();
+  useEffect(() => {
+    if (scrollPosition >= 98 && scrollPosition <= 99) {
+      setSize(size + 1);
+    }
+  }, [scrollPosition]);
+
+  return (
+    <main className="container pt-2 pb-16 mx-auto sm:pt-4 sm:pb-0">
+      <div className="flex items-center justify-between px-4 py-2 border-b-2 border-black">
+        <h1 className="font-bold text-md sm:text-xl md:text-lg xl:text-xl">
+          История
+        </h1>
+      </div>
+      {content && content.length > 0 ? (
+        <ReleaseSection content={content} />
+      ) : !isLoadingEnd || isLoading ? (
+        <div className="flex flex-col items-center justify-center min-w-full min-h-screen">
+          <Spinner />
+        </div>
+      ) : (
+        <div className="flex flex-col items-center justify-center min-w-full gap-4 mt-12 text-xl">
+          <span className="w-24 h-24 iconify-color twemoji--broken-heart"></span>
+          <p>В истории пока ничего нет...</p>
+        </div>
+      )}
+      {data &&
+        data[data.length - 1].current_page <
+          data[data.length - 1].total_page_count && (
+          <button
+            className="mx-auto w-[calc(100%-10rem)] border border-black rounded-lg p-2 mb-6 flex items-center justify-center gap-2 hover:bg-black hover:text-white transition"
+            onClick={() => setSize(size + 1)}
+          >
+            <span className="w-10 h-10 iconify mdi--plus"></span>
+            <span className="text-lg">Загрузить ещё</span>
+          </button>
+        )}
+    </main>
+  );
+}