mirror of
https://github.com/Radiquum/AniX.git
synced 2025-04-30 18:09:40 +05:00
Merge branch 'refactor__ProfilePage' into V3
This commit is contained in:
commit
c762c4a940
23 changed files with 1125 additions and 275 deletions
|
@ -5,12 +5,14 @@ export const Chip = (props: {
|
|||
name_2?: string;
|
||||
devider?: string;
|
||||
bg_color?: string;
|
||||
style?: React.CSSProperties;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`px-2 sm:px-4 py-0.5 sm:py-1 rounded-sm ${
|
||||
props.bg_color || "bg-gray-500"
|
||||
} ${props.icon_name ? "flex items-center justify-center gap-1" : ""}`}
|
||||
style={props.style || {}}
|
||||
>
|
||||
{props.icon_name && (
|
||||
<span
|
||||
|
|
148
app/components/Profile/Profile.Actions.tsx
Normal file
148
app/components/Profile/Profile.Actions.tsx
Normal file
|
@ -0,0 +1,148 @@
|
|||
"use client";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { Card, Button } from "flowbite-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
|
||||
// null - не друзья
|
||||
// 0 - заявка в друзья authUserId < profileId
|
||||
// 1 - заявка в друзья authUserId > profileId
|
||||
// 2 - друзья
|
||||
|
||||
// если id профиля больше id юзера, то 0 иначе 1
|
||||
|
||||
export const ProfileActions = (props: {
|
||||
isMyProfile: boolean;
|
||||
isFriendRequestsDisallowed: boolean;
|
||||
profile_id: number;
|
||||
my_profile_id: number;
|
||||
friendStatus: number;
|
||||
token: string;
|
||||
is_me_blocked: boolean;
|
||||
is_blocked: boolean;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const profileIdIsSmaller = props.my_profile_id < props.profile_id;
|
||||
const [friendRequestDisabled, setFriendRequestDisabled] = useState(false);
|
||||
const [blockRequestDisabled, setBlockRequestDisabled] = useState(false);
|
||||
const { mutate } = useSWRConfig();
|
||||
function _getFriendStatus() {
|
||||
const num = props.friendStatus;
|
||||
|
||||
if (num == null) {
|
||||
return null;
|
||||
}
|
||||
let z = true;
|
||||
if (num == 2) {
|
||||
return 1;
|
||||
}
|
||||
let z3 =
|
||||
(num == 0 && profileIdIsSmaller) || (num == 1 && !profileIdIsSmaller);
|
||||
if ((num != 1 || profileIdIsSmaller) && (num != 0 || !profileIdIsSmaller)) {
|
||||
z = false;
|
||||
}
|
||||
if (z3) {
|
||||
return 2;
|
||||
}
|
||||
if (z) {
|
||||
return 3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const FriendStatus = _getFriendStatus();
|
||||
const isRequestedStatus =
|
||||
FriendStatus != null
|
||||
? profileIdIsSmaller
|
||||
? profileIdIsSmaller && FriendStatus != 0
|
||||
: !profileIdIsSmaller && FriendStatus == 2
|
||||
: null;
|
||||
// ^ This is some messed up shit
|
||||
|
||||
function _addToFriends() {
|
||||
let url = `${ENDPOINTS.user.profile}/friend/request`;
|
||||
setFriendRequestDisabled(true);
|
||||
setBlockRequestDisabled(true);
|
||||
|
||||
FriendStatus == 1
|
||||
? (url += "/remove/")
|
||||
: isRequestedStatus
|
||||
? (url += "/remove/")
|
||||
: (url += "/send/");
|
||||
|
||||
url += `${props.profile_id}?token=${props.token}`;
|
||||
fetch(url).then((res) => {
|
||||
mutate(
|
||||
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
|
||||
);
|
||||
setTimeout(() => {
|
||||
setBlockRequestDisabled(false);
|
||||
setFriendRequestDisabled(false);
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
function _addToBlocklist() {
|
||||
let url = `${ENDPOINTS.user.profile}/blocklist`;
|
||||
setBlockRequestDisabled(true);
|
||||
setFriendRequestDisabled(true);
|
||||
|
||||
!props.is_blocked ? (url += "/add/") : (url += "/remove/");
|
||||
|
||||
url += `${props.profile_id}?token=${props.token}`;
|
||||
fetch(url).then((res) => {
|
||||
mutate(
|
||||
`${ENDPOINTS.user.profile}/${props.profile_id}?token=${props.token}`
|
||||
);
|
||||
setTimeout(() => {
|
||||
setBlockRequestDisabled(false);
|
||||
setFriendRequestDisabled(false);
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="h-fit">
|
||||
{isRequestedStatus != null && !isRequestedStatus && FriendStatus != 1 && (
|
||||
<p>Отправил(-а) вам заявку в друзья</p>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
{props.isMyProfile && <Button color={"blue"}>Редактировать</Button>}
|
||||
{!props.isMyProfile && (
|
||||
<>
|
||||
{(!props.isFriendRequestsDisallowed ||
|
||||
FriendStatus == 1 ||
|
||||
isRequestedStatus) &&
|
||||
!props.is_me_blocked &&
|
||||
!props.is_blocked && (
|
||||
<Button
|
||||
disabled={friendRequestDisabled}
|
||||
color={
|
||||
FriendStatus == 1
|
||||
? "red"
|
||||
: isRequestedStatus
|
||||
? "light"
|
||||
: "blue"
|
||||
}
|
||||
onClick={() => _addToFriends()}
|
||||
>
|
||||
{FriendStatus == 1
|
||||
? "Удалить из друзей"
|
||||
: isRequestedStatus
|
||||
? "Заявка отправлена"
|
||||
: "Добавить в друзья"}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
color={!props.is_blocked ? "red" : "blue"}
|
||||
disabled={blockRequestDisabled}
|
||||
onClick={() => _addToBlocklist()}
|
||||
>
|
||||
{!props.is_blocked ? "Заблокировать" : "Разблокировать"}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
49
app/components/Profile/Profile.Activity.tsx
Normal file
49
app/components/Profile/Profile.Activity.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
"use client";
|
||||
import { Card } from "flowbite-react";
|
||||
import Link from "next/link";
|
||||
import { numberDeclension } from "#/api/utils";
|
||||
|
||||
export function ProfileActivity(props: {
|
||||
profile_id: number;
|
||||
commentCount: number;
|
||||
videoCount: number;
|
||||
collectionCount: number;
|
||||
friendsCount: number;
|
||||
}) {
|
||||
return (
|
||||
<Card className="h-fit">
|
||||
<h1 className="text-2xl font-bold">Активность</h1>
|
||||
<div className="flex items-center gap-4 text-lg">
|
||||
<div>
|
||||
<p>
|
||||
{props.commentCount}{" "}
|
||||
{numberDeclension(
|
||||
props.commentCount,
|
||||
"комментарий",
|
||||
"комментария",
|
||||
"комментариев"
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-2">{props.videoCount} видео</p>
|
||||
</div>
|
||||
<div>
|
||||
<Link href={`/profile/${props.profile_id}/collections`}>
|
||||
<p className="border-b-2 border-gray-300 border-solid dark:border-gray-400 hover:border-gray-500 dark:hover:border-gray-200">
|
||||
{props.collectionCount}{" "}
|
||||
{numberDeclension(
|
||||
props.commentCount,
|
||||
"коллекция",
|
||||
"коллекции",
|
||||
"коллекций"
|
||||
)}
|
||||
</p>
|
||||
</Link>
|
||||
<p className="mt-2">
|
||||
{props.friendsCount}{" "}
|
||||
{numberDeclension(props.commentCount, "друзей", "друга", "друзей")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
26
app/components/Profile/Profile.PrivacyBanner.tsx
Normal file
26
app/components/Profile/Profile.PrivacyBanner.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
export const ProfilePrivacyBanner = (props: {
|
||||
is_privacy: boolean;
|
||||
is_me_blocked: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{props.is_privacy && (
|
||||
<div
|
||||
className={`flex flex-col justify-between w-full p-4 border rounded-md md:flex-row ${
|
||||
!props.is_me_blocked
|
||||
? "border-gray-200 bg-gray-50 dark:bg-gray-700 dark:border-gray-600"
|
||||
: "border-red-200 bg-red-50 dark:bg-red-700 dark:border-red-600"
|
||||
}`}
|
||||
>
|
||||
<div className="mb-4 md:mb-0 md:me-4">
|
||||
<p className="flex items-center text-sm font-normal text-gray-500 dark:text-gray-200">
|
||||
{!props.is_me_blocked
|
||||
? "У пользователя установлены настройки приватности. Некоторая информация для вас может быть недоступна."
|
||||
: "Вы заблокированы данным пользователем. Его информация для вас не доступна."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
40
app/components/Profile/Profile.ReleaseHistory.tsx
Normal file
40
app/components/Profile/Profile.ReleaseHistory.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Card, Carousel, RatingStar, Rating } from "flowbite-react";
|
||||
import type {
|
||||
FlowbiteCarouselIndicatorsTheme,
|
||||
FlowbiteCarouselControlTheme,
|
||||
} from "flowbite-react";
|
||||
import { ReleaseLink } from "../ReleaseLink/ReleaseLink";
|
||||
|
||||
const CarouselIndicatorsTheme: FlowbiteCarouselIndicatorsTheme = {
|
||||
active: {
|
||||
off: "bg-gray-300/50 hover:bg-gray-400 dark:bg-gray-400/50 dark:hover:bg-gray-200",
|
||||
on: "bg-gray-600 dark:bg-gray-200",
|
||||
},
|
||||
base: "h-3 w-3 rounded-full",
|
||||
wrapper: "absolute bottom-5 left-1/2 flex -translate-x-1/2 space-x-3",
|
||||
};
|
||||
|
||||
const CarouselControlsTheme: FlowbiteCarouselControlTheme = {
|
||||
base: "inline-flex h-8 w-8 items-center justify-center rounded-full bg-gray-600/30 group-hover:bg-gray-600/50 group-focus:outline-none group-focus:ring-4 group-focus:ring-gray-600 dark:bg-gray-400/30 dark:group-hover:bg-gray-400/60 dark:group-focus:ring-gray-400/70 sm:h-10 sm:w-10",
|
||||
icon: "h-5 w-5 text-gray-600 dark:text-gray-400 sm:h-6 sm:w-6",
|
||||
};
|
||||
|
||||
const CarouselTheme = {
|
||||
indicators: CarouselIndicatorsTheme,
|
||||
control: CarouselControlsTheme,
|
||||
};
|
||||
|
||||
export const ProfileReleaseHistory = (props: any) => {
|
||||
return (
|
||||
<Card className="h-fit">
|
||||
<h1 className="text-2xl font-bold">Недавно просмотренные</h1>
|
||||
<div className="max-w-[700px] min-h-[200px]">
|
||||
<Carousel theme={CarouselTheme}>
|
||||
{props.history.map((release) => {
|
||||
return <ReleaseLink key={`history-${release.id}`} {...release} />;
|
||||
})}
|
||||
</Carousel>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
67
app/components/Profile/Profile.ReleaseRatings.tsx
Normal file
67
app/components/Profile/Profile.ReleaseRatings.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { Card, Carousel, RatingStar, Rating } from "flowbite-react";
|
||||
import type {
|
||||
FlowbiteCarouselIndicatorsTheme,
|
||||
FlowbiteCarouselControlTheme,
|
||||
} from "flowbite-react";
|
||||
import Image from "next/image";
|
||||
import { unixToDate } from "#/api/utils";
|
||||
import Link from "next/link";
|
||||
|
||||
const CarouselIndicatorsTheme: FlowbiteCarouselIndicatorsTheme = {
|
||||
active: {
|
||||
off: "bg-gray-300/50 hover:bg-gray-400 dark:bg-gray-400/50 dark:hover:bg-gray-200",
|
||||
on: "bg-gray-600 dark:bg-gray-200",
|
||||
},
|
||||
base: "h-3 w-3 rounded-full",
|
||||
wrapper: "absolute bottom-5 left-1/2 flex -translate-x-1/2 space-x-3",
|
||||
};
|
||||
|
||||
const CarouselControlsTheme: FlowbiteCarouselControlTheme = {
|
||||
base: "inline-flex h-8 w-8 items-center justify-center rounded-full bg-gray-600/30 group-hover:bg-gray-600/50 group-focus:outline-none group-focus:ring-4 group-focus:ring-gray-600 dark:bg-gray-400/30 dark:group-hover:bg-gray-400/60 dark:group-focus:ring-gray-400/70 sm:h-10 sm:w-10",
|
||||
icon: "h-5 w-5 text-gray-600 dark:text-gray-400 sm:h-6 sm:w-6",
|
||||
};
|
||||
|
||||
const CarouselTheme = {
|
||||
indicators: CarouselIndicatorsTheme,
|
||||
control: CarouselControlsTheme,
|
||||
};
|
||||
|
||||
export const ProfileReleaseRatings = (props: any) => {
|
||||
return (
|
||||
<Card className="h-fit">
|
||||
<h1 className="text-2xl font-bold">Оценки</h1>
|
||||
<div className="max-w-[700px] min-h-[200px]">
|
||||
<Carousel theme={CarouselTheme}>
|
||||
{props.ratings.map((release) => {
|
||||
return (
|
||||
<Link href={`/release/${release.id}`} key={`vote-${release.id}`}>
|
||||
<div className="flex gap-4 xl:mx-20">
|
||||
<Image
|
||||
src={release.image}
|
||||
width={100}
|
||||
height={125}
|
||||
alt=""
|
||||
className="object-cover border-gray-200 rounded-lg shadow-md dark:border-gray-700 dark:bg-gray-800 w-[100px] h-[125px]"
|
||||
/>
|
||||
<div className="flex flex-col gap-1 py-4">
|
||||
<h2 className="text-lg">{release.title_ru}</h2>
|
||||
<Rating size="md">
|
||||
<RatingStar filled={release.my_vote >= 1} />
|
||||
<RatingStar filled={release.my_vote >= 2} />
|
||||
<RatingStar filled={release.my_vote >= 3} />
|
||||
<RatingStar filled={release.my_vote >= 4} />
|
||||
<RatingStar filled={release.my_vote >= 5} />
|
||||
</Rating>
|
||||
<h2 className="text-gray-500 text-md dark:text-gray-400">
|
||||
{unixToDate(release.voted_at, "full")}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Carousel>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
110
app/components/Profile/Profile.Stats.tsx
Normal file
110
app/components/Profile/Profile.Stats.tsx
Normal file
|
@ -0,0 +1,110 @@
|
|||
import { Card } from "flowbite-react";
|
||||
import Link from "next/link";
|
||||
import ApexCharts from "apexcharts";
|
||||
import { useEffect } from "react";
|
||||
import { minutesToTime } from "#/api/utils";
|
||||
|
||||
export const ProfileStats = (props: {
|
||||
lists: Array<number>;
|
||||
watched_count: number;
|
||||
watched_time: number;
|
||||
profile_id: number
|
||||
}) => {
|
||||
const getChartOptions = () => {
|
||||
return {
|
||||
series: props.lists,
|
||||
colors: ["#66bb6c", "#b566bb", "#5c6cc0", "#ffca28", "#ef5450"],
|
||||
chart: {
|
||||
height: 240,
|
||||
width: "100%",
|
||||
type: "donut",
|
||||
},
|
||||
stroke: {
|
||||
colors: ["transparent"],
|
||||
lineCap: "",
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
labels: [`Смотрю`, `В планах`, `Просмотрено`, `Отложено`, `Брошено`],
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 640,
|
||||
options: {
|
||||
chart: {
|
||||
height: 200,
|
||||
width: 200,
|
||||
type: "donut",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
useEffect(() => {
|
||||
if (
|
||||
document.getElementById("donut-chart") &&
|
||||
typeof ApexCharts !== "undefined"
|
||||
) {
|
||||
const chart = new ApexCharts(
|
||||
document.getElementById("donut-chart"),
|
||||
getChartOptions()
|
||||
);
|
||||
chart.render();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card className="font-light h-fit">
|
||||
<div className="flex justify-between">
|
||||
<h1 className="text-2xl font-bold">Статистика</h1>
|
||||
<Link href={`/profile/${props.profile_id}/bookmarks`}>
|
||||
<div className="flex items-center">
|
||||
<p className="hidden text-xl font-bold sm:block">Показать все</p>
|
||||
<span className="w-6 h-6 iconify mdi--arrow-right"></span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div>
|
||||
<p className="align-center whitespace-nowrap">
|
||||
<span className="inline-block rounded w-4 h-4 bg-[#66bb6c]"></span>{" "}
|
||||
Смотрю <span className="font-bold">{props.lists[0]}</span>
|
||||
</p>
|
||||
<p className="align-center whitespace-nowrap">
|
||||
<span className="inline-block rounded w-4 h-4 bg-[#b566bb]"></span>{" "}
|
||||
В планах <span className="font-bold">{props.lists[1]}</span>
|
||||
</p>
|
||||
<p className="align-center whitespace-nowrap">
|
||||
<span className="inline-block rounded w-4 h-4 bg-[#5c6cc0]"></span>{" "}
|
||||
Просмотрено <span className="font-bold">{props.lists[2]}</span>
|
||||
</p>
|
||||
<p className="align-center whitespace-nowrap">
|
||||
<span className="inline-block rounded w-4 h-4 bg-[#ffca28]"></span>{" "}
|
||||
Отложено <span className="font-bold">{props.lists[3]}</span>
|
||||
</p>
|
||||
<p className="align-center whitespace-nowrap">
|
||||
<span className="inline-block rounded w-4 h-4 bg-[#ef5450]"></span>{" "}
|
||||
Брошено <span className="font-bold">{props.lists[4]}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div id="donut-chart"></div>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
Просмотрено серий:{" "}
|
||||
<span className="font-bold">{props.watched_count}</span>
|
||||
</p>
|
||||
<p>
|
||||
Время просмотра:{" "}
|
||||
<span className="font-bold">
|
||||
~{minutesToTime(props.watched_time, "daysHours")}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
145
app/components/Profile/Profile.User.tsx
Normal file
145
app/components/Profile/Profile.User.tsx
Normal file
|
@ -0,0 +1,145 @@
|
|||
"use client";
|
||||
import { Avatar, Card, Button } from "flowbite-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Chip } from "../Chip/Chip";
|
||||
|
||||
export const ProfileUser = (props: {
|
||||
isOnline: boolean;
|
||||
avatar: string;
|
||||
login: string;
|
||||
status: string;
|
||||
socials: {
|
||||
isPrivate: boolean;
|
||||
hasSocials: boolean;
|
||||
socials: {
|
||||
name: string;
|
||||
nickname: any;
|
||||
icon: string;
|
||||
urlPrefix?: string | undefined;
|
||||
}[];
|
||||
};
|
||||
chips: {
|
||||
hasChips: boolean;
|
||||
isMyProfile: boolean;
|
||||
isVerified: boolean;
|
||||
isSponsor: boolean;
|
||||
isBlocked: boolean;
|
||||
roles?: {
|
||||
id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
}[];
|
||||
};
|
||||
rating: number;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<Card className="h-fit">
|
||||
{props.chips.hasChips && (
|
||||
<div className="flex gap-1 overflow-x-auto scrollbar-thin">
|
||||
{props.chips.isMyProfile && (
|
||||
<Chip bg_color="bg-blue-500" name="Мой профиль" />
|
||||
)}
|
||||
{props.chips.isVerified && (
|
||||
<Chip bg_color="bg-green-500" name="Верифицирован" />
|
||||
)}
|
||||
{props.chips.isSponsor && (
|
||||
<Chip bg_color="bg-yellow-500" name="Спонсор Anixart" />
|
||||
)}
|
||||
{props.chips.isBlocked && (
|
||||
<Chip bg_color="bg-red-500" name="Заблокирован" />
|
||||
)}
|
||||
{props.chips.roles &&
|
||||
props.chips.roles.length > 0 &&
|
||||
props.chips.roles.map((role: any) => (
|
||||
<Chip
|
||||
key={role.id}
|
||||
bg_color={`bg-[var(--role-color)]`}
|
||||
name={role.name}
|
||||
style={
|
||||
{
|
||||
"--role-color": `#${role.color}`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Avatar
|
||||
alt=""
|
||||
img={props.avatar}
|
||||
rounded={true}
|
||||
size={"lg"}
|
||||
className="relative flex-col items-center justify-center sm:justify-start sm:flex-row"
|
||||
bordered={true}
|
||||
color={props.isOnline ? "success" : "light"}
|
||||
>
|
||||
<div className="space-y-1 text-2xl font-medium whitespace-pre-wrap dark:text-white">
|
||||
<div className="text-center sm:text-left">
|
||||
{props.login}{" "}
|
||||
<span
|
||||
className={`border rounded-md px-2 py-1 text-sm ${
|
||||
props.rating > 0
|
||||
? "border-green-500 text-green-500"
|
||||
: "border-red-500 text-red-500"
|
||||
}`}
|
||||
>
|
||||
{props.rating}
|
||||
</span>
|
||||
</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>
|
||||
);
|
||||
};
|
102
app/components/Profile/Profile.WatchDynamic.tsx
Normal file
102
app/components/Profile/Profile.WatchDynamic.tsx
Normal file
|
@ -0,0 +1,102 @@
|
|||
import { Card } from "flowbite-react";
|
||||
import ApexCharts, { ApexOptions } from "apexcharts";
|
||||
import { useEffect } from "react";
|
||||
import { unixToDate } from "#/api/utils";
|
||||
export const ProfileWatchDynamic = (props: { watchDynamic: Array<any> }) => {
|
||||
const lastTenDays = props.watchDynamic.slice(
|
||||
Math.max(props.watchDynamic.length - 10, 0)
|
||||
);
|
||||
const data = {
|
||||
ids: lastTenDays.map((item) => item.id),
|
||||
counts: lastTenDays.map((item) => item.count),
|
||||
timestamps: lastTenDays.map((item) =>
|
||||
unixToDate(item.timestamp, "dayMonth")
|
||||
),
|
||||
};
|
||||
|
||||
const options: ApexOptions = {
|
||||
chart: {
|
||||
height: "100%",
|
||||
type: "area",
|
||||
fontFamily: "Inter, sans-serif",
|
||||
dropShadow: {
|
||||
enabled: false,
|
||||
},
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
x: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
type: "gradient",
|
||||
gradient: {
|
||||
opacityFrom: 0.55,
|
||||
opacityTo: 0,
|
||||
shade: "#1C64F2",
|
||||
gradientToColors: ["#1C64F2"],
|
||||
},
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false,
|
||||
},
|
||||
stroke: {
|
||||
width: 6,
|
||||
},
|
||||
grid: {
|
||||
show: true,
|
||||
strokeDashArray: 4,
|
||||
padding: {
|
||||
left: 2,
|
||||
right: 2,
|
||||
top: 0,
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "Серий",
|
||||
data: data.counts,
|
||||
color: "#1C64F2",
|
||||
},
|
||||
],
|
||||
xaxis: {
|
||||
categories: data.timestamps,
|
||||
labels: {
|
||||
show: false,
|
||||
},
|
||||
axisBorder: {
|
||||
show: false,
|
||||
},
|
||||
axisTicks: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
show: false,
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
document.getElementById("area-chart") &&
|
||||
typeof ApexCharts !== "undefined"
|
||||
) {
|
||||
const chart = new ApexCharts(
|
||||
document.getElementById("area-chart"),
|
||||
options
|
||||
);
|
||||
chart.render();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<h1 className="text-2xl font-bold">Динамика просмотра серий</h1>
|
||||
<div id="area-chart"></div>
|
||||
</Card>
|
||||
);
|
||||
};
|
28
app/components/Profile/ProfileBannedBanner.tsx
Normal file
28
app/components/Profile/ProfileBannedBanner.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { unixToDate } from "#/api/utils";
|
||||
|
||||
export const ProfileBannedBanner = (props: {
|
||||
is_banned: boolean;
|
||||
is_perm_banned: boolean;
|
||||
ban_reason: string;
|
||||
ban_expires: number;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{(props.is_banned || props.is_perm_banned) && (
|
||||
<div className="flex flex-col justify-between w-full p-4 border border-red-200 rounded-md md:flex-row bg-red-50 dark:bg-red-700 dark:border-red-600">
|
||||
<div className="mb-4 md:mb-0 md:me-4">
|
||||
<h2 className="mb-1 text-base font-semibold text-gray-900 dark:text-white">
|
||||
{props.is_perm_banned
|
||||
? "Пользователь был заблокирован администрацией навсегда"
|
||||
: `Пользователь был заблокирован администрацией до
|
||||
${unixToDate(props.ban_expires, "full")}`}
|
||||
</h2>
|
||||
<p className="flex items-center text-sm font-normal text-gray-500 dark:text-gray-200">
|
||||
{props.ban_reason}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -133,7 +133,7 @@ export const ReleaseInfoInfo = (props: {
|
|||
</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)
|
||||
unixToDate(props.aired_on_date, "full")
|
||||
) : props.year ? (
|
||||
<>
|
||||
{props.season && props.season != 0
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Link from "next/link";
|
||||
import { sinceUnixDate } from "#/api/utils";
|
||||
import { Chip } from "#/components/Chip/Chip";
|
||||
|
||||
const profile_lists = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue