pages
This commit is contained in:
BIN
src/assets/icons/groups/book.png
Normal file
BIN
src/assets/icons/groups/book.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
3
src/assets/icons/groups/chevron-down.svg
Normal file
3
src/assets/icons/groups/chevron-down.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 10L12.0008 14.58L17 10" stroke="#EDF6F7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 222 B |
3
src/assets/icons/groups/edit.svg
Normal file
3
src/assets/icons/groups/edit.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.7992 19.5516H19.7992M4.19922 19.5516L8.5652 18.6719C8.79698 18.6252 9.0098 18.5111 9.17694 18.3438L18.9506 8.5648C19.4192 8.09594 19.4189 7.33595 18.9499 6.86749L16.8795 4.79942C16.4107 4.33115 15.6511 4.33147 15.1827 4.80013L5.40798 14.5802C5.24117 14.7471 5.12727 14.9595 5.08052 15.1908L4.19922 19.5516Z" stroke="#EDF6F7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 507 B |
3
src/assets/icons/groups/eye-closed.svg
Normal file
3
src/assets/icons/groups/eye-closed.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.4004 19.5L5.40039 4.5M10.2004 10.4416C9.82697 10.8533 9.60039 11.394 9.60039 11.9863C9.60039 13.2761 10.6749 14.3217 12.0004 14.3217C12.6115 14.3217 13.1693 14.0994 13.593 13.7334M20.4392 14.3217C21.2654 13.0848 21.6004 12.0761 21.6004 12.0761C21.6004 12.0761 19.4158 5.1 12.0004 5.1C11.5841 5.1 11.1843 5.12199 10.8004 5.16349M17.4004 17.3494C16.023 18.2281 14.2497 18.8495 12.0004 18.8127C4.67731 18.693 2.40039 12.0761 2.40039 12.0761C2.40039 12.0761 3.45825 8.69808 6.60039 6.64332" stroke="#EDF6F7" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 662 B |
BIN
src/assets/icons/groups/eye-open.png
Normal file
BIN
src/assets/icons/groups/eye-open.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 662 B |
8
src/assets/icons/groups/index.ts
Normal file
8
src/assets/icons/groups/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import Book from "./book.png"
|
||||
import EyeClosed from "./eye-closed.svg";
|
||||
import EyeOpen from "./eye-open.png";
|
||||
import Edit from "./edit.svg";
|
||||
import UserAdd from "./user-profile-add.svg";
|
||||
import ChevroneDown from "./chevron-down.svg"
|
||||
|
||||
export {Book, Edit, EyeClosed, EyeOpen, UserAdd, ChevroneDown}
|
||||
3
src/assets/icons/groups/user-profile-add.svg
Normal file
3
src/assets/icons/groups/user-profile-add.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.40039 21.5999C1.40033 22.1522 1.84799 22.6 2.40028 22.6C2.95256 22.6001 3.40033 22.1524 3.40039 21.6001L2.40039 21.6L1.40039 21.5999ZM2.4008 17.9996L3.4008 17.9997L2.4008 17.9996ZM12.6004 15.4C13.1527 15.4 13.6004 14.9523 13.6004 14.4C13.6004 13.8477 13.1527 13.4 12.6004 13.4V14.4V15.4ZM21.6004 16.6C22.1527 16.6 22.6004 16.1523 22.6004 15.6C22.6004 15.0477 22.1527 14.6 21.6004 14.6V15.6V16.6ZM16.2004 14.6C15.6481 14.6 15.2004 15.0477 15.2004 15.6C15.2004 16.1523 15.6481 16.6 16.2004 16.6V15.6V14.6ZM17.9004 18.2999C17.9004 18.8522 18.3481 19.2999 18.9004 19.2999C19.4527 19.2999 19.9004 18.8522 19.9004 18.2999H18.9004H17.9004ZM19.9004 12.8999C19.9004 12.3476 19.4527 11.8999 18.9004 11.8999C18.3481 11.8999 17.9004 12.3476 17.9004 12.8999H18.9004H19.9004ZM14.4004 6.00002H13.4004C13.4004 7.43596 12.2363 8.60002 10.8004 8.60002V9.60002V10.6C13.3409 10.6 15.4004 8.54053 15.4004 6.00002H14.4004ZM10.8004 9.60002V8.60002C9.36445 8.60002 8.20039 7.43596 8.20039 6.00002H7.20039H6.20039C6.20039 8.54053 8.25988 10.6 10.8004 10.6V9.60002ZM7.20039 6.00002H8.20039C8.20039 4.56408 9.36445 3.40002 10.8004 3.40002V2.40002V1.40002C8.25988 1.40002 6.20039 3.45951 6.20039 6.00002H7.20039ZM10.8004 2.40002V3.40002C12.2363 3.40002 13.4004 4.56408 13.4004 6.00002H14.4004H15.4004C15.4004 3.45951 13.3409 1.40002 10.8004 1.40002V2.40002ZM2.40039 21.6L3.40039 21.6001L3.4008 17.9997L2.4008 17.9996L1.4008 17.9995L1.40039 21.5999L2.40039 21.6ZM6.00079 14.4V13.4C3.46049 13.4 1.40108 15.4592 1.4008 17.9995L2.4008 17.9996L3.4008 17.9997C3.40096 16.5639 4.56497 15.4 6.00079 15.4V14.4ZM6.00079 14.4V15.4H12.6004V14.4V13.4H6.00079V14.4ZM21.6004 15.6V14.6H18.9004V15.6V16.6H21.6004V15.6ZM18.9004 15.6V14.6H16.2004V15.6V16.6H18.9004V15.6ZM18.9004 18.2999H19.9004V15.6H18.9004H17.9004V18.2999H18.9004ZM18.9004 15.6H19.9004V12.8999H18.9004H17.9004V15.6H18.9004Z" fill="#EDF6F7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -8,6 +8,8 @@ import { useEffect } from "react";
|
||||
import { fetchWhoAmI } from "../redux/slices/auth";
|
||||
import Problems from "../views/home/problems/Problems";
|
||||
import Articles from "../views/home/articles/Articles";
|
||||
import Groups from "../views/home/groups/Groups";
|
||||
import Contests from "../views/home/contests/Contests";
|
||||
|
||||
const Home = () => {
|
||||
const name = useAppSelector((state) => state.auth.username);
|
||||
@@ -31,6 +33,8 @@ const Home = () => {
|
||||
<Route path="register" element={<Register />} />
|
||||
<Route path="problems/*" element={<Problems/>} />
|
||||
<Route path="articles/*" element={<Articles/>} />
|
||||
<Route path="groups/*" element={<Groups/>} />
|
||||
<Route path="contests/*" element={<Contests/>} />
|
||||
<Route path="*" element={name} />
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { Logo } from "../../../assets/logos";
|
||||
import { Account, Clipboard, Cup, Home, Openbook, Users } from "../../../assets/icons/menu";
|
||||
// import MenuItem from "./MenuItem";
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { IconError, IconSuccess } from "../../../assets/icons/problems";
|
||||
|
||||
export interface ArticleItemProps {
|
||||
id: number;
|
||||
@@ -10,17 +6,6 @@ export interface ArticleItemProps {
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export function formatMilliseconds(ms: number): string {
|
||||
const rounded = Math.round(ms) / 1000;
|
||||
const formatted = rounded.toString().replace(/\.?0+$/, '');
|
||||
return `${formatted} c`;
|
||||
}
|
||||
|
||||
export function formatBytesToMB(bytes: number): string {
|
||||
const megabytes = Math.floor(bytes / (1024 * 1024));
|
||||
return `${megabytes} МБ`;
|
||||
}
|
||||
|
||||
const ArticleItem: React.FC<ArticleItemProps> = ({
|
||||
id, name, tags
|
||||
}) => {
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import { useEffect } from "react";
|
||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
||||
import ArticleItem, { ArticleItemProps } from "./ArticleItem";
|
||||
import { useAppDispatch } from "../../../redux/hooks";
|
||||
import ArticleItem from "./ArticleItem";
|
||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
||||
|
||||
|
||||
export interface Problem {
|
||||
export interface Article {
|
||||
id: number;
|
||||
authorId: number;
|
||||
name: string;
|
||||
difficulty: "Easy" | "Medium" | "Hard";
|
||||
tags: string[];
|
||||
timeLimit: number;
|
||||
memoryLimit: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
|
||||
const Articles = () => {
|
||||
|
||||
const articles: ArticleItemProps[] = [
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const articles: Article[] = [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Todo List App",
|
||||
@@ -130,6 +129,9 @@ const Articles = () => {
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage("articles"))
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className=" h-full w-full box-border p-[20px] pt-[20px]">
|
||||
@@ -137,7 +139,7 @@ const Articles = () => {
|
||||
|
||||
<div className="relative flex items-center mb-[20px]">
|
||||
<div className="h-[50px] text-[40px] font-bold text-liquid-white flex items-center">
|
||||
База статей
|
||||
Статьи
|
||||
</div>
|
||||
<SecondaryButton
|
||||
onClick={() => { }}
|
||||
|
||||
@@ -26,6 +26,9 @@ const Login = () => {
|
||||
// После успешного логина
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage("account"))
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (jwt) {
|
||||
navigate("/home/offices"); // или другая страница после входа
|
||||
}
|
||||
@@ -58,8 +61,8 @@ const Login = () => {
|
||||
</div>
|
||||
|
||||
|
||||
<Input name="login" autocomplete="login" className="mt-[10px]" type="text" label="Логин" onChange={(v) => {setUsername(v)}} placeholder="login"/>
|
||||
<Input name="password" autocomplete="password" className="mt-[10px]" type="password" label="Пароль" onChange={(v) => {setPassword(v)}} placeholder="abCD1234" />
|
||||
<Input name="login" autocomplete="login" className="mt-[10px]" type="text" label="Логин" onChange={(v) => { setUsername(v) }} placeholder="login" />
|
||||
<Input name="password" autocomplete="password" className="mt-[10px]" type="password" label="Пароль" onChange={(v) => { setPassword(v) }} placeholder="abCD1234" />
|
||||
|
||||
<div className="flex justify-end mt-[10px]">
|
||||
<Link
|
||||
@@ -79,7 +82,7 @@ const Login = () => {
|
||||
/>
|
||||
<SecondaryButton
|
||||
className="w-full"
|
||||
onClick={() => {}}
|
||||
onClick={() => { }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<img src={googleLogo} className="h-[24px] w-[24px] mr-[15px]" />
|
||||
|
||||
@@ -26,8 +26,12 @@ const Register = () => {
|
||||
const { status, error, jwt } = useAppSelector((state) => state.auth);
|
||||
|
||||
// После успешной регистрации — переход в систему
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage("account"))
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage("account"));
|
||||
if (jwt) {
|
||||
navigate("/home");
|
||||
}
|
||||
|
||||
72
src/views/home/contests/ContestItem.tsx
Normal file
72
src/views/home/contests/ContestItem.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { cn } from "../../../lib/cn";
|
||||
|
||||
export interface ContestItemProps {
|
||||
id: number;
|
||||
name: string;
|
||||
authors: string[];
|
||||
startAt: string;
|
||||
registerAt: string;
|
||||
duration: number;
|
||||
members: number;
|
||||
statusRegister: "reg" | "nonreg";
|
||||
type: "first" | "second";
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const year = date.getFullYear();
|
||||
|
||||
const hours = date.getHours().toString().padStart(2, "0");
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||
|
||||
return `${day}/${month}/${year}\n${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ContestItem: React.FC<ContestItemProps> = ({
|
||||
id, name, authors, startAt, registerAt, duration, members, statusRegister, type
|
||||
}) => {
|
||||
const now = new Date();
|
||||
|
||||
const waitTime = new Date(startAt).getTime() - now.getTime();
|
||||
|
||||
return (
|
||||
<div className={cn("w-full box-border relative rounded-[10px] px-[20px] py-[10px] text-liquid-white",
|
||||
waitTime <= 0 ? "grid grid-cols-6" : "grid grid-cols-7",
|
||||
"items-center font-bold text-liquid-white",
|
||||
type == "first" ? " bg-liquid-lighter" : " bg-liquid-background"
|
||||
)}>
|
||||
<div className="text-left">
|
||||
{name}
|
||||
</div>
|
||||
<div className="text-center text-liquid-brightmain font-normal">
|
||||
{authors.map((v, i) => <p key={i}>{v}</p>)}
|
||||
</div>
|
||||
<div className="text-center text-nowrap">
|
||||
{formatDate(startAt)}
|
||||
</div>
|
||||
<div className="text-center">
|
||||
{duration}
|
||||
</div>
|
||||
{
|
||||
waitTime > 0 &&
|
||||
<div className="text-center">
|
||||
{waitTime}
|
||||
</div>
|
||||
}
|
||||
<div className="text-center">
|
||||
{members}
|
||||
</div>
|
||||
<div className="text-center">
|
||||
{statusRegister}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContestItem;
|
||||
131
src/views/home/contests/Contests.tsx
Normal file
131
src/views/home/contests/Contests.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useEffect } from "react";
|
||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { useAppDispatch } from "../../../redux/hooks";
|
||||
import ContestsBlock from "./ContestsBlock";
|
||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
||||
|
||||
|
||||
interface Contest {
|
||||
id: number;
|
||||
name: string;
|
||||
authors: string[];
|
||||
startAt: string;
|
||||
registerAt: string;
|
||||
duration: number;
|
||||
members: number;
|
||||
statusRegister: "reg" | "nonreg";
|
||||
}
|
||||
|
||||
|
||||
|
||||
const Contests = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const now = new Date();
|
||||
const contests: Contest[] = [
|
||||
// === Прошедшие контесты ===
|
||||
{
|
||||
id: 1,
|
||||
name: "Code Marathon 2025",
|
||||
authors: ["tourist", "Petr", "Semen", "Rotar"],
|
||||
startAt: "2025-09-15T10:00:00.000Z",
|
||||
registerAt: "2025-09-10T10:00:00.000Z",
|
||||
duration: 180,
|
||||
members: 4821,
|
||||
statusRegister: "reg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Autumn Cup 2025",
|
||||
authors: ["awoo", "Benq"],
|
||||
startAt: "2025-09-25T17:00:00.000Z",
|
||||
registerAt: "2025-09-20T17:00:00.000Z",
|
||||
duration: 150,
|
||||
members: 3670,
|
||||
statusRegister: "nonreg",
|
||||
},
|
||||
|
||||
// === Контесты, которые сейчас идут ===
|
||||
{
|
||||
id: 3,
|
||||
name: "Halloween Challenge",
|
||||
authors: ["Errichto", "Radewoosh"],
|
||||
startAt: "2025-10-29T10:00:00.000Z", // начался сегодня
|
||||
registerAt: "2025-10-25T10:00:00.000Z",
|
||||
duration: 240,
|
||||
members: 5123,
|
||||
statusRegister: "reg",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "October Blitz",
|
||||
authors: ["neal", "Um_nik"],
|
||||
startAt: "2025-10-29T12:00:00.000Z",
|
||||
registerAt: "2025-10-24T12:00:00.000Z",
|
||||
duration: 300,
|
||||
members: 2890,
|
||||
statusRegister: "nonreg",
|
||||
},
|
||||
|
||||
// === Контесты, которые еще не начались ===
|
||||
{
|
||||
id: 5,
|
||||
name: "Winter Warmup",
|
||||
authors: ["tourist", "rng_58"],
|
||||
startAt: "2025-11-05T18:00:00.000Z",
|
||||
registerAt: "2025-11-01T18:00:00.000Z",
|
||||
duration: 180,
|
||||
members: 2100,
|
||||
statusRegister: "reg",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Global Coding Cup",
|
||||
authors: ["maroonrk", "kostka"],
|
||||
startAt: "2025-11-12T15:00:00.000Z",
|
||||
registerAt: "2025-11-08T15:00:00.000Z",
|
||||
duration: 240,
|
||||
members: 1520,
|
||||
statusRegister: "nonreg",
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage("contests"))
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className=" h-full w-[calc(100%+250px)] box-border p-[20px] pt-[20p]">
|
||||
<div className="h-full box-border">
|
||||
|
||||
<div className="relative flex items-center mb-[20px]">
|
||||
<div className={cn("h-[50px] text-[40px] font-bold text-liquid-white flex items-center")}>
|
||||
Контесты
|
||||
</div>
|
||||
<SecondaryButton
|
||||
onClick={() => { }}
|
||||
text="Создать группу"
|
||||
className="absolute right-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-liquid-lighter h-[50px] mb-[20px]">
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<ContestsBlock className="mb-[20px]" title="Текущие" contests={contests.filter(contest => {
|
||||
const endTime = new Date(contest.startAt).getTime() + contest.duration * 60 * 1000;
|
||||
return endTime >= now.getTime();
|
||||
})} />
|
||||
<ContestsBlock className="mb-[20px]" title="Прошедшие" contests={contests.filter(contest => {
|
||||
const endTime = new Date(contest.startAt).getTime() + contest.duration * 60 * 1000;
|
||||
return endTime < now.getTime();
|
||||
})} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Contests;
|
||||
64
src/views/home/contests/ContestsBlock.tsx
Normal file
64
src/views/home/contests/ContestsBlock.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useState, FC } from "react";
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { ChevroneDown } from "../../../assets/icons/groups";
|
||||
import ContestItem from "./ContestItem";
|
||||
|
||||
|
||||
interface Contest {
|
||||
id: number;
|
||||
name: string;
|
||||
authors: string[];
|
||||
startAt: string;
|
||||
registerAt: string;
|
||||
duration: number;
|
||||
members: number;
|
||||
statusRegister: "reg" | "nonreg";
|
||||
}
|
||||
|
||||
interface GroupsBlockProps {
|
||||
contests: Contest[];
|
||||
title: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
||||
const GroupsBlock: FC<GroupsBlockProps> = ({ contests, title, className }) => {
|
||||
|
||||
|
||||
const [active, setActive] = useState<boolean>(title != "Скрытые");
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div className={cn(" border-b-[1px] border-b-liquid-lighter rounded-[10px]",
|
||||
className
|
||||
)}>
|
||||
<div className={cn(" h-[40px] text-[24px] font-bold flex gap-[10px] items-center cursor-pointer border-b-[1px] border-b-transparent transition-all duration-300",
|
||||
active && "border-b-liquid-lighter"
|
||||
)}
|
||||
onClick={() => {
|
||||
console.log(active);
|
||||
setActive(!active)
|
||||
}}>
|
||||
<span>{title}</span>
|
||||
<img src={ChevroneDown} className={cn("transition-all duration-300",
|
||||
active && "rotate-180"
|
||||
)} />
|
||||
</div>
|
||||
<div className={cn(" grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-300",
|
||||
active && "grid-rows-[1fr] opacity-100"
|
||||
)}>
|
||||
<div className="overflow-hidden">
|
||||
<div className="pb-[10px] pt-[20px]">
|
||||
{
|
||||
contests.map((v, i) => <ContestItem key={i} {...v} type={i % 2 ? "second" : "first"} />)
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupsBlock;
|
||||
59
src/views/home/groups/GroupItem.tsx
Normal file
59
src/views/home/groups/GroupItem.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { Book, UserAdd, Edit, EyeClosed, EyeOpen } from "../../../assets/icons/groups";
|
||||
|
||||
export interface GroupItemProps {
|
||||
id: number;
|
||||
role: "menager" | "member" | "owner" | "viewer";
|
||||
visible: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
|
||||
interface IconComponentProps {
|
||||
src: string;
|
||||
}
|
||||
|
||||
const IconComponent: React.FC<IconComponentProps> = ({
|
||||
src
|
||||
}) => {
|
||||
|
||||
return <img
|
||||
src={src}
|
||||
className="hover:bg-liquid-light rounded-[5px] cursor-pointer transition-all duration-300"
|
||||
/>
|
||||
}
|
||||
|
||||
const GroupItem: React.FC<GroupItemProps> = ({
|
||||
id, name, visible, role
|
||||
}) => {
|
||||
console.log(id);
|
||||
return (
|
||||
<div className={cn("w-full h-[120px] box-border relative rounded-[10px] p-[10px] text-liquid-white bg-liquid-lighter",
|
||||
)}>
|
||||
<div className="grid grid-cols-[100px,1fr] gap-[20px]">
|
||||
<img src={Book} className="bg-liquid-brightmain rounded-[10px]"/>
|
||||
<div className="grid grid-flow-row grid-rows-[1fr,24px]">
|
||||
<div className="text-[18px] font-bold">
|
||||
{name}
|
||||
</div>
|
||||
<div className=" flex gap-[10px]">
|
||||
{
|
||||
(role == "menager" || role == "owner") && <IconComponent src={UserAdd}/>
|
||||
}
|
||||
{
|
||||
(role == "menager" || role == "owner") && <IconComponent src={Edit}/>
|
||||
}
|
||||
{
|
||||
visible == false && <IconComponent src={EyeOpen} />
|
||||
}
|
||||
{
|
||||
visible == true && <IconComponent src={EyeClosed} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupItem;
|
||||
71
src/views/home/groups/Groups.tsx
Normal file
71
src/views/home/groups/Groups.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useEffect } from "react";
|
||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { useAppDispatch } from "../../../redux/hooks";
|
||||
import GroupsBlock from "./GroupsBlock";
|
||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
||||
|
||||
|
||||
export interface Group {
|
||||
id: number;
|
||||
role: "menager" | "member" | "owner" | "viewer";
|
||||
visible: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
|
||||
const Groups = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const groups: Group[] = [
|
||||
{ id: 1, role: "owner", name: "Main Administration", visible: true },
|
||||
{ id: 2, role: "menager", name: "Project Managers", visible: true },
|
||||
{ id: 3, role: "member", name: "Developers", visible: true },
|
||||
{ id: 4, role: "viewer", name: "QA Viewers", visible: true },
|
||||
{ id: 5, role: "member", name: "Design Team", visible: true },
|
||||
{ id: 6, role: "owner", name: "Executive Board", visible: true },
|
||||
{ id: 7, role: "menager", name: "HR Managers", visible: true },
|
||||
{ id: 8, role: "viewer", name: "Marketing Reviewers", visible: false },
|
||||
{ id: 9, role: "member", name: "Content Creators", visible: false },
|
||||
{ id: 10, role: "menager", name: "Support Managers", visible: true },
|
||||
{ id: 11, role: "viewer", name: "External Auditors", visible: false },
|
||||
{ id: 12, role: "member", name: "Frontend Developers", visible: true },
|
||||
{ id: 13, role: "member", name: "Backend Developers", visible: true },
|
||||
{ id: 14, role: "viewer", name: "Guest Access", visible: false },
|
||||
{ id: 15, role: "menager", name: "Operations", visible: true },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage("groups"))
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className=" h-full w-[calc(100%+250px)] box-border p-[20px] pt-[20p]">
|
||||
<div className="h-full box-border">
|
||||
|
||||
<div className="relative flex items-center mb-[20px]">
|
||||
<div className={cn("h-[50px] text-[40px] font-bold text-liquid-white flex items-center")}>
|
||||
Группы
|
||||
</div>
|
||||
<SecondaryButton
|
||||
onClick={() => { }}
|
||||
text="Создать группу"
|
||||
className="absolute right-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-liquid-lighter h-[50px] mb-[20px]">
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<GroupsBlock className="mb-[20px]" title="Управляемые" groups={groups.filter((v) => v.visible && (v.role == "owner" || v.role == "menager"))} />
|
||||
<GroupsBlock className="mb-[20px]" title="Текущие" groups={groups.filter((v) => v.visible && (v.role == "member" || v.role == "viewer"))} />
|
||||
<GroupsBlock className="mb-[20px]" title="Скрытые" groups={groups.filter((v) => v.visible == false)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Groups;
|
||||
60
src/views/home/groups/GroupsBlock.tsx
Normal file
60
src/views/home/groups/GroupsBlock.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useState, FC } from "react";
|
||||
import GroupItem from "./GroupItem";
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { ChevroneDown } from "../../../assets/icons/groups";
|
||||
|
||||
|
||||
export interface Group {
|
||||
id: number;
|
||||
role: "menager" | "member" | "owner" | "viewer";
|
||||
visible: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface GroupsBlockProps {
|
||||
groups: Group[];
|
||||
title: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
|
||||
const GroupsBlock: FC<GroupsBlockProps> = ({ groups, title, className }) => {
|
||||
|
||||
|
||||
const [active, setActive] = useState<boolean>(title != "Скрытые");
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div className={cn(" border-b-[1px] border-b-liquid-lighter rounded-[10px]",
|
||||
className
|
||||
)}>
|
||||
<div className={cn(" h-[40px] text-[24px] font-bold flex gap-[10px] border-b-[1px] border-b-transparent items-center cursor-pointer transition-all duration-300",
|
||||
active && " border-b-liquid-lighter"
|
||||
)}
|
||||
onClick={() => {
|
||||
console.log(active);
|
||||
setActive(!active)
|
||||
}}>
|
||||
<span>{title}</span>
|
||||
<img src={ChevroneDown} className={cn("transition-all duration-300",
|
||||
active && "rotate-180"
|
||||
)}/>
|
||||
</div>
|
||||
<div className={cn(" grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-300",
|
||||
active && "grid-rows-[1fr] opacity-100"
|
||||
)}>
|
||||
<div className="overflow-hidden">
|
||||
|
||||
<div className="grid grid-cols-3 gap-[20px] pt-[20px] pb-[20px] box-border">
|
||||
{
|
||||
groups.map((v, i) => <GroupItem key={i} {...v} />)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupsBlock;
|
||||
@@ -6,10 +6,10 @@ import { useAppSelector } from "../../../redux/hooks";
|
||||
const Menu = () => {
|
||||
const menuItems = [
|
||||
{text: "Главная", href: "/home", icon: Home, page: "home" },
|
||||
{text: "Задачи", href: "/home/problems", icon: Clipboard, page: "clipboard" },
|
||||
{text: "Статьи", href: "/home/articles", icon: Openbook, page: "openbool" },
|
||||
{text: "Группы", href: "/home", icon: Users, page: "users" },
|
||||
{text: "Контесты", href: "/home", icon: Cup, page: "cup" },
|
||||
{text: "Задачи", href: "/home/problems", icon: Clipboard, page: "problems" },
|
||||
{text: "Статьи", href: "/home/articles", icon: Openbook, page: "articles" },
|
||||
{text: "Группы", href: "/home/groups", icon: Users, page: "groups" },
|
||||
{text: "Контесты", href: "/home/contests", icon: Cup, page: "contests" },
|
||||
{text: "Аккаунт", href: "/home/account", icon: Account, page: "account" },
|
||||
];
|
||||
const activePage = useAppSelector((state) => state.store.menu.activePage);
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { Logo } from "../../../assets/logos";
|
||||
import { Account, Clipboard, Cup, Home, Openbook, Users } from "../../../assets/icons/menu";
|
||||
// import MenuItem from "./MenuItem";
|
||||
import { cn } from "../../../lib/cn";
|
||||
import { IconError, IconSuccess } from "../../../assets/icons/problems";
|
||||
|
||||
@@ -30,7 +27,7 @@ export function formatBytesToMB(bytes: number): string {
|
||||
}
|
||||
|
||||
const ProblemItem: React.FC<ProblemItemProps> = ({
|
||||
id, authorId, name, difficulty, tags, timeLimit, memoryLimit, createdAt, updatedAt, type, status
|
||||
id, name, difficulty, timeLimit, memoryLimit, type, status
|
||||
}) => {
|
||||
console.log(id);
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Logo } from "../../../assets/logos";
|
||||
import { Account, Clipboard, Cup, Home, Openbook, Users } from "../../../assets/icons/menu";
|
||||
// import MenuItem from "./MenuItem";
|
||||
import { useAppSelector } from "../../../redux/hooks";
|
||||
import ProblemItem from "./ProblemItem";
|
||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
||||
import { useAppDispatch } from "../../../redux/hooks";
|
||||
import { useEffect } from "react";
|
||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
||||
|
||||
|
||||
export interface Problem {
|
||||
@@ -21,6 +20,8 @@ export interface Problem {
|
||||
|
||||
const Problems = () => {
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const problems: Problem[] = [
|
||||
{
|
||||
"id": 1,
|
||||
@@ -464,6 +465,9 @@ const Problems = () => {
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage("problems"))
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className=" h-full w-full box-border p-[20px] pt-[20px]">
|
||||
@@ -471,7 +475,7 @@ const Problems = () => {
|
||||
|
||||
<div className="relative flex items-center mb-[20px]">
|
||||
<div className="h-[50px] text-[40px] font-bold text-liquid-white flex items-center">
|
||||
База задач
|
||||
Задачи
|
||||
</div>
|
||||
<SecondaryButton
|
||||
onClick={() => {}}
|
||||
|
||||
Reference in New Issue
Block a user