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> + ); +};