Compare commits

...

43 commits
3.4.0 ... V3

Author SHA1 Message Date
762c2f324a
feat: add user profile comments preview
Some checks are pending
V3 Preview Deployment / Deploy-Preview (push) Waiting to run
2025-04-04 15:25:16 +05:00
5572f31c60
remove video from profile counters 2025-04-04 14:38:46 +05:00
256ecea885
feat: add profile friend view 2025-04-04 14:15:47 +05:00
0730b7c7d4
add empty states
Some checks are pending
V3 Preview Deployment / Deploy-Preview (push) Waiting to run
2025-04-04 06:04:23 +05:00
0cd74983f3
feat: add user friends preview 2025-04-04 05:58:13 +05:00
2fce051a54
feat: add counts to activity 2025-04-04 05:29:36 +05:00
339d4150b1
feat: collection activity preview 2025-04-04 05:26:08 +05:00
6cc9cdaa9e
fix: color issues
Some checks are pending
V3 Preview Deployment / Deploy-Preview (push) Waiting to run
2025-04-04 03:44:12 +05:00
442a046043
remove unused deps 2025-04-04 03:17:48 +05:00
29ae447636
Revert to "fix: next plugins use"
This reverts commit 141cb9a1ce.
2025-04-04 03:14:00 +05:00
141cb9a1ce
fix: next plugins use 2025-04-04 01:05:37 +05:00
7df11a467a
fix build fr 2025-04-04 00:44:00 +05:00
491f9b48b5
fix build and lints 2025-04-04 00:41:04 +05:00
329448c9fc
chore: migrate to new flowbite-react 2025-04-04 00:29:19 +05:00
8daab3b3c1
feat: add view of user blocklist 2025-04-03 22:56:07 +05:00
4aa48f589b
small changes 2025-04-03 22:10:58 +05:00
c4c422904e
refactor: user recently watched 2025-04-03 21:59:18 +05:00
cbdcfedb9b
refactor: return user stats & made release votes preview as a list 2025-04-03 21:38:51 +05:00
8cf1bb534d
refactor: start redesign of user page 2025-04-03 21:07:44 +05:00
68a7117a97
allow fullscreen iframe if cannot use custom player
Some checks failed
V3 Preview Deployment / Deploy-Preview (push) Has been cancelled
2025-03-29 20:19:09 +05:00
9e4bf1ac71
feat: allow to drag slider in episode selector 2025-03-29 20:17:22 +05:00
66721c6d6a
chore: update packages 2025-03-29 20:00:07 +05:00
8ab668dcd6
chore: update readme
Some checks failed
V3 Preview Deployment / Deploy-Preview (push) Has been cancelled
2025-03-27 22:17:33 +05:00
663633c979
chore: bump version [3.5.0] 2025-03-27 22:10:49 +05:00
265be8d1e1
change line-height to 1 on poster text 2025-03-27 22:03:03 +05:00
7b97b33951
feat: add fifth navbar button settings for mobile 2025-03-27 21:55:53 +05:00
1959fcd437
refactor: remove 'show only link names in navbar' in settings on mobile 2025-03-27 20:54:43 +05:00
bf67b5d928
refactor: move mobile menu to the bottom on mobile 2025-03-27 20:41:53 +05:00
2241a8a226
feat: add link to about page in settings 2025-03-27 20:27:15 +05:00
956d35579b
feat: add about page 2025-03-26 16:53:54 +05:00
04b580d239
fix: reset player error on episode change 2025-03-26 15:53:27 +05:00
07c93338cb
fix: poster aspect ration in release carousel
Some checks are pending
V3 Preview Deployment / Deploy-Preview (push) Waiting to run
2025-03-26 15:45:25 +05:00
ac2425ba55
refactor: licensed platform location 2025-03-26 15:34:58 +05:00
ba5c149779
refactor: Profile release ratings 2025-03-26 01:57:20 +05:00
f6b8202877
remove old stuff 2025-03-26 01:50:10 +05:00
5abb6e8f11
refactor: create collection page release poster 2025-03-26 01:47:47 +05:00
967b9cfbb0
refactor: profile page recently watched 2025-03-26 01:08:59 +05:00
1530fa3937
refactor: release section 2025-03-26 00:51:30 +05:00
e1e176c24b
refactor: release courusel 2025-03-26 00:08:33 +05:00
34bbcc4893
fix: link 2025-03-25 23:45:14 +05:00
4496a7280b
refactor: related releases page 2025-03-25 23:35:20 +05:00
6cade5a7d0
refactor: update style of Related Releases Card on Release page 2025-03-25 23:01:11 +05:00
d2b38dcbe2
refactor: set release poster to center on release page on mobile
BREAKING: disable all image rendering
2025-03-25 20:39:25 +05:00
74 changed files with 3807 additions and 1776 deletions

View file

@ -1,7 +1,3 @@
{
"extends": ["next/core-web-vitals", "prettier"],
"rules": {
"prettier/prettier": "error"
},
"plugins": ["prettier"]
"extends": ["next/core-web-vitals"]
}

2
.flowbite-react/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
class-list.json
pid

View file

@ -0,0 +1,9 @@
{
"$schema": "https://unpkg.com/flowbite-react/schema.json",
"components": [],
"dark": true,
"prefix": "",
"path": "src/components",
"tsx": true,
"rsc": true
}

2
.gitignore vendored
View file

@ -59,3 +59,5 @@ videos/*
!videos/*.js
!videos/*.ts
public/_next-video
API-Trace/*

View file

@ -6,10 +6,10 @@ AniX is an unofficial web client for the Android application Anixart. It allows
## Changelog [RU]
- [3.5.0](./public/changelog/3.5.0.md)
- [3.4.0](./public/changelog/3.4.0.md)
- [3.3.0](./public/changelog/3.3.0.md)
- [3.2.3](./public/changelog/3.2.3.md)
- [3.2.2](./public/changelog/3.2.2.md)
[other versions](./public/changelog)

View file

@ -4,7 +4,7 @@ import { usePreferencesStore } from "./store/preferences";
import { Navbar } from "./components/Navbar/NavbarUpdate";
import { Inter } from "next/font/google";
import { useEffect, useState } from "react";
import { Button, Modal } from "flowbite-react";
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from "flowbite-react";
import { Spinner } from "./components/Spinner/Spinner";
import { ChangelogModal } from "#/components/ChangelogModal/ChangelogModal";
import PlausibleProvider from "next-plausible";
@ -80,8 +80,8 @@ export const App = (props) => {
show={preferencesStore.params.isFirstLaunch}
onClose={() => preferencesStore.setParams({ isFirstLaunch: false })}
>
<Modal.Header>Внимание</Modal.Header>
<Modal.Body>
<ModalHeader>Внимание</ModalHeader>
<ModalBody>
<p>
Данный сайт не связан с разработчиками приложения Anixart, это
неофициальная имплементация веб клиента для этого приложения.
@ -94,15 +94,15 @@ export const App = (props) => {
На сайте могут присутствовать ошибки и не доработки, а так-же
отсутствующий функционал.
</p>
</Modal.Body>
<Modal.Footer>
</ModalBody>
<ModalFooter>
<Button
color={"blue"}
onClick={() => preferencesStore.setParams({ isFirstLaunch: false })}
>
Принимаю
</Button>
</Modal.Footer>
</ModalFooter>
</Modal>
{preferencesStore.flags.enableAnalytics && (
<PlausibleProvider

16
app/about/page.tsx Normal file
View file

@ -0,0 +1,16 @@
export const metadata = {
title: "О приложении",
openGraph: {
title: "AniX - Неофициальный веб клиент для Anixart",
description:
"AniX - это неофициальный веб-клиент для Android-приложения Anixart. Он позволяет вам получать доступ к своей учетной записи Anixart и управлять ею из веб-браузера. Так-же можно синхронизировать и управлять списками и избранным. И самое главное смотреть все доступные аниме из базы Anixart.",
},
};
export const dynamic = "force-static";
import { AboutPage } from "#/pages/About";
export default function Index() {
return <AboutPage />;
}

View file

@ -1,4 +1,4 @@
export const CURRENT_APP_VERSION = "3.4.0";
export const CURRENT_APP_VERSION = "3.5.0";
export const API_URL = "https://api.anixart.tv";
export const API_PREFIX = "/api/proxy";
@ -18,6 +18,15 @@ export const ENDPOINTS = {
bookmark: `${API_PREFIX}/profile/list`,
history: `${API_PREFIX}/history`,
favorite: `${API_PREFIX}/favorite`,
blocklist: `${API_PREFIX}/profile/blocklist`,
friend: {
list: `${API_PREFIX}/profile/friend/all`,
add: `${API_PREFIX}/profile/friend/request/send`,
remove: `${API_PREFIX}/profile/friend/request/remove`,
hide: `${API_PREFIX}/profile/friend/request/hide`,
in: `${API_PREFIX}/profile/friend/requests/in`,
out: `${API_PREFIX}/profile/friend/requests/out`,
},
settings: {
my: `${API_PREFIX}/profile/preference/my`,
login: {

View file

@ -212,9 +212,9 @@ export function unixToDate(
" " +
date.getFullYear() +
", " +
date.getHours() +
`${date.getHours()}`.padStart(2, "0") +
":" +
date.getMinutes()
`${date.getMinutes()}`.padStart(2, "0")
);
if (type === "dayMonth")
return date.getDate() + " " + months[date.getMonth()];

View file

@ -1,6 +1,14 @@
"use client";
import { Modal, Accordion } from "flowbite-react";
import {
Accordion,
AccordionContent,
AccordionPanel,
AccordionTitle,
Modal,
ModalBody,
ModalHeader,
} from "flowbite-react";
import Markdown from "markdown-to-jsx";
import { useEffect, useState } from "react";
import Styles from "./ChangelogModal.module.css";
@ -37,8 +45,8 @@ export const ChangelogModal = (props: {
return (
<Modal show={props.isOpen} onClose={() => props.setIsOpen(false)}>
<Modal.Header>Список изменений v{props.version}</Modal.Header>
<Modal.Body>
<ModalHeader>Список изменений v{props.version}</ModalHeader>
<ModalBody>
<Markdown className={Styles.markdown}>
{currentVersionChangelog}
</Markdown>
@ -46,8 +54,8 @@ export const ChangelogModal = (props: {
{props.previousVersions.length > 0 &&
props.previousVersions.map((version) => {
return (
<Accordion.Panel key={version}>
<Accordion.Title
<AccordionPanel key={version}>
<AccordionTitle
onClickCapture={(e) => {
if (!previousVersionsChangelog.hasOwnProperty(version)) {
_fetchVersionChangelog(version).then((data) => {
@ -62,19 +70,19 @@ export const ChangelogModal = (props: {
}}
>
Список изменений v{version}
</Accordion.Title>
<Accordion.Content>
</AccordionTitle>
<AccordionContent>
{previousVersionsChangelog.hasOwnProperty(version) ?
<Markdown className={Styles.markdown}>
{previousVersionsChangelog[version]}
</Markdown>
: <div>Загрузка ...</div>}
</Accordion.Content>
</Accordion.Panel>
</AccordionContent>
</AccordionPanel>
);
})}
</Accordion>
</Modal.Body>
</ModalBody>
</Modal>
);
};

View file

@ -6,26 +6,22 @@ export const Chip = (props: {
devider?: string;
bg_color?: string;
style?: React.CSSProperties;
className?: string;
}) => {
return (
<div
className={`px-2 sm:px-4 py-0.5 sm:py-1 rounded-sm ${
props.bg_color || "bg-gray-500"
} ${props.icon_name ? "flex items-center justify-center gap-1" : ""}`}
style={props.style || {}}
>
<div className={`${props.bg_color || "bg-gray-500"} rounded-sm flex items-center justify-center ${props.className || ""}`}>
{props.icon_name && (
<span
className={`iconify w-4 h-4 sm:w-6 sm:h-6 ${props.icon_name}`}
className={`iconify w-4 h-4 sm:w-6 sm:h-6 ml-2 ${props.icon_name}`}
style={
{
"color": "var(--icon-color)",
color: "var(--icon-color)",
"--icon-color": props.icon_color || "#fff",
} as React.CSSProperties
}
></span>
)}
<p className="text-xs text-white xl:text-base">
<p className="px-2 py-1 text-white xl:text-base">
{props.name}
{props.name && props.devider ? props.devider : " "}
{props.name_2}

View file

@ -49,11 +49,13 @@ export const CollectionLink = (props: any) => {
{props.title}
</p>
</div>
<p className="text-xs font-light text-white md:text-sm lg:text-base xl:text-lg">
{`${props.description.slice(0, 125)}${
props.description.length > 125 ? "..." : ""
}`}
</p>
{props.description && (
<p className="text-xs font-light text-white md:text-sm lg:text-base xl:text-lg">
{`${props.description.slice(0, 125)}${
props.description.length > 125 ? "..." : ""
}`}
</p>
)}
</div>
</div>
</div>

View file

@ -1,4 +1,4 @@
import { Button, Modal, ToggleSwitch, Label, Textarea } from "flowbite-react";
import { Button, Label, Modal, ModalBody, ModalHeader, Textarea, ToggleSwitch } from "flowbite-react";
import { CommentsComment } from "./Comments.Comment";
import { useState } from "react";
import { ENDPOINTS } from "#/api/config";
@ -68,12 +68,12 @@ export const CommentsAddModal = (props: {
show={props.isOpen}
onClose={() => props.setIsOpen(false)}
>
<Modal.Header>
<ModalHeader>
<p className="text-lg font-bold text-gray-900 lg:text-2xl dark:text-white">
{props.isReply ? "Ответ на комментарий" : "Оставить комментарий"}
</p>
</Modal.Header>
<Modal.Body>
</ModalHeader>
<ModalBody>
{props.isReply && (
<div className="mb-4">
<CommentsComment
@ -98,7 +98,7 @@ export const CommentsAddModal = (props: {
<form className="flex flex-col gap-4" onSubmit={(e) => _sendComment(e)}>
<div>
<div className="block mb-2 sr-only">
<Label htmlFor="comment" value="Ваш комментарий." />
<Label htmlFor="comment">Ваш комментарий.</Label>
</div>
<Textarea
id="comment"
@ -132,7 +132,7 @@ export const CommentsAddModal = (props: {
</Button>
</div>
</form>
</Modal.Body>
</ModalBody>
</Modal>
);
};

View file

@ -1,7 +1,7 @@
import { unixToDate, sinceUnixDate } from "#/api/utils";
import { useEffect, useState } from "react";
import { ENDPOINTS } from "#/api/config";
import { Button, Dropdown } from "flowbite-react";
import { Button, Dropdown, DropdownItem } from "flowbite-react";
import Link from "next/link";
import { CommentsAddModal } from "./Comments.Add";
import { CommentsEditModal } from "./Comments.Edit";
@ -183,12 +183,12 @@ export const CommentsComment = (props: {
<span className="w-6 h-6 bg-gray-400 iconify mdi--more-horiz hover:bg-gray-800 active:bg-gray-800"></span>
)}
>
<Dropdown.Item onClick={() => setIsEditCommentsOpen(true)}>
<DropdownItem onClick={() => setIsEditCommentsOpen(true)}>
Редактировать
</Dropdown.Item>
<Dropdown.Item onClick={() => _deleteComment()}>
</DropdownItem>
<DropdownItem onClick={() => _deleteComment()}>
Удалить
</Dropdown.Item>
</DropdownItem>
</Dropdown>
)}
</footer>

View file

@ -1,4 +1,4 @@
import { Button, Modal, ToggleSwitch, Label, Textarea } from "flowbite-react";
import { Button, Label, Modal, ModalBody, ModalHeader, Textarea, ToggleSwitch } from "flowbite-react";
import { useState } from "react";
import { ENDPOINTS } from "#/api/config";
@ -56,16 +56,16 @@ export const CommentsEditModal = (props: {
show={props.isOpen}
onClose={() => props.setIsOpen(false)}
>
<Modal.Header>
<ModalHeader>
<p className="text-lg font-bold text-gray-900 lg:text-2xl dark:text-white">
Редактировать комментарий
</p>
</Modal.Header>
<Modal.Body>
</ModalHeader>
<ModalBody>
<form className="flex flex-col gap-4" onSubmit={(e) => _sendComment(e)}>
<div>
<div className="block mb-2 sr-only">
<Label htmlFor="comment" value="Редактировать ваш комментарий." />
<Label htmlFor="comment" >Редактировать ваш комментарий.</Label>
</div>
<Textarea
id="comment"
@ -99,7 +99,7 @@ export const CommentsEditModal = (props: {
</Button>
</div>
</form>
</Modal.Body>
</ModalBody>
</Modal>
);
};

View file

@ -1,4 +1,4 @@
import { Card, Button, Modal, Spinner } from "flowbite-react";
import { Button, Card, Modal, ModalHeader, Spinner } from "flowbite-react";
import { CommentsComment } from "./Comments.Comment";
import { useState, useEffect, useCallback } from "react";
import { ENDPOINTS } from "#/api/config";
@ -149,7 +149,7 @@ const CommentsAllModal = (props: {
show={props.isOpen}
onClose={() => props.setIsOpen(false)}
>
<Modal.Header>
<ModalHeader>
<div className="flex flex-col gap-1">
<h2 className="text-lg font-bold text-gray-900 lg:text-2xl dark:text-white">
Все комментарии
@ -158,7 +158,7 @@ const CommentsAllModal = (props: {
всего: {isLoading ? "загрузка..." : data[0].total_count}
</p>
</div>
</Modal.Header>
</ModalHeader>
<div
className="flex flex-col gap-2 p-4 overflow-y-auto"
onScroll={handleScroll}

View file

@ -1,7 +1,7 @@
import React, { useRef } from "react";
import Cropper, { ReactCropperElement } from "react-cropper";
import "cropperjs/dist/cropper.css";
import { Button, Modal } from "flowbite-react";
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from "flowbite-react";
type CropModalProps = {
isOpen: boolean;
@ -71,8 +71,8 @@ export const CropModal: React.FC<CropModalProps> = ({
}}
size={"7xl"}
>
<Modal.Header>Обрезать изображение</Modal.Header>
<Modal.Body>
<ModalHeader>Обрезать изображение</ModalHeader>
<ModalBody>
<Cropper
src={selectedImage}
style={{ height: 400, width: "100%" }}
@ -95,8 +95,8 @@ export const CropModal: React.FC<CropModalProps> = ({
</p>
<p>Используйте колёсико мыши что-бы изменить масштаб</p>
</div>
</Modal.Body>
<Modal.Footer>
</ModalBody>
<ModalFooter>
<Button
color={"blue"}
disabled={isActionsDisabled}
@ -120,7 +120,7 @@ export const CropModal: React.FC<CropModalProps> = ({
>
Отменить
</Button>
</Modal.Footer>
</ModalFooter>
</Modal>
);
};

View file

@ -87,8 +87,8 @@ export const Navbar = () => {
return (
<>
<header className="fixed bottom-0 left-0 z-50 w-full text-white bg-black rounded-t-lg sm:sticky sm:top-0 sm:rounded-t-none sm:rounded-b-lg">
<div className="container flex items-center justify-center gap-4 mx-auto sm:gap-0 sm:justify-between">
<div className="flex items-center gap-8 px-2 py-4 sm:gap-4">
<div className={`container flex items-center min-h-[76px] justify-center ${preferenceStore.flags.showFifthButton && preferenceStore.flags.showNavbarTitles == "always" ? "gap-0" : "gap-4"} mx-auto sm:gap-0 sm:justify-between`}>
<div className={`flex items-center ${preferenceStore.flags.showFifthButton && preferenceStore.flags.showNavbarTitles == "always" ? "gap-4" : "gap-8"} px-2 py-4 sm:gap-4`}>
{menuItems.map((item) => {
return (
<Link
@ -98,7 +98,7 @@ export const Navbar = () => {
item.isAuthRequired && !userStore.isAuth ? "hidden"
: item.isShownOnMobile ? "flex"
: "hidden sm:flex"
} ${[item.href, item.hrefInCategory].includes("/" + pathname.split("/")[1]) ? "font-bold" : "font-medium"}`}
} ${[item.href, item.hrefInCategory].includes("/" + pathname.split("/")[1]) ? "font-bold" : "font-medium"} transition-all`}
>
<span
className={`w-6 h-6 iconify ${[item.href, item.hrefInCategory].includes("/" + pathname.split("/")[1]) ? item.icon.active : item.icon.default}`}
@ -112,13 +112,13 @@ export const Navbar = () => {
);
})}
</div>
<div className="flex items-center gap-8 px-2 py-4 sm:gap-4">
<div className={`flex items-center ${preferenceStore.flags.showFifthButton && preferenceStore.flags.showNavbarTitles == "always" ? "gap-4" : "gap-8"} px-2 py-4 sm:gap-4`}>
{!userStore.isAuth ?
<Link
href={
pathname != "/login" ? `/login?redirect=${pathname}` : "#"
}
className={`flex items-center flex-col lg:flex-row gap-1 ${pathname == "/login" ? "font-bold" : "font-medium"}`}
className={`flex items-center flex-col lg:flex-row gap-1 ${pathname == "/login" ? "font-bold" : "font-medium"} transition-all`}
>
<span className="w-6 h-6 iconify material-symbols--login"></span>
<span
@ -130,7 +130,7 @@ export const Navbar = () => {
: <>
<Link
href={`/profile/${userStore.user.id}`}
className={`hidden lg:flex flex-col lg:flex-row items-center gap-1 ${pathname == `/profile/${userStore.user.id}` ? "font-bold" : "font-medium"}`}
className={`hidden lg:flex flex-col lg:flex-row items-center gap-1 ${pathname == `/profile/${userStore.user.id}` ? "font-bold" : "font-medium"} transition-all`}
>
<Image
src={userStore.user.avatar}
@ -139,13 +139,30 @@ export const Navbar = () => {
width={24}
height={24}
/>
<span className={`text-xs sm:text-base ${preferenceStore.flags.showNavbarTitles == "always" || preferenceStore.flags.showNavbarTitles == "links" || (preferenceStore.flags.showNavbarTitles == "selected" && pathname == `/profile/${userStore.user.id}`) ? "block" : "hidden"}`}>
<span
className={`text-xs sm:text-base ${preferenceStore.flags.showNavbarTitles == "always" || preferenceStore.flags.showNavbarTitles == "links" || (preferenceStore.flags.showNavbarTitles == "selected" && pathname == `/profile/${userStore.user.id}`) ? "block" : "hidden"}`}
>
{userStore.user.login}
</span>
</Link>
{preferenceStore.flags.showFifthButton ?
<Link
href={menuItems[preferenceStore.flags.showFifthButton].href}
className={`flex flex-col sm:hidden items-center gap-1 ${pathname == menuItems[preferenceStore.flags.showFifthButton].href ? "font-bold" : "font-medium"} transition-all`}
>
<span
className={`w-6 h-6 iconify ${pathname == menuItems[preferenceStore.flags.showFifthButton].href ? menuItems[preferenceStore.flags.showFifthButton].icon.active : menuItems[preferenceStore.flags.showFifthButton].icon.default}`}
></span>
<span
className={`text-xs sm:text-base ${preferenceStore.flags.showNavbarTitles == "always" || preferenceStore.flags.showNavbarTitles == "links" || (preferenceStore.flags.showNavbarTitles == "selected" && pathname == menuItems[preferenceStore.flags.showFifthButton].href) ? "block" : "hidden"}`}
>
{menuItems[preferenceStore.flags.showFifthButton].title}
</span>
</Link>
: ""}
<Link
href={`/menu`}
className={`flex flex-col lg:hidden items-center gap-1 ${pathname == `/menu` || pathname == `/profile/${userStore.user.id}` ? "font-bold" : "font-medium"}`}
className={`flex flex-col lg:hidden items-center gap-1 ${pathname == `/menu` || pathname == `/profile/${userStore.user.id}` ? "font-bold" : "font-medium"} transition-all`}
>
<Image
src={userStore.user.avatar}
@ -154,7 +171,9 @@ export const Navbar = () => {
width={24}
height={24}
/>
<span className={`text-xs sm:text-base ${preferenceStore.flags.showNavbarTitles == "always" || preferenceStore.flags.showNavbarTitles == "links" || (preferenceStore.flags.showNavbarTitles == "selected" && (pathname == `/menu` || pathname == `/profile/${userStore.user.id}`)) ? "block" : "hidden"}`}>
<span
className={`text-xs sm:text-base ${preferenceStore.flags.showNavbarTitles == "always" || preferenceStore.flags.showNavbarTitles == "links" || (preferenceStore.flags.showNavbarTitles == "selected" && (pathname == `/menu` || pathname == `/profile/${userStore.user.id}`)) ? "block" : "hidden"}`}
>
{userStore.user.login}
</span>
</Link>
@ -165,7 +184,11 @@ export const Navbar = () => {
onClick={() => setIsSettingModalOpen(true)}
>
<span className="w-6 h-6 iconify material-symbols--settings-outline-rounded"></span>
<span className={`text-xs sm:text-base ${preferenceStore.flags.showNavbarTitles == "always" ? "block" : "hidden"}`}>Настройки</span>
<span
className={`text-xs sm:text-base ${preferenceStore.flags.showNavbarTitles == "always" ? "block" : "hidden"}`}
>
Настройки
</span>
</button>
{userStore.isAuth && (
<button
@ -173,7 +196,9 @@ export const Navbar = () => {
onClick={() => userStore.logout()}
>
<span className="w-6 h-6 iconify material-symbols--logout"></span>
<span className={`text-xs sm:text-base ${preferenceStore.flags.showNavbarTitles == "always" ? "lg:hidden xl:block" : "hidden"}`}>
<span
className={`text-xs sm:text-base ${preferenceStore.flags.showNavbarTitles == "always" ? "lg:hidden xl:block" : "hidden"}`}
>
Выйти
</span>
</button>

View file

@ -1,49 +1,112 @@
"use client";
import { Card } from "flowbite-react";
import Link from "next/link";
import { numberDeclension } from "#/api/utils";
import { Button, ButtonGroup, Card } from "flowbite-react";
import { ProfileActivityCollections } from "./Profile.ActivityCollections";
import { useEffect, useState } from "react";
import { ProfileActivityFriends } from "./Profile.ActivityFriends";
import { ProfileActivityComment } from "./Profile.ActivityComment";
export function ProfileActivity(props: {
profile_id: number;
commentCount: number;
videoCount: number;
commentPreview: any;
collectionCount: number;
collectionPreview: any;
friendsCount: number;
friendsPreview: any;
token: string;
isMyProfile: boolean;
}) {
const [tab, setTab] = useState<"collections" | "comments" | "friends">(
"collections"
);
const [collections, setCollections] = useState<Record<number, any>>({});
function _setCollection(array: any[]) {
if (array && array.length == 0) {
return;
}
let _coll = array.filter((col) => {
if (typeof col == "number") {
return false;
}
return true;
});
_coll.map((col) => {
setCollections((prev) => {
return {
...prev,
[col.id]: col,
};
});
if (
col.creator.collections_preview &&
col.creator.collections_preview.length > 0
) {
_setCollection(col.creator.collections_preview || []);
}
});
}
useEffect(() => {
_setCollection(props.collectionPreview || []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.collectionPreview]);
return (
<Card className="h-fit">
<Card className="overflow-hidden h-fit">
<h1 className="text-2xl font-bold">Активность</h1>
<div className="flex items-center gap-4 text-lg">
<div>
<p>
{props.commentCount}{" "}
{numberDeclension(
props.commentCount,
"комментарий",
"комментария",
"комментариев"
)}
</p>
<p className="mt-2">{props.videoCount} видео</p>
</div>
<div>
<Link href={`/profile/${props.profile_id}/collections`}>
<p className="border-b-2 border-gray-300 border-solid dark:border-gray-400 hover:border-gray-500 dark:hover:border-gray-200">
{props.collectionCount}{" "}
{numberDeclension(
props.commentCount,
"коллекция",
"коллекции",
"коллекций"
)}
</p>
</Link>
<p className="mt-2">
{props.friendsCount}{" "}
{numberDeclension(props.commentCount, "друзей", "друга", "друзей")}
</p>
</div>
</div>
<ButtonGroup>
<Button
color={tab == "collections" ? "blue" : "light"}
onClick={() => setTab("collections")}
>
<div className="flex flex-col gap-1 sm:flex-row sm:items-center">
<p>Коллекции</p>
<p>( {props.collectionCount} )</p>
</div>
</Button>
<Button
color={tab == "comments" ? "blue" : "light"}
onClick={() => setTab("comments")}
>
<div className="flex flex-col gap-1 sm:flex-row sm:items-center">
<p>Комментарии</p>
<p>( {props.commentCount} )</p>
</div>
</Button>
<Button
color={tab == "friends" ? "blue" : "light"}
onClick={() => setTab("friends")}
>
<div className="flex flex-col gap-1 sm:flex-row sm:items-center">
<p>Друзья</p>
<p>( {props.friendsCount} )</p>
</div>
</Button>
</ButtonGroup>
{tab == "collections" && (
<ProfileActivityCollections
content={Object.values(collections) || []}
profile_id={props.profile_id}
/>
)}
{tab == "comments" && (
<ProfileActivityComment
content={props.commentPreview || []}
profile_id={props.profile_id}
/>
)}
{tab == "friends" && (
<ProfileActivityFriends
token={props.token}
content={props.friendsPreview || []}
isMyProfile={props.isMyProfile}
profile_id={props.profile_id}
/>
)}
</Card>
);
}

View file

@ -0,0 +1,64 @@
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/mousewheel";
import "swiper/css/scrollbar";
import { Navigation, Mousewheel, Scrollbar } from "swiper/modules";
import { CollectionLink } from "../CollectionLink/CollectionLink";
import Link from "next/link";
export const ProfileActivityCollections = (props: {
content: any;
profile_id: number;
}) => {
return (
<div className="max-w-full">
<Swiper
modules={[Navigation, Mousewheel, Scrollbar]}
spaceBetween={8}
slidesPerView={"auto"}
direction={"horizontal"}
mousewheel={{
enabled: true,
sensitivity: 4,
}}
scrollbar={{
enabled: true,
draggable: true,
}}
allowTouchMove={true}
style={
{
"--swiper-scrollbar-bottom": "0",
} as React.CSSProperties
}
>
{props.content &&
props.content.length > 0 &&
props.content.map((collection) => {
return (
<SwiperSlide
key={`col-prev-${collection.id}`}
style={{ width: "fit-content" }}
>
<div className="w-[350px] xl:w-[500px] aspect-video">
<CollectionLink {...collection} />
</div>
</SwiperSlide>
);
})}
{props.content && props.content.length > 0 ?
<SwiperSlide style={{ width: "fit-content" }}>
<Link href={`/profile/${props.profile_id}/collections`}>
<div className="w-[350px] xl:w-[500px] flex flex-col items-center justify-center gap-2 text-black transition-colors bg-gray-100 border hover:bg-gray-200 border-gray-50 hover:border-gray-100 dark:border-gray-700 dark:hover:border-gray-600 dark:hover:bg-gray-500 aspect-video group dark:bg-gray-600 dark:text-white">
<span className="w-8 h-8 iconify mdi--arrow-right dark:fill-white"></span>
<p>Все коллекции</p>
</div>
</Link>
</SwiperSlide>
: <p className="text-lg">У пользователя нет коллекций</p>}
</Swiper>
</div>
);
};

View file

@ -0,0 +1,92 @@
import Link from "next/link";
import Image from "next/image";
import { sinceUnixDate, unixToDate } from "#/api/utils";
export const ProfileActivityComment = (props: {
content: any;
profile_id: number;
}) => {
return (
<>
{props.content && props.content.length > 0 ?
props.content.map((comment) => {
let isHidden = comment.isSpoiler || comment.likes_count < -5 || false;
return (
<article
className="px-4 py-2 text-sm bg-gray-100 rounded-lg sm:text-base dark:bg-gray-900"
key={`comment-${comment.id}`}
>
<footer className="flex items-center justify-between mb-2">
<div className="flex flex-col items-start gap-1 sm:items-center sm:flex-row">
<Link
href={`/profile/${comment.profile.id}`}
className="inline-flex items-center mr-3 text-sm font-semibold text-gray-900 dark:text-white hover:underline"
>
<Image
className="w-6 h-6 mr-2 rounded-full"
width={24}
height={24}
src={comment.profile.avatar}
alt=""
/>
{comment.profile.login}
</Link>
<p className="text-sm text-gray-600 dark:text-gray-400">
<time
dateTime={comment.timestamp.toString()}
title={unixToDate(comment.timestamp, "full")}
>
{sinceUnixDate(comment.timestamp)}
</time>
</p>
<p
className={`text-sm font-medium border px-1 py-0.5 rounded-md text-center ml-4 min-w-8 ${
comment.likes_count > 0 ?
"text-green-500 dark:text-green-400 border-green-500 dark:border-green-400"
: comment.likes_count < 0 ?
"text-red-500 dark:text-red-400 border-red-500 dark:border-red-400"
: "text-gray-500 dark:text-gray-400 border-gray-500 dark:border-gray-400"
}`}
>
{comment.likes_count}
</p>
</div>
</footer>
<div className="relative flex flex-col py-2">
{comment.release && typeof comment.release != "number" && (
<Link href={`/release/${comment.release.id}`}>
<p className="text-gray-900 whitespace-pre-wrap dark:text-gray-500">
{!comment.isDeleted ?
`К релизу: ${comment.release.title_ru || comment.release.title_alt || comment.release.title_original} (${comment.release.year || "?"}) >>`
: ""}
</p>
</Link>
)}
<p className="text-gray-800 whitespace-pre-wrap dark:text-gray-400">
{!comment.isDeleted ?
comment.message
: "Комментарий был удалён."}
</p>
{isHidden && (
<button
className="absolute top-0 bottom-0 left-0 right-0"
onClick={() => isHidden == false}
>
<div className="min-w-full min-h-full px-2 py-1.5 rounded-md bg-black text-white bg-opacity-50 backdrop-blur-[8px] flex flex-col justify-center items-center">
<p>
{comment.likes_count < -5 ?
"У комментария слишком низкий рейтинг."
: "Данный комментарий может содержать спойлер."}
</p>
<p className="font-bold">Нажмите, чтобы прочитать</p>
</div>
</button>
)}
</div>
</article>
);
})
: <p className="text-lg">Пользователь не оставлял комментарии</p>}
</>
);
};

View file

@ -0,0 +1,87 @@
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/mousewheel";
import "swiper/css/scrollbar";
import { Navigation, Mousewheel, Scrollbar } from "swiper/modules";
import Link from "next/link";
import { Avatar, Button } from "flowbite-react";
import { useState } from "react";
import { ProfileFriendModal } from "./Profile.FriendsModal";
export const ProfileActivityFriends = (props: {
content: any;
token: string;
isMyProfile: boolean;
profile_id: number;
}) => {
const [isFriendModalOpen, setIsFriendModalOpen] = useState(false);
return (
<>
<div className="max-w-full">
<Swiper
modules={[Navigation, Mousewheel, Scrollbar]}
spaceBetween={8}
slidesPerView={"auto"}
direction={"horizontal"}
mousewheel={{
enabled: true,
sensitivity: 4,
}}
scrollbar={{
enabled: true,
draggable: true,
}}
allowTouchMove={true}
style={
{
"--swiper-scrollbar-bottom": "0",
} as React.CSSProperties
}
>
{props.content &&
props.content.length > 0 &&
props.content.map((profile) => {
return (
<SwiperSlide
key={`friend-prev-${profile.id}`}
style={{ width: "fit-content" }}
className="px-2 py-4"
>
<Link href={`/profile/${profile.id}`}>
<div className="flex items-center gap-2">
<Avatar
img={profile.avatar}
size="md"
rounded={true}
bordered={true}
color={profile.is_online ? "success" : "light"}
className="flex-shrink-0"
/>
<p className="text-lg">{profile.login}</p>
</div>
</Link>
</SwiperSlide>
);
})}
{(props.content && props.content.length > 0) || props.isMyProfile ?
<SwiperSlide style={{ width: "fit-content" }} className="px-2 py-4">
<Button onClick={() => setIsFriendModalOpen(true)}>
<p className="text-lg">Все друзья {props.isMyProfile ? "и заявки" : ""}</p>
<span className="w-8 h-8 iconify mdi--arrow-right dark:fill-white"></span>
</Button>
</SwiperSlide>
: <p className="text-lg">У пользователя нет друзей</p>}
</Swiper>
</div>
<ProfileFriendModal
isOpen={isFriendModalOpen}
setIsOpen={setIsFriendModalOpen}
token={props.token}
isMyProfile={props.isMyProfile}
profile_id={props.profile_id}
></ProfileFriendModal>
</>
);
};

View file

@ -0,0 +1,177 @@
import { ENDPOINTS } from "#/api/config";
import { tryCatchAPI, unixToDate, useSWRfetcher } from "#/api/utils";
import { Avatar, Button, Modal, ModalHeader, useThemeMode } from "flowbite-react";
import { useCallback, useEffect, useState } from "react";
import useSWRInfinite from "swr/infinite";
import { Spinner } from "../Spinner/Spinner";
import { toast } from "react-toastify";
export const ProfileBlockedUsersModal = (props: {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
token: string;
profile_id: number;
}) => {
const [currentRef, setCurrentRef] = useState<any>(null);
const theme = useThemeMode();
const [actionsDisabled, setActionsDisabled] = useState(false);
const [unblockedUsers, setUnblockedUsers] = useState([]);
const modalRef = useCallback((ref) => {
setCurrentRef(ref);
}, []);
const getKey = (pageIndex: number, previousPageData: any) => {
if (previousPageData && !previousPageData.content.length) return null;
let url = `${ENDPOINTS.user.blocklist}/all/${pageIndex}?token=${props.token}`;
return url;
};
const { data, error, isLoading, size, setSize } = useSWRInfinite(
getKey,
useSWRfetcher,
{ initialSize: 2 }
);
async function _addToBlocklist(profile_id) {
setActionsDisabled(true);
const tid = toast.loading(
unblockedUsers.includes(profile_id) ?
"Блокируем пользователя..."
: "Разблокируем пользователя...",
{
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
}
);
let url = `${ENDPOINTS.user.blocklist}`;
unblockedUsers.includes(profile_id) ?
(url += "/add/")
: (url += "/remove/");
url += `${profile_id}?token=${props.token}`;
const { data, error } = await tryCatchAPI(fetch(url));
if (error) {
toast.update(tid, {
render:
unblockedUsers.includes(profile_id) ?
"Ошибка блокировки"
: "Ошибка разблокировки",
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
setActionsDisabled(false);
return;
}
toast.update(tid, {
render:
unblockedUsers.includes(profile_id) ?
"Пользователь заблокирован"
: "Пользователь разблокирован",
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
if (unblockedUsers.includes(profile_id)) {
setUnblockedUsers((prev) => {
return prev.filter((item) => item !== profile_id);
});
} else {
setUnblockedUsers((prev) => {
return [...prev, profile_id];
});
}
setActionsDisabled(false);
}
const [content, setContent] = useState([]);
useEffect(() => {
if (data) {
let allReleases = [];
for (let i = 0; i < data.length; i++) {
allReleases.push(...data[i].content);
}
setContent(allReleases);
}
}, [data]);
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);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scrollPosition]);
return (
<>
<Modal
dismissible
show={props.isOpen}
onClose={() => props.setIsOpen(false)}
size={"7xl"}
>
<ModalHeader>Заблокированные пользователи</ModalHeader>
<div
className="flex flex-col gap-2 p-4 overflow-y-auto"
onScroll={handleScroll}
ref={modalRef}
>
{content && content.length > 0 ?
content.map((user) => {
return (
<div className="flex items-center justify-between gap-2" key={`blockeduser-${user.id}`}>
<div className="flex gap-2">
<Avatar
alt=""
img={user.avatar}
rounded={true}
size={"md"}
bordered={true}
color={user.is_online ? "success" : "light"}
className="flex-shrink-0"
/>
<div className="flex flex-col gap-1">
<p className="text-lg font-semibold">{user.login}</p>
<p>Заблокирован: {unixToDate(user.added_date, "full")}</p>
</div>
</div>
<Button
color={!unblockedUsers.includes(user.id) ? "blue" : "red"}
onClick={() => _addToBlocklist(user.id)}
disabled={actionsDisabled}
className="flex-grow-0 h-fit"
>
{!unblockedUsers.includes(user.id) ?
"Разблокировать"
: "Заблокировать"}
</Button>
</div>
);
})
: "Нет заблокированных пользователей"}
{isLoading && <Spinner />}
</div>
</Modal>
</>
);
};

View file

@ -1,6 +1,6 @@
"use client";
import { Button, Modal, Textarea, useThemeMode } from "flowbite-react";
import { Button, Modal, ModalBody, ModalFooter, ModalHeader, Textarea, useThemeMode } from "flowbite-react";
import { ENDPOINTS } from "#/api/config";
import { useEffect, useState } from "react";
import { useSWRConfig } from "swr";
@ -138,8 +138,8 @@ export const ProfileEditLoginModal = (props: {
onClose={() => props.setIsOpen(false)}
size={"4xl"}
>
<Modal.Header>Изменить никнейм</Modal.Header>
<Modal.Body>
<ModalHeader>Изменить никнейм</ModalHeader>
<ModalBody>
{loading ?
<div className="flex items-center justify-center py-8">
<Spinner />
@ -173,8 +173,8 @@ export const ProfileEditLoginModal = (props: {
}
</>
}
</Modal.Body>
<Modal.Footer>
</ModalBody>
<ModalFooter>
{_loginData.is_change_available && (
<Button
color="blue"
@ -191,7 +191,7 @@ export const ProfileEditLoginModal = (props: {
>
Отмена
</Button>
</Modal.Footer>
</ModalFooter>
</Modal>
);
};

View file

@ -1,6 +1,6 @@
"use client";
import { FileInput, Label, Modal, useThemeMode } from "flowbite-react";
import { FileInput, Label, Modal, ModalBody, ModalHeader, useThemeMode } from "flowbite-react";
import { Spinner } from "../Spinner/Spinner";
import useSWR from "swr";
import { ENDPOINTS } from "#/api/config";
@ -14,6 +14,7 @@ import { useSWRConfig } from "swr";
import { useUserStore } from "#/store/auth";
import { ProfileEditLoginModal } from "./Profile.EditLoginModal";
import { toast } from "react-toastify";
import { ProfileBlockedUsersModal } from "./Profile.BlockedUsersModal";
export const ProfileEditModal = (props: {
isOpen: boolean;
@ -25,6 +26,7 @@ export const ProfileEditModal = (props: {
const [statusModalOpen, setStatusModalOpen] = useState(false);
const [socialModalOpen, setSocialModalOpen] = useState(false);
const [loginModalOpen, setLoginModalOpen] = useState(false);
const [blockedModalOpen, setBlockedModalOpen] = useState(false);
const [privacyModalSetting, setPrivacyModalSetting] = useState("none");
const [privacySettings, setPrivacySettings] = useState({
privacy_stats: 9,
@ -186,6 +188,7 @@ export const ProfileEditModal = (props: {
if (avatarModalProps.croppedImage) {
_uploadAvatar();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [avatarModalProps.croppedImage]);
if (!prefData || !loginData || prefError || loginError) {
@ -199,8 +202,8 @@ export const ProfileEditModal = (props: {
onClose={() => props.setIsOpen(false)}
size={"7xl"}
>
<Modal.Header>Редактирование профиля</Modal.Header>
<Modal.Body>
<ModalHeader>Редактирование профиля</ModalHeader>
<ModalBody>
{prefLoading ?
<Spinner />
: <div className="flex flex-col gap-4">
@ -355,13 +358,18 @@ export const ProfileEditModal = (props: {
}
</p>
</button>
{/* <button className="p-2 text-left rounded-md hover:bg-gray-100 dark:hover:bg-gray-900">
<button
className="p-2 text-left rounded-md hover:bg-gray-100 dark:hover:bg-gray-900"
onClick={() => {
setBlockedModalOpen(true);
}}
>
<p className="text-lg">Блоклист</p>
<p className="text-base text-gray-500 dark:text-gray-400">
Список пользователей, которым запрещён доступ к вашей
странице
</p>
</button> */}
</button>
</div>
<div>
<div className="flex flex-col">
@ -387,7 +395,7 @@ export const ProfileEditModal = (props: {
</div>
</div>
}
</Modal.Body>
</ModalBody>
</Modal>
{props.token ?
<>
@ -431,6 +439,12 @@ export const ProfileEditModal = (props: {
setLogin={setLogin}
profile_id={props.profile_id}
/>
<ProfileBlockedUsersModal
isOpen={blockedModalOpen}
setIsOpen={setBlockedModalOpen}
token={props.token}
profile_id={props.profile_id}
/>
</>
: ""}
</>

View file

@ -1,6 +1,6 @@
"use client";
import { Modal, useThemeMode } from "flowbite-react";
import { Modal, ModalBody, ModalHeader, useThemeMode } from "flowbite-react";
import { ENDPOINTS } from "#/api/config";
import { useState } from "react";
import { toast } from "react-toastify";
@ -98,8 +98,8 @@ export const ProfileEditPrivacyModal = (props: {
onClose={() => props.setIsOpen(false)}
size={"4xl"}
>
<Modal.Header>{setting_text[props.setting]}</Modal.Header>
<Modal.Body>
<ModalHeader>{setting_text[props.setting]}</ModalHeader>
<ModalBody>
{props.setting != "none" ?
<>
<div className="flex flex-col gap-2">
@ -202,7 +202,7 @@ export const ProfileEditPrivacyModal = (props: {
</div>
</>
: ""}
</Modal.Body>
</ModalBody>
</Modal>
);
};

View file

@ -1,6 +1,15 @@
"use client";
import { Button, Modal, Label, TextInput, useThemeMode } from "flowbite-react";
import {
Button,
Label,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
TextInput,
useThemeMode,
} from "flowbite-react";
import { Spinner } from "../Spinner/Spinner";
import { ENDPOINTS } from "#/api/config";
import { useEffect, useState } from "react";
@ -164,8 +173,8 @@ export const ProfileEditSocialModal = (props: {
onClose={() => props.setIsOpen(false)}
size={"4xl"}
>
<Modal.Header>Соц. сети</Modal.Header>
<Modal.Body>
<ModalHeader>Соц. сети</ModalHeader>
<ModalBody>
<p className="p-2 text-gray-400 border-2 border-gray-200 rounded-md dark:border-gray-500 dark:text-gray-300">
Укажите ссылки на свои социальные сети, чтобы другие пользователи
могли с вами связаться
@ -177,7 +186,7 @@ export const ProfileEditSocialModal = (props: {
: <div className="flex flex-col gap-4 py-4">
<div>
<div className="block mb-2">
<Label htmlFor="vk-page" value="ВКонтакте" />
<Label htmlFor="vk-page">ВКонтакте</Label>
</div>
<TextInput
id="vk-page"
@ -189,7 +198,7 @@ export const ProfileEditSocialModal = (props: {
</div>
<div>
<div className="block mb-2">
<Label htmlFor="tg-page" value="Telegram" />
<Label htmlFor="tg-page">Telegram</Label>
</div>
<TextInput
id="tg-page"
@ -201,7 +210,7 @@ export const ProfileEditSocialModal = (props: {
</div>
<div>
<div className="block mb-2">
<Label htmlFor="discord-page" value="Discord" />
<Label htmlFor="discord-page">Discord</Label>
</div>
<TextInput
id="discord-page"
@ -213,7 +222,7 @@ export const ProfileEditSocialModal = (props: {
</div>
<div>
<div className="block mb-2">
<Label htmlFor="inst-page" value="Instagram" />
<Label htmlFor="inst-page">Instagram</Label>
</div>
<TextInput
id="inst-page"
@ -225,7 +234,7 @@ export const ProfileEditSocialModal = (props: {
</div>
<div>
<div className="block mb-2">
<Label htmlFor="tt-page" value="TikTok" />
<Label htmlFor="tt-page">TikTok</Label>
</div>
<TextInput
id="tt-page"
@ -237,8 +246,8 @@ export const ProfileEditSocialModal = (props: {
</div>
</div>
}
</Modal.Body>
<Modal.Footer>
</ModalBody>
<ModalFooter>
<Button
color="blue"
onClick={() => _setSocialSetting()}
@ -253,7 +262,7 @@ export const ProfileEditSocialModal = (props: {
>
Отмена
</Button>
</Modal.Footer>
</ModalFooter>
</Modal>
);
};

View file

@ -1,6 +1,6 @@
"use client";
import { Button, Modal, Textarea, useThemeMode } from "flowbite-react";
import { Button, Modal, ModalBody, ModalFooter, ModalHeader, Textarea, useThemeMode } from "flowbite-react";
import { ENDPOINTS } from "#/api/config";
import { useEffect, useState } from "react";
import { useSWRConfig } from "swr";
@ -96,8 +96,8 @@ export const ProfileEditStatusModal = (props: {
onClose={() => props.setIsOpen(false)}
size={"4xl"}
>
<Modal.Header>Изменить статус</Modal.Header>
<Modal.Body>
<ModalHeader>Изменить статус</ModalHeader>
<ModalBody>
<Textarea
disabled={loading}
rows={3}
@ -111,8 +111,8 @@ export const ProfileEditStatusModal = (props: {
<p className="text-sm text-right text-gray-500 dark:text-gray-300">
{_stringLength}/80
</p>
</Modal.Body>
<Modal.Footer>
</ModalBody>
<ModalFooter>
<Button
color="blue"
onClick={() => _setStatusSetting()}
@ -123,7 +123,7 @@ export const ProfileEditStatusModal = (props: {
<Button color="red" onClick={() => props.setIsOpen(false)}>
Отмена
</Button>
</Modal.Footer>
</ModalFooter>
</Modal>
);
};

View file

@ -0,0 +1,346 @@
import { ENDPOINTS } from "#/api/config";
import { tryCatchAPI, unixToDate, useSWRfetcher } from "#/api/utils";
import {
Avatar,
Button,
Modal,
ModalHeader,
useThemeMode,
} from "flowbite-react";
import { useCallback, useEffect, useState } from "react";
import useSWRInfinite from "swr/infinite";
import { Spinner } from "../Spinner/Spinner";
import { toast } from "react-toastify";
import useSWR, { mutate } from "swr";
import Link from "next/link";
export const ProfileFriendModal = (props: {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
token: string;
isMyProfile: boolean;
profile_id: number;
}) => {
const [currentRef, setCurrentRef] = useState<any>(null);
const theme = useThemeMode();
const [actionsDisabled, setActionsDisabled] = useState(false);
// const [requestInUsers, setRequestInUsers] = useState([]);
// const [requestOutUsers, setRequestOutUsers] = useState([]);
const [friends, setFriends] = useState([]);
const modalRef = useCallback((ref) => {
setCurrentRef(ref);
}, []);
const useFetchRequests = (url: string) => {
const { data, error, isLoading } = useSWR(url, useSWRfetcher);
return [data, error, isLoading];
};
const [requestInUsersData, requestInUsersError, requestInUsersIsLoading] =
useFetchRequests(
props.isMyProfile ?
`${ENDPOINTS.user.friend.in}/last?token=${props.token}&count=8`
: ""
);
const [requestOutUsersData, requestOutUsersError, requestOutUsersIsLoading] =
useFetchRequests(
props.isMyProfile ?
`${ENDPOINTS.user.friend.out}/last?token=${props.token}&count=8`
: ""
);
async function _hideRequestIn(profile_id) {
const tid = toast.loading("Скрываем заявку...", {
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
});
let url = `${ENDPOINTS.user.friend.hide}/${profile_id}?token=${props.token}`;
const { data, error } = await tryCatchAPI(fetch(url));
if (error) {
toast.update(tid, {
render: "Ошибка скрытия заявки",
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
return;
}
toast.update(tid, {
render: "Заявка скрыта",
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
mutate(`${ENDPOINTS.user.friend.in}/last?token=${props.token}&count=8`);
}
async function _acceptRequestIn(profile_id) {
const tid = toast.loading("Принимаем запрос...", {
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
});
let url = `${ENDPOINTS.user.friend.add}/${profile_id}?token=${props.token}`;
const { data, error } = await tryCatchAPI(fetch(url));
if (error) {
toast.update(tid, {
render: "Ошибка приёма запроса",
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
return;
}
toast.update(tid, {
render: "Запрос принят",
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
mutate(`${ENDPOINTS.user.friend.in}/last?token=${props.token}&count=8`);
}
async function _cancelRequestOut(profile_id) {
const tid = toast.loading("Отменяем запрос...", {
position: "bottom-center",
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
draggable: false,
theme: theme.mode == "light" ? "light" : "dark",
});
let url = `${ENDPOINTS.user.friend.remove}/${profile_id}?token=${props.token}`;
const { data, error } = await tryCatchAPI(fetch(url));
if (error) {
toast.update(tid, {
render: "Ошибка отмена запроса",
type: "error",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
return;
}
toast.update(tid, {
render: "Запрос отменён",
type: "success",
autoClose: 2500,
isLoading: false,
closeOnClick: true,
draggable: true,
});
mutate(`${ENDPOINTS.user.friend.out}/last?token=${props.token}&count=8`);
}
const getKey = (pageIndex: number, previousPageData: any) => {
if (previousPageData && !previousPageData.content.length) return null;
let url = `${ENDPOINTS.user.friend.list}/${props.profile_id}/${pageIndex}?token=${props.token}`;
return url;
};
const { data, error, isLoading, size, setSize } = useSWRInfinite(
getKey,
useSWRfetcher,
{ initialSize: 2 }
);
useEffect(() => {
if (data) {
let allFriends = [];
for (let i = 0; i < data.length; i++) {
allFriends.push(...data[i].content);
}
setFriends(allFriends);
}
}, [data]);
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);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scrollPosition]);
return (
<>
<Modal
dismissible
show={props.isOpen}
onClose={() => props.setIsOpen(false)}
size={"4xl"}
>
<ModalHeader>Друзья</ModalHeader>
<div
className="flex flex-col gap-4 p-4 overflow-y-auto"
onScroll={handleScroll}
ref={modalRef}
>
{props.isMyProfile && (
<>
<div className="flex flex-col gap-2">
<p className="text-lg font-semibold">Входящие заявки</p>
{(
requestInUsersData &&
requestInUsersData.content &&
requestInUsersData.content.length > 0
) ?
requestInUsersData.content.map((user) => {
return (
<div
className="flex items-center justify-between gap-2"
key={`friend_req_in-${user.id}`}
>
<Link href={`/profile/${user.id}`}>
<div className="flex gap-2">
<Avatar
alt=""
img={user.avatar}
rounded={true}
size={"md"}
bordered={true}
color={user.is_online ? "success" : "light"}
className="flex-shrink-0"
/>
<div className="flex flex-col gap-1">
<p className="text-lg font-semibold">
{user.login}
</p>
<p>Друзей: {user.friend_count}</p>
</div>
</div>
</Link>
<div className="flex gap-2">
<Button
color="blue"
onClick={() => _acceptRequestIn(user.id)}
>
Принять
</Button>
<Button
color="light"
onClick={() => _hideRequestIn(user.id)}
>
Скрыть
</Button>
</div>
</div>
);
})
: <p className="text-sm">Нет входящих заявок</p>}
</div>
<div className="flex flex-col gap-2">
<p className="text-lg font-semibold">Исходящие заявки</p>
{(
requestOutUsersData &&
requestOutUsersData.content &&
requestOutUsersData.content.length > 0
) ?
requestOutUsersData.content.map((user) => {
return (
<div
className="flex items-center justify-between gap-2"
key={`friend_req_out-${user.id}`}
>
<Link href={`/profile/${user.id}`}>
<div className="flex gap-2">
<Avatar
alt=""
img={user.avatar}
rounded={true}
size={"md"}
bordered={true}
color={user.is_online ? "success" : "light"}
className="flex-shrink-0"
/>
<div className="flex flex-col gap-1">
<p className="text-lg font-semibold">
{user.login}
</p>
<p>Друзей: {user.friend_count}</p>
</div>
</div>
</Link>
<div className="flex gap-2">
<Button
color="light"
onClick={() => _cancelRequestOut(user.id)}
>
Отменить
</Button>
</div>
</div>
);
})
: <p className="text-sm">Нет исходящих заявок</p>}
</div>
</>
)}
<div className="flex flex-col gap-2">
<p className="text-lg font-semibold">Все друзья</p>
{friends && friends.length > 0 ?
friends.map((user) => {
return (
<div
className="flex items-center justify-between gap-2"
key={`friend-${user.id}`}
>
<Link href={`/profile/${user.id}`}>
<div className="flex gap-2">
<Avatar
alt=""
img={user.avatar}
rounded={true}
size={"md"}
bordered={true}
color={user.is_online ? "success" : "light"}
className="flex-shrink-0"
/>
<div className="flex flex-col gap-1">
<p className="text-lg font-semibold">{user.login}</p>
<p>Друзей: {user.friend_count}</p>
</div>
</div>
</Link>
</div>
);
})
: <p className="text-sm">Нет друзей</p>}
</div>
{isLoading && <Spinner />}
</div>
</Modal>
</>
);
};

View file

@ -1,39 +1,79 @@
import { Card, Carousel, RatingStar, Rating } from "flowbite-react";
import type {
FlowbiteCarouselIndicatorsTheme,
FlowbiteCarouselControlTheme,
} from "flowbite-react";
import { ReleaseLink } from "../ReleaseLink/ReleaseLink";
import { Card } from "flowbite-react";
const CarouselIndicatorsTheme: FlowbiteCarouselIndicatorsTheme = {
active: {
off: "bg-gray-300/50 hover:bg-gray-400 dark:bg-gray-400/50 dark:hover:bg-gray-200",
on: "bg-gray-600 dark:bg-gray-200",
},
base: "h-3 w-3 rounded-full",
wrapper: "absolute bottom-5 left-1/2 flex -translate-x-1/2 space-x-3",
};
import { ReleaseChips } from "../ReleasePoster/Chips";
import { Poster } from "../ReleasePoster/Poster";
import Link from "next/link";
const CarouselControlsTheme: FlowbiteCarouselControlTheme = {
base: "inline-flex h-8 w-8 items-center justify-center rounded-full bg-gray-600/30 group-hover:bg-gray-600/50 group-focus:outline-none group-focus:ring-4 group-focus:ring-gray-600 dark:bg-gray-400/30 dark:group-hover:bg-gray-400/60 dark:group-focus:ring-gray-400/70 sm:h-10 sm:w-10",
icon: "h-5 w-5 text-gray-600 dark:text-gray-400 sm:h-6 sm:w-6",
};
const CarouselTheme = {
indicators: CarouselIndicatorsTheme,
control: CarouselControlsTheme,
const profile_lists = {
// 0: "Не смотрю",
1: { name: "Смотрю", bg_color: "bg-green-500" },
2: { name: "В планах", bg_color: "bg-purple-500" },
3: { name: "Просмотрено", bg_color: "bg-blue-500" },
4: { name: "Отложено", bg_color: "bg-yellow-500" },
5: { name: "Брошено", bg_color: "bg-red-500" },
};
export const ProfileReleaseHistory = (props: any) => {
return (
<Card className="h-fit">
<h1 className="text-2xl font-bold">Недавно просмотренные</h1>
<div className="max-w-[700px] min-h-[200px]">
<Carousel theme={CarouselTheme}>
{props.history.map((release) => {
return <ReleaseLink key={`history-${release.id}`} {...release} />;
})}
</Carousel>
<div className="flex flex-col gap-4">
{props.history.map((release) => {
const genres = [];
const grade = release.grade ? Number(release.grade.toFixed(1)) : null;
const profile_list_status = release.profile_list_status || null;
let user_list = null;
if (profile_list_status != null || profile_list_status != 0) {
user_list = profile_lists[profile_list_status];
}
if (release.genres) {
const genres_array = release.genres.split(",");
genres_array.forEach((genre) => {
genres.push(genre.trim());
});
}
return (
<Link href={`/release/${release.id}`} key={`history-${release.id}`}>
<div className="flex gap-2">
<div className="flex-shrink-0 w-32">
<Poster image={release.image} className="h-auto" />
</div>
<div className="flex flex-col gap-1">
<ReleaseChips
{...release}
user_list={user_list}
grade={grade}
settings={{ lastWatchedHidden: false }}
/>
<div>
{genres.length > 0 &&
genres.map((genre: string, index: number) => {
return (
<span
key={`release_${props.id}_genre_${genre}_${index}`}
className="text-sm font-light dark:text-white"
>
{index > 0 && ", "}
{genre}
</span>
);
})}
</div>
{release.title_ru && (
<p className="text-lg font-bold dark:text-white">
{release.title_ru}
</p>
)}
{release.title_original && (
<p className="text-sm text-gray-600 dark:text-gray-300">
{release.title_original}
</p>
)}
</div>
</div>
</Link>
);
})}
</div>
</Card>
);

View file

@ -1,41 +1,12 @@
import {
Card,
Carousel,
RatingStar,
Rating,
Modal,
Button,
} from "flowbite-react";
import type {
FlowbiteCarouselIndicatorsTheme,
FlowbiteCarouselControlTheme,
} from "flowbite-react";
import Image from "next/image";
import { Button, Card, Modal, ModalHeader, Rating, RatingStar } from "flowbite-react";
import { unixToDate, useSWRfetcher } from "#/api/utils";
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { ENDPOINTS } from "#/api/config";
import useSWRInfinite from "swr/infinite";
import { Spinner } from "../Spinner/Spinner";
const CarouselIndicatorsTheme: FlowbiteCarouselIndicatorsTheme = {
active: {
off: "bg-gray-300/50 hover:bg-gray-400 dark:bg-gray-400/50 dark:hover:bg-gray-200",
on: "bg-gray-600 dark:bg-gray-200",
},
base: "h-3 w-3 rounded-full",
wrapper: "absolute bottom-5 left-1/2 flex -translate-x-1/2 space-x-3",
};
const CarouselControlsTheme: FlowbiteCarouselControlTheme = {
base: "inline-flex h-8 w-8 items-center justify-center rounded-full bg-gray-600/30 group-hover:bg-gray-600/50 group-focus:outline-none group-focus:ring-4 group-focus:ring-gray-600 dark:bg-gray-400/30 dark:group-hover:bg-gray-400/60 dark:group-focus:ring-gray-400/70 sm:h-10 sm:w-10",
icon: "h-5 w-5 text-gray-600 dark:text-gray-400 sm:h-6 sm:w-6",
};
const CarouselTheme = {
indicators: CarouselIndicatorsTheme,
control: CarouselControlsTheme,
};
import { Poster } from "../ReleasePoster/Poster";
export const ProfileReleaseRatings = (props: any) => {
const [modal, setModal] = useState(false);
@ -47,37 +18,31 @@ export const ProfileReleaseRatings = (props: any) => {
Посмотреть все
</Button>
</div>
<div className="max-w-[700px] min-h-[200px]">
<Carousel theme={CarouselTheme}>
{props.ratings.map((release) => {
return (
<Link href={`/release/${release.id}`} key={`vote-${release.id}`}>
<div className="flex gap-4 xl:mx-20">
<Image
src={release.image}
width={100}
height={125}
alt=""
className="object-cover border-gray-200 rounded-lg shadow-md dark:border-gray-700 dark:bg-gray-800 w-[100px] h-[125px]"
/>
<div className="flex flex-col gap-1 py-4">
<h2 className="text-lg">{release.title_ru}</h2>
<Rating size="md">
<RatingStar filled={release.my_vote >= 1} />
<RatingStar filled={release.my_vote >= 2} />
<RatingStar filled={release.my_vote >= 3} />
<RatingStar filled={release.my_vote >= 4} />
<RatingStar filled={release.my_vote >= 5} />
</Rating>
<h2 className="text-gray-500 text-md dark:text-gray-400">
{unixToDate(release.voted_at, "full")}
</h2>
</div>
<div className="flex flex-col w-full gap-4">
{props.ratings.map((release) => {
return (
<Link href={`/release/${release.id}`} key={`vote-${release.id}`}>
<div className="flex gap-2">
<div className="max-w-32">
<Poster image={release.image} />
</div>
</Link>
);
})}
</Carousel>
<div className="flex flex-col gap-1">
<h2 className="text-lg font-bold dark:text-white">{release.title_ru}</h2>
<Rating size="md">
<RatingStar filled={release.my_vote >= 1} />
<RatingStar filled={release.my_vote >= 2} />
<RatingStar filled={release.my_vote >= 3} />
<RatingStar filled={release.my_vote >= 4} />
<RatingStar filled={release.my_vote >= 5} />
</Rating>
<h2 className="text-gray-500 text-md dark:text-gray-400">
{unixToDate(release.voted_at, "full")}
</h2>
</div>
</div>
</Link>
);
})}
</div>
<ProfileReleaseRatingsModal
profile_id={props.profile_id}
@ -148,14 +113,14 @@ const ProfileReleaseRatingsModal = (props: {
onClose={() => props.setIsOpen(false)}
size={"4xl"}
>
<Modal.Header>Оценки</Modal.Header>
<ModalHeader>Оценки</ModalHeader>
<div
className="flex flex-col gap-2 p-4 overflow-y-auto"
onScroll={handleScroll}
ref={modalRef}
>
{isLoading && <Spinner />}
{content && content.length > 0 ? (
{content && content.length > 0 ?
content.map((release) => {
return (
<Link
@ -163,13 +128,9 @@ const ProfileReleaseRatingsModal = (props: {
key={`vote-modal-${release.id}`}
>
<div className="flex gap-4 xl:mx-20">
<Image
src={release.image}
width={100}
height={125}
alt=""
className="object-cover border-gray-200 rounded-lg shadow-md dark:border-gray-700 dark:bg-gray-800 w-[100px] h-[125px]"
/>
<div className="flex-shrink-0 max-w-32">
<Poster image={release.image} />
</div>
<div className="flex flex-col gap-1 py-2">
<h2 className="text-lg">{release.title_ru}</h2>
<div className="flex items-center gap-1">
@ -200,9 +161,7 @@ const ProfileReleaseRatingsModal = (props: {
</Link>
);
})
) : (
<h1>Оценок нет</h1>
)}
: <h1>Оценок нет</h1>}
</div>
</Modal>
);

View file

@ -0,0 +1,15 @@
interface UserRoleProps {
name: string;
color: string;
}
export const UserRole = ({ name, color }: UserRoleProps) => {
return (
<div
className={`text-[var(--color)] border border-[var(--color)] rounded-md`}
style={{ "--color": `#${color}` } as React.CSSProperties}
>
<p className="px-1.5 py-0.5">{name}</p>
</div>
);
};

View file

@ -0,0 +1,20 @@
interface UserSocialProps {
icon: string;
url?: string;
nickname: string;
color: string;
}
export const UserSocial = ({ nickname, icon, color }: UserSocialProps) => {
return (
<div
className={`border border-[var(--color)] rounded-md`}
style={{ "--color": `#${color}` } as React.CSSProperties}
>
<div className="flex gap-1 items-center px-1.5 py-1">
<span className={`iconify w-6 h-6 bg-[var(--color)] ${icon}`}></span>
<p>{nickname}</p>
</div>
</div>
);
};

View file

@ -1,145 +1,154 @@
"use client";
import { Avatar, Card, Button } from "flowbite-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Chip } from "../Chip/Chip";
export const ProfileUser = (props: {
isOnline: boolean;
import { Avatar, Card, useThemeMode } from "flowbite-react";
import { UserRole } from "./Profile.Role";
import { UserSocial } from "./Profile.Social";
import Link from "next/link";
interface ProfileUserProps {
avatar: string;
login: string;
status: string;
socials: {
isPrivate: boolean;
hasSocials: boolean;
socials: {
name: string;
nickname: any;
icon: string;
urlPrefix?: string | undefined;
}[];
};
chips: {
hasChips: boolean;
isMyProfile: boolean;
isVerified: boolean;
isSponsor: boolean;
isBlocked: boolean;
roles?: {
id: number;
name: string;
color: string;
}[];
};
rating: number;
}) => {
const router = useRouter();
roles: {
id: number;
name: string;
color: string;
}[];
isMyProfile: boolean;
isSponsor: boolean;
isBlocked: boolean;
isVerified: boolean;
isOnline: boolean;
socials: {
vk: string;
tg: string;
tt: string;
inst: string;
discord: string;
};
is_social_hidden: boolean;
}
export const ProfileUser = ({
avatar,
login,
status,
rating,
roles,
isMyProfile,
isVerified,
isOnline,
isSponsor,
isBlocked,
socials,
is_social_hidden,
}: ProfileUserProps) => {
const theme = useThemeMode().mode;
return (
<Card className="h-fit">
{props.chips.hasChips && (
<div className="flex gap-1 overflow-x-auto scrollbar-thin">
{props.chips.isMyProfile && (
<Chip bg_color="bg-blue-500" name="Мой профиль" />
)}
{props.chips.isVerified && (
<Chip bg_color="bg-green-500" name="Верифицирован" />
)}
{props.chips.isSponsor && (
<Chip bg_color="bg-yellow-500" name="Спонсор Anixart" />
)}
{props.chips.isBlocked && (
<Chip bg_color="bg-red-500" name="Заблокирован" />
)}
{props.chips.roles &&
props.chips.roles.length > 0 &&
props.chips.roles.map((role: any) => (
<Chip
key={role.id}
bg_color={`bg-[var(--role-color)]`}
name={role.name}
style={
{
"--role-color": `#${role.color}`,
} as React.CSSProperties
}
/>
))}
<Card>
{(isMyProfile ||
isVerified ||
isSponsor ||
isBlocked ||
roles.length > 0) && (
<div className="flex flex-wrap gap-2">
{isMyProfile && <UserRole name="Мой профиль" color="3f83f8" />}
{isBlocked && <UserRole name="Заблокирован" color="f56565" />}
{isVerified && <UserRole name="Верифицирован" color="0e9f6e" />}
{isSponsor && <UserRole name="Спонсор Anixart" color="ecc94b" />}
{roles.map((role) => (
<UserRole key={role.id} name={role.name} color={role.color} />
))}
</div>
)}
<Avatar
alt=""
img={props.avatar}
rounded={true}
size={"lg"}
className="relative flex-col items-center justify-center sm:justify-start sm:flex-row"
bordered={true}
color={props.isOnline ? "success" : "light"}
>
<div className="space-y-1 text-2xl font-medium whitespace-pre-wrap dark:text-white">
<div className="text-center sm:text-left">
{props.login}{" "}
<div className="flex flex-col items-center gap-4 sm:items-start sm:flex-row">
<Avatar
alt=""
img={avatar}
rounded={true}
size={"lg"}
bordered={true}
color={isOnline ? "success" : "light"}
className="flex-shrink-0"
/>
<div className="flex flex-col gap-2">
<p className="flex items-center gap-2 text-2xl font-semibold">
{login}
<span
className={`border rounded-md px-2 py-1 text-sm ${
props.rating > 0
? "border-green-500 text-green-500"
: "border-red-500 text-red-500"
className={`border rounded-md px-2 py-1 min-w-8 text-sm flex items-center justify-center ${
rating > 0 ?
"border-green-500 text-green-500"
: "border-red-500 text-red-500"
}`}
>
{props.rating}
{rating}
</span>
</div>
<div className="text-sm text-gray-500 whitespace-pre-wrap sm:text-md dark:text-gray-400 ">
{props.status}
</div>
</p>
<p className="text-sm whitespace-pre-wrap sm:text-md">{status}</p>
</div>
</Avatar>
{props.socials.hasSocials && !props.socials.isPrivate && (
<div className="flex items-center gap-1 overflow-x-auto scrollbar-thin">
{props.socials.socials
.filter((social: any) => {
if (social.nickname == "") {
return false;
}
return true;
})
.map((social: any) => {
if (social.name == "discord" && social.nickname != "")
return (
<Button
color="light"
key={social.name}
onClick={() => {
window.navigator.clipboard.writeText(social.nickname);
alert("Скопировано!");
}}
>
<div className="flex items-center justify-center gap-2">
<span
className={`iconify h-4 w-4 sm:h-6 sm:w-6 ${social.icon} dark:fill-white`}
></span>
{social.nickname}
</div>
</Button>
);
return (
<Link
key={social.name}
href={`${social.urlPrefix}${social.nickname}`}
target="_blank"
>
<Button color="light">
<div className="flex items-center justify-center gap-2">
<span
className={`iconify h-4 w-4 sm:h-6 sm:w-6 ${social.icon} dark:fill-white`}
></span>
{social.nickname}
</div>
</Button>
</Link>
);
})}
</div>
)}
</div>
{!is_social_hidden &&
(socials.vk ||
socials.tg ||
socials.discord ||
socials.tt ||
socials.inst) && (
<div className="flex flex-wrap gap-2">
{socials.vk && (
<Link href={`https://vk.com/${socials.vk}`} target="_blank">
<UserSocial
nickname={socials.vk}
icon="fa6-brands--vk"
url={`https://vk.com/${socials.vk}`}
color="4a76a8"
/>
</Link>
)}
{socials.tg && (
<Link href={`https://t.me/${socials.tg}`} target="_blank">
<UserSocial
nickname={socials.tg}
icon="fa6-brands--telegram"
url={`https://t.me/${socials.tg}`}
color="2aabee"
/>
</Link>
)}
{socials.tt && (
<Link href={`https://tiktok.com/@${socials.tt}`} target="_blank">
<UserSocial
nickname={socials.tt}
icon="fa6-brands--tiktok"
url={`https://tiktok.com/@${socials.tt}`}
color={theme == "light" ? "000000" : "ffffff"}
/>
</Link>
)}
{socials.inst && (
<Link
href={`https://instagram.com/${socials.inst}`}
target="_blank"
>
<UserSocial
nickname={socials.inst}
icon="fa6-brands--instagram"
url={`https://instagram.com/${socials.inst}`}
color="c32aa3"
/>
</Link>
)}
{socials.discord && (
<UserSocial
nickname={socials.discord}
icon="fa6-brands--discord"
url={`https://discord.com/${socials.discord}`}
color="5865f2"
/>
)}
</div>
)}
</Card>
);
};

View file

@ -28,6 +28,7 @@ export const ProfileWatchDynamic = (props: { watchDynamic: Array<any> }) => {
},
tooltip: {
enabled: true,
theme:"dark",
x: {
show: false,
},

View file

@ -1,5 +1,5 @@
"use client";
import { ReleaseLink } from "../ReleaseLink/ReleaseLink";
import { ReleaseLink } from "../ReleaseLink/ReleaseLinkUpdate";
import Link from "next/link";
import { Swiper, SwiperSlide } from 'swiper/react';
@ -42,7 +42,7 @@ export const ReleaseCourusel = (props: {
allowTouchMove={true}
breakpoints={{
1800: {
initialSlide: 1,
initialSlide: 2,
centeredSlides: true
}
}}
@ -52,7 +52,7 @@ export const ReleaseCourusel = (props: {
return (
<SwiperSlide
key={release.id}
className="xl:max-w-[600px] sm:max-w-[400px] lg:aspect-video"
className="h-full max-w-64 md:max-w-96 aspect-[384/538]"
>
<ReleaseLink {...release} />
</SwiperSlide>

View file

@ -2,6 +2,7 @@ import { Card, Button } from "flowbite-react";
import { useState } from "react";
import Image from "next/image";
import { ReleaseInfoStreaming } from "./ReleaseInfo.LicensedPlatforms";
import { Poster } from "../ReleasePoster/Poster";
export const ReleaseInfoBasics = (props: {
release_id: number;
@ -14,14 +15,20 @@ export const ReleaseInfoBasics = (props: {
return (
<Card className="h-full row-span-2">
<div className="flex flex-col w-full h-full gap-4 lg:flex-row">
<Image
className="w-[285px] max-h-[385px] object-cover border border-gray-200 rounded-lg shadow-md dark:border-gray-700"
src={props.image}
alt=""
width={285}
height={385}
/>
<div className="flex flex-col w-full h-full gap-4 lg:grid lg:grid-cols-[1fr_2fr] items-center lg:items-start justify-center lg:justify-start">
<div className="flex flex-col gap-2">
<div className="relative flex items-center justify-center w-full overflow-hidden rounded-lg">
<Poster
image={props.image}
className="z-10 sm:scale-95 lg:scale-100"
/>
<Poster
image={props.image}
className="absolute top-0 left-0 w-full scale-125 opacity-75 blur-xl brightness-75"
/>
</div>
<ReleaseInfoStreaming release_id={props.release_id} />
</div>
<div className="flex flex-col max-w-2xl gap-2 text-sm md:text-base">
<div className="flex flex-col gap-1">
<p className="text-xl font-bold text-black md:text-2xl dark:text-white">
@ -54,7 +61,6 @@ export const ReleaseInfoBasics = (props: {
>
{isFullDescription ? "Скрыть" : "Показать полностью"}
</Button>
<ReleaseInfoStreaming release_id={props.release_id} />
</div>
</div>
</Card>

View file

@ -1,4 +1,4 @@
import { Card, Table } from "flowbite-react";
import { Card, Table, TableBody, TableCell, TableRow } from "flowbite-react";
import { ReleaseInfoSearchLink } from "#/components/ReleaseInfo/ReleaseInfo.SearchLink";
import { unixToDate, minutesToTime } from "#/api/utils";
const weekDay = [
@ -30,9 +30,9 @@ export const ReleaseInfoInfo = (props: {
return (
<Card>
<Table>
<Table.Body>
<Table.Row>
<Table.Cell className="py-0">
<TableBody>
<TableRow>
<TableCell className="py-0">
{props.country ?
props.country.toLowerCase() == "япония" ?
<span className="w-8 h-8 iconify-color twemoji--flag-for-japan"></span>
@ -40,45 +40,45 @@ export const ReleaseInfoInfo = (props: {
: <span className="w-8 h-8 iconify-color twemoji--flag-for-united-nations "></span>
}
</Table.Cell>
<Table.Cell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
</TableCell>
<TableCell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
{props.country && props.country}
{(props.aired_on_date != 0 || props.year) && ", "}
{props.season && props.season != 0 ?
`${YearSeason[props.season]} `
: ""}
{props.year && `${props.year} г.`}
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell className="py-0">
</TableCell>
</TableRow>
<TableRow>
<TableCell className="py-0">
<span className="w-8 h-8 iconify-color mdi--animation-play-outline dark:invert"></span>
</Table.Cell>
<Table.Cell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
</TableCell>
<TableCell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
{props.episodes.released ? props.episodes.released : "?"}
{"/"}
{props.episodes.total ? props.episodes.total + " эп. " : "? эп. "}
{props.duration != 0 &&
`по ${minutesToTime(props.duration, "daysHours")}`}
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell className="py-0">
</TableCell>
</TableRow>
<TableRow>
<TableCell className="py-0">
<span className="w-8 h-8 iconify-color mdi--calendar-outline dark:invert"></span>
</Table.Cell>
<Table.Cell className="font-medium text-gray-900 dark:text-white">
</TableCell>
<TableCell className="font-medium text-gray-900 dark:text-white">
{props.category}
{", "}
{props.broadcast == 0 ?
props.status.toLowerCase()
: `выходит ${weekDay[props.broadcast]}`}
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell className="py-0">
</TableCell>
</TableRow>
<TableRow>
<TableCell className="py-0">
<span className="w-8 h-8 iconify-color mdi--people-group-outline dark:invert"></span>
</Table.Cell>
<Table.Cell className="font-medium text-gray-900 dark:text-white">
</TableCell>
<TableCell className="font-medium text-gray-900 dark:text-white">
{props.studio && (
<>
{"Студия: "}
@ -117,13 +117,13 @@ export const ReleaseInfoInfo = (props: {
/>
</>
)}
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell className="py-0">
</TableCell>
</TableRow>
<TableRow>
<TableCell className="py-0">
<span className="w-8 h-8 iconify-color mdi--tag-outline dark:invert"></span>
</Table.Cell>
<Table.Cell className="font-medium text-gray-900 dark:text-white">
</TableCell>
<TableCell className="font-medium text-gray-900 dark:text-white">
{props.genres &&
props.genres.split(", ").map((genre: string, index: number) => {
return (
@ -133,14 +133,14 @@ export const ReleaseInfoInfo = (props: {
</div>
);
})}
</Table.Cell>
</Table.Row>
</TableCell>
</TableRow>
{props.status.toLowerCase() == "анонс" && (
<Table.Row>
<Table.Cell className="py-0">
<TableRow>
<TableCell className="py-0">
<span className="w-8 h-8 iconify-color mdi--clock-outline dark:invert"></span>
</Table.Cell>
<Table.Cell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
</TableCell>
<TableCell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
{props.aired_on_date != 0 ?
unixToDate(props.aired_on_date, "full")
: props.year ?
@ -151,10 +151,10 @@ export const ReleaseInfoInfo = (props: {
{props.year && `${props.year} г.`}
</>
: "Скоро"}
</Table.Cell>
</Table.Row>
</TableCell>
</TableRow>
)}
</Table.Body>
</TableBody>
</Table>
</Card>
);

View file

@ -14,7 +14,7 @@ export const ReleaseInfoStreaming = (props: { release_id: number }) => {
setData(await response.json());
};
_getData();
}, []);
}, [props.release_id]);
return (
<>
@ -23,17 +23,18 @@ export const ReleaseInfoStreaming = (props: { release_id: number }) => {
: !(data.content.length > 0) ?
""
: <div>
<p className="mt-4 mb-1 text-lg">Официальные источники: </p>
<div className="grid grid-cols-2 gap-2 md:grid-cols-4">
<div className="grid grid-flow-row-dense grid-cols-1 gap-1 2xl:grid-cols-2">
{data.content.map((item: any) => {
return (
<a
href={item.url}
target="_blank"
key={`platform_${item.id}`}
className="flex items-center gap-2 px-2 py-1 transition-colors bg-gray-100 rounded-lg hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 "
className="flex items-center gap-2 px-4 py-2 transition-colors bg-gray-100 rounded-lg hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 "
>
<img src={item.icon} className="w-6 h-6 rounded-full" />
<p className="text-lg">{item.name}</p>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img alt="" src={item.icon} className="w-6 h-6 rounded-full" />
<p className="text-sm line-clamp-2">{item.name}</p>
</a>
);
})}

View file

@ -1,20 +1,26 @@
import {
Card,
Rating,
Flowbite,
Button,
CustomFlowbiteTheme,
Card,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
Rating,
RatingAdvanced,
RatingStar,
RatingAdvancedTheme,
} from "flowbite-react";
import { numberDeclension } from "#/api/utils";
import { useState } from "react";
import { ENDPOINTS } from "#/api/config";
const RatingTheme: CustomFlowbiteTheme = {
ratingAdvanced: {
progress: {
base: "mx-4 h-5 w-3/4 rounded bg-gray-200 dark:bg-gray-700",
},
const CustomRatingTheme: RatingAdvancedTheme = {
base: "flex items-center",
label: "text-sm font-medium text-cyan-600 dark:text-cyan-500",
progress: {
base: "mx-4 h-5 w-3/4 rounded bg-gray-200 dark:bg-gray-700",
fill: "h-5 rounded bg-yellow-400",
label: "text-sm font-medium text-cyan-600 dark:text-cyan-500",
},
};
export const ReleaseInfoRating = (props: {
@ -39,7 +45,7 @@ export const ReleaseInfoRating = (props: {
<Card>
<div className="flex flex-col gap-2 sm:items-center sm:flex-row">
<Rating>
<Rating.Star />
<RatingStar />
<p className="ml-2 text-sm font-bold dark:text-white">
{props.grade.toFixed(2)} из 5
</p>
@ -48,7 +54,7 @@ export const ReleaseInfoRating = (props: {
<>
<span className="mx-1.5 h-1 w-1 rounded-full bg-gray-500 dark:bg-gray-400 hidden lg:block" />
<div className="flex items-center gap-2">
{vote ? (
{vote ?
<>
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">
ваша оценка: {vote}
@ -62,8 +68,7 @@ export const ReleaseInfoRating = (props: {
изменить
</Button>
</>
) : (
<Button
: <Button
size={"xs"}
className="text-gray-500 border border-gray-600 rounded-full hover:bg-black hover:text-white hover:border-black dark:text-gray-400 dark:border-gray-500"
color="inline"
@ -71,7 +76,7 @@ export const ReleaseInfoRating = (props: {
>
оценить
</Button>
)}
}
</div>
</>
)}
@ -80,47 +85,50 @@ export const ReleaseInfoRating = (props: {
{props.votes.total}{" "}
{numberDeclension(props.votes.total, "голос", "голоса", "голосов")}
</p>
<Flowbite theme={{ theme: RatingTheme }}>
<Rating.Advanced
percentFilled={Math.floor(
(props.votes["5"] / props.votes.total) * 100
)}
className="mb-2"
>
5
</Rating.Advanced>
<Rating.Advanced
percentFilled={Math.floor(
(props.votes["4"] / props.votes.total) * 100
)}
className="mb-2"
>
4
</Rating.Advanced>
<Rating.Advanced
percentFilled={Math.floor(
(props.votes["3"] / props.votes.total) * 100
)}
className="mb-2"
>
3
</Rating.Advanced>
<Rating.Advanced
percentFilled={Math.floor(
(props.votes["2"] / props.votes.total) * 100
)}
className="mb-2"
>
2
</Rating.Advanced>
<Rating.Advanced
percentFilled={Math.floor(
(props.votes["1"] / props.votes.total) * 100
)}
>
1
</Rating.Advanced>
</Flowbite>
<RatingAdvanced
theme={CustomRatingTheme}
percentFilled={Math.floor(
(props.votes["5"] / props.votes.total) * 100
)}
className="mb-2"
>
5
</RatingAdvanced>
<RatingAdvanced
theme={CustomRatingTheme}
percentFilled={Math.floor(
(props.votes["4"] / props.votes.total) * 100
)}
className="mb-2"
>
4
</RatingAdvanced>
<RatingAdvanced
theme={CustomRatingTheme}
percentFilled={Math.floor(
(props.votes["3"] / props.votes.total) * 100
)}
className="mb-2"
>
3
</RatingAdvanced>
<RatingAdvanced
theme={CustomRatingTheme}
percentFilled={Math.floor(
(props.votes["2"] / props.votes.total) * 100
)}
className="mb-2"
>
2
</RatingAdvanced>
<RatingAdvanced
theme={CustomRatingTheme}
percentFilled={Math.floor(
(props.votes["1"] / props.votes.total) * 100
)}
>
1
</RatingAdvanced>
</Card>
<ReleaseInfoRatingModal
isOpen={isRatingModalOpen}
@ -178,8 +186,8 @@ const ReleaseInfoRatingModal = (props: {
show={props.isOpen}
onClose={() => props.setIsOpen(false)}
>
<Modal.Header>Оценка</Modal.Header>
<Modal.Body>
<ModalHeader>Оценка</ModalHeader>
<ModalBody>
<div>
<div className="block sm:hidden">
<Rating size="md" className="justify-center">
@ -191,7 +199,7 @@ const ReleaseInfoRatingModal = (props: {
onMouseOut={() => setCurElement(0)}
onClick={() => setVote(element)}
>
<Rating.Star
<RatingStar
filled={index + 1 <= curElement || index + 1 <= vote}
/>
</Button>
@ -208,7 +216,7 @@ const ReleaseInfoRatingModal = (props: {
onMouseOut={() => setCurElement(0)}
onClick={() => setVote(element)}
>
<Rating.Star
<RatingStar
filled={index + 1 <= curElement || index + 1 <= vote}
/>
</Button>
@ -216,8 +224,8 @@ const ReleaseInfoRatingModal = (props: {
</Rating>
</div>
</div>
</Modal.Body>
<Modal.Footer>
</ModalBody>
<ModalFooter>
<div className="flex gap-1 ml-auto">
<Button
disabled={isSending}
@ -241,7 +249,7 @@ const ReleaseInfoRatingModal = (props: {
Оценить
</Button>
</div>
</Modal.Footer>
</ModalFooter>
</Modal>
);
};

View file

@ -1,15 +1,9 @@
"use client";
import { Card, Carousel, CustomFlowbiteTheme } from "flowbite-react";
import { ReleaseLink } from "#/components/ReleaseLink/ReleaseLink";
import { Card, Carousel } from "flowbite-react";
import { ReleaseLink } from "#/components/ReleaseLink/ReleaseLinkUpdate";
import Link from "next/link";
const CarouselTheme: CustomFlowbiteTheme["carousel"] = {
root: {
base: "relative h-full w-full max-w-[300px]",
},
};
export const ReleaseInfoRelated = (props: {
release_id: number;
related: any;
@ -29,7 +23,7 @@ export const ReleaseInfoRelated = (props: {
)}
</div>
<div className="flex justify-center mt-2">
<Carousel pauseOnHover={true} theme={CarouselTheme}>
<Carousel pauseOnHover={true}>
{props.related_releases
.filter((release: any) => {
if (release.id == props.release_id) {
@ -39,7 +33,11 @@ export const ReleaseInfoRelated = (props: {
})
.map((release: any) => {
return (
<ReleaseLink key={release.id} {...release} type={"poster"} />
<ReleaseLink
key={release.id}
{...release}
settings={{ showGenres: false, showDescription: false }}
/>
);
})}
</Carousel>

View file

@ -5,7 +5,7 @@ import useSWRInfinite from "swr/infinite";
import { useCallback, useEffect, useState } from "react";
import { tryCatchAPI, useSWRfetcher } from "#/api/utils";
import { toast } from "react-toastify";
import { useThemeMode } from "flowbite-react";
import { DropdownItem, ModalHeader, useThemeMode } from "flowbite-react";
const lists = [
{ list: 0, name: "Не смотрю" },
@ -174,12 +174,12 @@ export const ReleaseInfoUserList = (props: {
disabled={listEventDisabled}
>
{lists.map((list) => (
<Dropdown.Item
<DropdownItem
key={list.list}
onClick={() => _addToList(list.list)}
>
{list.name}
</Dropdown.Item>
</DropdownItem>
))}
</Dropdown>
<Button
@ -321,7 +321,7 @@ const AddReleaseToCollectionModal = (props: {
show={props.isOpen}
onClose={() => props.setIsOpen(false)}
>
<Modal.Header>Выбор коллекции</Modal.Header>
<ModalHeader>Выбор коллекции</ModalHeader>
<div
className="flex flex-col gap-2 p-4 overflow-y-auto"
onScroll={handleScroll}

View file

@ -1,133 +0,0 @@
import Link from "next/link";
import { sinceUnixDate } from "#/api/utils";
import { Chip } from "#/components/Chip/Chip";
import Image from "next/image";
const profile_lists = {
// 0: "Не смотрю",
1: { name: "Смотрю", bg_color: "bg-green-500" },
2: { name: "В планах", bg_color: "bg-purple-500" },
3: { name: "Просмотрено", bg_color: "bg-blue-500" },
4: { name: "Отложено", bg_color: "bg-yellow-500" },
5: { name: "Брошено", bg_color: "bg-red-500" },
};
export const ReleaseLink169 = (props: any) => {
const grade = props.grade ? props.grade.toFixed(1) : null;
const profile_list_status = props.profile_list_status;
let user_list = null;
if (profile_list_status != null || profile_list_status != 0) {
user_list = profile_lists[profile_list_status];
}
return (
<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={{
backgroundImage: `linear-gradient(to bottom, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.9) 100%)`,
}}
>
<Image
src={props.image}
fill={true}
alt={props.title || ""}
className="-z-[1] object-cover"
sizes="
(max-width: 768px) 300px,
(max-width: 1024px) 600px,
900px
"
/>
<div className="absolute flex flex-wrap items-start justify-start gap-0.5 sm:gap-1 left-0 top-0 p-1 sm:p-2">
{grade ? (
<Chip
bg_color={
grade == 0
? "hidden"
: grade < 2
? "bg-red-500"
: grade < 3
? "bg-orange-500"
: grade < 4
? "bg-yellow-500"
: "bg-green-500"
}
name={grade}
/>
) : (
""
)}
{user_list && (
<Chip bg_color={user_list.bg_color} name={user_list.name} />
)}
{props.status ? (
<Chip name={props.status.name} />
) : (
props.status_id != 0 && (
<Chip
name={
props.status_id == 1
? "Завершено"
: props.status_id == 2
? "Онгоинг"
: props.status_id == 3 && "Анонс"
}
/>
)
)}
<Chip
name={props.episodes_released && props.episodes_released}
name_2={
props.episodes_total ? props.episodes_total + " эп." : "? эп."
}
devider="/"
/>
{props.last_view_episode && (
<Chip
name={
props.last_view_episode.name
? props.last_view_episode.name
: `${props.last_view_episode.position + 1} серия`
}
name_2={
"last_view_timestamp" in props &&
sinceUnixDate(props.last_view_timestamp)
}
devider=", "
/>
)}
{props.category && <Chip name={props.category.name} />}
{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>
)}
</div>
<div className="absolute bottom-0 left-0 p-1 sm:p-2 lg:translate-y-[100%] group-hover:lg:translate-y-0 transition-transform">
<div className="transition-transform lg:-translate-y-[calc(100%_+_1rem)] group-hover:lg:translate-y-0">
{props.genres && (
<p className="text-xs font-light text-white md:text-sm lg:text-base xl:text-lg">
{props.genres}
</p>
)}
<p className="text-sm font-bold text-white md:text-base lg:text-lg xl:text-xl">
{props.title_ru}
</p>
</div>
<p className="text-xs font-light text-white md:text-sm lg:text-base xl:text-lg">
{`${props.description.slice(0, 125)}${
props.description.length > 125 ? "..." : ""
}`}
</p>
</div>
</div>
</div>
</Link>
);
};

View file

@ -1,124 +0,0 @@
import Link from "next/link";
import { sinceUnixDate } from "#/api/utils";
import { Chip } from "#/components/Chip/Chip";
import Image from "next/image";
const profile_lists = {
// 0: "Не смотрю",
1: { name: "Смотрю", bg_color: "bg-green-500" },
2: { name: "В планах", bg_color: "bg-purple-500" },
3: { name: "Просмотрено", bg_color: "bg-blue-500" },
4: { name: "Отложено", bg_color: "bg-yellow-500" },
5: { name: "Брошено", bg_color: "bg-red-500" },
};
export const ReleaseLink169Poster = (props: any) => {
const grade = props.grade ? props.grade.toFixed(1) : null;
const profile_list_status = props.profile_list_status;
let user_list = null;
if (profile_list_status != null || profile_list_status != 0) {
user_list = profile_lists[profile_list_status];
}
return (
<Link
href={`/release/${props.id}`}
className={props.isLinkDisabled ? "pointer-events-none" : ""}
aria-disabled={props.isLinkDisabled}
tabIndex={props.isLinkDisabled ? -1 : undefined}
>
<div className="w-full h-auto p-2 bg-gray-100 rounded-lg dark:bg-slate-800">
<div className="flex w-full h-full gap-2 overflow-hidden">
<div className="flex-shrink-0">
<Image
src={props.image}
height={250}
width={250}
alt={props.title || ""}
className="object-cover aspect-[9/16] h-auto w-24 md:w-32 lg:w-48 rounded-md"
/>
</div>
<div className="flex flex-col flex-1 w-full h-full">
<div>
{props.genres && (
<p className="text-xs font-light text-black dark:text-white md:text-sm lg:text-base xl:text-lg">
{props.genres}
</p>
)}
<p className="text-sm font-bold text-black dark:text-white md:text-base lg:text-lg xl:text-xl">
{`${props.title_ru.slice(0, 47)}${
props.title_ru.length > 47 ? "..." : ""
}`}
</p>
<p className="text-xs font-light text-black dark:text-white md:text-sm lg:text-base xl:text-lg">
{`${props.description.slice(0, 97)}${
props.description.length > 97 ? "..." : ""
}`}
</p>
</div>
<div className="flex flex-wrap gap-1 mt-1">
{grade ? <Chip
bg_color={
grade == 0
? "hidden"
: grade < 2
? "bg-red-500"
: grade < 3
? "bg-orange-500"
: grade < 4
? "bg-yellow-500"
: "bg-green-500"
}
name={grade}
/> : ""}
{user_list && (
<Chip bg_color={user_list.bg_color} name={user_list.name} />
)}
{props.status ? (
<Chip name={props.status.name} />
) : (
props.status_id != 0 && (
<Chip
name={
props.status_id == 1
? "Завершено"
: props.status_id == 2
? "Онгоинг"
: props.status_id == 3 && "Анонс"
}
/>
)
)}
<Chip
name={props.episodes_released && props.episodes_released}
name_2={
props.episodes_total ? props.episodes_total + " эп." : "? эп."
}
devider="/"
/>
{props.last_view_episode && (
<Chip
name={
props.last_view_episode.name
? props.last_view_episode.name
: `${props.last_view_episode.position + 1} серия`
}
name_2={
"last_view_timestamp" in props &&
sinceUnixDate(props.last_view_timestamp)
}
devider=", "
/>
)}
{props.category && <Chip name={props.category.name} />}
{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>
)}
</div>
</div>
</div>
</div>
</Link>
);
};

View file

@ -1,136 +0,0 @@
import Link from "next/link";
import { sinceUnixDate } from "#/api/utils";
import { Chip } from "#/components/Chip/Chip";
import Image from "next/image";
const profile_lists = {
// 0: "Не смотрю",
1: { name: "Смотрю", bg_color: "bg-green-500" },
2: { name: "В планах", bg_color: "bg-purple-500" },
3: { name: "Просмотрено", bg_color: "bg-blue-500" },
4: { name: "Отложено", bg_color: "bg-yellow-500" },
5: { name: "Брошено", bg_color: "bg-red-500" },
};
const YearSeason = ["_", "Зима", "Весна", "Лето", "Осень"];
export const ReleaseLink169Related = (props: any) => {
const grade = props.grade.toFixed(1);
const profile_list_status = props.profile_list_status;
let user_list = null;
if (profile_list_status != null || profile_list_status != 0) {
user_list = profile_lists[profile_list_status];
}
return (
<Link
href={`/release/${props.id}`}
className={`${
props.isLinkDisabled ? "pointer-events-none" : ""
} flex gap-4 items-center justify-between mx-auto w-full max-w-[1024px]`}
aria-disabled={props.isLinkDisabled}
tabIndex={props.isLinkDisabled ? -1 : undefined}
>
<div className="items-center justify-center flex-1 hidden lg:flex">
<h1 className="inline-block text-6xl font-bold text-center text-transparent bg-gradient-to-r from-blue-600 via-purple-500 to-indigo-500 dark:from-blue-500 dark:via-purple-400 dark:to-indigo-300 bg-clip-text ">
{props.season ? YearSeason[props.season] : ""}
<br/>
{props.year ? props.year : ""}
</h1>
</div>
<div className="w-full max-w-[768px] h-auto p-2 bg-gray-100 rounded-lg dark:bg-slate-800">
<div className="flex w-full h-full gap-2 overflow-hidden">
<div className="flex-shrink-0">
<Image
src={props.image}
height={250}
width={250}
alt={props.title || ""}
className="object-cover aspect-[9/16] lg:aspect-[12/16] h-auto w-24 md:w-32 lg:w-48 rounded-md"
/>
</div>
<div className="flex flex-col flex-1 w-full h-full">
<div>
{props.genres && (
<p className="text-xs font-light text-black dark:text-white md:text-sm lg:text-base xl:text-lg">
{props.genres}
</p>
)}
<p className="block text-sm font-bold text-black dark:text-white md:text-base lg:text-lg xl:text-xl md:hidden">
{`${props.title_ru.slice(0, 47)}${
props.title_ru.length > 47 ? "..." : ""
}`}
</p>
<p className="block text-xs font-light text-black dark:text-white md:text-sm lg:text-base xl:text-lg md:hidden">
{`${props.description.slice(0, 97)}${
props.description.length > 97 ? "..." : ""
}`}
</p>
<p className="hidden text-sm font-bold text-black dark:text-white md:text-base lg:text-lg xl:text-xl md:block max-w-[512px]">
{props.title_ru}
</p>
<p className="hidden text-xs font-light text-black dark:text-white md:text-sm:text-base xl:text-lg md:block max-w-[512px]">
{props.description}
</p>
</div>
<div className="flex flex-wrap gap-1 mt-1">
<Chip
bg_color={
grade == 0
? "hidden"
: grade < 2
? "bg-red-500"
: grade < 3
? "bg-orange-500"
: grade < 4
? "bg-yellow-500"
: "bg-green-500"
}
name={grade}
/>
{user_list && (
<Chip bg_color={user_list.bg_color} name={user_list.name} />
)}
{props.status ? (
<Chip name={props.status.name} />
) : (
props.status_id != 0 && (
<Chip
name={
props.status_id == 1
? "Завершено"
: props.status_id == 2
? "Онгоинг"
: props.status_id == 3 && "Анонс"
}
/>
)
)}
<Chip
name={props.episodes_released && props.episodes_released}
name_2={
props.episodes_total ? props.episodes_total + " эп." : "? эп."
}
devider="/"
/>
{props.category && <Chip name={props.category.name} />}
{props.season || props.year ? (
<Chip
bg_color="lg:hidden bg-gray-500"
name={props.season ? YearSeason[props.season] : ""}
name_2={props.year ? `${props.year} год` : ""}
devider=" "
/>
) : (
""
)}
{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>
)}
</div>
</div>
</div>
</div>
</Link>
);
};

View file

@ -1,84 +0,0 @@
import Link from "next/link";
import { Chip } from "#/components/Chip/Chip";
const profile_lists = {
// 0: "Не смотрю",
1: { name: "Смотрю", bg_color: "bg-green-500" },
2: { name: "В планах", bg_color: "bg-purple-500" },
3: { name: "Просмотрено", bg_color: "bg-blue-500" },
4: { name: "Отложено", bg_color: "bg-yellow-500" },
5: { name: "Брошено", bg_color: "bg-red-500" },
};
export const ReleaseLinkPoster = (props: any) => {
const grade = props.grade.toFixed(1);
const profile_list_status = props.profile_list_status;
let user_list = null;
if (profile_list_status != null || profile_list_status != 0) {
user_list = profile_lists[profile_list_status];
}
return (
<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
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 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>
);
};

View file

@ -1,19 +0,0 @@
import { ReleaseLink169 } from "./ReleaseLink.16_9FullImage";
import { ReleaseLink169Poster } from "./ReleaseLink.16_9Poster";
import { ReleaseLinkPoster } from "./ReleaseLink.Poster";
export const ReleaseLink = (props: {type?: "16_9"|"poster"}) => {
const type = props.type || "16_9";
if (type == "16_9") {
return (
<>
<div className="hidden lg:block"><ReleaseLink169 {...props} /></div>
<div className="block lg:hidden"><ReleaseLink169Poster {...props} /></div>
</>
)
}
if (type == "poster") {
return <ReleaseLinkPoster {...props} />;
}
};

View file

@ -0,0 +1,43 @@
import Link from "next/link";
import { PosterWithStuff } from "../ReleasePoster/PosterWithStuff";
export const ReleaseLink = (props: {
image: string;
title_ru: string;
title_original: string;
description?: string;
genres?: string;
grade?: number;
id: number;
settings?: {
showGenres?: boolean;
showDescription?: boolean;
};
chipsSettings?: {
enabled: boolean;
gradeHidden?: boolean;
statusHidden?: boolean;
categoryHidden?: boolean;
episodesHidden?: boolean;
listHidden?: boolean;
favHidden?: boolean;
lastWatchedHidden?: boolean;
};
profile_list_status?: number;
status?: {
name: string;
};
category?: {
name: string;
};
status_id?: number;
episodes_released?: string;
episodes_total?: string;
is_favorite?: boolean;
}) => {
return (
<Link href={`/release/${props.id}`}>
<PosterWithStuff {...props} />
</Link>
);
};

View file

@ -67,7 +67,10 @@ export const EpisodeSelector = (props: {
enabled: true,
sensitivity: 4,
}}
scrollbar={true}
scrollbar={{
enabled: true,
draggable: true,
}}
allowTouchMove={true}
style={
{
@ -79,6 +82,7 @@ export const EpisodeSelector = (props: {
<SwiperSlide
key={`episode_${episode.position}`}
style={{ maxWidth: "fit-content" }}
className="pb-2"
>
<Button
color={

View file

@ -3,7 +3,7 @@
import { Spinner } from "#/components/Spinner/Spinner";
import { useUserStore } from "#/store/auth";
import { useUserPlayerPreferencesStore } from "#/store/player";
import { Card, Dropdown, Button } from "flowbite-react";
import { Button, Card, Dropdown, DropdownItem } from "flowbite-react";
import { ENDPOINTS } from "#/api/config";
import { useState, useEffect } from "react";
import { Swiper, SwiperSlide } from "swiper/react";
@ -270,14 +270,14 @@ export const ReleasePlayer = (props: { id: number }) => {
theme={DropdownTheme}
>
{voiceoverInfo.map((voiceover: any) => (
<Dropdown.Item
<DropdownItem
key={`voiceover_${voiceover.id}`}
onClick={() =>
setSelectedVoiceoverAndSaveAsPreferred(voiceover)
}
>
{voiceover.name}
</Dropdown.Item>
</DropdownItem>
))}
</Dropdown>
<Dropdown
@ -286,12 +286,12 @@ export const ReleasePlayer = (props: { id: number }) => {
theme={DropdownTheme}
>
{sourcesInfo.map((source: any) => (
<Dropdown.Item
<DropdownItem
key={`source_${source.id}`}
onClick={() => setSelectedPlayerAndSaveAsPreferred(source)}
>
{source.name}
</Dropdown.Item>
</DropdownItem>
))}
</Dropdown>
</div>
@ -314,9 +314,12 @@ export const ReleasePlayer = (props: { id: number }) => {
direction={"horizontal"}
mousewheel={{
enabled: true,
sensitivity: 2,
sensitivity: 4,
}}
scrollbar={{
enabled: true,
draggable: true,
}}
scrollbar={true}
allowTouchMove={true}
style={
{
@ -328,6 +331,7 @@ export const ReleasePlayer = (props: { id: number }) => {
<SwiperSlide
key={`episode_${episode.position}`}
style={{ maxWidth: "fit-content" }}
className="pb-2"
>
<Button
color={

View file

@ -212,7 +212,8 @@ export const ReleasePlayerCustom = (props: {
}
};
__getInfo();
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.id, props.token]);
useEffect(() => {
const __getInfo = async () => {
@ -244,6 +245,7 @@ export const ReleasePlayerCustom = (props: {
if (voiceover.selected) {
__getInfo();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [voiceover.selected]);
useEffect(() => {
@ -284,6 +286,7 @@ export const ReleasePlayerCustom = (props: {
if (source.selected) {
__getInfo();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [source.selected]);
useEffect(() => {
@ -342,8 +345,11 @@ export const ReleasePlayerCustom = (props: {
setIsLoading(false);
};
if (episode.selected) {
setIsLoading(true);
setPlayerError(null);
__getInfo();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [episode.selected]);
return (
@ -431,7 +437,7 @@ export const ReleasePlayerCustom = (props: {
}
</div>
: <iframe src={playerProps.src} className="w-full aspect-video" />}
: <iframe src={playerProps.src} className="w-full aspect-video" allowFullScreen={true} />}
</div>
<div>

View file

@ -1,6 +1,6 @@
"use client";
import { Dropdown } from "flowbite-react";
import { Dropdown, DropdownItem } from "flowbite-react";
import { numberDeclension } from "#/api/utils";
import { useUserPlayerPreferencesStore } from "#/store/player";
@ -20,7 +20,7 @@ const DropdownTrigger = ({ name }: Source) => {
);
};
const DropdownItem = ({ name, episodes_count }: Source) => {
const DropdownItemInternal = ({ name, episodes_count }: Source) => {
return (
<div className="flex flex-col gap-2 cursor-pointer">
<div className="flex items-center gap-2">
@ -55,7 +55,7 @@ export const SourceSelector = (props: {
)}
>
{props.availableSource.map((source: Source) => (
<Dropdown.Item
<DropdownItem
key={`source_${source.id}`}
onClick={() => {
playerPreferenceStore.setPreferredPlayer(
@ -68,8 +68,8 @@ export const SourceSelector = (props: {
});
}}
>
<DropdownItem {...source} />
</Dropdown.Item>
<DropdownItemInternal {...source} />
</DropdownItem>
))}
</Dropdown>
);

View file

@ -1,6 +1,6 @@
"use client";
import { Dropdown } from "flowbite-react";
import { Dropdown, DropdownItem } from "flowbite-react";
import { numberDeclension } from "#/api/utils";
import { useUserPlayerPreferencesStore } from "#/store/player";
@ -16,7 +16,8 @@ interface Voiceover {
const DropdownTrigger = ({ icon, name, pinned }: Voiceover) => {
return (
<div className="flex items-center gap-2 cursor-pointer">
{icon && <img className="w-6 h-6 rounded-full" src={icon}></img>}
{/* eslint-disable-next-line @next/next/no-img-element */}
{icon && <img alt="" className="w-6 h-6 rounded-full" src={icon}></img>}
<p>{name}</p>
{pinned && (
<span className="h-6 bg-gray-700 dark:bg-gray-300 iconify material-symbols--push-pin"></span>
@ -26,7 +27,7 @@ const DropdownTrigger = ({ icon, name, pinned }: Voiceover) => {
);
};
const DropdownItem = ({
const DropdownItemInternal = ({
icon,
name,
pinned,
@ -36,7 +37,8 @@ const DropdownItem = ({
return (
<div className="flex flex-col gap-2 cursor-pointer">
<div className="flex items-center gap-2">
{icon && <img className="w-6 h-6 rounded-full" src={icon}></img>}
{/* eslint-disable-next-line @next/next/no-img-element */}
{icon && <img alt="" className="w-6 h-6 rounded-full" src={icon}></img>}
<p>{name}</p>
{pinned && (
<span className="h-6 iconify material-symbols--push-pin"></span>
@ -80,7 +82,7 @@ export const VoiceoverSelector = (props: {
)}
>
{props.availableVoiceover.map((voiceover: Voiceover) => (
<Dropdown.Item
<DropdownItem
className="w-fit"
key={`voiceover_${voiceover.id}`}
onClick={() => {
@ -94,8 +96,8 @@ export const VoiceoverSelector = (props: {
});
}}
>
<DropdownItem {...voiceover} />
</Dropdown.Item>
<DropdownItemInternal {...voiceover} />
</DropdownItem>
))}
</Dropdown>
);

View file

@ -0,0 +1,103 @@
import { sinceUnixDate } from "#/api/utils";
import { Chip } from "../Chip/Chip";
interface ChipProps {
settings?: any;
grade?: any;
status?: any;
status_id?: any;
user_list?: any;
episodes_released?: any;
episodes_total?: any;
category?: any;
is_favorite?: any;
last_view_episode?: any;
last_view_timestamp?: any;
}
export const ReleaseChips = ({
settings,
grade,
status,
status_id,
user_list,
episodes_released,
episodes_total,
category,
is_favorite,
last_view_episode,
last_view_timestamp,
}: ChipProps) => {
const chipSettings = {
enabled: true,
gradeHidden: false,
statusHidden: false,
categoryHidden: false,
episodesHidden: false,
listHidden: false,
favHidden: false,
lastWatchedHidden: true,
...settings,
};
return (
<div className={`${chipSettings.enabled ? "flex" : "hidden"} gap-1 flex-wrap`}>
{!chipSettings.gradeHidden && grade ?
<Chip
className="w-12"
bg_color={
grade == 0 ? "hidden"
: grade < 2 ?
"bg-red-500"
: grade < 3 ?
"bg-orange-500"
: grade < 4 ?
"bg-yellow-500"
: "bg-green-500"
}
name={`${grade}`}
/>
: ""}
{!chipSettings.listHidden && user_list && (
<Chip bg_color={user_list.bg_color} name={user_list.name} />
)}
{!chipSettings.statusHidden && status ?
<Chip name={status.name} />
: status_id != 0 && (
<Chip
name={
status_id == 1 ? "Завершено"
: status_id == 2 ?
"Онгоинг"
: status_id == 3 && "Анонс"
}
/>
)
}
{!chipSettings.episodesHidden && (
<Chip
name={episodes_released && episodes_released}
name_2={episodes_total ? episodes_total + " эп." : "? эп."}
devider="/"
/>
)}
{!chipSettings.categoryHidden && category && <Chip name={category.name} />}
{!chipSettings.favHidden && 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>
)}
{!chipSettings.lastWatchedHidden && last_view_episode && (
<Chip
name={
last_view_episode.name ?
last_view_episode.name
: `${last_view_episode.position + 1} серия`
}
name_2={last_view_timestamp && sinceUnixDate(last_view_timestamp)}
devider=", "
/>
)}
</div>
);
};

View file

@ -0,0 +1,22 @@
import Image from "next/image";
export const Poster = (props: {
image: string;
alt?: string;
className?: string;
}) => {
return (
<Image
className={`object-cover rounded-lg shadow-md ${props.className}`}
// className="w-[285px] max-h-[385px] object-cover border border-gray-200 rounded-lg shadow-md dark:border-gray-700"
src={props.image}
alt={props.alt || ""}
width={285}
height={385}
style={{
width: "100%",
height: "auto",
}}
/>
);
};

View file

@ -0,0 +1,117 @@
import { Poster } from "./Poster";
import { ReleaseChips } from "./Chips";
const profile_lists = {
// 0: "Не смотрю",
1: { name: "Смотрю", bg_color: "bg-green-500" },
2: { name: "В планах", bg_color: "bg-purple-500" },
3: { name: "Просмотрено", bg_color: "bg-blue-500" },
4: { name: "Отложено", bg_color: "bg-yellow-500" },
5: { name: "Брошено", bg_color: "bg-red-500" },
};
export const PosterWithStuff = (props: {
image: string;
title_ru: string;
title_original: string;
description?: string;
genres?: string;
grade?: number;
id: number;
settings?: {
showGenres?: boolean;
showDescription?: boolean;
};
chipsSettings?: {
enabled: boolean;
gradeHidden?: boolean;
statusHidden?: boolean;
categoryHidden?: boolean;
episodesHidden?: boolean;
listHidden?: boolean;
favHidden?: boolean;
lastWatchedHidden?: boolean;
};
profile_list_status?: number;
status?: {
name: string;
};
category?: {
name: string;
};
status_id?: number;
episodes_released?: string;
episodes_total?: string;
is_favorite?: boolean;
}) => {
const genres = [];
const settings = {
showGenres: true,
showDescription: true,
...props.settings,
};
const chipsSettings = props.chipsSettings || {}
const grade = props.grade ? Number(props.grade.toFixed(1)) : null;
const profile_list_status = props.profile_list_status || null;
let user_list = null;
if (profile_list_status != null || profile_list_status != 0) {
user_list = profile_lists[profile_list_status];
}
if (props.genres) {
const genres_array = props.genres.split(",");
genres_array.forEach((genre) => {
genres.push(genre.trim());
});
}
return (
<div className="relative w-full h-full overflow-hidden rounded-lg group">
<div className="absolute z-20 top-2 left-2 right-2">
<ReleaseChips
{...props}
user_list={user_list}
grade={grade}
settings={chipsSettings}
></ReleaseChips>
</div>
<div className="absolute z-20 bottom-2 left-2 right-2 lg:translate-y-[100%] group-hover:lg:translate-y-0 transition-transform">
<div className="lg:-translate-y-[calc(100%_+_1rem)] group-hover:lg:translate-y-0 transition-transform">
{settings.showGenres &&
genres.length > 0 &&
genres.map((genre: string, index: number) => {
return (
<span
key={`release_${props.id}_genre_${genre}_${index}`}
className="font-light leading-none text-white md:text-sm lg:text-base xl:text-lg"
>
{index > 0 && ", "}
{genre}
</span>
);
})}
{props.title_ru && (
<p className="py-1 text-xl font-bold leading-none text-white md:text-2xl md:py-0">
{props.title_ru}
</p>
)}
{props.title_original && (
<p className="text-sm leading-none text-gray-300 md:text-base">
{props.title_original}
</p>
)}
</div>
{settings.showDescription && props.description && (
<p className="mt-2 text-sm font-light leading-none text-white lg:text-base xl:text-lg line-clamp-4">
{props.description}
</p>
)}
</div>
<div className="absolute w-full h-full rounded-b-lg bg-gradient-to-t from-black to-transparent"></div>
<Poster
image={props.image}
className="w-auto h-auto min-w-full min-h-full flex-grow-1"
></Poster>
</div>
);
};

View file

@ -1,4 +1,4 @@
import { ReleaseLink } from "../ReleaseLink/ReleaseLink";
import { ReleaseLink } from "../ReleaseLink/ReleaseLinkUpdate";
export const ReleaseSection = (props: {
sectionTitle?: string;
@ -14,15 +14,22 @@ export const ReleaseSection = (props: {
</div>
)}
<div className="m-4">
<div className="lg:justify-center lg:grid-cols-[repeat(auto-fit,minmax(400px,1fr))] gap-4 lg:gap-2 min-w-full flex flex-col lg:grid">
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
{props.content.map((release) => {
return (
<div key={release.id} className="w-full h-full lg:aspect-video">
<ReleaseLink {...release} />
<div key={release.id} className="w-full h-full">
<ReleaseLink
{...release}
chipsSettings={{
enabled: true,
lastWatchedHidden:
(props.sectionTitle &&
props.sectionTitle.toLowerCase() != "история")
}}
/>
</div>
);
})}
{props.content.length == 1 && <div></div>}
</div>
</div>
</section>

View file

@ -1,13 +1,19 @@
"use client";
import { CURRENT_APP_VERSION } from "#/api/config";
import { useUserStore } from "#/store/auth";
import { usePreferencesStore } from "#/store/preferences";
import {
Modal,
Button,
useThemeMode,
ToggleSwitch,
HR,
ButtonGroup,
Dropdown,
DropdownItem,
HR,
Modal,
ModalBody,
ModalHeader,
ToggleSwitch,
useThemeMode,
} from "flowbite-react";
import Link from "next/link";
@ -32,10 +38,17 @@ const NavbarTitles = {
links: "Только ссылки",
selected: "Только выбранные",
never: "Никогда",
}
};
const FifthButton = {
3: "Избранное",
4: "Коллекции",
5: "История",
};
export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
const preferenceStore = usePreferencesStore();
const userStore = useUserStore();
const { computedMode, setMode } = useThemeMode();
@ -45,8 +58,8 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
show={props.isOpen}
onClose={() => props.setIsOpen(false)}
>
<Modal.Header>Настройки</Modal.Header>
<Modal.Body>
<ModalHeader>Настройки</ModalHeader>
<ModalBody>
<div className="space-y-6">
<div className="flex items-center gap-2">
<span className="w-6 h-6 iconify material-symbols--palette-outline"></span>
@ -54,20 +67,20 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
</div>
<div className="flex items-center justify-between">
<p className=" dark:text-white">Тема</p>
<Button.Group>
<ButtonGroup>
<Button
color={computedMode == "light" ? "blue" : "gray"}
color={computedMode == "light" ? "blue" : "light"}
onClick={() => setMode("light")}
>
Светлая
</Button>
<Button
color={computedMode == "dark" ? "blue" : "gray"}
color={computedMode == "dark" ? "blue" : "light"}
onClick={() => setMode("dark")}
>
Темная
</Button>
</Button.Group>
</ButtonGroup>
</div>
<div className="flex items-center justify-between">
<p className=" dark:text-white max-w-96">
@ -76,15 +89,6 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
</p>
<ToggleSwitch
color="blue"
theme={{
toggle: {
checked: {
color: {
blue: "border-blue-700 bg-blue-700",
},
},
},
}}
onChange={() =>
preferenceStore.setParams({
skipToCategory: {
@ -112,7 +116,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
>
{Object.keys(HomeCategory).map((key) => {
return (
<Dropdown.Item
<DropdownItem
key={key}
onClick={() =>
preferenceStore.setParams({
@ -124,7 +128,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
}
>
{HomeCategory[key]}
</Dropdown.Item>
</DropdownItem>
);
})}
</Dropdown>
@ -143,7 +147,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
>
{Object.keys(BookmarksCategory).map((key) => {
return (
<Dropdown.Item
<DropdownItem
key={key}
onClick={() =>
preferenceStore.setParams({
@ -155,7 +159,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
}
>
{BookmarksCategory[key]}
</Dropdown.Item>
</DropdownItem>
);
})}
</Dropdown>
@ -168,26 +172,66 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
</p>
<Dropdown
color="blue"
label={
NavbarTitles[preferenceStore.flags.showNavbarTitles]
}
label={NavbarTitles[preferenceStore.flags.showNavbarTitles]}
>
{Object.keys(NavbarTitles).map((key: "always" | "links" | "selected" | "never") => {
return (
<Dropdown.Item
key={`navbar-titles-${key}`}
onClick={() =>
preferenceStore.setFlags({
showNavbarTitles: key,
})
}
>
{NavbarTitles[key]}
</Dropdown.Item>
);
})}
{Object.keys(NavbarTitles).map(
(key: "always" | "links" | "selected" | "never") => {
return (
<DropdownItem
className={`${key == "links" ? "hidden lg:flex" : ""}`}
key={`navbar-titles-${key}`}
onClick={() =>
preferenceStore.setFlags({
showNavbarTitles: key,
})
}
>
{NavbarTitles[key]}
</DropdownItem>
);
}
)}
</Dropdown>
</div>
{userStore.isAuth ?
<div className="flex items-center justify-between sm:hidden">
<p className=" dark:text-white max-w-96">
Пятый пункт в навигации
</p>
<Dropdown
color="blue"
label={
preferenceStore.flags.showFifthButton ?
FifthButton[preferenceStore.flags.showFifthButton]
: "Нет"
}
>
<DropdownItem
onClick={() =>
preferenceStore.setFlags({
showFifthButton: null,
})
}
>
Не показывать
</DropdownItem>
{Object.keys(FifthButton).map((key) => {
return (
<DropdownItem
key={`navbar-fifthbutton-${key}`}
onClick={() =>
preferenceStore.setFlags({
showFifthButton: Number(key) as 3 | 4 | 5,
})
}
>
{FifthButton[key]}
</DropdownItem>
);
})}
</Dropdown>
</div>
: ""}
<HR className="my-4 dark:bg-slate-400" />
<div className="flex items-center gap-2">
<span className="w-6 h-6 iconify material-symbols--settings-outline"></span>
@ -197,15 +241,6 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
<p className=" dark:text-white">Показывать список изменений</p>
<ToggleSwitch
color="blue"
theme={{
toggle: {
checked: {
color: {
blue: "border-blue-700 bg-blue-700",
},
},
},
}}
onChange={() =>
preferenceStore.setFlags({
showChangelog: !preferenceStore.flags.showChangelog,
@ -223,15 +258,6 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
</div>
<ToggleSwitch
color="blue"
theme={{
toggle: {
checked: {
color: {
blue: "border-blue-700 bg-blue-700",
},
},
},
}}
onChange={() =>
preferenceStore.setFlags({
enableAnalytics: !preferenceStore.flags.enableAnalytics,
@ -254,15 +280,6 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
</div>
<ToggleSwitch
color="blue"
theme={{
toggle: {
checked: {
color: {
blue: "border-blue-700 bg-blue-700",
},
},
},
}}
onChange={() =>
preferenceStore.setParams({
experimental: {
@ -278,31 +295,20 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
<HR className="my-4 dark:bg-slate-400" />
<div>
<Link
href={"https://t.me/anix_web"}
href={"/about"}
className="flex items-center gap-2 p-2 text-left rounded-md hover:bg-gray-100 dark:hover:bg-gray-900"
onClick={() => props.setIsOpen(false)}
>
<span className="w-8 h-8 iconify fa6-brands--telegram"></span>
<span className="w-8 h-8 iconify material-symbols--info"></span>
<div>
<p>Телеграм канал</p>
<p>О приложении</p>
<p className="text-sm text-gray-400 dark:text-gray-200">
@anix_web
</p>
</div>
</Link>
<Link
href={"https://wah.su/radiquum"}
className="flex items-center gap-2 p-2 text-left rounded-md hover:bg-gray-100 dark:hover:bg-gray-900"
>
<span className="w-8 h-8 iconify mdi--code"></span>
<div>
<p>Разработчик</p>
<p className="text-sm text-gray-400 dark:text-gray-200">
Radiquum
v{CURRENT_APP_VERSION}
</p>
</div>
</Link>
</div>
</Modal.Body>
</ModalBody>
</Modal>
);
};

135
app/pages/About.tsx Normal file
View file

@ -0,0 +1,135 @@
"use server";
import { Card } from "flowbite-react";
import Image from "next/image";
import * as fs from "node:fs";
import * as path from "node:path";
import { CURRENT_APP_VERSION } from "#/api/config";
import Styles from "../components/ChangelogModal/ChangelogModal.module.css";
import Markdown from "markdown-to-jsx";
import {
Accordion,
AccordionContent,
AccordionPanel,
AccordionTitle,
} from "flowbite-react";
import Link from "next/link";
export const AboutPage = () => {
const directoryPath = path.join(process.cwd(), "public/changelog");
const files = fs.readdirSync(directoryPath);
const current = {
version: CURRENT_APP_VERSION,
changelog: `#${CURRENT_APP_VERSION}\r\nНет списка изменений`,
};
const previous = [];
if (files.includes(`${CURRENT_APP_VERSION}.md`)) {
const changelog = fs.readFileSync(
path.join(directoryPath, `${CURRENT_APP_VERSION}.md`),
"utf8"
);
current.changelog = changelog;
}
files.forEach((file) => {
if (file != `${CURRENT_APP_VERSION}.md`) {
const changelog = fs.readFileSync(path.join(directoryPath, file), "utf8");
previous.push({
version: file.replace(".md", ""),
changelog: changelog,
});
}
});
return (
<div className="grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3">
<Card className="md:col-span-2 lg:col-span-3">
<div className="flex flex-col items-center gap-4 md:flex-row">
<Image
src="/images/icons/icon-512x512.png"
className="flex-shrink-0 w-32 h-32 rounded-full"
alt="about image"
width={128}
height={128}
/>
<div>
<h1 className="text-xl font-bold">
AniX - Неофициальный веб клиент для Anixart
</h1>
<p className="max-w-[900px]">
AniX - это неофициальный веб-клиент для Android-приложения
Anixart. Он позволяет вам получать доступ к своей учетной записи
Anixart и управлять ею из веб-браузера. Так-же можно
синхронизировать и управлять списками и избранным. И самое главное
смотреть все доступные аниме из базы Anixart.
</p>
</div>
</div>
</Card>
<Link href={"https://wah.su/radiquum"} target="_blank">
<Card>
<div className="flex items-center gap-4">
<Image
src="https://radiquum.wah.su/static/avatar_512.jpg"
className="flex-shrink-0 w-16 h-16 rounded-full"
alt="developer image"
width={128}
height={128}
/>
<div>
<h1 className="text-xl font-bold">Radiquum</h1>
<p className="text-sm text-gray-500 dark:text-gray-200">
Разработчик
</p>
</div>
</div>
</Card>
</Link>
<Link href={"https://t.me/anix_web"} target="_blank">
<Card>
<div className="flex items-center gap-4">
<span className="w-16 h-16 iconify fa6-brands--telegram text-[#001725] dark:text-[#faf8f9]"></span>
<div>
<h1 className="text-xl font-bold">Телеграм канал</h1>
<p className="text-sm text-gray-500 dark:text-gray-200">
@anix_web
</p>
</div>
</div>
</Card>
</Link>
<Link href={"https://github.com/Radiquum/AniX"} target="_blank">
<Card>
<div className="flex items-center gap-4">
<span className="flex-shrink-0 w-16 h-16 iconify fa6-brands--github text-[#001725] dark:text-[#faf8f9]"></span>
<div>
<h1 className="text-xl font-bold">Код на GitHub</h1>
<p className="text-sm text-gray-500 dark:text-gray-200">
github.com/Radiquum/AniX
</p>
</div>
</div>
</Card>
</Link>
<Card className="md:col-span-2 lg:col-span-3">
<h1 className="text-2xl font-bold">Список изменений</h1>
<Markdown className={Styles.markdown}>{current.changelog}</Markdown>
<Accordion collapseAll={true}>
{previous.reverse().map((changelog) => (
<AccordionPanel key={changelog.version}>
<AccordionTitle>v{changelog.version}</AccordionTitle>
<AccordionContent>
<Markdown className={Styles.markdown}>
{changelog.changelog}
</Markdown>
</AccordionContent>
</AccordionPanel>
))}
</Accordion>
</Card>
</div>
);
};

View file

@ -5,7 +5,7 @@ import { Spinner } from "#/components/Spinner/Spinner";
import { useState, useEffect } from "react";
import { useScrollPosition } from "#/hooks/useScrollPosition";
import { useUserStore } from "../store/auth";
import { Dropdown, Button } from "flowbite-react";
import { Button, ButtonGroup, Dropdown, DropdownItem } from "flowbite-react";
import { sort } from "./common";
import { ENDPOINTS } from "#/api/config";
import { BookmarksList, useSWRfetcher } from "#/api/utils";
@ -151,7 +151,7 @@ export function BookmarksCategoryPage(props: any) {
</form>
: ""}
<div className="m-4 overflow-auto">
<Button.Group>
<ButtonGroup>
<Button
className="whitespace-nowrap"
disabled={props.slug == "watching"}
@ -222,7 +222,7 @@ export function BookmarksCategoryPage(props: any) {
>
{props.SectionTitleMapping["abandoned"]}
</Button>
</Button.Group>
</ButtonGroup>
</div>
<div className="flex items-center 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">
@ -237,7 +237,7 @@ export function BookmarksCategoryPage(props: any) {
theme={DropdownTheme}
>
{sort.values.map((item, index) => (
<Dropdown.Item key={index} onClick={() => setSelectedSort(index)}>
<DropdownItem key={index} onClick={() => setSelectedSort(index)}>
<span
className={`w-6 h-6 iconify ${
sort.values[index].value.split("_")[1] == "descending" ?
@ -246,7 +246,7 @@ export function BookmarksCategoryPage(props: any) {
}`}
></span>
{item.name}
</Dropdown.Item>
</DropdownItem>
))}
</Dropdown>
</div>

View file

@ -5,17 +5,18 @@ import { useEffect, useState, useCallback } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { ENDPOINTS } from "#/api/config";
import {
Card,
Button,
Card,
Checkbox,
TextInput,
Textarea,
FileInput,
Label,
Modal,
ModalHeader,
Textarea,
TextInput,
useThemeMode,
} from "flowbite-react";
import { ReleaseLink } from "#/components/ReleaseLink/ReleaseLink";
import { PosterWithStuff } from "#/components/ReleasePoster/PosterWithStuff";
import { CropModal } from "#/components/CropModal/CropModal";
import { b64toBlob, tryCatchAPI } from "#/api/utils";
@ -33,6 +34,7 @@ export const CreateCollectionPage = () => {
if (userStore.state === "finished" && !userStore.token) {
router.push("/login?redirect=/collections/create");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userStore]);
const [edit, setEdit] = useState(false);
@ -388,10 +390,9 @@ export const CreateCollectionPage = () => {
</Label>
<div className="flex-1">
<div className="block mb-2">
<Label
htmlFor="title"
value="Название (минимум 10, максимум 60 символов)"
/>
<Label htmlFor="title">
Название (минимум 10, максимум 60 символов)
</Label>
</div>
<TextInput
id="title"
@ -408,10 +409,9 @@ export const CreateCollectionPage = () => {
{stringLength.title}/60
</p>
<div className="block mt-2 mb-2">
<Label
htmlFor="description"
value="Описание (максимум 1000 символов)"
/>
<Label htmlFor="description">
Описание (максимум 1000 символов)
</Label>
</div>
<Textarea
rows={4}
@ -434,7 +434,7 @@ export const CreateCollectionPage = () => {
checked={isPrivate}
onChange={(e) => setIsPrivate(e.target.checked)}
/>
<Label htmlFor="private" value="Приватная коллекция" />
<Label htmlFor="private">Приватная коллекция</Label>
</div>
</div>
<Button
@ -462,20 +462,17 @@ export const CreateCollectionPage = () => {
</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">
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
{addedReleases.map((release) => {
return (
<div
key={release.id}
className="relative w-full h-full aspect-video group"
>
<div key={release.id} className="relative w-full h-full group">
<button
className="absolute inset-0 z-10 text-black transition-opacity bg-white opacity-0 group-hover:opacity-75"
className="absolute inset-0 z-30 text-black transition-opacity bg-white rounded-lg opacity-0 group-hover:opacity-75"
onClick={() => _deleteRelease(release)}
>
Удалить
</button>
<ReleaseLink {...release} isLinkDisabled={true} />
<PosterWithStuff {...release} />
</div>
);
})}
@ -592,6 +589,9 @@ export const ReleasesEditModal = (props: {
return;
}
const newContent = content.filter((r) => r.id !== release.id);
setContent(newContent);
props.setReleases([...props.releases, release]);
props.setReleasesIds([...props.releasesIds, release.id]);
}
@ -603,7 +603,7 @@ export const ReleasesEditModal = (props: {
onClose={() => props.setIsOpen(false)}
size={"7xl"}
>
<Modal.Header>Изменить релизы в коллекции</Modal.Header>
<ModalHeader>Изменить релизы в коллекции</ModalHeader>
<div
onScroll={handleScroll}
ref={modalRef}
@ -611,11 +611,7 @@ export const ReleasesEditModal = (props: {
>
<form
className="max-w-full mx-auto"
onSubmit={(e) => {
e.preventDefault();
setContent([]);
setQuery(e.target[0].value.trim());
}}
onSubmit={(e) => e.preventDefault()}
>
<label
htmlFor="default-search"
@ -647,30 +643,35 @@ export const ReleasesEditModal = (props: {
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 || ""}
onChange={(e) => setQuery(e.target.value)}
value={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"
type="button"
className="text-white absolute end-2.5 bottom-2.5 bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800"
onClick={() => {
setSize(0);
setContent([]);
setQuery("");
}}
>
Поиск
Очистить
</button>
</div>
</form>
<div className="grid grid-cols-2 gap-2 mt-2 md:grid-cols-4">
<div className="grid grid-cols-1 gap-2 mt-2 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
{content.map((release) => {
return (
<button
className="text-left"
key={release.id}
className="overflow-hidden"
onClick={() => _addRelease(release)}
>
<ReleaseLink type="poster" {...release} isLinkDisabled={true} />
<PosterWithStuff {...release} />
</button>
);
})}
{content.length == 1 && <div></div>}
</div>
{isLoading && (
<div className="flex items-center justify-center h-full min-h-24">

View file

@ -5,7 +5,7 @@ import { Spinner } from "#/components/Spinner/Spinner";
import { useState, useEffect } from "react";
import { useScrollPosition } from "#/hooks/useScrollPosition";
import { useUserStore } from "../store/auth";
import { Dropdown, Button } from "flowbite-react";
import { Button, Dropdown, DropdownItem } from "flowbite-react";
import { sort } from "./common";
import { ENDPOINTS } from "#/api/config";
import { useRouter } from "next/navigation";
@ -126,7 +126,7 @@ export function FavoritesPage() {
theme={DropdownTheme}
>
{sort.values.map((item, index) => (
<Dropdown.Item key={index} onClick={() => setSelectedSort(index)}>
<DropdownItem key={index} onClick={() => setSelectedSort(index)}>
<span
className={`w-6 h-6 iconify ${
sort.values[index].value.split("_")[1] == "descending"
@ -135,7 +135,7 @@ export function FavoritesPage() {
}`}
></span>
{item.name}
</Dropdown.Item>
</DropdownItem>
))}
</Dropdown>
</div>

View file

@ -5,7 +5,7 @@ import { useState, useEffect } from "react";
import { useScrollPosition } from "#/hooks/useScrollPosition";
import { useUserStore } from "../store/auth";
import { _FetchHomePageReleases } from "#/api/utils";
import { Button } from "flowbite-react";
import { Button, ButtonGroup } from "flowbite-react";
import { useRouter } from "next/navigation";
export function IndexCategoryPage(props) {
@ -53,13 +53,13 @@ export function IndexCategoryPage(props) {
return (
<>
<div className="mb-4 overflow-auto">
<Button.Group>
<ButtonGroup>
<Button className="whitespace-nowrap" disabled={props.slug == "last"} color="light" onClick={() => router.push("/home/last")}>{props.SectionTitleMapping["last"]}</Button>
<Button className="whitespace-nowrap" disabled={props.slug == "finished"} color="light" onClick={() => router.push("/home/finished")}>{props.SectionTitleMapping["finished"]}</Button>
<Button className="whitespace-nowrap" disabled={props.slug == "ongoing"} color="light" onClick={() => router.push("/home/ongoing")}>{props.SectionTitleMapping["ongoing"]}</Button>
<Button className="whitespace-nowrap" disabled={props.slug == "announce"} color="light" onClick={() => router.push("/home/announce")}>{props.SectionTitleMapping["announce"]}</Button>
<Button className="whitespace-nowrap" disabled={props.slug == "films"} color="light" onClick={() => router.push("/home/films")}>{props.SectionTitleMapping["films"]}</Button>
</Button.Group>
</ButtonGroup>
</div>
{content && content.length > 0 ? (
<ReleaseSection

View file

@ -6,9 +6,11 @@ import { useRouter } from "next/navigation";
import { SettingsModal } from "#/components/SettingsModal/SettingsModal";
import { useEffect, useState } from "react";
import Image from "next/image";
import { usePreferencesStore } from "#/store/preferences";
export const MenuPage = () => {
const userStore = useUserStore();
const preferenceStore = usePreferencesStore();
const router = useRouter();
const [isSettingModalOpen, setIsSettingModalOpen] = useState(false);
@ -22,7 +24,7 @@ export const MenuPage = () => {
return (
<>
{userStore.user && (
<div className="flex flex-col gap-2">
<div className="fixed flex flex-col justify-end gap-2 left-4 right-4 bottom-24 sm:static">
<div className="flex flex-wrap items-center gap-2">
<Link
href={`/profile/${userStore.user.id}`}
@ -81,62 +83,42 @@ export const MenuPage = () => {
</button>
</div>
</div>
<Link href="/favorites" className="flex-1 sm:hidden">
<Card>
<div className="flex items-center gap-2">
<span
className={`iconify material-symbols--favorite-outline w-6 h-6`}
></span>
<p>Избранное</p>
</div>
</Card>
</Link>
<Link href="/collections" className="flex-1 sm:hidden">
<Card>
<div className="flex items-center gap-2">
<span
className={`iconify material-symbols--collections-bookmark-outline w-6 h-6`}
></span>
<p>Коллекции</p>
</div>
</Card>
</Link>
<Link href="/history" className="flex-1 sm:hidden">
<Card>
<div className="flex items-center gap-2">
<span
className={`iconify material-symbols--history w-6 h-6`}
></span>
<p>История</p>
</div>
</Card>
</Link>
<Link href={"https://t.me/anix_web"} className="flex-1">
<Card>
<div className="flex items-center gap-2">
<span className="w-8 h-8 iconify fa6-brands--telegram"></span>
<div>
<p>Телеграм канал</p>
<p className="text-sm text-gray-400 dark:text-gray-200">
@anix_web
</p>
{preferenceStore.flags.showFifthButton != 3 ?
<Link href="/favorites" className="flex-1 sm:hidden">
<Card>
<div className="flex items-center gap-2">
<span
className={`iconify material-symbols--favorite-outline w-6 h-6`}
></span>
<p>Избранное</p>
</div>
</div>
</Card>
</Link>
<Link href={"https://wah.su/radiquum"} className="flex-1">
<Card>
<div className="flex items-center gap-2">
<span className="w-8 h-8 iconify mdi--code"></span>
<div>
<p>Разработчик</p>
<p className="text-sm text-gray-400 dark:text-gray-200">
Radiquum
</p>
</Card>
</Link>
: ""}
{preferenceStore.flags.showFifthButton != 4 ?
<Link href="/collections" className="flex-1 sm:hidden">
<Card>
<div className="flex items-center gap-2">
<span
className={`iconify material-symbols--collections-bookmark-outline w-6 h-6`}
></span>
<p>Коллекции</p>
</div>
</div>
</Card>
</Link>
</Card>
</Link>
: ""}
{preferenceStore.flags.showFifthButton != 5 ?
<Link href="/history" className="flex-1 sm:hidden">
<Card>
<div className="flex items-center gap-2">
<span
className={`iconify material-symbols--history w-6 h-6`}
></span>
<p>История</p>
</div>
</Card>
</Link>
: ""}
<SettingsModal
isOpen={isSettingModalOpen}
setIsOpen={setIsSettingModalOpen}

View file

@ -59,53 +59,6 @@ export const ProfilePage = (props: any) => {
);
}
const hasSocials =
user.vk_page != "" ||
user.tg_page != "" ||
user.tt_page != "" ||
user.inst_page != "" ||
user.discord_page != "" ||
false;
const socials = [
{
name: "vk",
nickname: user.vk_page,
icon: "fa6-brands--vk",
urlPrefix: "https://vk.com/",
},
{
name: "telegram",
nickname: user.tg_page,
icon: "fa6-brands--telegram",
urlPrefix: "https://t.me/",
},
{
name: "discord",
nickname: user.discord_page,
icon: "fa6-brands--discord",
},
{
name: "tiktok",
nickname: user.tt_page,
icon: "fa6-brands--tiktok",
urlPrefix: "https://tiktok.com/@",
},
{
name: "instagram",
nickname: user.inst_page,
icon: "fa6-brands--instagram",
urlPrefix: "https://instagram.com/",
},
];
const hasChips =
user.is_verified ||
user.is_blocked ||
(user.roles && user.roles.length > 0) ||
isMyProfile;
const isPrivacy =
user.is_stats_hidden || user.is_counts_hidden || user.is_social_hidden;
return (
<>
<div className="flex flex-col gap-2">
@ -116,61 +69,48 @@ export const ProfilePage = (props: any) => {
ban_expires={user.ban_expires}
/>
<ProfilePrivacyBanner
is_privacy={isPrivacy}
is_privacy={
user.is_stats_hidden ||
user.is_counts_hidden ||
user.is_social_hidden
}
is_me_blocked={user.is_me_blocked}
/>
</div>
<div
className={`flex flex-wrap gap-2 ${
isPrivacy || user.is_banned || user.is_perm_banned ? "mt-4" : ""
}`}
className={`grid grid-cols-1 gap-2 lg:grid-cols-2 ${
(
user.is_banned ||
user.is_perm_banned ||
user.is_stats_hidden ||
user.is_counts_hidden ||
user.is_social_hidden
) ?
"mt-4"
: ""
} mb-4`}
>
<div className="flex flex-col gap-2 w-full xl:w-[50%]">
<div className="flex flex-col gap-2">
<ProfileUser
isOnline={user.is_online}
avatar={user.avatar}
login={user.login}
status={user.status}
avatar={user.avatar || ""}
login={user.login || ""}
status={user.status || ""}
roles={user.roles || []}
rating={user.rating_score || 0}
isMyProfile={isMyProfile || false}
isVerified={user.is_verified || false}
isOnline={user.is_online || false}
isSponsor={user.is_sponsor || false}
isBlocked={user.is_blocked || false}
socials={{
isPrivate: user.is_social_hidden,
hasSocials: hasSocials,
socials: socials,
vk: user.vk_page || null,
tg: user.tg_page || null,
tt: user.tt_page || null,
inst: user.inst_page || null,
discord: user.discord_page || null,
}}
chips={{
hasChips: hasChips,
isMyProfile: isMyProfile,
isVerified: user.is_verified,
isSponsor: user.is_sponsor,
isBlocked: user.is_blocked,
roles: user.roles,
}}
rating={user.rating_score}
is_social_hidden={user.is_social_hidden}
/>
{!user.is_counts_hidden && (
<ProfileActivity
profile_id={user.id}
commentCount={user.comment_count}
videoCount={user.video_count}
collectionCount={user.collection_count}
friendsCount={user.friend_count}
/>
)}
{!user.is_stats_hidden && (
<div className="flex-col hidden gap-2 xl:flex">
{user.votes && user.votes.length > 0 && (
<ProfileReleaseRatings
ratings={user.votes}
token={authUser.token}
profile_id={user.id}
/>
)}
{user.history && user.history.length > 0 && (
<ProfileReleaseHistory history={user.history} />
)}
</div>
)}
</div>
<div className="flex flex-col w-full gap-2 xl:flex-1 xl:w-auto ">
{authUser.token && (
<ProfileActions
isMyProfile={isMyProfile}
@ -185,6 +125,32 @@ export const ProfilePage = (props: any) => {
edit_setIsOpen={setIsOpen}
/>
)}
{!user.is_counts_hidden && (
<ProfileActivity
profile_id={user.id}
commentCount={user.comment_count}
commentPreview={user.release_comments_preview || []}
collectionCount={user.collection_count}
collectionPreview={user.collections_preview || []}
friendsCount={user.friend_count}
friendsPreview={user.friends_preview || []}
token={authUser.token}
isMyProfile={isMyProfile || false}
/>
)}
{!user.is_stats_hidden && (
<div className="flex-col hidden gap-2 lg:flex">
{user.votes && user.votes.length > 0 && (
<ProfileReleaseRatings
ratings={user.votes}
token={authUser.token}
profile_id={user.id}
/>
)}
</div>
)}
</div>
<div className="flex flex-col gap-2">
{!user.is_stats_hidden && (
<>
<ProfileStats
@ -200,7 +166,7 @@ export const ProfilePage = (props: any) => {
profile_id={user.id}
/>
<ProfileWatchDynamic watchDynamic={user.watch_dynamics || []} />
<div className="flex flex-col gap-2 xl:hidden">
<div className="flex flex-col gap-2 lg:hidden">
{user.votes && user.votes.length > 0 && (
<ProfileReleaseRatings
ratings={user.votes}
@ -208,10 +174,10 @@ export const ProfilePage = (props: any) => {
profile_id={user.id}
/>
)}
{user.history && user.history.length > 0 && (
<ProfileReleaseHistory history={user.history} />
)}
</div>
{user.history && user.history.length > 0 && (
<ProfileReleaseHistory history={user.history} />
)}
</>
)}
</div>

View file

@ -5,11 +5,24 @@ import { useState, useEffect } from "react";
import { useScrollPosition } from "#/hooks/useScrollPosition";
import { useUserStore } from "../store/auth";
import { ENDPOINTS } from "#/api/config";
import { ReleaseLink169Related } from "#/components/ReleaseLink/ReleaseLink.16_9Related";
import { useSWRfetcher } from "#/api/utils";
import { Card } from "flowbite-react";
import { Poster } from "#/components/ReleasePoster/Poster";
import { ReleaseChips } from "#/components/ReleasePoster/Chips";
import { PosterWithStuff } from "#/components/ReleasePoster/PosterWithStuff";
import Link from "next/link";
const profile_lists = {
// 0: "Не смотрю",
1: { name: "Смотрю", bg_color: "bg-green-500" },
2: { name: "В планах", bg_color: "bg-purple-500" },
3: { name: "Просмотрено", bg_color: "bg-blue-500" },
4: { name: "Отложено", bg_color: "bg-yellow-500" },
5: { name: "Брошено", bg_color: "bg-red-500" },
};
const YearSeason = ["_", "Зима", "Весна", "Лето", "Осень"];
export function RelatedPage(props: {id: number|string, title: string}) {
export function RelatedPage(props: { id: number | string; title: string }) {
const token = useUserStore((state) => state.token);
const getKey = (pageIndex: number, previousPageData: any) => {
@ -52,22 +65,95 @@ export function RelatedPage(props: {id: number|string, title: string}) {
Франшиза {props.title}
</h1>
</div>
{content && content.length > 0 ? (
{content && content.length > 0 ?
<div className="flex flex-col gap-4 my-4">
{content.map((release, index) => {
return <ReleaseLink169Related {...release} key={release.id} _position={index + 1} />
const genres = [];
const grade =
release.grade ? Number(release.grade.toFixed(1)) : null;
const profile_list_status = release.profile_list_status || null;
let user_list = null;
if (profile_list_status != null || profile_list_status != 0) {
user_list = profile_lists[profile_list_status];
}
if (release.genres) {
const genres_array = release.genres.split(",");
genres_array.forEach((genre) => {
genres.push(genre.trim());
});
}
return (
<Link href={`/release/${release.id}`} key={release.id}>
<Card>
<div className="grid grid-cols-1 justify-center lg:grid-cols-[1fr_1fr_2fr] gap-4">
<div className="flex items-center justify-center">
<h1 className="inline-block text-6xl font-bold text-center text-transparent bg-gradient-to-r from-blue-600 via-purple-500 to-indigo-500 dark:from-blue-500 dark:via-purple-400 dark:to-indigo-300 bg-clip-text ">
{release.season ? YearSeason[release.season] : ""}
{release.season ?
<br />
: ""}
{release.year ? release.year : ""}
</h1>
</div>
<div className="flex items-center justify-center lg:hidden">
<div className="max-w-64">
<PosterWithStuff {...release} />
</div>
</div>
<div className="hidden lg:flex">
<Poster image={release.image} className="h-auto" />
</div>
<div className="flex-col hidden gap-2 lg:flex">
<ReleaseChips
{...release}
user_list={user_list}
grade={grade}
/>
<div>
{genres.length > 0 &&
genres.map((genre: string, index: number) => {
return (
<span
key={`release_${props.id}_genre_${genre}_${index}`}
className="font-light dark:text-white md:text-sm lg:text-base xl:text-lg"
>
{index > 0 && ", "}
{genre}
</span>
);
})}
</div>
{release.title_ru && (
<p className="text-xl font-bold dark:text-white md:text-2xl">
{release.title_ru}
</p>
)}
{release.title_original && (
<p className="text-sm text-gray-600 dark:text-gray-300 md:text-base">
{release.title_original}
</p>
)}
{release.description && (
<p className="mt-2 text-sm font-light dark:text-white lg:text-base xl:text-lg line-clamp-2">
{release.description}
</p>
)}
</div>
</div>
</Card>
</Link>
);
})}
</div>
) : isLoading ? (
: 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">
: <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 && (

View file

@ -8,7 +8,7 @@ import { useScrollPosition } from "#/hooks/useScrollPosition";
import { useRouter } from "next/navigation";
import { useSearchParams } from "next/navigation";
import { useUserStore } from "../store/auth";
import { Button, Dropdown, Modal } from "flowbite-react";
import { Button, Dropdown, DropdownItem, Modal, ModalBody, ModalFooter, ModalHeader } from "flowbite-react";
import { CollectionsSection } from "#/components/CollectionsSection/CollectionsSection";
import { UserSection } from "#/components/UserSection/UserSection";
import { useSWRfetcher } from "#/api/utils";
@ -360,8 +360,8 @@ const FiltersModal = (props: {
return (
<Modal show={props.isOpen} onClose={() => _cancel()}>
<Modal.Header>Фильтры</Modal.Header>
<Modal.Body>
<ModalHeader>Фильтры</ModalHeader>
<ModalBody>
<div className="my-4">
<div className="flex items-center justify-between">
<p className="font-bold dark:text-white">Искать в</p>
@ -376,12 +376,12 @@ const FiltersModal = (props: {
return <></>;
} else {
return (
<Dropdown.Item
<DropdownItem
onClick={() => setWhere(item)}
key={`where--${item}`}
>
{WhereMapping[item]}
</Dropdown.Item>
</DropdownItem>
);
}
})}
@ -395,12 +395,12 @@ const FiltersModal = (props: {
<Dropdown label={ListsMapping[list].name} color="blue">
{Object.keys(ListsMapping).map((item) => {
return (
<Dropdown.Item
<DropdownItem
onClick={() => setList(item)}
key={`list--${item}`}
>
{ListsMapping[item].name}
</Dropdown.Item>
</DropdownItem>
);
})}
</Dropdown>
@ -414,20 +414,20 @@ const FiltersModal = (props: {
<Dropdown label={TagMapping[searchBy].name} color="blue">
{Object.keys(TagMapping).map((item) => {
return (
<Dropdown.Item
<DropdownItem
onClick={() => setSearchBy(item)}
key={`tag--${item}`}
>
{TagMapping[item].name}
</Dropdown.Item>
</DropdownItem>
);
})}
</Dropdown>
</div>
</div>
: ""}
</Modal.Body>
<Modal.Footer>
</ModalBody>
<ModalFooter>
<div className="flex justify-end w-full gap-2">
<Button color="red" onClick={() => _cancel()}>
Отменить
@ -436,7 +436,7 @@ const FiltersModal = (props: {
Применить
</Button>
</div>
</Modal.Footer>
</ModalFooter>
</Modal>
);
};

View file

@ -11,6 +11,7 @@ interface preferencesState {
showChangelog?: boolean;
enableAnalytics?: boolean;
showNavbarTitles?: "always" | "links" | "selected" | "never";
showFifthButton?: null | 3 | 4 | 5;
};
params: {
isFirstLaunch?: boolean;
@ -22,7 +23,7 @@ interface preferencesState {
};
experimental?: {
newPlayer: boolean;
}
};
// color: {
// primary: string;
// secondary: string;
@ -44,6 +45,7 @@ export const usePreferencesStore = create<preferencesState>()(
showChangelog: true,
enableAnalytics: true,
showNavbarTitles: "always",
showFifthButton: null,
},
params: {
isFirstLaunch: true,
@ -54,8 +56,8 @@ export const usePreferencesStore = create<preferencesState>()(
bookmarksCategory: "watching",
},
experimental: {
newPlayer: false
}
newPlayer: false,
},
},
setHasHydrated: (state) => {
set({
@ -74,9 +76,12 @@ export const usePreferencesStore = create<preferencesState>()(
onRehydrateStorage: (state) => {
return () => state.setHasHydrated(true);
},
merge: (persistedState , currentState) => {
return deepmerge(currentState as preferencesState, persistedState as preferencesState);
}
merge: (persistedState, currentState) => {
return deepmerge(
currentState as preferencesState,
persistedState as preferencesState
);
},
}
)
);

View file

@ -6,10 +6,10 @@ AniX - это неофициальный веб-клиент для Android-пр
## Список изменений
- [3.5.0](/public/changelog/3.5.0.md)
- [3.4.0](/public/changelog/3.4.0.md)
- [3.3.0](/public/changelog/3.3.0.md)
- [3.2.3](/public/changelog/3.2.3.md)
- [3.2.2](/public/changelog/3.2.2.md)
[другие версии](/public/changelog)

View file

@ -1,8 +1,7 @@
const { withPlausibleProxy } = require("next-plausible");
module.exports = withPlausibleProxy({
customDomain: "https://analytics.wah.su",
})({
const withFlowbiteReact = require("flowbite-react/plugin/nextjs");
/** @type {import('next').NextConfig} */
const NextConfig = {
reactStrictMode: false,
images: {
unoptimized: true,
@ -10,68 +9,74 @@ module.exports = withPlausibleProxy({
async headers() {
return [
{
source: '/bookmarks/:slug*',
source: "/bookmarks/:slug*",
headers: [
{
key: 'Cache-Control',
value: 's-maxage=2592000, stale-while-revalidate=86400',
key: "Cache-Control",
value: "s-maxage=2592000, stale-while-revalidate=86400",
},
],
},
{
source: '/collection/:slug*',
source: "/collection/:slug*",
headers: [
{
key: 'Cache-Control',
value: 's-maxage=2592000, stale-while-revalidate=86400',
key: "Cache-Control",
value: "s-maxage=2592000, stale-while-revalidate=86400",
},
],
},
{
source: '/home/:slug*',
source: "/home/:slug*",
headers: [
{
key: 'Cache-Control',
value: 's-maxage=2592000, stale-while-revalidate=86400',
key: "Cache-Control",
value: "s-maxage=2592000, stale-while-revalidate=86400",
},
],
},
{
source: '/profile/:slug*',
source: "/profile/:slug*",
headers: [
{
key: 'Cache-Control',
value: 's-maxage=2592000, stale-while-revalidate=86400',
key: "Cache-Control",
value: "s-maxage=2592000, stale-while-revalidate=86400",
},
],
},
{
source: '/release/:slug*',
source: "/release/:slug*",
headers: [
{
key: 'Cache-Control',
value: 's-maxage=2592000, stale-while-revalidate=86400',
key: "Cache-Control",
value: "s-maxage=2592000, stale-while-revalidate=86400",
},
],
},
{
source: '/related/:slug*',
source: "/related/:slug*",
headers: [
{
key: 'Cache-Control',
value: 's-maxage=2592000, stale-while-revalidate=86400',
key: "Cache-Control",
value: "s-maxage=2592000, stale-while-revalidate=86400",
},
],
},
{
source: '/search',
source: "/search",
headers: [
{
key: 'Cache-Control',
value: 's-maxage=2592000, stale-while-revalidate=86400',
key: "Cache-Control",
value: "s-maxage=2592000, stale-while-revalidate=86400",
},
],
},
];
},
});
};
const config = withPlausibleProxy({
customDomain: "https://analytics.wah.su",
})(withFlowbiteReact(NextConfig));
module.exports = config;

1837
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -6,18 +6,20 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"postinstall": "flowbite-react patch"
},
"dependencies": {
"apexcharts": "^3.52.0",
"deepmerge-ts": "^7.1.0",
"flowbite": "^2.4.1",
"flowbite-react": "^0.10.1",
"flowbite-react": "^0.11.7",
"hls-video-element": "^1.5.0",
"markdown-to-jsx": "^7.4.7",
"media-chrome": "^4.8.0",
"next": "^14.2.13",
"next": "^14.2.26",
"next-plausible": "^3.12.1",
"prettier": "^3.5.3",
"react": "^18",
"react-cropper": "^2.3.3",
"react-dom": "^18",
@ -39,8 +41,6 @@
"eslint": "^8",
"eslint-config-next": "14.2.5",
"postcss": "^8",
"prettier": "^3.5.3",
"tailwind-scrollbar": "^3.1.0",
"tailwindcss": "^3.4.1"
}
}

17
public/changelog/3.5.0.md Normal file
View file

@ -0,0 +1,17 @@
# 3.5.0
## Добавлено
- Добавлена страница о приложении
- Добавлена возможность добавить пятую кнопку в меню навигации на мобильных устройствах
## Изменено
- Стиль карточек для релизов был изменён на вертикальный
- На мобильных устройствах постер на странице релиза теперь по середине
- Позиция лицензированных сервисов теперь под постером
- На мобильных устройствах позиция меню была смещена вниз
## Исправлено
- Ошибка своего плеера не сбрасывалась, если удалось получить ссылку при переключении серии

View file

@ -1,5 +1,9 @@
const { addIconSelectors } = require("@iconify/tailwind");
import flowbite from "flowbite-react/tailwind";
const flowbiteReact = require("flowbite-react/plugin/tailwindcss");
flowbiteReact.config = {
charts: true,
}
/** @type {import('tailwindcss').Config} */
module.exports = {
@ -7,14 +11,11 @@ module.exports = {
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
flowbite.content(),
".flowbite-react\\class-list.json"
],
plugins: [
addIconSelectors(["mdi", "material-symbols", "twemoji", "fa6-brands"]),
require("tailwind-scrollbar"),
flowbite.plugin()({
charts: true,
}),
flowbiteReact
],
darkMode: "selector",
theme: {
@ -43,4 +44,4 @@ module.exports = {
},
},
},
};
};