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