From 530fc1aad07ba4036a0c4d0e625b56a92bda5b90 Mon Sep 17 00:00:00 2001
From: Kentai Radiquum <kentai.waah@gmail.com>
Date: Fri, 16 Aug 2024 15:32:22 +0500
Subject: [PATCH] add release search, adding and removing to collection create
 page

---
 .../ReleaseLink/ReleaseLink.16_9.tsx          |  14 +-
 .../ReleaseLink/ReleaseLink.Poster.tsx        | 109 ++++----
 app/pages/CreateCollection.tsx                | 247 +++++++++++++++++-
 3 files changed, 308 insertions(+), 62 deletions(-)

diff --git a/app/components/ReleaseLink/ReleaseLink.16_9.tsx b/app/components/ReleaseLink/ReleaseLink.16_9.tsx
index e17f73f..dcddc23 100644
--- a/app/components/ReleaseLink/ReleaseLink.16_9.tsx
+++ b/app/components/ReleaseLink/ReleaseLink.16_9.tsx
@@ -19,11 +19,19 @@ export const ReleaseLink169 = (props: any) => {
     user_list = profile_lists[profile_list_status];
   }
   return (
-    <Link href={`/release/${props.id}`}>
+    <Link
+      href={`/release/${props.id}`}
+      className={props.isLinkDisabled ? "pointer-events-none" : ""}
+      aria-disabled={props.isLinkDisabled}
+      tabIndex={props.isLinkDisabled ? -1 : undefined}
+    >
       <div className="w-full aspect-video group">
-        <div className="relative w-full h-full overflow-hidden bg-center bg-no-repeat bg-cover rounded-sm group-hover:animate-bg_zoom animate-bg_zoom_rev group-hover:[background-size:110%] " style={{
+        <div
+          className="relative w-full h-full overflow-hidden bg-center bg-no-repeat bg-cover rounded-sm group-hover:animate-bg_zoom animate-bg_zoom_rev group-hover:[background-size:110%] "
+          style={{
             backgroundImage: `linear-gradient(to bottom, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.9) 100%), url(${props.image})`,
-          }}>
+          }}
+        >
           <div className="absolute flex flex-wrap items-start justify-start gap-0.5 sm:gap-1 left-2 top-2">
             <Chip
               bg_color={
diff --git a/app/components/ReleaseLink/ReleaseLink.Poster.tsx b/app/components/ReleaseLink/ReleaseLink.Poster.tsx
index a46ba67..13ad6ef 100644
--- a/app/components/ReleaseLink/ReleaseLink.Poster.tsx
+++ b/app/components/ReleaseLink/ReleaseLink.Poster.tsx
@@ -19,62 +19,65 @@ export const ReleaseLinkPoster = (props: any) => {
     user_list = profile_lists[profile_list_status];
   }
   return (
-    <Link href={`/release/${props.id}`}>
-      <div className="flex flex-col w-full h-full gap-4 lg:flex-row">
-        <div
-          className="relative w-full h-64 gap-8 p-4 overflow-hidden bg-white bg-center bg-no-repeat bg-cover border border-gray-200 rounded-lg shadow-md lg:min-w-[300px] lg:min-h-[385px] lg:max-w-[300px] lg:max-h-[385px] lg:bg-top dark:border-gray-700 dark:bg-gray-800"
-          style={{
-            backgroundImage: `linear-gradient(to bottom, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.9) 100%), url(${props.image})`,
-          }}
-        >
-          <div className="flex flex-wrap gap-1">
+    <Link
+      href={`/release/${props.id}`}
+      className={props.isLinkDisabled ? "pointer-events-none" : ""}
+      aria-disabled={props.isLinkDisabled}
+      tabIndex={props.isLinkDisabled ? -1 : undefined}
+    >
+      <div
+        className="relative w-full h-64 gap-8 p-2 overflow-hidden bg-white bg-center bg-no-repeat bg-cover border border-gray-200 rounded-lg shadow-md lg:min-w-[300px] lg:min-h-[385px] lg:max-w-[300px] lg:max-h-[385px] lg:bg-top dark:border-gray-700 dark:bg-gray-800"
+        style={{
+          backgroundImage: `linear-gradient(to bottom, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.9) 100%), url(${props.image})`,
+        }}
+      >
+        <div className="flex flex-wrap gap-1">
+          <Chip
+            bg_color={
+              props.grade.toFixed(1) == 0
+                ? "hidden"
+                : props.grade.toFixed(1) < 2
+                ? "bg-red-500"
+                : props.grade.toFixed(1) < 3
+                ? "bg-orange-500"
+                : props.grade.toFixed(1) < 4
+                ? "bg-yellow-500"
+                : "bg-green-500"
+            }
+            name={props.grade.toFixed(1)}
+          />
+          {props.status ? (
+            <Chip name={props.status.name} />
+          ) : (
             <Chip
-              bg_color={
-                props.grade.toFixed(1) == 0
-                  ? "hidden"
-                  : props.grade.toFixed(1) < 2
-                  ? "bg-red-500"
-                  : props.grade.toFixed(1) < 3
-                  ? "bg-orange-500"
-                  : props.grade.toFixed(1) < 4
-                  ? "bg-yellow-500"
-                  : "bg-green-500"
+              name={
+                props.status_id == 1
+                  ? "Завершено"
+                  : props.status_id == 2
+                  ? "Онгоинг"
+                  : "Анонс"
               }
-              name={props.grade.toFixed(1)}
             />
-            {props.status ? (
-              <Chip name={props.status.name} />
-            ) : (
-              <Chip
-                name={
-                  props.status_id == 1
-                    ? "Завершено"
-                    : props.status_id == 2
-                    ? "Онгоинг"
-                    : "Анонс"
-                }
-              />
-            )}
-            <Chip
-              name={props.episodes_released && props.episodes_released}
-              name_2={
-                props.episodes_total ? props.episodes_total + " эп." : "? эп."
-              }
-              devider="/"
-            />
-          </div>
-          <div className="absolute flex flex-col gap-2 text-white bottom-4">
-            {props.title_ru && (
-              <p className="text-xl font-bold text-white md:text-2xl">
-                {props.title_ru}
-              </p>
-            )}
-            {props.title_original && (
-              <p className="text-sm text-gray-300 md:text-base">
-                {props.title_original}
-              </p>
-            )}
-          </div>
+          )}
+          <Chip
+            name={props.episodes_released && props.episodes_released}
+            name_2={
+              props.episodes_total ? props.episodes_total + " эп." : "? эп."
+            }
+            devider="/"
+          />
+        </div>
+        <div className="absolute flex flex-col gap-2 text-white bottom-4 left-2 right-2">
+          {props.title_ru && (
+            <p className="text-xl font-bold text-white md:text-2xl">
+              {props.title_ru}
+            </p>
+          )}
+          {props.title_original && (
+            <p className="text-sm text-gray-300 md:text-base">
+              {props.title_original}
+            </p>
+          )}
         </div>
       </div>
     </Link>
diff --git a/app/pages/CreateCollection.tsx b/app/pages/CreateCollection.tsx
index 0f2cd40..9269b56 100644
--- a/app/pages/CreateCollection.tsx
+++ b/app/pages/CreateCollection.tsx
@@ -1,6 +1,8 @@
 "use client";
+import useSWR from "swr";
+import useSWRInfinite from "swr/infinite";
 import { useUserStore } from "#/store/auth";
-import { useEffect, useState } from "react";
+import { useEffect, useState, useCallback } from "react";
 import { useSearchParams, useRouter } from "next/navigation";
 import { ENDPOINTS } from "#/api/config";
 import {
@@ -11,7 +13,24 @@ import {
   Textarea,
   FileInput,
   Label,
+  Modal,
 } from "flowbite-react";
+import { ReleaseSection } from "#/components/ReleaseSection/ReleaseSection";
+import { ReleaseLink } from "#/components/ReleaseLink/ReleaseLink";
+
+const fetcher = async (url: string) => {
+  const res = await fetch(url);
+
+  if (!res.ok) {
+    const error = new Error(
+      `An error occurred while fetching the data. status: ${res.status}`
+    );
+    error.message = await res.json();
+    throw error;
+  }
+
+  return res.json();
+};
 
 export const CreateCollectionPage = () => {
   const userStore = useUserStore();
@@ -31,6 +50,9 @@ export const CreateCollectionPage = () => {
     title: 0,
     description: 0,
   });
+  const [addedReleases, setAddedReleases] = useState([]);
+  const [addedReleasesIds, setAddedReleasesIds] = useState([]);
+  const [releasesEditModalOpen, setReleasesEditModalOpen] = useState(false);
 
   const collection_id = searchParams.get("id") || null;
   const mode = searchParams.get("mode") || null;
@@ -105,6 +127,21 @@ export const CreateCollectionPage = () => {
     });
   }
 
+  function _deleteRelease(release: any) {
+    let releasesArray = [];
+    let idsArray = [];
+
+    for (let i = 0; i < addedReleases.length; i++) {
+      if (addedReleases[i].id != release.id) {
+        releasesArray.push(addedReleases[i]);
+        idsArray.push(addedReleasesIds[i]);
+      }
+    }
+
+    setAddedReleases(releasesArray);
+    setAddedReleasesIds(idsArray);
+  }
+
   return (
     <main className="container pt-2 pb-16 mx-auto sm:pt-4 sm:pb-0">
       <Card>
@@ -112,14 +149,14 @@ export const CreateCollectionPage = () => {
           {edit ? "Редактирование коллекции" : "Создание коллекции"}
         </p>
         <form
-          className="flex flex-wrap items-center w-full gap-2"
+          className="flex flex-col w-full gap-2 lg:items-center lg:flex-row"
           onSubmit={(e) => submit(e)}
         >
           <Label
             htmlFor="dropzone-file"
-            className="flex flex-col items-center w-[600px] h-[337px] border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:hover:border-gray-500 dark:hover:bg-gray-600"
+            className="flex flex-col items-center w-full sm:max-w-[600px] h-[337px] border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:hover:border-gray-500 dark:hover:bg-gray-600"
           >
-            <div className="flex flex-col items-center justify-center w-[595px] h-[inherit] rounded-[inherit] pt-5 pb-6 overflow-hidden">
+            <div className="flex flex-col items-center justify-center max-w-[595px] h-[inherit] rounded-[inherit] pt-5 pb-6 overflow-hidden">
               {!imageUrl ? (
                 <>
                   <svg
@@ -180,7 +217,9 @@ export const CreateCollectionPage = () => {
               value={collectionInfo.title}
               maxLength={60}
             />
-            <p className="text-sm text-gray-500 dark:text-gray-300">{stringLength.title}/60</p>
+            <p className="text-sm text-gray-500 dark:text-gray-300">
+              {stringLength.title}/60
+            </p>
             <div className="block mt-2 mb-2">
               <Label
                 htmlFor="description"
@@ -196,7 +235,9 @@ export const CreateCollectionPage = () => {
               value={collectionInfo.description}
               maxLength={1000}
             />
-            <p className="text-sm text-gray-500 dark:text-gray-300">{stringLength.description}/1000</p>
+            <p className="text-sm text-gray-500 dark:text-gray-300">
+              {stringLength.description}/1000
+            </p>
             <div className="mt-2">
               <div className="flex items-center gap-1">
                 <Checkbox
@@ -214,6 +255,200 @@ export const CreateCollectionPage = () => {
           </div>
         </form>
       </Card>
+      <div className="mt-4">
+        <div className="flex justify-between px-4 py-2 border-b-2 border-black dark:border-white">
+          <h1 className="font-bold text-md sm:text-xl md:text-lg xl:text-xl">
+            {"Релизов в коллекции: " + addedReleases.length}/100
+          </h1>
+          <Button
+            color={"blue"}
+            size={"xs"}
+            onClick={() => setReleasesEditModalOpen(!releasesEditModalOpen)}
+          >
+            Добавить
+          </Button>
+        </div>
+        <div className="m-4">
+          <div className="grid justify-center sm:grid-cols-[repeat(auto-fit,minmax(400px,1fr))] grid-cols-[100%] gap-2 min-w-full">
+            {addedReleases.map((release) => {
+              return (
+                <div
+                  key={release.id}
+                  className="relative w-full h-full aspect-video group"
+                >
+                  <button
+                    className="absolute inset-0 z-10 text-black transition-opacity bg-white opacity-0 group-hover:opacity-75"
+                    onClick={() => _deleteRelease(release)}
+                  >
+                    Удалить
+                  </button>
+                  <ReleaseLink {...release} isLinkDisabled={true} />
+                </div>
+              );
+            })}
+            {addedReleases.length == 1 && <div></div>}
+          </div>
+        </div>
+      </div>
+      <ReleasesEditModal
+        isOpen={releasesEditModalOpen}
+        setIsOpen={setReleasesEditModalOpen}
+        releases={addedReleases}
+        releasesIds={addedReleasesIds}
+        setReleases={setAddedReleases}
+        setReleasesIds={setAddedReleasesIds}
+      />
     </main>
   );
 };
+
+export const ReleasesEditModal = (props: {
+  isOpen: boolean;
+  setIsOpen: any;
+  releases: any;
+  setReleases: any;
+  releasesIds: any;
+  setReleasesIds: any;
+}) => {
+  const [query, setQuery] = useState("");
+
+  const getKey = (pageIndex: number, previousPageData: any) => {
+    if (previousPageData && !previousPageData.releases.length) return null;
+
+    const url = new URL("/api/search", window.location.origin);
+    url.searchParams.set("page", pageIndex.toString());
+    if (!query) return null
+    url.searchParams.set("q", query);
+    return url.toString();
+  };
+
+  const { data, error, isLoading, size, setSize } = useSWRInfinite(
+    getKey,
+    fetcher,
+    { initialSize: 2, revalidateFirstPage: false }
+  );
+
+  const [content, setContent] = useState([]);
+  useEffect(() => {
+    if (data) {
+      let allReleases = [];
+      for (let i = 0; i < data.length; i++) {
+        allReleases.push(...data[i].releases);
+      }
+      setContent(allReleases);
+    }
+  }, [data]);
+
+  const [currentRef, setCurrentRef] = useState<any>(null);
+  const modalRef = useCallback((ref) => {
+    setCurrentRef(ref);
+  }, []);
+
+  const [scrollPosition, setScrollPosition] = useState(0);
+  function handleScroll() {
+    const height = currentRef.scrollHeight - currentRef.clientHeight;
+    const windowScroll = currentRef.scrollTop;
+    const scrolled = (windowScroll / height) * 100;
+    setScrollPosition(Math.floor(scrolled));
+  }
+  useEffect(() => {
+    if (scrollPosition >= 95 && scrollPosition <= 96) {
+      setSize(size + 1);
+    }
+  }, [scrollPosition]);
+
+  function _addRelease(release: any) {
+    if (props.releasesIds.length == 100) {
+      alert("Достигнуто максимальное количество релизов в коллекции - 100");
+      return;
+    }
+
+    if (props.releasesIds.includes(release.id)) {
+      alert("Релиз уже добавлен в коллекцию");
+      return;
+    }
+
+    props.setReleases([...props.releases, release]);
+    props.setReleasesIds([...props.releasesIds, release.id]);
+  }
+
+  return (
+    <Modal
+      dismissible
+      show={props.isOpen}
+      onClose={() => props.setIsOpen(false)}
+      size={"7xl"}
+    >
+      <Modal.Header>Изменить релизы в коллекции</Modal.Header>
+      <div
+        onScroll={handleScroll}
+        ref={modalRef}
+        className="px-4 py-4 overflow-auto"
+      >
+        <form
+          className="max-w-full mx-auto"
+          onSubmit={(e) => {
+            e.preventDefault();
+            props.setReleases([]);
+            setQuery(e.target[0].value.trim());
+          }}
+        >
+          <label
+            htmlFor="default-search"
+            className="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white"
+          >
+            Поиск
+          </label>
+          <div className="relative">
+            <div className="absolute inset-y-0 flex items-center pointer-events-none start-0 ps-3">
+              <svg
+                className="w-4 h-4 text-gray-500 dark:text-gray-400"
+                aria-hidden="true"
+                xmlns="http://www.w3.org/2000/svg"
+                fill="none"
+                viewBox="0 0 20 20"
+              >
+                <path
+                  stroke="currentColor"
+                  strokeLinecap="round"
+                  strokeLinejoin="round"
+                  strokeWidth="2"
+                  d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"
+                />
+              </svg>
+            </div>
+            <input
+              type="search"
+              id="default-search"
+              className="block w-full p-4 text-sm text-gray-900 border border-gray-300 rounded-lg ps-10 bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
+              placeholder="Поиск аниме..."
+              required
+              defaultValue={query || ""}
+            />
+            <button
+              type="submit"
+              className="text-white absolute end-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
+            >
+              Поиск
+            </button>
+          </div>
+        </form>
+
+        <div className="flex flex-wrap gap-1 mt-2">
+          {content.map((release) => {
+            return (
+              <button
+                key={release.id}
+                className=""
+                onClick={() => _addRelease(release)}
+              >
+                <ReleaseLink type="poster" {...release} isLinkDisabled={true} />
+              </button>
+            );
+          })}
+          {content.length == 1 && <div></div>}
+        </div>
+      </div>
+    </Modal>
+  );
+};