Compare commits
No commits in common. "V3" and "3.5.0" have entirely different histories.
|
@ -1,3 +1,7 @@
|
|||
{
|
||||
"extends": ["next/core-web-vitals"]
|
||||
"extends": ["next/core-web-vitals", "prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error"
|
||||
},
|
||||
"plugins": ["prettier"]
|
||||
}
|
||||
|
|
2
.flowbite-react/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
class-list.json
|
||||
pid
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"$schema": "https://unpkg.com/flowbite-react/schema.json",
|
||||
"components": [],
|
||||
"dark": true,
|
||||
"prefix": "",
|
||||
"path": "src/components",
|
||||
"tsx": true,
|
||||
"rsc": true
|
||||
}
|
2
.gitignore
vendored
|
@ -59,5 +59,3 @@ videos/*
|
|||
!videos/*.js
|
||||
!videos/*.ts
|
||||
public/_next-video
|
||||
|
||||
API-Trace/*
|
|
@ -6,10 +6,10 @@ AniX is an unofficial web client for the Android application Anixart. It allows
|
|||
|
||||
## Changelog [RU]
|
||||
|
||||
- [3.6.0](./public/changelog/3.6.0.md)
|
||||
- [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)
|
||||
|
||||
[other versions](./public/changelog)
|
||||
|
||||
|
|
12
app/App.tsx
|
@ -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, ModalBody, ModalFooter, ModalHeader } from "flowbite-react";
|
||||
import { Button, Modal } 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 })}
|
||||
>
|
||||
<ModalHeader>Внимание</ModalHeader>
|
||||
<ModalBody>
|
||||
<Modal.Header>Внимание</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>
|
||||
Данный сайт не связан с разработчиками приложения Anixart, это
|
||||
неофициальная имплементация веб клиента для этого приложения.
|
||||
|
@ -94,15 +94,15 @@ export const App = (props) => {
|
|||
На сайте могут присутствовать ошибки и не доработки, а так-же
|
||||
отсутствующий функционал.
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
color={"blue"}
|
||||
onClick={() => preferencesStore.setParams({ isFirstLaunch: false })}
|
||||
>
|
||||
Принимаю
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
{preferencesStore.flags.enableAnalytics && (
|
||||
<PlausibleProvider
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export const CURRENT_APP_VERSION = "3.6.0";
|
||||
export const CURRENT_APP_VERSION = "3.5.0";
|
||||
|
||||
export const API_URL = "https://api.anixart.tv";
|
||||
export const API_PREFIX = "/api/proxy";
|
||||
|
@ -18,15 +18,6 @@ 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: {
|
||||
|
|
|
@ -212,9 +212,9 @@ export function unixToDate(
|
|||
" " +
|
||||
date.getFullYear() +
|
||||
", " +
|
||||
`${date.getHours()}`.padStart(2, "0") +
|
||||
date.getHours() +
|
||||
":" +
|
||||
`${date.getMinutes()}`.padStart(2, "0")
|
||||
date.getMinutes()
|
||||
);
|
||||
if (type === "dayMonth")
|
||||
return date.getDate() + " " + months[date.getMonth()];
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionPanel,
|
||||
AccordionTitle,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
} from "flowbite-react";
|
||||
import { Modal, Accordion } from "flowbite-react";
|
||||
import Markdown from "markdown-to-jsx";
|
||||
import { useEffect, useState } from "react";
|
||||
import Styles from "./ChangelogModal.module.css";
|
||||
|
@ -45,8 +37,8 @@ export const ChangelogModal = (props: {
|
|||
|
||||
return (
|
||||
<Modal show={props.isOpen} onClose={() => props.setIsOpen(false)}>
|
||||
<ModalHeader>Список изменений v{props.version}</ModalHeader>
|
||||
<ModalBody>
|
||||
<Modal.Header>Список изменений v{props.version}</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Markdown className={Styles.markdown}>
|
||||
{currentVersionChangelog}
|
||||
</Markdown>
|
||||
|
@ -54,8 +46,8 @@ export const ChangelogModal = (props: {
|
|||
{props.previousVersions.length > 0 &&
|
||||
props.previousVersions.map((version) => {
|
||||
return (
|
||||
<AccordionPanel key={version}>
|
||||
<AccordionTitle
|
||||
<Accordion.Panel key={version}>
|
||||
<Accordion.Title
|
||||
onClickCapture={(e) => {
|
||||
if (!previousVersionsChangelog.hasOwnProperty(version)) {
|
||||
_fetchVersionChangelog(version).then((data) => {
|
||||
|
@ -70,19 +62,19 @@ export const ChangelogModal = (props: {
|
|||
}}
|
||||
>
|
||||
Список изменений v{version}
|
||||
</AccordionTitle>
|
||||
<AccordionContent>
|
||||
</Accordion.Title>
|
||||
<Accordion.Content>
|
||||
{previousVersionsChangelog.hasOwnProperty(version) ?
|
||||
<Markdown className={Styles.markdown}>
|
||||
{previousVersionsChangelog[version]}
|
||||
</Markdown>
|
||||
: <div>Загрузка ...</div>}
|
||||
</AccordionContent>
|
||||
</AccordionPanel>
|
||||
</Accordion.Content>
|
||||
</Accordion.Panel>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
</ModalBody>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -49,13 +49,11 @@ export const CollectionLink = (props: any) => {
|
|||
{props.title}
|
||||
</p>
|
||||
</div>
|
||||
{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>
|
||||
)}
|
||||
<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>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Button, Label, Modal, ModalBody, ModalHeader, Textarea, ToggleSwitch } from "flowbite-react";
|
||||
import { Button, Modal, ToggleSwitch, Label, Textarea } 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)}
|
||||
>
|
||||
<ModalHeader>
|
||||
<Modal.Header>
|
||||
<p className="text-lg font-bold text-gray-900 lg:text-2xl dark:text-white">
|
||||
{props.isReply ? "Ответ на комментарий" : "Оставить комментарий"}
|
||||
</p>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{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">Ваш комментарий.</Label>
|
||||
<Label htmlFor="comment" value="Ваш комментарий." />
|
||||
</div>
|
||||
<Textarea
|
||||
id="comment"
|
||||
|
@ -132,7 +132,7 @@ export const CommentsAddModal = (props: {
|
|||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalBody>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { unixToDate, sinceUnixDate } from "#/api/utils";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { Button, Dropdown, DropdownItem } from "flowbite-react";
|
||||
import { Button, Dropdown } 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>
|
||||
)}
|
||||
>
|
||||
<DropdownItem onClick={() => setIsEditCommentsOpen(true)}>
|
||||
<Dropdown.Item onClick={() => setIsEditCommentsOpen(true)}>
|
||||
Редактировать
|
||||
</DropdownItem>
|
||||
<DropdownItem onClick={() => _deleteComment()}>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item onClick={() => _deleteComment()}>
|
||||
Удалить
|
||||
</DropdownItem>
|
||||
</Dropdown.Item>
|
||||
</Dropdown>
|
||||
)}
|
||||
</footer>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Button, Label, Modal, ModalBody, ModalHeader, Textarea, ToggleSwitch } from "flowbite-react";
|
||||
import { Button, Modal, ToggleSwitch, Label, Textarea } 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)}
|
||||
>
|
||||
<ModalHeader>
|
||||
<Modal.Header>
|
||||
<p className="text-lg font-bold text-gray-900 lg:text-2xl dark:text-white">
|
||||
Редактировать комментарий
|
||||
</p>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<form className="flex flex-col gap-4" onSubmit={(e) => _sendComment(e)}>
|
||||
<div>
|
||||
<div className="block mb-2 sr-only">
|
||||
<Label htmlFor="comment" >Редактировать ваш комментарий.</Label>
|
||||
<Label htmlFor="comment" value="Редактировать ваш комментарий." />
|
||||
</div>
|
||||
<Textarea
|
||||
id="comment"
|
||||
|
@ -99,7 +99,7 @@ export const CommentsEditModal = (props: {
|
|||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalBody>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Button, Card, Modal, ModalHeader, Spinner } from "flowbite-react";
|
||||
import { Card, Button, Modal, 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)}
|
||||
>
|
||||
<ModalHeader>
|
||||
<Modal.Header>
|
||||
<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>
|
||||
</ModalHeader>
|
||||
</Modal.Header>
|
||||
<div
|
||||
className="flex flex-col gap-2 p-4 overflow-y-auto"
|
||||
onScroll={handleScroll}
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { Card } from "flowbite-react";
|
||||
import { ReleaseLinkList } from "#/components/ReleaseLink/ReleaseLinkList";
|
||||
import { useUserStore } from "#/store/auth";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { BookmarksList, useSWRfetcher } from "#/api/utils";
|
||||
import useSWR from "swr";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const ContinueWatching = () => {
|
||||
const userStore = useUserStore();
|
||||
|
||||
function useFetchReleases(listName: string) {
|
||||
let url: string;
|
||||
if (userStore.token) {
|
||||
url = `${ENDPOINTS.user.bookmark}/all/${BookmarksList[listName]}/0?sort=1&token=${userStore.token}`;
|
||||
}
|
||||
const { data, isLoading, error } = useSWR(url, useSWRfetcher);
|
||||
return [data, isLoading, error];
|
||||
}
|
||||
|
||||
const [watchingData, watchingLoading, watchingError] =
|
||||
useFetchReleases("watching");
|
||||
const [plannedData, plannedLoading, plannedError] =
|
||||
useFetchReleases("planned");
|
||||
const [delayedData, delayedLoading, delayedError] =
|
||||
useFetchReleases("delayed");
|
||||
|
||||
const [releaseData, setReleaseData] = useState<any[]>([]);
|
||||
|
||||
const firstN = (arr, n = 1) => arr.slice(0, n);
|
||||
function _randomize(array: any[], limit: number) {
|
||||
const toRand = array.slice();
|
||||
let currentIndex = toRand.length;
|
||||
while (currentIndex != 0) {
|
||||
let randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex--;
|
||||
[toRand[currentIndex], toRand[randomIndex]] = [
|
||||
toRand[randomIndex],
|
||||
toRand[currentIndex],
|
||||
];
|
||||
}
|
||||
return firstN(toRand, limit);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!watchingLoading && !plannedLoading && !delayedLoading) {
|
||||
const data = [
|
||||
...(watchingData.content || []),
|
||||
...(plannedData.content || []),
|
||||
...(delayedData.content || []),
|
||||
];
|
||||
const randomizedData = _randomize(data, 3);
|
||||
setReleaseData(randomizedData);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [watchingLoading, plannedLoading, delayedLoading]);
|
||||
|
||||
if (
|
||||
!userStore.isAuth ||
|
||||
watchingLoading ||
|
||||
plannedLoading ||
|
||||
delayedLoading ||
|
||||
releaseData.length == 0
|
||||
)
|
||||
return <></>;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex justify-between py-2 border-b-2 border-black dark:border-white">
|
||||
<h1>Продолжить просмотр</h1>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 mt-2">
|
||||
{releaseData.map((release: any) => {
|
||||
return (
|
||||
<ReleaseLinkList
|
||||
key={release.id}
|
||||
{...release}
|
||||
settings={{ showDescription: false }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useRef } from "react";
|
||||
import Cropper, { ReactCropperElement } from "react-cropper";
|
||||
import "cropperjs/dist/cropper.css";
|
||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from "flowbite-react";
|
||||
import { Button, Modal } from "flowbite-react";
|
||||
|
||||
type CropModalProps = {
|
||||
isOpen: boolean;
|
||||
|
@ -71,8 +71,8 @@ export const CropModal: React.FC<CropModalProps> = ({
|
|||
}}
|
||||
size={"7xl"}
|
||||
>
|
||||
<ModalHeader>Обрезать изображение</ModalHeader>
|
||||
<ModalBody>
|
||||
<Modal.Header>Обрезать изображение</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Cropper
|
||||
src={selectedImage}
|
||||
style={{ height: 400, width: "100%" }}
|
||||
|
@ -95,8 +95,8 @@ export const CropModal: React.FC<CropModalProps> = ({
|
|||
</p>
|
||||
<p>Используйте колёсико мыши что-бы изменить масштаб</p>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
color={"blue"}
|
||||
disabled={isActionsDisabled}
|
||||
|
@ -120,7 +120,7 @@ export const CropModal: React.FC<CropModalProps> = ({
|
|||
>
|
||||
Отменить
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,112 +1,49 @@
|
|||
"use client";
|
||||
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";
|
||||
import { Card } from "flowbite-react";
|
||||
import Link from "next/link";
|
||||
import { numberDeclension } from "#/api/utils";
|
||||
|
||||
export function ProfileActivity(props: {
|
||||
profile_id: number;
|
||||
commentCount: number;
|
||||
commentPreview: any;
|
||||
videoCount: number;
|
||||
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="overflow-hidden h-fit">
|
||||
<Card className="h-fit">
|
||||
<h1 className="text-2xl font-bold">Активность</h1>
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
<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>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
|
@ -1,92 +0,0 @@
|
|||
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 items-center gap-1">
|
||||
<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>}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,87 +0,0 @@
|
|||
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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,177 +0,0 @@
|
|||
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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader, Textarea, useThemeMode } from "flowbite-react";
|
||||
import { Button, Modal, 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"}
|
||||
>
|
||||
<ModalHeader>Изменить никнейм</ModalHeader>
|
||||
<ModalBody>
|
||||
<Modal.Header>Изменить никнейм</Modal.Header>
|
||||
<Modal.Body>
|
||||
{loading ?
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Spinner />
|
||||
|
@ -173,8 +173,8 @@ export const ProfileEditLoginModal = (props: {
|
|||
}
|
||||
</>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{_loginData.is_change_available && (
|
||||
<Button
|
||||
color="blue"
|
||||
|
@ -191,7 +191,7 @@ export const ProfileEditLoginModal = (props: {
|
|||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { FileInput, Label, Modal, ModalBody, ModalHeader, useThemeMode } from "flowbite-react";
|
||||
import { FileInput, Label, Modal, useThemeMode } from "flowbite-react";
|
||||
import { Spinner } from "../Spinner/Spinner";
|
||||
import useSWR from "swr";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
|
@ -14,7 +14,6 @@ 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;
|
||||
|
@ -26,7 +25,6 @@ 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,
|
||||
|
@ -188,7 +186,6 @@ export const ProfileEditModal = (props: {
|
|||
if (avatarModalProps.croppedImage) {
|
||||
_uploadAvatar();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [avatarModalProps.croppedImage]);
|
||||
|
||||
if (!prefData || !loginData || prefError || loginError) {
|
||||
|
@ -202,8 +199,8 @@ export const ProfileEditModal = (props: {
|
|||
onClose={() => props.setIsOpen(false)}
|
||||
size={"7xl"}
|
||||
>
|
||||
<ModalHeader>Редактирование профиля</ModalHeader>
|
||||
<ModalBody>
|
||||
<Modal.Header>Редактирование профиля</Modal.Header>
|
||||
<Modal.Body>
|
||||
{prefLoading ?
|
||||
<Spinner />
|
||||
: <div className="flex flex-col gap-4">
|
||||
|
@ -358,18 +355,13 @@ export const ProfileEditModal = (props: {
|
|||
}
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
className="p-2 text-left rounded-md hover:bg-gray-100 dark:hover:bg-gray-900"
|
||||
onClick={() => {
|
||||
setBlockedModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{/* <button className="p-2 text-left rounded-md hover:bg-gray-100 dark:hover:bg-gray-900">
|
||||
<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">
|
||||
|
@ -395,7 +387,7 @@ export const ProfileEditModal = (props: {
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
</ModalBody>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
{props.token ?
|
||||
<>
|
||||
|
@ -439,12 +431,6 @@ export const ProfileEditModal = (props: {
|
|||
setLogin={setLogin}
|
||||
profile_id={props.profile_id}
|
||||
/>
|
||||
<ProfileBlockedUsersModal
|
||||
isOpen={blockedModalOpen}
|
||||
setIsOpen={setBlockedModalOpen}
|
||||
token={props.token}
|
||||
profile_id={props.profile_id}
|
||||
/>
|
||||
</>
|
||||
: ""}
|
||||
</>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { Modal, ModalBody, ModalHeader, useThemeMode } from "flowbite-react";
|
||||
import { Modal, 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"}
|
||||
>
|
||||
<ModalHeader>{setting_text[props.setting]}</ModalHeader>
|
||||
<ModalBody>
|
||||
<Modal.Header>{setting_text[props.setting]}</Modal.Header>
|
||||
<Modal.Body>
|
||||
{props.setting != "none" ?
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
|
@ -202,7 +202,7 @@ export const ProfileEditPrivacyModal = (props: {
|
|||
</div>
|
||||
</>
|
||||
: ""}
|
||||
</ModalBody>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,15 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
TextInput,
|
||||
useThemeMode,
|
||||
} from "flowbite-react";
|
||||
import { Button, Modal, Label, TextInput, useThemeMode } from "flowbite-react";
|
||||
import { Spinner } from "../Spinner/Spinner";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { useEffect, useState } from "react";
|
||||
|
@ -173,8 +164,8 @@ export const ProfileEditSocialModal = (props: {
|
|||
onClose={() => props.setIsOpen(false)}
|
||||
size={"4xl"}
|
||||
>
|
||||
<ModalHeader>Соц. сети</ModalHeader>
|
||||
<ModalBody>
|
||||
<Modal.Header>Соц. сети</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p className="p-2 text-gray-400 border-2 border-gray-200 rounded-md dark:border-gray-500 dark:text-gray-300">
|
||||
Укажите ссылки на свои социальные сети, чтобы другие пользователи
|
||||
могли с вами связаться
|
||||
|
@ -186,7 +177,7 @@ export const ProfileEditSocialModal = (props: {
|
|||
: <div className="flex flex-col gap-4 py-4">
|
||||
<div>
|
||||
<div className="block mb-2">
|
||||
<Label htmlFor="vk-page">ВКонтакте</Label>
|
||||
<Label htmlFor="vk-page" value="ВКонтакте" />
|
||||
</div>
|
||||
<TextInput
|
||||
id="vk-page"
|
||||
|
@ -198,7 +189,7 @@ export const ProfileEditSocialModal = (props: {
|
|||
</div>
|
||||
<div>
|
||||
<div className="block mb-2">
|
||||
<Label htmlFor="tg-page">Telegram</Label>
|
||||
<Label htmlFor="tg-page" value="Telegram" />
|
||||
</div>
|
||||
<TextInput
|
||||
id="tg-page"
|
||||
|
@ -210,7 +201,7 @@ export const ProfileEditSocialModal = (props: {
|
|||
</div>
|
||||
<div>
|
||||
<div className="block mb-2">
|
||||
<Label htmlFor="discord-page">Discord</Label>
|
||||
<Label htmlFor="discord-page" value="Discord" />
|
||||
</div>
|
||||
<TextInput
|
||||
id="discord-page"
|
||||
|
@ -222,7 +213,7 @@ export const ProfileEditSocialModal = (props: {
|
|||
</div>
|
||||
<div>
|
||||
<div className="block mb-2">
|
||||
<Label htmlFor="inst-page">Instagram</Label>
|
||||
<Label htmlFor="inst-page" value="Instagram" />
|
||||
</div>
|
||||
<TextInput
|
||||
id="inst-page"
|
||||
|
@ -234,7 +225,7 @@ export const ProfileEditSocialModal = (props: {
|
|||
</div>
|
||||
<div>
|
||||
<div className="block mb-2">
|
||||
<Label htmlFor="tt-page">TikTok</Label>
|
||||
<Label htmlFor="tt-page" value="TikTok" />
|
||||
</div>
|
||||
<TextInput
|
||||
id="tt-page"
|
||||
|
@ -246,8 +237,8 @@ export const ProfileEditSocialModal = (props: {
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
color="blue"
|
||||
onClick={() => _setSocialSetting()}
|
||||
|
@ -262,7 +253,7 @@ export const ProfileEditSocialModal = (props: {
|
|||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader, Textarea, useThemeMode } from "flowbite-react";
|
||||
import { Button, Modal, 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"}
|
||||
>
|
||||
<ModalHeader>Изменить статус</ModalHeader>
|
||||
<ModalBody>
|
||||
<Modal.Header>Изменить статус</Modal.Header>
|
||||
<Modal.Body>
|
||||
<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>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
color="blue"
|
||||
onClick={() => _setStatusSetting()}
|
||||
|
@ -123,7 +123,7 @@ export const ProfileEditStatusModal = (props: {
|
|||
<Button color="red" onClick={() => props.setIsOpen(false)}>
|
||||
Отмена
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,346 +0,0 @@
|
|||
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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,79 +1,49 @@
|
|||
import { Card } from "flowbite-react";
|
||||
import { Card, Carousel } from "flowbite-react";
|
||||
import type {
|
||||
FlowbiteCarouselIndicatorsTheme,
|
||||
FlowbiteCarouselControlTheme,
|
||||
CustomFlowbiteTheme,
|
||||
} from "flowbite-react";
|
||||
import { ReleaseLink } from "../ReleaseLink/ReleaseLinkUpdate";
|
||||
|
||||
import { ReleaseChips } from "../ReleasePoster/Chips";
|
||||
import { Poster } from "../ReleasePoster/Poster";
|
||||
import Link from "next/link";
|
||||
const CarouselIndicatorsTheme: FlowbiteCarouselIndicatorsTheme = {
|
||||
active: {
|
||||
off: "bg-gray-400/50 hover:bg-gray-200",
|
||||
on: "bg-gray-200",
|
||||
},
|
||||
base: "h-3 w-3 rounded-full max-w-[300px]",
|
||||
wrapper: "absolute bottom-5 left-1/2 flex -translate-x-1/2 space-x-3",
|
||||
};
|
||||
|
||||
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 CarouselControlsTheme: FlowbiteCarouselControlTheme = {
|
||||
base: "inline-flex h-8 w-8 items-center justify-center rounded-full group-focus:outline-none group-focus:ring-4 bg-gray-400/30 group-hover:bg-gray-400/60 group-focus:ring-gray-400/70 sm:h-10 sm:w-10",
|
||||
icon: "h-5 w-5 text-gray-400 sm:h-6 sm:w-6",
|
||||
};
|
||||
|
||||
const CarouselTheme: CustomFlowbiteTheme["carousel"] = {
|
||||
root: {
|
||||
base: "relative h-full w-full max-w-[375px]",
|
||||
},
|
||||
indicators: CarouselIndicatorsTheme,
|
||||
control: CarouselControlsTheme,
|
||||
};
|
||||
|
||||
export const ProfileReleaseHistory = (props: any) => {
|
||||
return (
|
||||
<Card className="h-fit">
|
||||
<h1 className="text-2xl font-bold">Недавно просмотренные</h1>
|
||||
<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 className="flex justify-center">
|
||||
<Carousel theme={CarouselTheme}>
|
||||
{props.history.map((release) => {
|
||||
return (
|
||||
<ReleaseLink
|
||||
key={`history-${release.id}`}
|
||||
{...release}
|
||||
chipsSettings={{ lastWatchedHidden: false }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Carousel>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
import { Button, Card, Modal, ModalHeader, Rating, RatingStar } from "flowbite-react";
|
||||
|
||||
import {
|
||||
Card,
|
||||
Carousel,
|
||||
RatingStar,
|
||||
Rating,
|
||||
Modal,
|
||||
Button,
|
||||
} from "flowbite-react";
|
||||
import type {
|
||||
FlowbiteCarouselIndicatorsTheme,
|
||||
FlowbiteCarouselControlTheme,
|
||||
} from "flowbite-react";
|
||||
import Image from "next/image";
|
||||
import { unixToDate, useSWRfetcher } from "#/api/utils";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
@ -8,6 +19,28 @@ import useSWRInfinite from "swr/infinite";
|
|||
import { Spinner } from "../Spinner/Spinner";
|
||||
import { Poster } from "../ReleasePoster/Poster";
|
||||
|
||||
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 = {
|
||||
root: {
|
||||
base: "relative h-full w-full max-w-[700px]",
|
||||
},
|
||||
indicators: CarouselIndicatorsTheme,
|
||||
control: CarouselControlsTheme,
|
||||
};
|
||||
|
||||
export const ProfileReleaseRatings = (props: any) => {
|
||||
const [modal, setModal] = useState(false);
|
||||
return (
|
||||
|
@ -18,31 +51,33 @@ export const ProfileReleaseRatings = (props: any) => {
|
|||
Посмотреть все
|
||||
</Button>
|
||||
</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 className="flex min-h-[200px] items-center justify-center">
|
||||
<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">
|
||||
<div className="max-w-32">
|
||||
<Poster image={release.image} />
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Carousel>
|
||||
</div>
|
||||
<ProfileReleaseRatingsModal
|
||||
profile_id={props.profile_id}
|
||||
|
@ -113,7 +148,7 @@ const ProfileReleaseRatingsModal = (props: {
|
|||
onClose={() => props.setIsOpen(false)}
|
||||
size={"4xl"}
|
||||
>
|
||||
<ModalHeader>Оценки</ModalHeader>
|
||||
<Modal.Header>Оценки</Modal.Header>
|
||||
<div
|
||||
className="flex flex-col gap-2 p-4 overflow-y-auto"
|
||||
onScroll={handleScroll}
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
|
@ -1,20 +0,0 @@
|
|||
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>
|
||||
);
|
||||
};
|
|
@ -1,154 +1,145 @@
|
|||
"use client";
|
||||
|
||||
import { Avatar, Card, useThemeMode } from "flowbite-react";
|
||||
import { UserRole } from "./Profile.Role";
|
||||
import { UserSocial } from "./Profile.Social";
|
||||
import { Avatar, Card, Button } from "flowbite-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Chip } from "../Chip/Chip";
|
||||
|
||||
interface ProfileUserProps {
|
||||
export const ProfileUser = (props: {
|
||||
isOnline: boolean;
|
||||
avatar: string;
|
||||
login: string;
|
||||
status: string;
|
||||
rating: number;
|
||||
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;
|
||||
isPrivate: boolean;
|
||||
hasSocials: boolean;
|
||||
socials: {
|
||||
name: string;
|
||||
nickname: any;
|
||||
icon: string;
|
||||
urlPrefix?: string | undefined;
|
||||
}[];
|
||||
};
|
||||
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;
|
||||
|
||||
chips: {
|
||||
hasChips: boolean;
|
||||
isMyProfile: boolean;
|
||||
isVerified: boolean;
|
||||
isSponsor: boolean;
|
||||
isBlocked: boolean;
|
||||
roles?: {
|
||||
id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
}[];
|
||||
};
|
||||
rating: number;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<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} />
|
||||
))}
|
||||
<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
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<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}
|
||||
<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}{" "}
|
||||
<span
|
||||
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"
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
{rating}
|
||||
{props.rating}
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-sm whitespace-pre-wrap sm:text-md">{status}</p>
|
||||
</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>
|
||||
)}
|
||||
<div className="text-sm text-gray-500 whitespace-pre-wrap sm:text-md dark:text-gray-400 ">
|
||||
{props.status}
|
||||
</div>
|
||||
</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>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -28,7 +28,6 @@ export const ProfileWatchDynamic = (props: { watchDynamic: Array<any> }) => {
|
|||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
theme:"dark",
|
||||
x: {
|
||||
show: false,
|
||||
},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Card, Table, TableBody, TableCell, TableRow } from "flowbite-react";
|
||||
import { Card, Table } 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>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="py-0">
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
<Table.Cell 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>
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
</Table.Cell>
|
||||
<Table.Cell 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} г.`}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-0">
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="py-0">
|
||||
<span className="w-8 h-8 iconify-color mdi--animation-play-outline dark:invert"></span>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
</Table.Cell>
|
||||
<Table.Cell 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")}`}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-0">
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="py-0">
|
||||
<span className="w-8 h-8 iconify-color mdi--calendar-outline dark:invert"></span>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium text-gray-900 dark:text-white">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="font-medium text-gray-900 dark:text-white">
|
||||
{props.category}
|
||||
{", "}
|
||||
{props.broadcast == 0 ?
|
||||
props.status.toLowerCase()
|
||||
: `выходит ${weekDay[props.broadcast]}`}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-0">
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="py-0">
|
||||
<span className="w-8 h-8 iconify-color mdi--people-group-outline dark:invert"></span>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium text-gray-900 dark:text-white">
|
||||
</Table.Cell>
|
||||
<Table.Cell className="font-medium text-gray-900 dark:text-white">
|
||||
{props.studio && (
|
||||
<>
|
||||
{"Студия: "}
|
||||
|
@ -117,13 +117,13 @@ export const ReleaseInfoInfo = (props: {
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-0">
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell className="py-0">
|
||||
<span className="w-8 h-8 iconify-color mdi--tag-outline dark:invert"></span>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium text-gray-900 dark:text-white">
|
||||
</Table.Cell>
|
||||
<Table.Cell 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>
|
||||
);
|
||||
})}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{props.status.toLowerCase() == "анонс" && (
|
||||
<TableRow>
|
||||
<TableCell className="py-0">
|
||||
<Table.Row>
|
||||
<Table.Cell className="py-0">
|
||||
<span className="w-8 h-8 iconify-color mdi--clock-outline dark:invert"></span>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
</Table.Cell>
|
||||
<Table.Cell 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} г.`}
|
||||
</>
|
||||
: "Скоро"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
@ -14,7 +14,7 @@ export const ReleaseInfoStreaming = (props: { release_id: number }) => {
|
|||
setData(await response.json());
|
||||
};
|
||||
_getData();
|
||||
}, [props.release_id]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -32,8 +32,7 @@ export const ReleaseInfoStreaming = (props: { release_id: number }) => {
|
|||
key={`platform_${item.id}`}
|
||||
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 "
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img alt="" src={item.icon} className="w-6 h-6 rounded-full" />
|
||||
<img src={item.icon} className="w-6 h-6 rounded-full" />
|
||||
<p className="text-sm line-clamp-2">{item.name}</p>
|
||||
</a>
|
||||
);
|
||||
|
|
|
@ -1,26 +1,20 @@
|
|||
import {
|
||||
Button,
|
||||
Card,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Rating,
|
||||
RatingAdvanced,
|
||||
RatingStar,
|
||||
RatingAdvancedTheme,
|
||||
Flowbite,
|
||||
Button,
|
||||
CustomFlowbiteTheme,
|
||||
Modal,
|
||||
} from "flowbite-react";
|
||||
import { numberDeclension } from "#/api/utils";
|
||||
import { useState } from "react";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
|
||||
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",
|
||||
const RatingTheme: CustomFlowbiteTheme = {
|
||||
ratingAdvanced: {
|
||||
progress: {
|
||||
base: "mx-4 h-5 w-3/4 rounded bg-gray-200 dark:bg-gray-700",
|
||||
},
|
||||
},
|
||||
};
|
||||
export const ReleaseInfoRating = (props: {
|
||||
|
@ -45,7 +39,7 @@ export const ReleaseInfoRating = (props: {
|
|||
<Card>
|
||||
<div className="flex flex-col gap-2 sm:items-center sm:flex-row">
|
||||
<Rating>
|
||||
<RatingStar />
|
||||
<Rating.Star />
|
||||
<p className="ml-2 text-sm font-bold dark:text-white">
|
||||
{props.grade.toFixed(2)} из 5
|
||||
</p>
|
||||
|
@ -54,7 +48,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}
|
||||
|
@ -68,7 +62,8 @@ 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"
|
||||
|
@ -76,7 +71,7 @@ export const ReleaseInfoRating = (props: {
|
|||
>
|
||||
оценить
|
||||
</Button>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
@ -85,50 +80,47 @@ export const ReleaseInfoRating = (props: {
|
|||
{props.votes.total}{" "}
|
||||
{numberDeclension(props.votes.total, "голос", "голоса", "голосов")}
|
||||
</p>
|
||||
<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>
|
||||
<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>
|
||||
</Card>
|
||||
<ReleaseInfoRatingModal
|
||||
isOpen={isRatingModalOpen}
|
||||
|
@ -186,8 +178,8 @@ const ReleaseInfoRatingModal = (props: {
|
|||
show={props.isOpen}
|
||||
onClose={() => props.setIsOpen(false)}
|
||||
>
|
||||
<ModalHeader>Оценка</ModalHeader>
|
||||
<ModalBody>
|
||||
<Modal.Header>Оценка</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div>
|
||||
<div className="block sm:hidden">
|
||||
<Rating size="md" className="justify-center">
|
||||
|
@ -199,7 +191,7 @@ const ReleaseInfoRatingModal = (props: {
|
|||
onMouseOut={() => setCurElement(0)}
|
||||
onClick={() => setVote(element)}
|
||||
>
|
||||
<RatingStar
|
||||
<Rating.Star
|
||||
filled={index + 1 <= curElement || index + 1 <= vote}
|
||||
/>
|
||||
</Button>
|
||||
|
@ -216,7 +208,7 @@ const ReleaseInfoRatingModal = (props: {
|
|||
onMouseOut={() => setCurElement(0)}
|
||||
onClick={() => setVote(element)}
|
||||
>
|
||||
<RatingStar
|
||||
<Rating.Star
|
||||
filled={index + 1 <= curElement || index + 1 <= vote}
|
||||
/>
|
||||
</Button>
|
||||
|
@ -224,8 +216,8 @@ const ReleaseInfoRatingModal = (props: {
|
|||
</Rating>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<div className="flex gap-1 ml-auto">
|
||||
<Button
|
||||
disabled={isSending}
|
||||
|
@ -249,7 +241,7 @@ const ReleaseInfoRatingModal = (props: {
|
|||
Оценить
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,9 +1,31 @@
|
|||
"use client";
|
||||
|
||||
import { Card } from "flowbite-react";
|
||||
import { ReleaseLinkList } from "#/components/ReleaseLink/ReleaseLinkList";
|
||||
import { Card, Carousel, CustomFlowbiteTheme, FlowbiteCarouselControlTheme, FlowbiteCarouselIndicatorsTheme } from "flowbite-react";
|
||||
import { ReleaseLink } from "#/components/ReleaseLink/ReleaseLinkUpdate";
|
||||
import Link from "next/link";
|
||||
|
||||
const CarouselIndicatorsTheme: FlowbiteCarouselIndicatorsTheme = {
|
||||
active: {
|
||||
off: "bg-gray-400/50 hover:bg-gray-200",
|
||||
on: "bg-gray-200",
|
||||
},
|
||||
base: "h-3 w-3 rounded-full max-w-[300px]",
|
||||
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 group-focus:outline-none group-focus:ring-4 bg-gray-400/30 group-hover:bg-gray-400/60 group-focus:ring-gray-400/70 sm:h-10 sm:w-10",
|
||||
icon: "h-5 w-5 text-gray-400 sm:h-6 sm:w-6",
|
||||
};
|
||||
|
||||
const CarouselTheme: CustomFlowbiteTheme["carousel"] = {
|
||||
root: {
|
||||
base: "relative h-full w-full max-w-[300px]",
|
||||
},
|
||||
indicators: CarouselIndicatorsTheme,
|
||||
control: CarouselControlsTheme,
|
||||
};
|
||||
|
||||
export const ReleaseInfoRelated = (props: {
|
||||
release_id: number;
|
||||
related: any;
|
||||
|
@ -22,23 +44,25 @@ export const ReleaseInfoRelated = (props: {
|
|||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 mt-2">
|
||||
{props.related_releases
|
||||
.filter((release: any) => {
|
||||
if (release.id == props.release_id) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((release: any) => {
|
||||
return (
|
||||
<ReleaseLinkList
|
||||
key={release.id}
|
||||
{...release}
|
||||
settings={{ showGenres: false, showDescription: false }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="flex justify-center mt-2">
|
||||
<Carousel pauseOnHover={true} theme={CarouselTheme}>
|
||||
{props.related_releases
|
||||
.filter((release: any) => {
|
||||
if (release.id == props.release_id) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((release: any) => {
|
||||
return (
|
||||
<ReleaseLink
|
||||
key={release.id}
|
||||
{...release}
|
||||
settings={{ showGenres: false, showDescription: false }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Carousel>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
@ -1,63 +1,21 @@
|
|||
import { Card } from "flowbite-react";
|
||||
import { Card, Carousel } from "flowbite-react";
|
||||
import Image from "next/image";
|
||||
|
||||
import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import "swiper/css";
|
||||
import "swiper/css/navigation";
|
||||
import "swiper/css/pagination";
|
||||
import "swiper/css/autoplay";
|
||||
import { Navigation, Pagination, Autoplay } from "swiper/modules";
|
||||
|
||||
export const ReleaseInfoScreenshots = (props: { images: string[] }) => {
|
||||
return (
|
||||
<Card>
|
||||
<Swiper
|
||||
modules={[Navigation, Pagination, Autoplay]}
|
||||
spaceBetween={8}
|
||||
slidesPerView={2}
|
||||
direction={"horizontal"}
|
||||
allowTouchMove={true}
|
||||
autoplay={true}
|
||||
pagination={true}
|
||||
breakpoints={{
|
||||
1024: {
|
||||
slidesPerView: 1,
|
||||
},
|
||||
}}
|
||||
style={{
|
||||
height: "100%",
|
||||
minHeight: 0,
|
||||
maxHeight: "100%",
|
||||
width: "100%",
|
||||
minWidth: 0,
|
||||
maxWidth: "100%",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{props.images.map((image: string, index: number) => {
|
||||
return (
|
||||
<SwiperSlide
|
||||
key={`release-screenshot-${index}`}
|
||||
style={{
|
||||
width: "fit-content",
|
||||
flexShrink: 0,
|
||||
display: "block",
|
||||
height: "100%",
|
||||
maxHeight: "100%",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
key={index}
|
||||
className="object-cover"
|
||||
src={image}
|
||||
width={400}
|
||||
height={225}
|
||||
alt=""
|
||||
/>
|
||||
</SwiperSlide>
|
||||
);
|
||||
})}
|
||||
</Swiper>
|
||||
<Carousel className="aspect-[16/10]">
|
||||
{props.images.map((image: string, index: number) => (
|
||||
<Image
|
||||
key={index}
|
||||
className="object-cover"
|
||||
src={image}
|
||||
width={400}
|
||||
height={300}
|
||||
alt=""
|
||||
/>
|
||||
))}
|
||||
</Carousel>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 { DropdownItem, ModalHeader, useThemeMode } from "flowbite-react";
|
||||
import { useThemeMode } from "flowbite-react";
|
||||
|
||||
const lists = [
|
||||
{ list: 0, name: "Не смотрю" },
|
||||
|
@ -174,12 +174,12 @@ export const ReleaseInfoUserList = (props: {
|
|||
disabled={listEventDisabled}
|
||||
>
|
||||
{lists.map((list) => (
|
||||
<DropdownItem
|
||||
<Dropdown.Item
|
||||
key={list.list}
|
||||
onClick={() => _addToList(list.list)}
|
||||
>
|
||||
{list.name}
|
||||
</DropdownItem>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
<Button
|
||||
|
@ -321,7 +321,7 @@ const AddReleaseToCollectionModal = (props: {
|
|||
show={props.isOpen}
|
||||
onClose={() => props.setIsOpen(false)}
|
||||
>
|
||||
<ModalHeader>Выбор коллекции</ModalHeader>
|
||||
<Modal.Header>Выбор коллекции</Modal.Header>
|
||||
<div
|
||||
className="flex flex-col gap-2 p-4 overflow-y-auto"
|
||||
onScroll={handleScroll}
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
import Link from "next/link";
|
||||
import { Poster } from "../ReleasePoster/Poster";
|
||||
import { ReleaseChips } from "../ReleasePoster/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 ReleaseLinkList = (props: {
|
||||
image: string;
|
||||
title_ru: string;
|
||||
title_original: string;
|
||||
description?: string;
|
||||
genres?: string;
|
||||
grade?: number;
|
||||
id: number;
|
||||
settings?: {
|
||||
showGenres?: boolean;
|
||||
showDescription?: boolean;
|
||||
showOrigTitle?: 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,
|
||||
showOrigTitle: 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 (
|
||||
<Link href={`/release/${props.id}`}>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-shrink-0 w-32">
|
||||
<Poster image={props.image} className="h-auto" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<ReleaseChips
|
||||
{...props}
|
||||
user_list={user_list}
|
||||
grade={grade}
|
||||
settings={{ lastWatchedHidden: false }}
|
||||
/>
|
||||
<div>
|
||||
{settings.showGenres &&
|
||||
genres.length > 0 &&
|
||||
genres.map((genre: string, index: number) => {
|
||||
return (
|
||||
<span
|
||||
key={`release_${props.id}_genre_${genre}_${index}`}
|
||||
className="text-sm font-light leading-none dark:text-white"
|
||||
>
|
||||
{index > 0 && ", "}
|
||||
{genre}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{props.title_ru && (
|
||||
<p className="text-lg font-bold line-clamp-2 dark:text-white">
|
||||
{props.title_ru}
|
||||
</p>
|
||||
)}
|
||||
{settings.showOrigTitle && props.title_original && (
|
||||
<p className="text-sm text-gray-600 line-clamp-2 dark:text-gray-300">
|
||||
{props.title_original}
|
||||
</p>
|
||||
)}
|
||||
{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>
|
||||
</Link>
|
||||
);
|
||||
};
|
|
@ -67,10 +67,7 @@ export const EpisodeSelector = (props: {
|
|||
enabled: true,
|
||||
sensitivity: 4,
|
||||
}}
|
||||
scrollbar={{
|
||||
enabled: true,
|
||||
draggable: true,
|
||||
}}
|
||||
scrollbar={true}
|
||||
allowTouchMove={true}
|
||||
style={
|
||||
{
|
||||
|
@ -82,7 +79,6 @@ export const EpisodeSelector = (props: {
|
|||
<SwiperSlide
|
||||
key={`episode_${episode.position}`}
|
||||
style={{ maxWidth: "fit-content" }}
|
||||
className="pb-2"
|
||||
>
|
||||
<Button
|
||||
color={
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import { useUserStore } from "#/store/auth";
|
||||
import { useUserPlayerPreferencesStore } from "#/store/player";
|
||||
import { Button, Card, Dropdown, DropdownItem } from "flowbite-react";
|
||||
import { Card, Dropdown, Button } 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) => (
|
||||
<DropdownItem
|
||||
<Dropdown.Item
|
||||
key={`voiceover_${voiceover.id}`}
|
||||
onClick={() =>
|
||||
setSelectedVoiceoverAndSaveAsPreferred(voiceover)
|
||||
}
|
||||
>
|
||||
{voiceover.name}
|
||||
</DropdownItem>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
|
@ -286,12 +286,12 @@ export const ReleasePlayer = (props: { id: number }) => {
|
|||
theme={DropdownTheme}
|
||||
>
|
||||
{sourcesInfo.map((source: any) => (
|
||||
<DropdownItem
|
||||
<Dropdown.Item
|
||||
key={`source_${source.id}`}
|
||||
onClick={() => setSelectedPlayerAndSaveAsPreferred(source)}
|
||||
>
|
||||
{source.name}
|
||||
</DropdownItem>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
@ -314,12 +314,9 @@ export const ReleasePlayer = (props: { id: number }) => {
|
|||
direction={"horizontal"}
|
||||
mousewheel={{
|
||||
enabled: true,
|
||||
sensitivity: 4,
|
||||
}}
|
||||
scrollbar={{
|
||||
enabled: true,
|
||||
draggable: true,
|
||||
sensitivity: 2,
|
||||
}}
|
||||
scrollbar={true}
|
||||
allowTouchMove={true}
|
||||
style={
|
||||
{
|
||||
|
@ -331,7 +328,6 @@ export const ReleasePlayer = (props: { id: number }) => {
|
|||
<SwiperSlide
|
||||
key={`episode_${episode.position}`}
|
||||
style={{ maxWidth: "fit-content" }}
|
||||
className="pb-2"
|
||||
>
|
||||
<Button
|
||||
color={
|
||||
|
|
|
@ -105,30 +105,18 @@ export const ReleasePlayerCustom = (props: {
|
|||
}
|
||||
|
||||
const _fetchKodikManifest = async (url: string) => {
|
||||
// Fetch data through a proxy
|
||||
const data = await _fetchPlayer(
|
||||
`https://anix-player.wah.su/?url=${url}&player=kodik`
|
||||
);
|
||||
if (data) {
|
||||
let lowQualityLink = data.links["360"][0].src; // we assume that 360p is always present
|
||||
|
||||
if (!lowQualityLink.includes("//")) { // check if link is encrypted, else do nothing
|
||||
const decryptedBase64 = lowQualityLink.replace(/[a-zA-Z]/g, (e) => {
|
||||
return String.fromCharCode(
|
||||
(e <= "Z" ? 90 : 122) >= (e = e.charCodeAt(0) + 18) ? e : e - 26
|
||||
);
|
||||
});
|
||||
lowQualityLink = atob(decryptedBase64);
|
||||
}
|
||||
|
||||
if (lowQualityLink.includes("https://")) { // string the https prefix, since we add it manually
|
||||
let lowQualityLink = data.links["360"][0].src;
|
||||
if (lowQualityLink.includes("https://")) {
|
||||
lowQualityLink = lowQualityLink.replace("https://", "//");
|
||||
}
|
||||
|
||||
let manifest = `https:${lowQualityLink.replace("360.mp4:hls:", "")}`;
|
||||
let poster = `https:${lowQualityLink.replace("360.mp4:hls:manifest.m3u8", "thumb001.jpg")}`;
|
||||
|
||||
if (lowQualityLink.includes("animetvseries")) { // if link includes "animetvseries" we need to construct manifest ourselves
|
||||
if (lowQualityLink.includes("animetvseries")) {
|
||||
let blobTxt = "#EXTM3U\n";
|
||||
|
||||
if (data.links.hasOwnProperty("240")) {
|
||||
|
@ -224,8 +212,7 @@ export const ReleasePlayerCustom = (props: {
|
|||
}
|
||||
};
|
||||
__getInfo();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.id, props.token]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const __getInfo = async () => {
|
||||
|
@ -257,7 +244,6 @@ export const ReleasePlayerCustom = (props: {
|
|||
if (voiceover.selected) {
|
||||
__getInfo();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [voiceover.selected]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -298,7 +284,6 @@ export const ReleasePlayerCustom = (props: {
|
|||
if (source.selected) {
|
||||
__getInfo();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [source.selected]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -361,7 +346,6 @@ export const ReleasePlayerCustom = (props: {
|
|||
setPlayerError(null);
|
||||
__getInfo();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [episode.selected]);
|
||||
|
||||
return (
|
||||
|
@ -449,12 +433,7 @@ export const ReleasePlayerCustom = (props: {
|
|||
}
|
||||
</div>
|
||||
|
||||
: <iframe
|
||||
src={playerProps.src}
|
||||
className="w-full aspect-video"
|
||||
allowFullScreen={true}
|
||||
/>
|
||||
}
|
||||
: <iframe src={playerProps.src} className="w-full aspect-video" />}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { Dropdown, DropdownItem } from "flowbite-react";
|
||||
import { Dropdown } from "flowbite-react";
|
||||
import { numberDeclension } from "#/api/utils";
|
||||
import { useUserPlayerPreferencesStore } from "#/store/player";
|
||||
|
||||
|
@ -20,7 +20,7 @@ const DropdownTrigger = ({ name }: Source) => {
|
|||
);
|
||||
};
|
||||
|
||||
const DropdownItemInternal = ({ name, episodes_count }: Source) => {
|
||||
const DropdownItem = ({ 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) => (
|
||||
<DropdownItem
|
||||
<Dropdown.Item
|
||||
key={`source_${source.id}`}
|
||||
onClick={() => {
|
||||
playerPreferenceStore.setPreferredPlayer(
|
||||
|
@ -68,8 +68,8 @@ export const SourceSelector = (props: {
|
|||
});
|
||||
}}
|
||||
>
|
||||
<DropdownItemInternal {...source} />
|
||||
</DropdownItem>
|
||||
<DropdownItem {...source} />
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { Dropdown, DropdownItem } from "flowbite-react";
|
||||
import { Dropdown } from "flowbite-react";
|
||||
import { numberDeclension } from "#/api/utils";
|
||||
import { useUserPlayerPreferencesStore } from "#/store/player";
|
||||
|
||||
|
@ -16,8 +16,7 @@ interface Voiceover {
|
|||
const DropdownTrigger = ({ icon, name, pinned }: Voiceover) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2 cursor-pointer">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
{icon && <img alt="" className="w-6 h-6 rounded-full" src={icon}></img>}
|
||||
{icon && <img 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>
|
||||
|
@ -27,7 +26,7 @@ const DropdownTrigger = ({ icon, name, pinned }: Voiceover) => {
|
|||
);
|
||||
};
|
||||
|
||||
const DropdownItemInternal = ({
|
||||
const DropdownItem = ({
|
||||
icon,
|
||||
name,
|
||||
pinned,
|
||||
|
@ -37,8 +36,7 @@ const DropdownItemInternal = ({
|
|||
return (
|
||||
<div className="flex flex-col gap-2 cursor-pointer">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
{icon && <img alt="" className="w-6 h-6 rounded-full" src={icon}></img>}
|
||||
{icon && <img className="w-6 h-6 rounded-full" src={icon}></img>}
|
||||
<p>{name}</p>
|
||||
{pinned && (
|
||||
<span className="h-6 iconify material-symbols--push-pin"></span>
|
||||
|
@ -82,7 +80,7 @@ export const VoiceoverSelector = (props: {
|
|||
)}
|
||||
>
|
||||
{props.availableVoiceover.map((voiceover: Voiceover) => (
|
||||
<DropdownItem
|
||||
<Dropdown.Item
|
||||
className="w-fit"
|
||||
key={`voiceover_${voiceover.id}`}
|
||||
onClick={() => {
|
||||
|
@ -96,8 +94,8 @@ export const VoiceoverSelector = (props: {
|
|||
});
|
||||
}}
|
||||
>
|
||||
<DropdownItemInternal {...voiceover} />
|
||||
</DropdownItem>
|
||||
<DropdownItem {...voiceover} />
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
);
|
||||
|
|
|
@ -4,16 +4,12 @@ import { CURRENT_APP_VERSION } from "#/api/config";
|
|||
import { useUserStore } from "#/store/auth";
|
||||
import { usePreferencesStore } from "#/store/preferences";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
HR,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ToggleSwitch,
|
||||
Button,
|
||||
useThemeMode,
|
||||
ToggleSwitch,
|
||||
HR,
|
||||
Dropdown,
|
||||
} from "flowbite-react";
|
||||
import Link from "next/link";
|
||||
|
||||
|
@ -58,8 +54,8 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
|||
show={props.isOpen}
|
||||
onClose={() => props.setIsOpen(false)}
|
||||
>
|
||||
<ModalHeader>Настройки</ModalHeader>
|
||||
<ModalBody>
|
||||
<Modal.Header>Настройки</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-6 h-6 iconify material-symbols--palette-outline"></span>
|
||||
|
@ -67,20 +63,20 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
|||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className=" dark:text-white">Тема</p>
|
||||
<ButtonGroup>
|
||||
<Button.Group>
|
||||
<Button
|
||||
color={computedMode == "light" ? "blue" : "light"}
|
||||
color={computedMode == "light" ? "blue" : "gray"}
|
||||
onClick={() => setMode("light")}
|
||||
>
|
||||
Светлая
|
||||
</Button>
|
||||
<Button
|
||||
color={computedMode == "dark" ? "blue" : "light"}
|
||||
color={computedMode == "dark" ? "blue" : "gray"}
|
||||
onClick={() => setMode("dark")}
|
||||
>
|
||||
Темная
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Button.Group>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className=" dark:text-white max-w-96">
|
||||
|
@ -89,6 +85,15 @@ 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: {
|
||||
|
@ -116,7 +121,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
|||
>
|
||||
{Object.keys(HomeCategory).map((key) => {
|
||||
return (
|
||||
<DropdownItem
|
||||
<Dropdown.Item
|
||||
key={key}
|
||||
onClick={() =>
|
||||
preferenceStore.setParams({
|
||||
|
@ -128,7 +133,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
|||
}
|
||||
>
|
||||
{HomeCategory[key]}
|
||||
</DropdownItem>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</Dropdown>
|
||||
|
@ -147,7 +152,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
|||
>
|
||||
{Object.keys(BookmarksCategory).map((key) => {
|
||||
return (
|
||||
<DropdownItem
|
||||
<Dropdown.Item
|
||||
key={key}
|
||||
onClick={() =>
|
||||
preferenceStore.setParams({
|
||||
|
@ -159,7 +164,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
|||
}
|
||||
>
|
||||
{BookmarksCategory[key]}
|
||||
</DropdownItem>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</Dropdown>
|
||||
|
@ -177,7 +182,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
|||
{Object.keys(NavbarTitles).map(
|
||||
(key: "always" | "links" | "selected" | "never") => {
|
||||
return (
|
||||
<DropdownItem
|
||||
<Dropdown.Item
|
||||
className={`${key == "links" ? "hidden lg:flex" : ""}`}
|
||||
key={`navbar-titles-${key}`}
|
||||
onClick={() =>
|
||||
|
@ -187,7 +192,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
|||
}
|
||||
>
|
||||
{NavbarTitles[key]}
|
||||
</DropdownItem>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
)}
|
||||
|
@ -206,7 +211,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
|||
: "Нет"
|
||||
}
|
||||
>
|
||||
<DropdownItem
|
||||
<Dropdown.Item
|
||||
onClick={() =>
|
||||
preferenceStore.setFlags({
|
||||
showFifthButton: null,
|
||||
|
@ -214,10 +219,10 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
|||
}
|
||||
>
|
||||
Не показывать
|
||||
</DropdownItem>
|
||||
</Dropdown.Item>
|
||||
{Object.keys(FifthButton).map((key) => {
|
||||
return (
|
||||
<DropdownItem
|
||||
<Dropdown.Item
|
||||
key={`navbar-fifthbutton-${key}`}
|
||||
onClick={() =>
|
||||
preferenceStore.setFlags({
|
||||
|
@ -226,7 +231,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
|||
}
|
||||
>
|
||||
{FifthButton[key]}
|
||||
</DropdownItem>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</Dropdown>
|
||||
|
@ -241,6 +246,15 @@ 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,
|
||||
|
@ -258,6 +272,15 @@ 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,
|
||||
|
@ -280,6 +303,15 @@ 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: {
|
||||
|
@ -308,7 +340,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
|||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
AccordionPanel,
|
||||
AccordionTitle,
|
||||
} from "flowbite-react";
|
||||
import { version } from "node:os";
|
||||
import Link from "next/link";
|
||||
|
||||
export const AboutPage = () => {
|
||||
|
|
|
@ -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 { Button, ButtonGroup, Dropdown, DropdownItem } from "flowbite-react";
|
||||
import { Dropdown, Button } 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">
|
||||
<ButtonGroup>
|
||||
<Button.Group>
|
||||
<Button
|
||||
className="whitespace-nowrap"
|
||||
disabled={props.slug == "watching"}
|
||||
|
@ -222,7 +222,7 @@ export function BookmarksCategoryPage(props: any) {
|
|||
>
|
||||
{props.SectionTitleMapping["abandoned"]}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Button.Group>
|
||||
</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) => (
|
||||
<DropdownItem key={index} onClick={() => setSelectedSort(index)}>
|
||||
<Dropdown.Item 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}
|
||||
</DropdownItem>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
|
|
@ -5,15 +5,14 @@ import { useEffect, useState, useCallback } from "react";
|
|||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Button,
|
||||
Checkbox,
|
||||
TextInput,
|
||||
Textarea,
|
||||
FileInput,
|
||||
Label,
|
||||
Modal,
|
||||
ModalHeader,
|
||||
Textarea,
|
||||
TextInput,
|
||||
useThemeMode,
|
||||
} from "flowbite-react";
|
||||
import { PosterWithStuff } from "#/components/ReleasePoster/PosterWithStuff";
|
||||
|
@ -34,7 +33,6 @@ 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);
|
||||
|
@ -390,9 +388,10 @@ export const CreateCollectionPage = () => {
|
|||
</Label>
|
||||
<div className="flex-1">
|
||||
<div className="block mb-2">
|
||||
<Label htmlFor="title">
|
||||
Название (минимум 10, максимум 60 символов)
|
||||
</Label>
|
||||
<Label
|
||||
htmlFor="title"
|
||||
value="Название (минимум 10, максимум 60 символов)"
|
||||
/>
|
||||
</div>
|
||||
<TextInput
|
||||
id="title"
|
||||
|
@ -409,9 +408,10 @@ export const CreateCollectionPage = () => {
|
|||
{stringLength.title}/60
|
||||
</p>
|
||||
<div className="block mt-2 mb-2">
|
||||
<Label htmlFor="description">
|
||||
Описание (максимум 1000 символов)
|
||||
</Label>
|
||||
<Label
|
||||
htmlFor="description"
|
||||
value="Описание (максимум 1000 символов)"
|
||||
/>
|
||||
</div>
|
||||
<Textarea
|
||||
rows={4}
|
||||
|
@ -434,7 +434,7 @@ export const CreateCollectionPage = () => {
|
|||
checked={isPrivate}
|
||||
onChange={(e) => setIsPrivate(e.target.checked)}
|
||||
/>
|
||||
<Label htmlFor="private">Приватная коллекция</Label>
|
||||
<Label htmlFor="private" value="Приватная коллекция" />
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
|
@ -603,7 +603,7 @@ export const ReleasesEditModal = (props: {
|
|||
onClose={() => props.setIsOpen(false)}
|
||||
size={"7xl"}
|
||||
>
|
||||
<ModalHeader>Изменить релизы в коллекции</ModalHeader>
|
||||
<Modal.Header>Изменить релизы в коллекции</Modal.Header>
|
||||
<div
|
||||
onScroll={handleScroll}
|
||||
ref={modalRef}
|
||||
|
|
|
@ -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 { Button, Dropdown, DropdownItem } from "flowbite-react";
|
||||
import { Dropdown, Button } 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) => (
|
||||
<DropdownItem key={index} onClick={() => setSelectedSort(index)}>
|
||||
<Dropdown.Item 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}
|
||||
</DropdownItem>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
|
|
@ -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, ButtonGroup } from "flowbite-react";
|
||||
import { Button } 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">
|
||||
<ButtonGroup>
|
||||
<Button.Group>
|
||||
<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>
|
||||
</ButtonGroup>
|
||||
</Button.Group>
|
||||
</div>
|
||||
{content && content.length > 0 ? (
|
||||
<ReleaseSection
|
||||
|
|
|
@ -59,6 +59,53 @@ 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">
|
||||
|
@ -69,48 +116,61 @@ export const ProfilePage = (props: any) => {
|
|||
ban_expires={user.ban_expires}
|
||||
/>
|
||||
<ProfilePrivacyBanner
|
||||
is_privacy={
|
||||
user.is_stats_hidden ||
|
||||
user.is_counts_hidden ||
|
||||
user.is_social_hidden
|
||||
}
|
||||
is_privacy={isPrivacy}
|
||||
is_me_blocked={user.is_me_blocked}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
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`}
|
||||
className={`flex flex-wrap gap-2 ${
|
||||
isPrivacy || user.is_banned || user.is_perm_banned ? "mt-4" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-2 w-full xl:w-[50%]">
|
||||
<ProfileUser
|
||||
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}
|
||||
isOnline={user.is_online}
|
||||
avatar={user.avatar}
|
||||
login={user.login}
|
||||
status={user.status}
|
||||
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,
|
||||
isPrivate: user.is_social_hidden,
|
||||
hasSocials: hasSocials,
|
||||
socials: socials,
|
||||
}}
|
||||
is_social_hidden={user.is_social_hidden}
|
||||
chips={{
|
||||
hasChips: hasChips,
|
||||
isMyProfile: isMyProfile,
|
||||
isVerified: user.is_verified,
|
||||
isSponsor: user.is_sponsor,
|
||||
isBlocked: user.is_blocked,
|
||||
roles: user.roles,
|
||||
}}
|
||||
rating={user.rating_score}
|
||||
/>
|
||||
{!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}
|
||||
|
@ -125,32 +185,6 @@ 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
|
||||
|
@ -166,7 +200,7 @@ export const ProfilePage = (props: any) => {
|
|||
profile_id={user.id}
|
||||
/>
|
||||
<ProfileWatchDynamic watchDynamic={user.watch_dynamics || []} />
|
||||
<div className="flex flex-col gap-2 lg:hidden">
|
||||
<div className="flex flex-col gap-2 xl:hidden">
|
||||
{user.votes && user.votes.length > 0 && (
|
||||
<ProfileReleaseRatings
|
||||
ratings={user.votes}
|
||||
|
@ -174,10 +208,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>
|
||||
|
|
|
@ -18,7 +18,6 @@ import { CommentsMain } from "#/components/Comments/Comments.Main";
|
|||
import { InfoLists } from "#/components/InfoLists/InfoLists";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { usePreferencesStore } from "#/store/preferences";
|
||||
import { ContinueWatching } from "#/components/ContinueWatching/ContinueWatching";
|
||||
|
||||
export const ReleasePage = (props: any) => {
|
||||
const userStore = useUserStore();
|
||||
|
@ -72,7 +71,7 @@ export const ReleasePage = (props: any) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 pb-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[70%_30%] gap-2 grid-flow-row-dense">
|
||||
<ReleaseInfoBasics
|
||||
image={data.release.image}
|
||||
|
@ -123,13 +122,11 @@ export const ReleasePage = (props: any) => {
|
|||
: <ReleasePlayer id={props.id} />}
|
||||
</>
|
||||
)}
|
||||
<div className="hidden lg:block">
|
||||
<CommentsMain
|
||||
release_id={props.id}
|
||||
token={userStore.token}
|
||||
comments={data.release.comments}
|
||||
/>
|
||||
</div>
|
||||
<CommentsMain
|
||||
release_id={props.id}
|
||||
token={userStore.token}
|
||||
comments={data.release.comments}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{data.release.status &&
|
||||
|
@ -168,14 +165,6 @@ export const ReleasePage = (props: any) => {
|
|||
related_releases={data.release.related_releases}
|
||||
/>
|
||||
)}
|
||||
{userStore.token && <ContinueWatching />}
|
||||
<div className="block lg:hidden">
|
||||
<CommentsMain
|
||||
release_id={props.id}
|
||||
token={userStore.token}
|
||||
comments={data.release.comments}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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, DropdownItem, Modal, ModalBody, ModalFooter, ModalHeader } from "flowbite-react";
|
||||
import { Button, Dropdown, Modal } 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()}>
|
||||
<ModalHeader>Фильтры</ModalHeader>
|
||||
<ModalBody>
|
||||
<Modal.Header>Фильтры</Modal.Header>
|
||||
<Modal.Body>
|
||||
<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 (
|
||||
<DropdownItem
|
||||
<Dropdown.Item
|
||||
onClick={() => setWhere(item)}
|
||||
key={`where--${item}`}
|
||||
>
|
||||
{WhereMapping[item]}
|
||||
</DropdownItem>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
@ -395,12 +395,12 @@ const FiltersModal = (props: {
|
|||
<Dropdown label={ListsMapping[list].name} color="blue">
|
||||
{Object.keys(ListsMapping).map((item) => {
|
||||
return (
|
||||
<DropdownItem
|
||||
<Dropdown.Item
|
||||
onClick={() => setList(item)}
|
||||
key={`list--${item}`}
|
||||
>
|
||||
{ListsMapping[item].name}
|
||||
</DropdownItem>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</Dropdown>
|
||||
|
@ -414,20 +414,20 @@ const FiltersModal = (props: {
|
|||
<Dropdown label={TagMapping[searchBy].name} color="blue">
|
||||
{Object.keys(TagMapping).map((item) => {
|
||||
return (
|
||||
<DropdownItem
|
||||
<Dropdown.Item
|
||||
onClick={() => setSearchBy(item)}
|
||||
key={`tag--${item}`}
|
||||
>
|
||||
{TagMapping[item].name}
|
||||
</DropdownItem>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
: ""}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<div className="flex justify-end w-full gap-2">
|
||||
<Button color="red" onClick={() => _cancel()}>
|
||||
Отменить
|
||||
|
@ -436,7 +436,7 @@ const FiltersModal = (props: {
|
|||
Применить
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,10 +6,10 @@ AniX - это неофициальный веб-клиент для Android-пр
|
|||
|
||||
## Список изменений
|
||||
|
||||
- [3.6.0](/public/changelog/3.6.0.md)
|
||||
- [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)
|
||||
|
||||
[другие версии](/public/changelog)
|
||||
|
||||
|
|
Before Width: | Height: | Size: 529 KiB After Width: | Height: | Size: 120 KiB |
Before Width: | Height: | Size: 632 KiB After Width: | Height: | Size: 212 KiB |
Before Width: | Height: | Size: 731 KiB After Width: | Height: | Size: 204 KiB |
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 267 KiB |
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 225 KiB |
|
@ -1,7 +1,8 @@
|
|||
const { withPlausibleProxy } = require("next-plausible");
|
||||
const withFlowbiteReact = require("flowbite-react/plugin/nextjs");
|
||||
/** @type {import('next').NextConfig} */
|
||||
const NextConfig = {
|
||||
|
||||
module.exports = withPlausibleProxy({
|
||||
customDomain: "https://analytics.wah.su",
|
||||
})({
|
||||
reactStrictMode: false,
|
||||
images: {
|
||||
unoptimized: true,
|
||||
|
@ -9,74 +10,68 @@ const NextConfig = {
|
|||
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;
|
||||
});
|
||||
|
|
1844
package-lock.json
generated
11
package.json
|
@ -6,20 +6,18 @@
|
|||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"postinstall": "flowbite-react patch"
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"apexcharts": "^3.52.0",
|
||||
"deepmerge-ts": "^7.1.0",
|
||||
"flowbite": "^2.4.1",
|
||||
"flowbite-react": "^0.11.7",
|
||||
"flowbite-react": "^0.10.1",
|
||||
"hls-video-element": "^1.5.0",
|
||||
"markdown-to-jsx": "^7.4.7",
|
||||
"media-chrome": "^4.8.0",
|
||||
"next": "^14.2.26",
|
||||
"next": "^14.2.13",
|
||||
"next-plausible": "^3.12.1",
|
||||
"prettier": "^3.5.3",
|
||||
"react": "^18",
|
||||
"react-cropper": "^2.3.3",
|
||||
"react-dom": "^18",
|
||||
|
@ -40,8 +38,9 @@
|
|||
"copy-webpack-plugin": "^12.0.2",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.5",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.5.3",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
"tailwindcss": "^3.4.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
# 3.6.0
|
||||
|
||||
## Добавлено
|
||||
|
||||
- Добавлена возможность смотреть заблокированных пользователей (в меню редактирования профиля)
|
||||
- Добавлена возможность смотреть друзей пользователя
|
||||
- Добавлена возможность смотреть, принимать и скрывать заявки в друзья
|
||||
- Добавлен просмотр популярных комментариев пользователя
|
||||
- Добавлен блок "Продолжить просмотр" на странице релиза если был произведён вход в аккаунт (3 рандомных релиза из списков сморю, в планах и отложено)
|
||||
|
||||
## Изменено
|
||||
|
||||
- Стиль некоторых элементов был изменён в связи с обновление библиотеки компонентов
|
||||
- Блок скриншотов был перенесён на другую библиотеку
|
||||
- Изменён вид блока связанных релизов на странице релиза с карусели карточек на список
|
||||
- Изменён блок активности на странице профиля
|
||||
- Изменены виды блоков "Оценки" и "Недавно просмотренные" на странице профиля с карусели карточек на список
|
||||
- Изменён вид блока профиля (соц. сети и роли)
|
||||
|
||||
## Исправлено
|
||||
|
||||
- Получение прямых ссылок на источник кодик в собственном плеере, если кодик отдал зашифрованные ссылки
|
|
@ -1,9 +1,5 @@
|
|||
const { addIconSelectors } = require("@iconify/tailwind");
|
||||
const flowbiteReact = require("flowbite-react/plugin/tailwindcss");
|
||||
|
||||
flowbiteReact.config = {
|
||||
charts: true,
|
||||
}
|
||||
import flowbite from "flowbite-react/tailwind";
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
|
@ -11,11 +7,14 @@ module.exports = {
|
|||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
".flowbite-react\\class-list.json"
|
||||
flowbite.content(),
|
||||
],
|
||||
plugins: [
|
||||
addIconSelectors(["mdi", "material-symbols", "twemoji", "fa6-brands"]),
|
||||
flowbiteReact
|
||||
require("tailwind-scrollbar"),
|
||||
flowbite.plugin()({
|
||||
charts: true,
|
||||
}),
|
||||
],
|
||||
darkMode: "selector",
|
||||
theme: {
|
||||
|
@ -44,4 +43,4 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|