Merge branch 'refactor__ProfilePage' into V3

This commit is contained in:
Kentai Radiquum 2024-08-27 16:29:24 +05:00
commit c762c4a940
Signed by: Radiquum
GPG key ID: 858E8EE696525EED
23 changed files with 1125 additions and 275 deletions

View file

@ -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

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

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

View 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>
)}
</>
);
};

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

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

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

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

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

View 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>
)}
</>
);
};

View file

@ -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

View file

@ -1,5 +1,4 @@
import Link from "next/link";
import { sinceUnixDate } from "#/api/utils";
import { Chip } from "#/components/Chip/Chip";
const profile_lists = {