Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions app/(pages)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { FC } from 'react';

import Block from '@/components/ui/block';
import UserCover from '@/components/user-cover';

import Edits from '@/features/dashboard/edits-block/edits.component';
import Moderation from '@/features/dashboard/moderation-log/moderation.component';

import { key, prefetchSession } from '@/services/hooks/auth/use-session';
import { prefetchTodoAnime } from '@/services/hooks/edit/todo/use-todo-anime';
import { prefetchEditList } from '@/services/hooks/edit/use-edit-list';
import { prefetchModerationLog } from '@/services/hooks/moderation/use-moderation-log';
import { EDIT_STATUSES } from '@/utils/constants';
import getQueryClient from '@/utils/get-query-client';

interface Props {}

const DashboardPage: FC<Props> = async () => {
const queryClient = await getQueryClient();

await prefetchSession();

const loggedUser: API.User = queryClient.getQueryData(key())!;

(Object.keys(EDIT_STATUSES) as API.EditStatus[]).map(async (status) => {
await prefetchEditList({ status });
});
await prefetchEditList({ author: loggedUser.username });

await prefetchModerationLog({});

await prefetchTodoAnime({ param: 'title_ua' });
await prefetchTodoAnime({ param: 'synopsis_ua' });

const dehydratedState = dehydrate(queryClient);

return (
<HydrationBoundary state={dehydratedState}>
<div className="flex flex-col gap-12">
<UserCover />
<Block className="flex flex-row">
<Edits />
<Moderation />
</Block>
</div>
</HydrationBoundary>
);
};

export default DashboardPage;
27 changes: 19 additions & 8 deletions features/common/navbar/profile-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import MaterialSymbolsMenuBookRounded from '~icons/material-symbols/menu-book-ro
import MaterialSymbolsPalette from '~icons/material-symbols/palette';
import MaterialSymbolsPerson from '~icons/material-symbols/person';
import MaterialSymbolsSettingsOutline from '~icons/material-symbols/settings-outline';
import MaterialSymbolsShieldPerson from '~icons/material-symbols/shield-person';

import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
Expand Down Expand Up @@ -55,15 +56,25 @@ const ProfileMenu = () => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<div className="-m-1 flex items-center gap-2 bg-secondary/30 p-1">
<Avatar className="size-9 rounded-md">
<AvatarImage src={loggedUser.avatar} alt="pfp" />
</Avatar>
<div className="flex flex-col">
<Label className="truncate">
{loggedUser.username}
</Label>
<div className="-m-1 flex items-center justify-between bg-secondary/30 p-1">
<div className="flex items-center gap-2">
<Avatar className="size-9 rounded-md">
<AvatarImage src={loggedUser.avatar} alt="pfp" />
</Avatar>
<div className="flex flex-col">
<Label className="truncate">
{loggedUser.username}
</Label>
</div>
</div>
{(loggedUser.role === 'moderator' ||
loggedUser.role === 'admin') && (
<Button variant="outline" size="icon-sm">
<Link href="/dashboard">
<MaterialSymbolsShieldPerson className="text-[#ffc9c9]" />
</Link>
</Button>
)}
</div>
<DropdownMenuSeparator />
<DropdownMenuGroup>
Expand Down
94 changes: 94 additions & 0 deletions features/dashboard/components/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import clsx from 'clsx';
import Link from 'next/link';
import { PropsWithChildren, ReactNode, memo } from 'react';
import MaterialSymbolsArrowRightAltRounded from '~icons/material-symbols/arrow-right-alt-rounded';

import H1 from '@/components/typography/h1';
import H2 from '@/components/typography/h2';
import H3 from '@/components/typography/h3';
import H4 from '@/components/typography/h4';
import H5 from '@/components/typography/h5';
import { Button } from '@/components/ui/button';

interface Props extends PropsWithChildren {
title: string | ReactNode;
href?: string;
onClick?: () => void;
variant?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5';
className?: string;
titleClassName?: string;
}

const Header = ({
title,
href,
onClick,
variant,
children,
className,
titleClassName,
}: Props) => {
const getTitle = () => {
switch (variant) {
case 'h1':
return H1;
case 'h2':
return H2;
case 'h3':
return H3;
case 'h4':
return H4;
case 'h5':
return H5;
default:
return H3;
}
};

const Title = getTitle();

return (
<div
className={clsx(
'flex items-center justify-between gap-2',
className,
)}
>
<div
className={clsx(
'flex items-center gap-4 overflow-hidden',
titleClassName,
)}
>
{href ? (
<Link href={href} className="hover:underline">
<Title>{title}</Title>
</Link>
) : onClick ? (
<button onClick={onClick} className="hover:underline">
<Title>{title}</Title>
</button>
) : (
<Title>{title}</Title>
)}
</div>
<div className="flex gap-2">
{children}
{href && (
<Button
size="icon-sm"
variant="outline"
className="backdrop-blur"
asChild
>
<Link href={href}>
<MaterialSymbolsArrowRightAltRounded className="text-lg" />
</Link>
</Button>
)}
</div>
</div>
);
};

export default memo(Header);
56 changes: 56 additions & 0 deletions features/dashboard/components/target-type.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client';

import { useSearchParams } from 'next/navigation';
import { FC } from 'react';

import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectList,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';

import { MODERATION_TYPES } from '@/utils/constants';

import useChangeParam from './use-change-params';

interface Props {
targetTypes: API.ModerationType[];
}

const TargetType: FC<Props> = ({ targetTypes }) => {
const searchParams = useSearchParams()!;

const target_type = searchParams.get('target_type');

const handleChangeParam = useChangeParam();

return (
<Select
value={target_type ? [target_type] : undefined}
onValueChange={(value) =>
handleChangeParam('target_type', value[0])
}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="Тип модерації" />
</SelectTrigger>
<SelectContent>
<SelectList>
<SelectGroup>
{targetTypes.map((item) => (
<SelectItem key={item} value={item}>
{MODERATION_TYPES[item].title_ua}
</SelectItem>
))}
</SelectGroup>
</SelectList>
</SelectContent>
</Select>
);
};

export default TargetType;
25 changes: 25 additions & 0 deletions features/dashboard/components/use-change-params.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { usePathname, useRouter, useSearchParams } from 'next/navigation';

import createQueryString from '@/utils/create-query-string';

const useChangeParam = () => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams()!;

const handleChangeParam = (
name: string,
value: string | string[] | boolean,
) => {
const query = createQueryString(
name,
value,
new URLSearchParams(searchParams),
);
router.replace(`${pathname}?${query}`);
};

return handleChangeParam;
};

export default useChangeParam;
81 changes: 81 additions & 0 deletions features/dashboard/components/user.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use client';

import { useSearchParams } from 'next/navigation';
import { FC, useState } from 'react';

import {
Select,
SelectContent,
SelectEmpty,
SelectGroup,
SelectItem,
SelectList,
SelectSearch,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';

import useUsers from '@/services/hooks/user/use-users';

import useChangeParam from './use-change-params';

interface Props {
className?: string;
}

const ModerationAuthor: FC<Props> = ({ className }) => {
const searchParams = useSearchParams()!;
const [userSearch, setUserSearch] = useState<string>();
const { data: users, isFetching: isUsersFetching } = useUsers({
query: userSearch,
});

const author = searchParams.get('author');

const handleChangeParam = useChangeParam();

const handleUserSearch = (keyword: string) => {
if (keyword.length < 3) {
setUserSearch(undefined);
return;
}

setUserSearch(keyword);
};

return (
<Select
value={author !== null ? [author] : []}
onValueChange={(value) => handleChangeParam('author', value[0])}
onOpenChange={() => setUserSearch(undefined)}
onSearch={handleUserSearch}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="Користувач" />
</SelectTrigger>
<SelectContent align="end">
<SelectSearch placeholder="Імʼя користувача..." />
<SelectList>
<SelectGroup>
{!isUsersFetching &&
users?.map((item) => (
<SelectItem
key={item.username}
value={item.username}
>
{item.username}
</SelectItem>
))}
<SelectEmpty>
{isUsersFetching
? 'Завантаження...'
: 'Користувачів не знайдено'}
</SelectEmpty>
</SelectGroup>
</SelectList>
</SelectContent>
</Select>
);
};

export default ModerationAuthor;
36 changes: 36 additions & 0 deletions features/dashboard/edits-block/card-item.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Link from 'next/link';
import { FC } from 'react';
import MaterialSymbolsArrowForwardIosRounded from '~icons/material-symbols/arrow-forward-ios-rounded';

interface Props {
title: string;
href: string;
text?: number;
icon?: React.ReactNode;
}

const CardItem: FC<Props> = ({ title, href, text, icon }) => {
return (
<Link
className="flex justify-between transition hover:scale-[1.02]"
href={href}
>
<div className="flex items-center gap-2">
{icon && (
<div className="flex size-6 items-center justify-center rounded-sm bg-secondary/60 p-1">
{icon}
</div>
)}
<h5 className="font-display text-sm font-medium text-muted-foreground">
{title}
</h5>
</div>
<div className="flex items-center gap-2">
<h5 className="font-display text-sm font-medium">{text}</h5>
<MaterialSymbolsArrowForwardIosRounded className="size-2 text-muted-foreground" />
</div>
</Link>
);
};

export default CardItem;
Loading