formatting
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import Balloon from "./balloon.svg";
|
import Balloon from './balloon.svg';
|
||||||
import Account from "./account.svg"
|
import Account from './account.svg';
|
||||||
|
|
||||||
export {Balloon, Account};
|
export { Balloon, Account };
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import Book from "./book.png"
|
import Book from './book.png';
|
||||||
import EyeClosed from "./eye-closed.svg";
|
import EyeClosed from './eye-closed.svg';
|
||||||
import EyeOpen from "./eye-open.png";
|
import EyeOpen from './eye-open.png';
|
||||||
import Edit from "./edit.svg";
|
import Edit from './edit.svg';
|
||||||
import UserAdd from "./user-profile-add.svg";
|
import UserAdd from './user-profile-add.svg';
|
||||||
import ChevroneDown from "./chevron-down.svg"
|
import ChevroneDown from './chevron-down.svg';
|
||||||
|
|
||||||
export {Book, Edit, EyeClosed, EyeOpen, UserAdd, ChevroneDown}
|
export { Book, Edit, EyeClosed, EyeOpen, UserAdd, ChevroneDown };
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import arrowLeft from "./arrow-left-sm.svg";
|
import arrowLeft from './arrow-left-sm.svg';
|
||||||
import chevroneLeft from "./chevron-left.svg"
|
import chevroneLeft from './chevron-left.svg';
|
||||||
import chevroneRight from "./chevron-right.svg"
|
import chevroneRight from './chevron-right.svg';
|
||||||
|
|
||||||
export {arrowLeft, chevroneLeft, chevroneRight}
|
export { arrowLeft, chevroneLeft, chevroneRight };
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import eyeClosed from "./eye-closed.svg"
|
import eyeClosed from './eye-closed.svg';
|
||||||
import eyeOpen from "./eye-open.png"
|
import eyeOpen from './eye-open.png';
|
||||||
import googleLogo from "./google-logo.svg"
|
import googleLogo from './google-logo.svg';
|
||||||
import upload from "./upload.svg"
|
import upload from './upload.svg';
|
||||||
import chevroneDropDownList from "./chevron-drop-down.svg"
|
import chevroneDropDownList from './chevron-drop-down.svg';
|
||||||
import checkMark from "./check-mark.svg"
|
import checkMark from './check-mark.svg';
|
||||||
|
|
||||||
export {eyeClosed, eyeOpen, googleLogo, upload, chevroneDropDownList, checkMark}
|
export {
|
||||||
|
eyeClosed,
|
||||||
|
eyeOpen,
|
||||||
|
googleLogo,
|
||||||
|
upload,
|
||||||
|
chevroneDropDownList,
|
||||||
|
checkMark,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import Account from "./account.svg";
|
import Account from './account.svg';
|
||||||
import Clipboard from "./clipboard.svg";
|
import Clipboard from './clipboard.svg';
|
||||||
import Cup from "./cup.svg";
|
import Cup from './cup.svg';
|
||||||
import Home from "./home.svg";
|
import Home from './home.svg';
|
||||||
import Openbook from "./openbook.svg";
|
import Openbook from './openbook.svg';
|
||||||
import Users from "./users.svg";
|
import Users from './users.svg';
|
||||||
|
|
||||||
export {Account, Clipboard, Cup, Home, Openbook, Users};
|
export { Account, Clipboard, Cup, Home, Openbook, Users };
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import IconSuccess from "./icon-success.svg"
|
import IconSuccess from './icon-success.svg';
|
||||||
import IconError from "./icon-error.svg"
|
import IconError from './icon-error.svg';
|
||||||
import CopyIcon from "./copy-icon.svg"
|
import CopyIcon from './copy-icon.svg';
|
||||||
|
|
||||||
|
export { IconError, IconSuccess, CopyIcon };
|
||||||
export {IconError, IconSuccess, CopyIcon}
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import Logo from "./Logo.svg"
|
import Logo from './Logo.svg';
|
||||||
|
|
||||||
export {Logo}
|
export { Logo };
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import axios from "axios";
|
import axios from 'axios';
|
||||||
|
|
||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_URL,
|
baseURL: import.meta.env.VITE_API_URL,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Request interceptor: автоматически подставляет JWT, если есть
|
// Request interceptor: автоматически подставляет JWT, если есть
|
||||||
instance.interceptors.request.use(
|
instance.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
const token = localStorage.getItem("jwt"); // или можно брать из Redux через store.getState()
|
const token = localStorage.getItem('jwt'); // или можно брать из Redux через store.getState()
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ instance.interceptors.request.use(
|
|||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export default instance;
|
export default instance;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { cn } from "../../lib/cn";
|
import { cn } from '../../lib/cn';
|
||||||
|
|
||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -7,77 +7,79 @@ interface ButtonProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
color?: "primary" | "secondary" | "error" | "warning" | "success";
|
color?: 'primary' | 'secondary' | 'error' | 'warning' | 'success';
|
||||||
}
|
}
|
||||||
|
|
||||||
const ColorBgVariants = {
|
const ColorBgVariants = {
|
||||||
"primary": "bg-liquid-brightmain group-hover:ring-liquid-brightmain",
|
primary: 'bg-liquid-brightmain group-hover:ring-liquid-brightmain',
|
||||||
"secondary": "bg-liquid-darkmain group-hover:ring-liquid-darkmain",
|
secondary: 'bg-liquid-darkmain group-hover:ring-liquid-darkmain',
|
||||||
"error": "bg-liquid-red group-hover:ring-liquid-red",
|
error: 'bg-liquid-red group-hover:ring-liquid-red',
|
||||||
"warning": "bg-liquid-orange group-hover:ring-liquid-orange",
|
warning: 'bg-liquid-orange group-hover:ring-liquid-orange',
|
||||||
"success": "bg-liquid-green group-hover:ring-liquid-green",
|
success: 'bg-liquid-green group-hover:ring-liquid-green',
|
||||||
}
|
};
|
||||||
|
|
||||||
const ColorTextVariants = {
|
const ColorTextVariants = {
|
||||||
"primary": "group-hover:text-liquid-brightmain ",
|
primary: 'group-hover:text-liquid-brightmain ',
|
||||||
"secondary": "group-hover:text-liquid-brightmain ",
|
secondary: 'group-hover:text-liquid-brightmain ',
|
||||||
"error": "group-hover:text-liquid-red ",
|
error: 'group-hover:text-liquid-red ',
|
||||||
"warning": "group-hover:text-liquid-orange ",
|
warning: 'group-hover:text-liquid-orange ',
|
||||||
"success": "group-hover:text-liquid-green ",
|
success: 'group-hover:text-liquid-green ',
|
||||||
}
|
};
|
||||||
|
|
||||||
export const PrimaryButton: React.FC<ButtonProps> = ({
|
export const PrimaryButton: React.FC<ButtonProps> = ({
|
||||||
disabled = false,
|
disabled = false,
|
||||||
text = "",
|
text = '',
|
||||||
className,
|
className,
|
||||||
onClick,
|
onClick,
|
||||||
children,
|
children,
|
||||||
color = "secondary",
|
color = 'secondary',
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid relative cursor-pointer select-none group w-fit box-border",
|
'grid relative cursor-pointer select-none group w-fit box-border',
|
||||||
disabled && "pointer-events-none",
|
disabled && 'pointer-events-none',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Основной контейнер, */}
|
{/* Основной контейнер, */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"group-active:scale-90 flex items-center justify-center box-border z-10 relative transition-all duration-300",
|
'group-active:scale-90 flex items-center justify-center box-border z-10 relative transition-all duration-300',
|
||||||
"rounded-[10px]",
|
'rounded-[10px]',
|
||||||
"group-hover:bg-liquid-lighter group-hover:ring-[1px] group-hover:ring-liquid-darkmain group-hover:ring-inset",
|
'group-hover:bg-liquid-lighter group-hover:ring-[1px] group-hover:ring-liquid-darkmain group-hover:ring-inset',
|
||||||
"px-[16px] py-[8px]",
|
'px-[16px] py-[8px]',
|
||||||
ColorBgVariants[color],
|
ColorBgVariants[color],
|
||||||
disabled && "bg-liquid-lighter"
|
disabled && 'bg-liquid-lighter',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Скрытый button */}
|
{/* Скрытый button */}
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute opacity-0 -z-10 h-0 w-0",
|
'absolute opacity-0 -z-10 h-0 w-0',
|
||||||
"[&:focus-visible+*]:outline-liquid-brightmain",
|
'[&:focus-visible+*]:outline-liquid-brightmain',
|
||||||
)}
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={() => { onClick() }}
|
onClick={() => {
|
||||||
|
onClick();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Граница при выделении через tab */}
|
{/* Граница при выделении через tab */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute outline-offset-[2.5px] border-[2px] border-transparent outline-[2.5px] outline outline-transparent transition-all duration-300 text-transparent box-border text-[18px] font-bold p-0 ,m-0 leading-[23px]",
|
'absolute outline-offset-[2.5px] border-[2px] border-transparent outline-[2.5px] outline outline-transparent transition-all duration-300 text-transparent box-border text-[18px] font-bold p-0 ,m-0 leading-[23px]',
|
||||||
"rounded-[10px]",
|
'rounded-[10px]',
|
||||||
"px-[16px] py-[8px]",
|
'px-[16px] py-[8px]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children || text}
|
{children || text}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-all duration-300 text-liquid-white text-[18px] font-bold p-0 m-0 leading-[23px]",
|
'transition-all duration-300 text-liquid-white text-[18px] font-bold p-0 m-0 leading-[23px]',
|
||||||
ColorTextVariants[color],
|
ColorTextVariants[color],
|
||||||
disabled && "text-liquid-light"
|
disabled && 'text-liquid-light',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children || text}
|
{children || text}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { cn } from "../../lib/cn";
|
import { cn } from '../../lib/cn';
|
||||||
|
|
||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -11,7 +11,7 @@ interface ButtonProps {
|
|||||||
|
|
||||||
export const ReverseButton: React.FC<ButtonProps> = ({
|
export const ReverseButton: React.FC<ButtonProps> = ({
|
||||||
disabled = false,
|
disabled = false,
|
||||||
text = "",
|
text = '',
|
||||||
className,
|
className,
|
||||||
onClick,
|
onClick,
|
||||||
children,
|
children,
|
||||||
@@ -19,47 +19,49 @@ export const ReverseButton: React.FC<ButtonProps> = ({
|
|||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid relative cursor-pointer select-none group w-fit box-border",
|
'grid relative cursor-pointer select-none group w-fit box-border',
|
||||||
disabled && "pointer-events-none",
|
disabled && 'pointer-events-none',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Основной контейнер, */}
|
{/* Основной контейнер, */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"group-active:scale-90 flex items-center justify-center box-border z-10 relative transition-all duration-300",
|
'group-active:scale-90 flex items-center justify-center box-border z-10 relative transition-all duration-300',
|
||||||
"rounded-[10px]",
|
'rounded-[10px]',
|
||||||
"group-hover:bg-liquid-darkmain ",
|
'group-hover:bg-liquid-darkmain ',
|
||||||
"px-[16px] py-[8px]",
|
'px-[16px] py-[8px]',
|
||||||
"bg-liquid-lighter ring-[1px] ring-liquid-darkmain ring-inset",
|
'bg-liquid-lighter ring-[1px] ring-liquid-darkmain ring-inset',
|
||||||
disabled && "bg-liquid-lighter"
|
disabled && 'bg-liquid-lighter',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Скрытый button */}
|
{/* Скрытый button */}
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute opacity-0 -z-10 h-0 w-0",
|
'absolute opacity-0 -z-10 h-0 w-0',
|
||||||
"[&:focus-visible+*]:outline-liquid-brightmain",
|
'[&:focus-visible+*]:outline-liquid-brightmain',
|
||||||
)}
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={() => { onClick() }}
|
onClick={() => {
|
||||||
|
onClick();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Граница при выделении через tab */}
|
{/* Граница при выделении через tab */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute outline-offset-[2.5px] border-[2px] border-transparent outline-[2.5px] outline outline-transparent transition-all duration-300 text-transparent box-border text-[18px] font-bold p-0 ,m-0 leading-[23px]",
|
'absolute outline-offset-[2.5px] border-[2px] border-transparent outline-[2.5px] outline outline-transparent transition-all duration-300 text-transparent box-border text-[18px] font-bold p-0 ,m-0 leading-[23px]',
|
||||||
"rounded-[10px]",
|
'rounded-[10px]',
|
||||||
"px-[16px] py-[8px]",
|
'px-[16px] py-[8px]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children || text}
|
{children || text}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-all duration-300 text-liquid-brightmain text-[18px] font-bold p-0 m-0 leading-[23px]",
|
'transition-all duration-300 text-liquid-brightmain text-[18px] font-bold p-0 m-0 leading-[23px]',
|
||||||
"group-hover:text-liquid-white ",
|
'group-hover:text-liquid-white ',
|
||||||
disabled && "text-liquid-light"
|
disabled && 'text-liquid-light',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children || text}
|
{children || text}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { cn } from "../../lib/cn";
|
import { cn } from '../../lib/cn';
|
||||||
|
|
||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -7,12 +7,11 @@ interface ButtonProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SecondaryButton: React.FC<ButtonProps> = ({
|
export const SecondaryButton: React.FC<ButtonProps> = ({
|
||||||
disabled = false,
|
disabled = false,
|
||||||
text = "",
|
text = '',
|
||||||
className,
|
className,
|
||||||
onClick,
|
onClick,
|
||||||
children,
|
children,
|
||||||
@@ -20,45 +19,47 @@ export const SecondaryButton: React.FC<ButtonProps> = ({
|
|||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid relative cursor-pointer select-none group w-fit box-border",
|
'grid relative cursor-pointer select-none group w-fit box-border',
|
||||||
disabled && "pointer-events-none",
|
disabled && 'pointer-events-none',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Основной контейнер, */}
|
{/* Основной контейнер, */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"group-active:scale-90 flex items-center justify-center box-border z-10 relative transition-all duration-300",
|
'group-active:scale-90 flex items-center justify-center box-border z-10 relative transition-all duration-300',
|
||||||
"rounded-[10px]",
|
'rounded-[10px]',
|
||||||
"group-hover:bg-liquid-background",
|
'group-hover:bg-liquid-background',
|
||||||
"px-[16px] py-[8px]",
|
'px-[16px] py-[8px]',
|
||||||
"bg-liquid-lighter"
|
'bg-liquid-lighter',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Скрытый button */}
|
{/* Скрытый button */}
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute opacity-0 -z-10 h-0 w-0",
|
'absolute opacity-0 -z-10 h-0 w-0',
|
||||||
"[&:focus-visible+*]:outline-liquid-brightmain",
|
'[&:focus-visible+*]:outline-liquid-brightmain',
|
||||||
)}
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={() => { onClick() }}
|
onClick={() => {
|
||||||
|
onClick();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Граница при выделении через tab */}
|
{/* Граница при выделении через tab */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute outline-offset-[2.5px] border-[2px] border-transparent outline-[2.5px] outline outline-transparent transition-all duration-300 text-transparent box-border text-[18px] font-bold p-0 ,m-0 leading-[23px]",
|
'absolute outline-offset-[2.5px] border-[2px] border-transparent outline-[2.5px] outline outline-transparent transition-all duration-300 text-transparent box-border text-[18px] font-bold p-0 ,m-0 leading-[23px]',
|
||||||
"rounded-[10px]",
|
'rounded-[10px]',
|
||||||
"px-[16px] py-[8px]",
|
'px-[16px] py-[8px]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children || text}
|
{children || text}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-all duration-300 text-liquid-white text-[18px] font-bold p-0 m-0 leading-[23px]",
|
'transition-all duration-300 text-liquid-white text-[18px] font-bold p-0 m-0 leading-[23px]',
|
||||||
disabled && "text-liquid-light"
|
disabled && 'text-liquid-light',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children || text}
|
{children || text}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { cn } from "../../lib/cn";
|
import { cn } from '../../lib/cn';
|
||||||
import { motion } from "framer-motion";
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
const pathVariants = {
|
const pathVariants = {
|
||||||
hidden: {
|
hidden: {
|
||||||
@@ -13,78 +13,77 @@ const pathVariants = {
|
|||||||
transition: {
|
transition: {
|
||||||
delay: 0.15,
|
delay: 0.15,
|
||||||
duration: 0.4,
|
duration: 0.4,
|
||||||
ease: "easeInOut",
|
ease: 'easeInOut',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeVariants = {
|
const sizeVariants = {
|
||||||
sm: "h-4 w-4",
|
sm: 'h-4 w-4',
|
||||||
md: "h-5 w-5",
|
md: 'h-5 w-5',
|
||||||
lg: "h-6 w-6",
|
lg: 'h-6 w-6',
|
||||||
};
|
};
|
||||||
|
|
||||||
const colorsVariants = {
|
const colorsVariants = {
|
||||||
default: "bg-default",
|
default: 'bg-default',
|
||||||
primary: "bg-liquid-brightmain",
|
primary: 'bg-liquid-brightmain',
|
||||||
secondary: "bg-liquid-darkmain",
|
secondary: 'bg-liquid-darkmain',
|
||||||
success: "bg-liquid-green",
|
success: 'bg-liquid-green',
|
||||||
warning: "bg-liquid-orange",
|
warning: 'bg-liquid-orange',
|
||||||
danger: "bg-liquid-red",
|
danger: 'bg-liquid-red',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const borderColorsVariants = {
|
const borderColorsVariants = {
|
||||||
default: "border-default",
|
default: 'border-default',
|
||||||
primary: "border-liquid-brightmain",
|
primary: 'border-liquid-brightmain',
|
||||||
secondary: "border-liquid-darkmain",
|
secondary: 'border-liquid-darkmain',
|
||||||
success: "border-liquid-green",
|
success: 'border-liquid-green',
|
||||||
warning: "border-liquid-orange",
|
warning: 'border-liquid-orange',
|
||||||
danger: "border-liquid-red",
|
danger: 'border-liquid-red',
|
||||||
};
|
};
|
||||||
|
|
||||||
const focuseOutlineVariants = {
|
const focuseOutlineVariants = {
|
||||||
default: "[&:focus-visible+*]:outline-default",
|
default: '[&:focus-visible+*]:outline-default',
|
||||||
primary: "[&:focus-visible+*]:outline-liquid-brightmain",
|
primary: '[&:focus-visible+*]:outline-liquid-brightmain',
|
||||||
secondary: "[&:focus-visible+*]:outline-liquid-darkmain",
|
secondary: '[&:focus-visible+*]:outline-liquid-darkmain',
|
||||||
success: "[&:focus-visible+*]:outline-liquid-green",
|
success: '[&:focus-visible+*]:outline-liquid-green',
|
||||||
warning: "[&:focus-visible+*]:outline-liquid-orange",
|
warning: '[&:focus-visible+*]:outline-liquid-orange',
|
||||||
danger: "[&:focus-visible+*]:outline-liquid-red",
|
danger: '[&:focus-visible+*]:outline-liquid-red',
|
||||||
};
|
};
|
||||||
|
|
||||||
const radiusVraiants = {
|
const radiusVraiants = {
|
||||||
none: "",
|
none: '',
|
||||||
sm: "rounded-[3.5px]",
|
sm: 'rounded-[3.5px]',
|
||||||
md: "rounded-[5px]",
|
md: 'rounded-[5px]',
|
||||||
lg: "rounded-[7px]",
|
lg: 'rounded-[7px]',
|
||||||
full: "rounded-full",
|
full: 'rounded-full',
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CheckboxProps {
|
interface CheckboxProps {
|
||||||
size?: "sm" | "md" | "lg";
|
size?: 'sm' | 'md' | 'lg';
|
||||||
radius?: "none" | "sm" | "md" | "lg" | "full";
|
radius?: 'none' | 'sm' | 'md' | 'lg' | 'full';
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
color?:
|
color?:
|
||||||
| "default"
|
| 'default'
|
||||||
| "primary"
|
| 'primary'
|
||||||
| "secondary"
|
| 'secondary'
|
||||||
| "success"
|
| 'success'
|
||||||
| "warning"
|
| 'warning'
|
||||||
| "danger";
|
| 'danger';
|
||||||
label?: string;
|
label?: string;
|
||||||
variant?: "default" | "label";
|
variant?: 'default' | 'label';
|
||||||
className?: string;
|
className?: string;
|
||||||
defaultState?: boolean;
|
defaultState?: boolean;
|
||||||
onChange: (state: boolean) => void;
|
onChange: (state: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Checkbox: React.FC<CheckboxProps> = ({
|
export const Checkbox: React.FC<CheckboxProps> = ({
|
||||||
size = "md",
|
size = 'md',
|
||||||
radius = "md",
|
radius = 'md',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
color = "primary",
|
color = 'primary',
|
||||||
label = "",
|
label = '',
|
||||||
variant = "label",
|
variant = 'label',
|
||||||
className,
|
className,
|
||||||
onChange,
|
onChange,
|
||||||
defaultState = false,
|
defaultState = false,
|
||||||
@@ -96,25 +95,25 @@ export const Checkbox: React.FC<CheckboxProps> = ({
|
|||||||
return (
|
return (
|
||||||
<motion.label
|
<motion.label
|
||||||
className={cn(
|
className={cn(
|
||||||
variant == "label" && "grid-cols-[auto_1fr] items-center gap-2",
|
variant == 'label' && 'grid-cols-[auto_1fr] items-center gap-2',
|
||||||
"grid relative cursor-pointer p-2 select-none group ",
|
'grid relative cursor-pointer p-2 select-none group ',
|
||||||
className,
|
className,
|
||||||
disabled && "pointer-events-none opacity-50",
|
disabled && 'pointer-events-none opacity-50',
|
||||||
variant == "default" && ""
|
variant == 'default' && '',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"group-hover:bg-default-100 group-active:scale-90 flex items-center justify-center bg-transparent hover:bg-default-100 box-border border-solid border-[1px] border-liquid-white z-10 relative transition-all duration-300",
|
'group-hover:bg-default-100 group-active:scale-90 flex items-center justify-center bg-transparent hover:bg-default-100 box-border border-solid border-[1px] border-liquid-white z-10 relative transition-all duration-300',
|
||||||
sizeVariants[size],
|
sizeVariants[size],
|
||||||
radiusVraiants[radius],
|
radiusVraiants[radius],
|
||||||
active && borderColorsVariants[color]
|
active && borderColorsVariants[color],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute opacity-0 -z-10 h-0 w-0",
|
'absolute opacity-0 -z-10 h-0 w-0',
|
||||||
focuseOutlineVariants[color]
|
focuseOutlineVariants[color],
|
||||||
)}
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -124,19 +123,19 @@ export const Checkbox: React.FC<CheckboxProps> = ({
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute outline-offset-[2.5px] outline-[2.5px] outline outline-transparent transition-all duration-200",
|
'absolute outline-offset-[2.5px] outline-[2.5px] outline outline-transparent transition-all duration-200',
|
||||||
sizeVariants[size],
|
sizeVariants[size],
|
||||||
radiusVraiants[radius]
|
radiusVraiants[radius],
|
||||||
)}
|
)}
|
||||||
></div>
|
></div>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute transition-all duration-300",
|
'absolute transition-all duration-300',
|
||||||
sizeVariants[size],
|
sizeVariants[size],
|
||||||
colorsVariants[color],
|
colorsVariants[color],
|
||||||
radiusVraiants[radius],
|
radiusVraiants[radius],
|
||||||
active && "opacity-100 scale-100",
|
active && 'opacity-100 scale-100',
|
||||||
!active && "opacity-0 scale-0"
|
!active && 'opacity-0 scale-0',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -158,7 +157,7 @@ export const Checkbox: React.FC<CheckboxProps> = ({
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{variant == "label" && (
|
{variant == 'label' && (
|
||||||
<div className="select-none text-layout-foeground transition-all duration-200">
|
<div className="select-none text-layout-foeground transition-all duration-200">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { cn } from "../../lib/cn";
|
import { cn } from '../../lib/cn';
|
||||||
import { checkMark, chevroneDropDownList } from "../../assets/icons/input";
|
import { checkMark, chevroneDropDownList } from '../../assets/icons/input';
|
||||||
import { useClickOutside } from "../../hooks/useClickOutside";
|
import { useClickOutside } from '../../hooks/useClickOutside';
|
||||||
|
|
||||||
export interface DropDownListItem {
|
export interface DropDownListItem {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -18,15 +18,16 @@ interface DropDownListProps {
|
|||||||
|
|
||||||
export const DropDownList: React.FC<DropDownListProps> = ({
|
export const DropDownList: React.FC<DropDownListProps> = ({
|
||||||
// disabled = false,
|
// disabled = false,
|
||||||
className = "",
|
className = '',
|
||||||
onChange,
|
onChange,
|
||||||
defaultState,
|
defaultState,
|
||||||
items = [{ text: "", value: "" }],
|
items = [{ text: '', value: '' }],
|
||||||
}) => {
|
}) => {
|
||||||
if (items.length == 0)
|
if (items.length == 0) items.push({ text: '', value: '' });
|
||||||
items.push({ text: "", value: "" });
|
|
||||||
|
|
||||||
const [value, setValue] = React.useState<DropDownListItem>(defaultState != undefined ? defaultState : items[0]);
|
const [value, setValue] = React.useState<DropDownListItem>(
|
||||||
|
defaultState != undefined ? defaultState : items[0],
|
||||||
|
);
|
||||||
const [active, setActive] = React.useState<boolean>(false);
|
const [active, setActive] = React.useState<boolean>(false);
|
||||||
|
|
||||||
React.useEffect(() => onChange(value.value), [value]);
|
React.useEffect(() => onChange(value.value), [value]);
|
||||||
@@ -37,67 +38,73 @@ export const DropDownList: React.FC<DropDownListProps> = ({
|
|||||||
setActive(false);
|
setActive(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div className={cn('relative', className)} ref={ref}>
|
||||||
"relative",
|
<div
|
||||||
className
|
className={cn(
|
||||||
)}
|
' flex items-center h-[40px] rounded-[10px] bg-liquid-lighter px-[16px] w-[180px]',
|
||||||
ref={ref}
|
'text-[18px] font-bold cursor-pointer select-none',
|
||||||
>
|
'transitin-all active:scale-95 duration-300',
|
||||||
<div className={cn(" flex items-center h-[40px] rounded-[10px] bg-liquid-lighter px-[16px] w-[180px]",
|
|
||||||
"text-[18px] font-bold cursor-pointer select-none",
|
|
||||||
"transitin-all active:scale-95 duration-300"
|
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActive(!active);
|
setActive(!active);
|
||||||
}
|
}}
|
||||||
}>
|
>
|
||||||
{value.text}
|
{value.text}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img src={chevroneDropDownList}
|
<img
|
||||||
className={cn(" absolute right-[16px] h-[24px] w-[24px] top-[8.5px] rotate-0 transition-all duration-300 pointer-events-none",
|
src={chevroneDropDownList}
|
||||||
active && " rotate-180"
|
className={cn(
|
||||||
)} />
|
' absolute right-[16px] h-[24px] w-[24px] top-[8.5px] rotate-0 transition-all duration-300 pointer-events-none',
|
||||||
|
active && ' rotate-180',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(" absolute rounded-[10px] bg-liquid-lighter w-[180px] left-0 top-[48px] z-50 transition-all duration-300",
|
className={cn(
|
||||||
"grid overflow-hidden",
|
' absolute rounded-[10px] bg-liquid-lighter w-[180px] left-0 top-[48px] z-50 transition-all duration-300',
|
||||||
active ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0",
|
'grid overflow-hidden',
|
||||||
)}>
|
active
|
||||||
|
? 'grid-rows-[1fr] opacity-100'
|
||||||
|
: 'grid-rows-[0fr] opacity-0',
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className=" overflow-hidden p-[8px]">
|
<div className=" overflow-hidden p-[8px]">
|
||||||
<div className={cn(
|
<div
|
||||||
" overflow-y-scroll max-h-[200px] thin-scrollbar pr-[8px] ",
|
className={cn(
|
||||||
)}>
|
' overflow-y-scroll max-h-[200px] thin-scrollbar pr-[8px] ',
|
||||||
|
)}
|
||||||
{items.map((v, i) =>
|
>
|
||||||
|
{items.map((v, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer h-[36px] relative transition-all duration-300",
|
'cursor-pointer h-[36px] relative transition-all duration-300',
|
||||||
i + 1 != items.length && "border-b-liquid-light border-b-[1px]",
|
i + 1 != items.length &&
|
||||||
"text-[16px] font-medium cursor-pointer select-none flex items-center pl-[8px]",
|
'border-b-liquid-light border-b-[1px]',
|
||||||
"hover:bg-liquid-background",
|
'text-[16px] font-medium cursor-pointer select-none flex items-center pl-[8px]',
|
||||||
"first:rounded-t-[6px] last:rounded-b-[6px]"
|
'hover:bg-liquid-background',
|
||||||
|
'first:rounded-t-[6px] last:rounded-b-[6px]',
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setValue(v);
|
setValue(v);
|
||||||
setActive(false);
|
setActive(false);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{v.text}
|
{v.text}
|
||||||
|
|
||||||
{v.text == value.text &&
|
{v.text == value.text && (
|
||||||
<img src={checkMark} className=" absolute right-[8px]" />
|
<img
|
||||||
}
|
src={checkMark}
|
||||||
</div>
|
className=" absolute right-[8px]"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { cn } from "../../lib/cn";
|
import { cn } from '../../lib/cn';
|
||||||
import { eyeClosed, eyeOpen } from "../../assets/icons/input";
|
import { eyeClosed, eyeOpen } from '../../assets/icons/input';
|
||||||
|
|
||||||
interface inputProps {
|
interface inputProps {
|
||||||
name?: string;
|
name?: string;
|
||||||
type: "text" | "email" | "password" | "first_name" | "number";
|
type: 'text' | 'email' | 'password' | 'first_name' | 'number';
|
||||||
error?: string;
|
error?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
@@ -18,72 +18,78 @@ interface inputProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Input: React.FC<inputProps> = ({
|
export const Input: React.FC<inputProps> = ({
|
||||||
type = "text",
|
type = 'text',
|
||||||
error = "",
|
error = '',
|
||||||
// disabled = false,
|
// disabled = false,
|
||||||
// required = false,
|
// required = false,
|
||||||
label = "",
|
label = '',
|
||||||
placeholder = "",
|
placeholder = '',
|
||||||
className = "",
|
className = '',
|
||||||
onChange,
|
onChange,
|
||||||
defaultState = "",
|
defaultState = '',
|
||||||
name = "",
|
name = '',
|
||||||
autocomplete = "",
|
autocomplete = '',
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
}) => {
|
}) => {
|
||||||
const [value, setValue] = React.useState<string>(defaultState);
|
const [value, setValue] = React.useState<string>(defaultState);
|
||||||
const [visible, setVIsible] = React.useState<boolean>(type != "password");
|
const [visible, setVIsible] = React.useState<boolean>(type != 'password');
|
||||||
|
|
||||||
React.useEffect(() => onChange(value), [value]);
|
React.useEffect(() => onChange(value), [value]);
|
||||||
React.useEffect(() => setValue(defaultState), [defaultState]);
|
React.useEffect(() => setValue(defaultState), [defaultState]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div className={cn('relative', className)}>
|
||||||
"relative",
|
<div
|
||||||
className
|
className={cn(
|
||||||
)}>
|
'text-[18px] text-liquid-white font-medium h-[23px] mb-[10px] transition-all',
|
||||||
<div className={cn("text-[18px] text-liquid-white font-medium h-[23px] mb-[10px] transition-all",
|
label == '' && 'h-0 mb-0',
|
||||||
label == "" && "h-0 mb-0"
|
)}
|
||||||
)}>
|
>
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-liquid-lighter w-full rounded-[10px] outline-none pl-[16px] py-[8px] placeholder:text-liquid-light",
|
'bg-liquid-lighter w-full rounded-[10px] outline-none pl-[16px] py-[8px] placeholder:text-liquid-light',
|
||||||
type == "password" ? "h-[40px]" : "h-[36px]"
|
type == 'password' ? 'h-[40px]' : 'h-[36px]',
|
||||||
)}
|
)}
|
||||||
value={value}
|
value={value}
|
||||||
name={name}
|
name={name}
|
||||||
autoComplete={autocomplete}
|
autoComplete={autocomplete}
|
||||||
type={type == "password" ? (visible ? "text" : "password") : type}
|
type={
|
||||||
|
type == 'password'
|
||||||
|
? visible
|
||||||
|
? 'text'
|
||||||
|
: 'password'
|
||||||
|
: type
|
||||||
|
}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setValue(e.target.value);
|
setValue(e.target.value);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (onKeyDown)
|
if (onKeyDown) onKeyDown(e);
|
||||||
onKeyDown(e);
|
}}
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
{
|
{type == 'password' && (
|
||||||
type == "password" &&
|
<img
|
||||||
<img src={visible ? eyeOpen : eyeClosed} className="w-[24px] h-[24px] cursor-pointer right-[16px] top-[8px] absolute" onClick={() => {
|
src={visible ? eyeOpen : eyeClosed}
|
||||||
|
className="w-[24px] h-[24px] cursor-pointer right-[16px] top-[8px] absolute"
|
||||||
|
onClick={() => {
|
||||||
setVIsible(!visible);
|
setVIsible(!visible);
|
||||||
}} />
|
}}
|
||||||
}
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn("text-liquid-red text-[14px] h-[18px] text-right mt-[5px]",
|
<div
|
||||||
error == "" && "h-0 mt-0"
|
className={cn(
|
||||||
)}>
|
'text-liquid-red text-[14px] h-[18px] text-right mt-[5px]',
|
||||||
|
error == '' && 'h-0 mt-0',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { cn } from "../../lib/cn";
|
import { cn } from '../../lib/cn';
|
||||||
import { useClickOutside } from "../../hooks/useClickOutside";
|
import { useClickOutside } from '../../hooks/useClickOutside';
|
||||||
|
|
||||||
type ModalBackdrop = "opaque" | "blur";
|
type ModalBackdrop = 'opaque' | 'blur';
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -47,9 +47,11 @@ export const Modal: React.FC<ModalProps> = ({
|
|||||||
exit={modalbgVariants.closed}
|
exit={modalbgVariants.closed}
|
||||||
transition={{ duration: 0.15 }}
|
transition={{ duration: 0.15 }}
|
||||||
className={cn(
|
className={cn(
|
||||||
" fixed top-0 left-0 h-svh w-svw backdrop-filter transition-all z-50",
|
' fixed top-0 left-0 h-svh w-svw backdrop-filter transition-all z-50',
|
||||||
backdrop == "blur" && open && "backdrop-blur-sm",
|
backdrop == 'blur' && open && 'backdrop-blur-sm',
|
||||||
backdrop == "opaque" && open && "bg-[#00000055] pointer-events-none",
|
backdrop == 'opaque' &&
|
||||||
|
open &&
|
||||||
|
'bg-[#00000055] pointer-events-none',
|
||||||
)}
|
)}
|
||||||
></motion.div>
|
></motion.div>
|
||||||
)}
|
)}
|
||||||
@@ -60,8 +62,8 @@ export const Modal: React.FC<ModalProps> = ({
|
|||||||
<motion.div
|
<motion.div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-fit w-fit rounded-md pointer-events-auto",
|
'h-fit w-fit rounded-md pointer-events-auto',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
initial={modalVariants.closed}
|
initial={modalVariants.closed}
|
||||||
animate={modalVariants.open}
|
animate={modalVariants.open}
|
||||||
|
|||||||
@@ -1,48 +1,48 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { cn } from "../../lib/cn";
|
import { cn } from '../../lib/cn';
|
||||||
|
|
||||||
/* Варианты размера контейнера */
|
/* Варианты размера контейнера */
|
||||||
const sizeVariants = {
|
const sizeVariants = {
|
||||||
sm: "h-6 w-10",
|
sm: 'h-6 w-10',
|
||||||
md: "h-7 w-12",
|
md: 'h-7 w-12',
|
||||||
lg: "h-8 w-14",
|
lg: 'h-8 w-14',
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Варианты для скользящего шарика */
|
/* Варианты для скользящего шарика */
|
||||||
const switchVariants = {
|
const switchVariants = {
|
||||||
size: {
|
size: {
|
||||||
sm: "h-4 w-4",
|
sm: 'h-4 w-4',
|
||||||
md: "h-5 w-5",
|
md: 'h-5 w-5',
|
||||||
lg: "h-6 w-6",
|
lg: 'h-6 w-6',
|
||||||
},
|
},
|
||||||
activeSize: {
|
activeSize: {
|
||||||
sm: "group-active:w-5",
|
sm: 'group-active:w-5',
|
||||||
md: "group-active:w-6",
|
md: 'group-active:w-6',
|
||||||
lg: "group-active:w-7",
|
lg: 'group-active:w-7',
|
||||||
},
|
},
|
||||||
iconSize: {
|
iconSize: {
|
||||||
sm: "h-3 w-3",
|
sm: 'h-3 w-3',
|
||||||
md: "h-[0.875rem] w-[0.875rem]",
|
md: 'h-[0.875rem] w-[0.875rem]',
|
||||||
lg: "h-4 w-4",
|
lg: 'h-4 w-4',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const colorsVariants = {
|
const colorsVariants = {
|
||||||
default: "bg-default",
|
default: 'bg-default',
|
||||||
primary: "bg-liquid-brightmain",
|
primary: 'bg-liquid-brightmain',
|
||||||
secondary: "bg-liquid-darkmain",
|
secondary: 'bg-liquid-darkmain',
|
||||||
success: "bg-liquid-green",
|
success: 'bg-liquid-green',
|
||||||
warning: "bg-liquid-orange",
|
warning: 'bg-liquid-orange',
|
||||||
danger: "bg-liquid-red",
|
danger: 'bg-liquid-red',
|
||||||
};
|
};
|
||||||
|
|
||||||
const focuseOutlineVariants = {
|
const focuseOutlineVariants = {
|
||||||
default: "[&:focus-visible+*]:outline-default",
|
default: '[&:focus-visible+*]:outline-default',
|
||||||
primary: "[&:focus-visible+*]:outline-liquid-brightmain",
|
primary: '[&:focus-visible+*]:outline-liquid-brightmain',
|
||||||
secondary: "[&:focus-visible+*]:outline-liquid-darkmain",
|
secondary: '[&:focus-visible+*]:outline-liquid-darkmain',
|
||||||
success: "[&:focus-visible+*]:outline-liquid-green",
|
success: '[&:focus-visible+*]:outline-liquid-green',
|
||||||
warning: "[&:focus-visible+*]:outline-liquid-orange",
|
warning: '[&:focus-visible+*]:outline-liquid-orange',
|
||||||
danger: "[&:focus-visible+*]:outline-liquid-red",
|
danger: '[&:focus-visible+*]:outline-liquid-red',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,28 +74,28 @@ const moon = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
interface SwitchProps {
|
interface SwitchProps {
|
||||||
size?: "sm" | "md" | "lg";
|
size?: 'sm' | 'md' | 'lg';
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
color?:
|
color?:
|
||||||
| "default"
|
| 'default'
|
||||||
| "primary"
|
| 'primary'
|
||||||
| "secondary"
|
| 'secondary'
|
||||||
| "success"
|
| 'success'
|
||||||
| "warning"
|
| 'warning'
|
||||||
| "danger";
|
| 'danger';
|
||||||
label?: string;
|
label?: string;
|
||||||
variant?: "default" | "label" | "icon" | "theme";
|
variant?: 'default' | 'label' | 'icon' | 'theme';
|
||||||
className?: string;
|
className?: string;
|
||||||
defaultState?: boolean;
|
defaultState?: boolean;
|
||||||
onChange: (state: boolean) => void;
|
onChange: (state: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Switch: React.FC<SwitchProps> = ({
|
export const Switch: React.FC<SwitchProps> = ({
|
||||||
size = "sm",
|
size = 'sm',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
color = "primary",
|
color = 'primary',
|
||||||
label = "",
|
label = '',
|
||||||
variant = "default",
|
variant = 'default',
|
||||||
className,
|
className,
|
||||||
onChange,
|
onChange,
|
||||||
defaultState = false,
|
defaultState = false,
|
||||||
@@ -107,25 +107,25 @@ export const Switch: React.FC<SwitchProps> = ({
|
|||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={cn(
|
className={cn(
|
||||||
variant == "label" && "grid-cols-[auto_1fr] items-center gap-2",
|
variant == 'label' && 'grid-cols-[auto_1fr] items-center gap-2',
|
||||||
"grid relative cursor-pointer p-2 select-none group",
|
'grid relative cursor-pointer p-2 select-none group',
|
||||||
disabled && "pointer-events-none opacity-50",
|
disabled && 'pointer-events-none opacity-50',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Основной контейнер, */}
|
{/* Основной контейнер, */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
" flex items-center justify-center box-border z-10 relative transition-all duration-300 rounded-full",
|
' flex items-center justify-center box-border z-10 relative transition-all duration-300 rounded-full',
|
||||||
sizeVariants[size],
|
sizeVariants[size],
|
||||||
active ? colorsVariants[color] : "bg-default-200"
|
active ? colorsVariants[color] : 'bg-default-200',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Скрытый checkbox */}
|
{/* Скрытый checkbox */}
|
||||||
<input
|
<input
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute opacity-0 -z-10 h-0 w-0",
|
'absolute opacity-0 -z-10 h-0 w-0',
|
||||||
focuseOutlineVariants[color]
|
focuseOutlineVariants[color],
|
||||||
)}
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -136,38 +136,42 @@ export const Switch: React.FC<SwitchProps> = ({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute outline-offset-[2.5px] outline-[2.5px] outline outline-transparent transition-all duration-300 rounded-full",
|
'absolute outline-offset-[2.5px] outline-[2.5px] outline outline-transparent transition-all duration-300 rounded-full',
|
||||||
sizeVariants[size]
|
sizeVariants[size],
|
||||||
)}
|
)}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
{/* Шарик */}
|
{/* Шарик */}
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-white rounded-full absolute transition-all duration-300 m-1 flex items-center justify-center",
|
'bg-white rounded-full absolute transition-all duration-300 m-1 flex items-center justify-center',
|
||||||
switchVariants.size[size],
|
switchVariants.size[size],
|
||||||
switchVariants.activeSize[size],
|
switchVariants.activeSize[size],
|
||||||
active
|
active
|
||||||
? "right-[0%]"
|
? 'right-[0%]'
|
||||||
: "right-[calc(50%-0.25rem)] group-active:right-[calc(50%-0.5rem)]"
|
: 'right-[calc(50%-0.25rem)] group-active:right-[calc(50%-0.5rem)]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{variant == "theme" && (
|
{variant == 'theme' && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute transition-all duration-300",
|
'absolute transition-all duration-300',
|
||||||
switchVariants.iconSize[size],
|
switchVariants.iconSize[size],
|
||||||
active ? "opacity-100 scale-100" : "opacity-0 scale-50"
|
active
|
||||||
|
? 'opacity-100 scale-100'
|
||||||
|
: 'opacity-0 scale-50',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{moon}
|
{moon}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute transition-all duration-300",
|
'absolute transition-all duration-300',
|
||||||
switchVariants.iconSize[size],
|
switchVariants.iconSize[size],
|
||||||
active ? "opacity-0 scale-50" : "opacity-100 scale-100"
|
active
|
||||||
|
? 'opacity-0 scale-50'
|
||||||
|
: 'opacity-100 scale-100',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{sun}
|
{sun}
|
||||||
@@ -177,7 +181,7 @@ export const Switch: React.FC<SwitchProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{variant == "label" && (
|
{variant == 'label' && (
|
||||||
<div className="select-none text-layout-foreground transition-all duration-200">
|
<div className="select-none text-layout-foreground transition-all duration-200">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
export default {
|
export default {
|
||||||
liquid: {
|
liquid: {
|
||||||
brightmain: "var(--color-liquid-brightmain)",
|
brightmain: 'var(--color-liquid-brightmain)',
|
||||||
darkmain: "var(--color-liquid-darkmain)",
|
darkmain: 'var(--color-liquid-darkmain)',
|
||||||
darker: "var(--color-liquid-darker)",
|
darker: 'var(--color-liquid-darker)',
|
||||||
background: "var(--color-liquid-background)",
|
background: 'var(--color-liquid-background)',
|
||||||
lighter: "var(--color-liquid-lighter)",
|
lighter: 'var(--color-liquid-lighter)',
|
||||||
white: "var(--color-liquid-white)",
|
white: 'var(--color-liquid-white)',
|
||||||
red: "var(--color-liquid-red)",
|
red: 'var(--color-liquid-red)',
|
||||||
green: "var(--color-liquid-green)",
|
green: 'var(--color-liquid-green)',
|
||||||
light: "var(--color-liquid-light)",
|
light: 'var(--color-liquid-light)',
|
||||||
orange: "var(--color-liquid-orange)",
|
orange: 'var(--color-liquid-orange)',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
|
|
||||||
export const useClickOutside = (ref: React.RefObject<any>, onClickOutside: () => void) => {
|
export const useClickOutside = (
|
||||||
|
ref: React.RefObject<any>,
|
||||||
|
onClickOutside: () => void,
|
||||||
|
) => {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
|
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
|
||||||
if (ref.current && !ref.current.contains(event.target)) {
|
if (ref.current && !ref.current.contains(event.target)) {
|
||||||
onClickOutside();
|
onClickOutside();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
document.addEventListener("touchstart", handleClickOutside);
|
document.addEventListener('touchstart', handleClickOutside);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
document.removeEventListener("touchstart", handleClickOutside);
|
document.removeEventListener('touchstart', handleClickOutside);
|
||||||
}
|
};
|
||||||
}, [ref, onClickOutside]);
|
}, [ref, onClickOutside]);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ClassValue, clsx } from "clsx";
|
import { ClassValue, clsx } from 'clsx';
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
|
|||||||
20
src/main.tsx
20
src/main.tsx
@@ -1,16 +1,16 @@
|
|||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from 'react-dom/client';
|
||||||
import App from "./App.tsx";
|
import App from './App.tsx';
|
||||||
import "./styles/index.css";
|
import './styles/index.css';
|
||||||
import "./styles/palette/theme-dark.css";
|
import './styles/palette/theme-dark.css';
|
||||||
import "./styles/palette/theme-light.css";
|
import './styles/palette/theme-light.css';
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from 'react-redux';
|
||||||
import { store } from "./redux/store";
|
import { store } from './redux/store';
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<App />
|
<App />
|
||||||
</Provider>
|
</Provider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,66 +1,80 @@
|
|||||||
import { Route, Routes, useNavigate } from "react-router-dom";
|
import { Route, Routes, useNavigate } from 'react-router-dom';
|
||||||
import Header from '../views/articleeditor/Header';
|
import Header from '../views/articleeditor/Header';
|
||||||
import MarkdownEditor from "../views/articleeditor/Editor";
|
import MarkdownEditor from '../views/articleeditor/Editor';
|
||||||
import { useState } from "react";
|
import { useState } from 'react';
|
||||||
import { PrimaryButton } from "../components/button/PrimaryButton";
|
import { PrimaryButton } from '../components/button/PrimaryButton';
|
||||||
import MarkdownPreview from "../views/articleeditor/MarckDownPreview";
|
import MarkdownPreview from '../views/articleeditor/MarckDownPreview';
|
||||||
import { Input } from "../components/input/Input";
|
import { Input } from '../components/input/Input';
|
||||||
|
|
||||||
|
|
||||||
const ArticleEditor = () => {
|
const ArticleEditor = () => {
|
||||||
const [code, setCode] = useState<string>("");
|
const [code, setCode] = useState<string>('');
|
||||||
const [name, setName] = useState<string>("");
|
const [name, setName] = useState<string>('');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [tagInput, setTagInput] = useState<string>('');
|
||||||
const [tagInput, setTagInput] = useState<string>("");
|
|
||||||
const [tags, setTags] = useState<string[]>([]);
|
const [tags, setTags] = useState<string[]>([]);
|
||||||
|
|
||||||
const addTag = () => {
|
const addTag = () => {
|
||||||
const newTag = tagInput.trim();
|
const newTag = tagInput.trim();
|
||||||
if (newTag && !tags.includes(newTag)) {
|
if (newTag && !tags.includes(newTag)) {
|
||||||
setTags([...tags, newTag]);
|
setTags([...tags, newTag]);
|
||||||
setTagInput("");
|
setTagInput('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeTag = (tagToRemove: string) => {
|
const removeTag = (tagToRemove: string) => {
|
||||||
setTags(tags.filter(tag => tag !== tagToRemove));
|
setTags(tags.filter((tag) => tag !== tagToRemove));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen grid grid-rows-[60px,1fr]">
|
<div className="h-screen grid grid-rows-[60px,1fr]">
|
||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="editor" element={<Header backUrl="/article/create" />} />
|
<Route
|
||||||
|
path="editor"
|
||||||
|
element={<Header backUrl="/article/create" />}
|
||||||
|
/>
|
||||||
<Route path="*" element={<Header backUrl="/home/articles" />} />
|
<Route path="*" element={<Header backUrl="/home/articles" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="editor" element={<MarkdownEditor onChange={setCode} />} />
|
<Route
|
||||||
<Route path="*" element={
|
path="editor"
|
||||||
|
element={<MarkdownEditor onChange={setCode} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="*"
|
||||||
|
element={
|
||||||
<div className="text-liquid-white">
|
<div className="text-liquid-white">
|
||||||
<div className="text-[40px] font-bold">Создание статьи</div>
|
<div className="text-[40px] font-bold">
|
||||||
|
Создание статьи
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PrimaryButton
|
||||||
<PrimaryButton onClick={() => {
|
onClick={() => {
|
||||||
console.log({
|
console.log({
|
||||||
name: name,
|
name: name,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
text: code,
|
text: code,
|
||||||
})
|
});
|
||||||
|
}}
|
||||||
}} text="Опубликовать" className="mt-[20px]" />
|
text="Опубликовать"
|
||||||
|
className="mt-[20px]"
|
||||||
|
/>
|
||||||
<Input name="articleName" autocomplete="articleName" className="mt-[20px] max-w-[600px]" type="text" label="Название" onChange={(v) => { setName(v) }} placeholder="Новая статья" />
|
|
||||||
|
|
||||||
|
<Input
|
||||||
|
name="articleName"
|
||||||
|
autocomplete="articleName"
|
||||||
|
className="mt-[20px] max-w-[600px]"
|
||||||
|
type="text"
|
||||||
|
label="Название"
|
||||||
|
onChange={(v) => {
|
||||||
|
setName(v);
|
||||||
|
}}
|
||||||
|
placeholder="Новая статья"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Блок для тегов */}
|
{/* Блок для тегов */}
|
||||||
<div className="mt-[20px] max-w-[600px]">
|
<div className="mt-[20px] max-w-[600px]">
|
||||||
|
|
||||||
<div className="grid grid-cols-[1fr,140px] items-end gap-2">
|
<div className="grid grid-cols-[1fr,140px] items-end gap-2">
|
||||||
<Input
|
<Input
|
||||||
name="articleTag"
|
name="articleTag"
|
||||||
@@ -68,36 +82,52 @@ const ArticleEditor = () => {
|
|||||||
className="mt-[20px] max-w-[600px]"
|
className="mt-[20px] max-w-[600px]"
|
||||||
type="text"
|
type="text"
|
||||||
label="Теги"
|
label="Теги"
|
||||||
onChange={(v) => { setTagInput(v) }}
|
onChange={(v) => {
|
||||||
|
setTagInput(v);
|
||||||
|
}}
|
||||||
defaultState={tagInput}
|
defaultState={tagInput}
|
||||||
placeholder="arrays"
|
placeholder="arrays"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
console.log(e.key);
|
console.log(e.key);
|
||||||
if (e.key == "Enter")
|
if (e.key == 'Enter') addTag();
|
||||||
addTag();
|
}}
|
||||||
}
|
/>
|
||||||
}
|
<PrimaryButton
|
||||||
|
onClick={addTag}
|
||||||
|
text="Добавить"
|
||||||
|
className="h-[40px] w-[140px]"
|
||||||
/>
|
/>
|
||||||
<PrimaryButton onClick={addTag} text="Добавить" className="h-[40px] w-[140px]" />
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-[10px] mt-2">
|
<div className="flex flex-wrap gap-[10px] mt-2">
|
||||||
{tags.map(tag => (
|
{tags.map((tag) => (
|
||||||
<div
|
<div
|
||||||
key={tag}
|
key={tag}
|
||||||
className="flex items-center gap-1 bg-liquid-lighter px-3 py-1 rounded-full"
|
className="flex items-center gap-1 bg-liquid-lighter px-3 py-1 rounded-full"
|
||||||
>
|
>
|
||||||
<span>{tag}</span>
|
<span>{tag}</span>
|
||||||
<button onClick={() => removeTag(tag)} className="text-liquid-red font-bold ml-[5px]">×</button>
|
<button
|
||||||
|
onClick={() => removeTag(tag)}
|
||||||
|
className="text-liquid-red font-bold ml-[5px]"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PrimaryButton onClick={() => navigate("editor")} text="Редактировать текст" className="mt-[20px]" />
|
<PrimaryButton
|
||||||
<MarkdownPreview content={code} className="bg-transparent border-liquid-lighter border-[3px] rounder-[20px] mt-[20px]" />
|
onClick={() => navigate('editor')}
|
||||||
|
text="Редактировать текст"
|
||||||
|
className="mt-[20px]"
|
||||||
|
/>
|
||||||
|
<MarkdownPreview
|
||||||
|
content={code}
|
||||||
|
className="bg-transparent border-liquid-lighter border-[3px] rounder-[20px] mt-[20px]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
} />
|
}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
// import React from "react";
|
// import React from "react";
|
||||||
import { Route, Routes } from "react-router-dom";
|
import { Route, Routes } from 'react-router-dom';
|
||||||
import Login from "../views/home/auth/Login";
|
import Login from '../views/home/auth/Login';
|
||||||
import Register from "../views/home/auth/Register";
|
import Register from '../views/home/auth/Register';
|
||||||
import Menu from "../views/home/menu/Menu";
|
import Menu from '../views/home/menu/Menu';
|
||||||
import { useAppDispatch, useAppSelector } from "../redux/hooks";
|
import { useAppDispatch, useAppSelector } from '../redux/hooks';
|
||||||
import { useEffect } from "react";
|
import { useEffect } from 'react';
|
||||||
import { fetchWhoAmI, logout } from "../redux/slices/auth";
|
import { fetchWhoAmI, logout } from '../redux/slices/auth';
|
||||||
import Missions from "../views/home/missions/Missions";
|
import Missions from '../views/home/missions/Missions';
|
||||||
import Articles from "../views/home/articles/Articles";
|
import Articles from '../views/home/articles/Articles';
|
||||||
import Groups from "../views/home/groups/Groups";
|
import Groups from '../views/home/groups/Groups';
|
||||||
import Contests from "../views/home/contests/Contests";
|
import Contests from '../views/home/contests/Contests';
|
||||||
import { PrimaryButton } from "../components/button/PrimaryButton";
|
import { PrimaryButton } from '../components/button/PrimaryButton';
|
||||||
import Group from "../views/home/groups/Group";
|
import Group from '../views/home/groups/Group';
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const name = useAppSelector((state) => state.auth.username);
|
const name = useAppSelector((state) => state.auth.username);
|
||||||
@@ -20,8 +20,7 @@ const Home = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchWhoAmI());
|
dispatch(fetchWhoAmI());
|
||||||
}, [jwt])
|
}, [jwt]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full bg-liquid-background grid grid-cols-[250px,1fr,250px] divide-x-[1px] divide-liquid-lighter">
|
<div className="w-full bg-liquid-background grid grid-cols-[250px,1fr,250px] divide-x-[1px] divide-liquid-lighter">
|
||||||
@@ -33,16 +32,32 @@ const Home = () => {
|
|||||||
<Route path="login" element={<Login />} />
|
<Route path="login" element={<Login />} />
|
||||||
<Route path="account" element={<Login />} />
|
<Route path="account" element={<Login />} />
|
||||||
<Route path="register" element={<Register />} />
|
<Route path="register" element={<Register />} />
|
||||||
<Route path="missions/*" element={<Missions/>} />
|
<Route path="missions/*" element={<Missions />} />
|
||||||
<Route path="articles/*" element={<Articles/>} />
|
<Route path="articles/*" element={<Articles />} />
|
||||||
<Route path="group/:groupId" element={<Group/>} />
|
<Route path="group/:groupId" element={<Group />} />
|
||||||
<Route path="groups/*" element={<Groups/>} />
|
<Route path="groups/*" element={<Groups />} />
|
||||||
<Route path="contests/*" element={<Contests/>} />
|
<Route path="contests/*" element={<Contests />} />
|
||||||
<Route path="*" element={<>
|
<Route
|
||||||
|
path="*"
|
||||||
|
element={
|
||||||
|
<>
|
||||||
<p>{jwt}</p>
|
<p>{jwt}</p>
|
||||||
<PrimaryButton onClick={() => {if (jwt) navigator.clipboard.writeText(jwt);}} text="скопировать токен" className="pt-[20px]"/>
|
<PrimaryButton
|
||||||
|
onClick={() => {
|
||||||
|
if (jwt)
|
||||||
|
navigator.clipboard.writeText(jwt);
|
||||||
|
}}
|
||||||
|
text="скопировать токен"
|
||||||
|
className="pt-[20px]"
|
||||||
|
/>
|
||||||
<p className="py-[20px]">{name}</p>
|
<p className="py-[20px]">{name}</p>
|
||||||
<PrimaryButton onClick={() => {dispatch(logout())}}>выйти</PrimaryButton>
|
<PrimaryButton
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(logout());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
выйти
|
||||||
|
</PrimaryButton>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import Header from '../views/mission/statement/Header';
|
|||||||
import MissionSubmissions from '../views/mission/statement/MissionSubmissions';
|
import MissionSubmissions from '../views/mission/statement/MissionSubmissions';
|
||||||
|
|
||||||
const Mission = () => {
|
const Mission = () => {
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
// Получаем параметры из URL
|
// Получаем параметры из URL
|
||||||
@@ -21,26 +20,25 @@ const Mission = () => {
|
|||||||
return <Navigate to="/home" replace />;
|
return <Navigate to="/home" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [code, setCode] = useState<string>("");
|
const [code, setCode] = useState<string>('');
|
||||||
const [language, setLanguage] = useState<string>("");
|
const [language, setLanguage] = useState<string>('');
|
||||||
|
|
||||||
const pollingRef = useRef<number | null>(null);
|
const pollingRef = useRef<number | null>(null);
|
||||||
const submissions = useAppSelector((state) => state.submin.submitsById[missionIdNumber] || []);
|
const submissions = useAppSelector(
|
||||||
|
(state) => state.submin.submitsById[missionIdNumber] || [],
|
||||||
|
);
|
||||||
const submissionsRef = useRef(submissions);
|
const submissionsRef = useRef(submissions);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const startPolling = () => {
|
const startPolling = () => {
|
||||||
if (pollingRef.current)
|
if (pollingRef.current) return;
|
||||||
return;
|
|
||||||
|
|
||||||
pollingRef.current = setInterval(async () => {
|
pollingRef.current = setInterval(async () => {
|
||||||
dispatch(fetchMySubmitsByMission(missionIdNumber));
|
dispatch(fetchMySubmitsByMission(missionIdNumber));
|
||||||
|
|
||||||
const hasWaiting = submissionsRef.current.some(
|
const hasWaiting = submissionsRef.current.some(
|
||||||
(s: any) => s.solution.status == "Waiting" || s.solution.testerState === "Waiting"
|
(s: any) =>
|
||||||
|
s.solution.status == 'Waiting' ||
|
||||||
|
s.solution.testerState === 'Waiting',
|
||||||
);
|
);
|
||||||
if (!hasWaiting) {
|
if (!hasWaiting) {
|
||||||
// Всё проверено — стоп
|
// Всё проверено — стоп
|
||||||
@@ -52,15 +50,12 @@ const Mission = () => {
|
|||||||
}, 5000); // 10 секунд
|
}, 5000); // 10 секунд
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchMissionById(missionIdNumber));
|
dispatch(fetchMissionById(missionIdNumber));
|
||||||
dispatch(fetchMySubmitsByMission(missionIdNumber));
|
dispatch(fetchMySubmitsByMission(missionIdNumber));
|
||||||
}, [missionIdNumber]);
|
}, [missionIdNumber]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {}, [submissions]);
|
||||||
}, [submissions]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -71,13 +66,14 @@ const Mission = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
submissionsRef.current = submissions;
|
submissionsRef.current = submissions;
|
||||||
|
|
||||||
if (submissions.length) {
|
if (submissions.length) {
|
||||||
const hasWaiting = submissions.some(
|
const hasWaiting = submissions.some(
|
||||||
s => s.solution.status === "Waiting" || s.solution.testerState === "Waiting"
|
(s) =>
|
||||||
|
s.solution.status === 'Waiting' ||
|
||||||
|
s.solution.testerState === 'Waiting',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasWaiting) {
|
if (hasWaiting) {
|
||||||
@@ -86,7 +82,6 @@ const Mission = () => {
|
|||||||
}
|
}
|
||||||
}, [submissions]);
|
}, [submissions]);
|
||||||
|
|
||||||
|
|
||||||
if (!mission || !mission.statements || mission.statements.length === 0) {
|
if (!mission || !mission.statements || mission.statements.length === 0) {
|
||||||
return <div>Загрузка...</div>;
|
return <div>Загрузка...</div>;
|
||||||
}
|
}
|
||||||
@@ -111,19 +106,23 @@ const Mission = () => {
|
|||||||
try {
|
try {
|
||||||
// 1. Берём первый statement с форматом Latex и языком russian
|
// 1. Берём первый statement с форматом Latex и языком russian
|
||||||
const latexStatement = mission.statements.find(
|
const latexStatement = mission.statements.find(
|
||||||
(stmt: any) => stmt && stmt.language === "russian" && stmt.format === "Latex"
|
(stmt: any) =>
|
||||||
|
stmt && stmt.language === 'russian' && stmt.format === 'Latex',
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. Берём первый statement с форматом Html и языком russian
|
// 2. Берём первый statement с форматом Html и языком russian
|
||||||
const htmlStatement = mission.statements.find(
|
const htmlStatement = mission.statements.find(
|
||||||
(stmt: any) => stmt && stmt.language === "russian" && stmt.format === "Html"
|
(stmt: any) =>
|
||||||
|
stmt && stmt.language === 'russian' && stmt.format === 'Html',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!latexStatement) throw new Error("Не найден блок Latex на русском");
|
if (!latexStatement) throw new Error('Не найден блок Latex на русском');
|
||||||
if (!htmlStatement) throw new Error("Не найден блок Html на русском");
|
if (!htmlStatement) throw new Error('Не найден блок Html на русском');
|
||||||
|
|
||||||
// 3. Парсим данные из problem-properties.json
|
// 3. Парсим данные из problem-properties.json
|
||||||
const statementTexts = JSON.parse(latexStatement.statementTexts["problem-properties.json"]);
|
const statementTexts = JSON.parse(
|
||||||
|
latexStatement.statementTexts['problem-properties.json'],
|
||||||
|
);
|
||||||
|
|
||||||
statementData = {
|
statementData = {
|
||||||
id: missionIdNumber,
|
id: missionIdNumber,
|
||||||
@@ -136,18 +135,14 @@ const Mission = () => {
|
|||||||
memoryLimit: statementTexts.memoryLimit,
|
memoryLimit: statementTexts.memoryLimit,
|
||||||
tags: mission.tags,
|
tags: mission.tags,
|
||||||
notes: statementTexts.notes,
|
notes: statementTexts.notes,
|
||||||
html: htmlStatement.statementTexts["problem.html"],
|
html: htmlStatement.statementTexts['problem.html'],
|
||||||
mediaFiles: latexStatement.mediaFiles
|
mediaFiles: latexStatement.mediaFiles,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Ошибка парсинга statementTexts:", err);
|
console.error('Ошибка парсинга statementTexts:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<div className="h-screen grid grid-rows-[60px,1fr]">
|
<div className="h-screen grid grid-rows-[60px,1fr]">
|
||||||
<div className="">
|
<div className="">
|
||||||
<Header missionId={missionIdNumber} />
|
<Header missionId={missionIdNumber} />
|
||||||
@@ -155,35 +150,44 @@ const Mission = () => {
|
|||||||
|
|
||||||
<div className="grid grid-cols-2 h-full min-h-0 gap-[20px]">
|
<div className="grid grid-cols-2 h-full min-h-0 gap-[20px]">
|
||||||
<div className="overflow-y-auto min-h-0 overflow-hidden">
|
<div className="overflow-y-auto min-h-0 overflow-hidden">
|
||||||
<Statement
|
<Statement {...statementData} />
|
||||||
{...statementData}
|
|
||||||
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-y-auto min-h-0 overflow-hidden pb-[20px]">
|
<div className="overflow-y-auto min-h-0 overflow-hidden pb-[20px]">
|
||||||
<div className=' grid grid-rows-[1fr,45px,230px] grid-flow-row h-full w-full gap-[20px] '>
|
<div className=" grid grid-rows-[1fr,45px,230px] grid-flow-row h-full w-full gap-[20px] ">
|
||||||
<div className='w-full relative '>
|
<div className="w-full relative ">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
onChange={(value: string) => { setCode(value); }}
|
onChange={(value: string) => {
|
||||||
onChangeLanguage={((value: string) => { setLanguage(value); })}
|
setCode(value);
|
||||||
|
}}
|
||||||
|
onChangeLanguage={(value: string) => {
|
||||||
|
setLanguage(value);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<PrimaryButton text='Отправить' onClick={async () => {
|
<PrimaryButton
|
||||||
await dispatch(submitMission({
|
text="Отправить"
|
||||||
|
onClick={async () => {
|
||||||
|
await dispatch(
|
||||||
|
submitMission({
|
||||||
missionId: missionIdNumber,
|
missionId: missionIdNumber,
|
||||||
language: language,
|
language: language,
|
||||||
languageVersion: "latest",
|
languageVersion: 'latest',
|
||||||
sourceCode: code,
|
sourceCode: code,
|
||||||
contestId: null,
|
contestId: null,
|
||||||
|
}),
|
||||||
})).unwrap();
|
).unwrap();
|
||||||
dispatch(fetchMySubmitsByMission(missionIdNumber));
|
dispatch(
|
||||||
}} />
|
fetchMySubmitsByMission(
|
||||||
|
missionIdNumber,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='h-full w-full '>
|
<div className="h-full w-full ">
|
||||||
<MissionSubmissions missionId={missionIdNumber} />
|
<MissionSubmissions missionId={missionIdNumber} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import axios from "../../axios";
|
import axios from '../../axios';
|
||||||
|
|
||||||
// Типы данных
|
// Типы данных
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
jwt: string | null;
|
jwt: string | null;
|
||||||
refreshToken: string | null;
|
refreshToken: string | null;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
status: "idle" | "loading" | "successful" | "failed";
|
status: 'idle' | 'loading' | 'successful' | 'failed';
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,74 +15,95 @@ const initialState: AuthState = {
|
|||||||
jwt: null,
|
jwt: null,
|
||||||
refreshToken: null,
|
refreshToken: null,
|
||||||
username: null,
|
username: null,
|
||||||
status: "idle",
|
status: 'idle',
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// AsyncThunk: Регистрация
|
// AsyncThunk: Регистрация
|
||||||
export const registerUser = createAsyncThunk(
|
export const registerUser = createAsyncThunk(
|
||||||
"auth/register",
|
'auth/register',
|
||||||
async (
|
async (
|
||||||
{ username, email, password }: { username: string; email: string; password: string },
|
{
|
||||||
{ rejectWithValue }
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
}: { username: string; email: string; password: string },
|
||||||
|
{ rejectWithValue },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post("/authentication/register", { username, email, password });
|
const response = await axios.post('/authentication/register', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
return response.data; // { jwt, refreshToken }
|
return response.data; // { jwt, refreshToken }
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Registration failed");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Registration failed',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// AsyncThunk: Логин
|
// AsyncThunk: Логин
|
||||||
export const loginUser = createAsyncThunk(
|
export const loginUser = createAsyncThunk(
|
||||||
"auth/login",
|
'auth/login',
|
||||||
async (
|
async (
|
||||||
{ username, password }: { username: string; password: string },
|
{ username, password }: { username: string; password: string },
|
||||||
{ rejectWithValue }
|
{ rejectWithValue },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post("/authentication/login", { username, password });
|
const response = await axios.post('/authentication/login', {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
});
|
||||||
return response.data; // { jwt, refreshToken }
|
return response.data; // { jwt, refreshToken }
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Login failed");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Login failed',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// AsyncThunk: Обновление токена
|
// AsyncThunk: Обновление токена
|
||||||
export const refreshToken = createAsyncThunk(
|
export const refreshToken = createAsyncThunk(
|
||||||
"auth/refresh",
|
'auth/refresh',
|
||||||
async ({ refreshToken }: { refreshToken: string }, { rejectWithValue }) => {
|
async ({ refreshToken }: { refreshToken: string }, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post("/authentication/refresh", { refreshToken });
|
const response = await axios.post('/authentication/refresh', {
|
||||||
|
refreshToken,
|
||||||
|
});
|
||||||
return response.data; // { username }
|
return response.data; // { username }
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Refresh token failed");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Refresh token failed',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// AsyncThunk: Получение информации о пользователе
|
// AsyncThunk: Получение информации о пользователе
|
||||||
export const fetchWhoAmI = createAsyncThunk(
|
export const fetchWhoAmI = createAsyncThunk(
|
||||||
"auth/whoami",
|
'auth/whoami',
|
||||||
async (_, { rejectWithValue }) => {
|
async (_, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get("/authentication/whoami");
|
const response = await axios.get('/authentication/whoami');
|
||||||
return response.data; // { username }
|
return response.data; // { username }
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Failed to fetch user info");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Failed to fetch user info',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// AsyncThunk: Загрузка токенов из localStorage
|
// AsyncThunk: Загрузка токенов из localStorage
|
||||||
export const loadTokensFromLocalStorage = createAsyncThunk(
|
export const loadTokensFromLocalStorage = createAsyncThunk(
|
||||||
"auth/loadTokens",
|
'auth/loadTokens',
|
||||||
async (_, { }) => {
|
async (_, {}) => {
|
||||||
const jwt = localStorage.getItem("jwt");
|
const jwt = localStorage.getItem('jwt');
|
||||||
const refreshToken = localStorage.getItem("refreshToken");
|
const refreshToken = localStorage.getItem('refreshToken');
|
||||||
|
|
||||||
if (jwt && refreshToken) {
|
if (jwt && refreshToken) {
|
||||||
axios.defaults.headers.common['Authorization'] = `Bearer ${jwt}`;
|
axios.defaults.headers.common['Authorization'] = `Bearer ${jwt}`;
|
||||||
@@ -90,98 +111,149 @@ export const loadTokensFromLocalStorage = createAsyncThunk(
|
|||||||
} else {
|
} else {
|
||||||
return { jwt: null, refreshToken: null };
|
return { jwt: null, refreshToken: null };
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Slice
|
// Slice
|
||||||
const authSlice = createSlice({
|
const authSlice = createSlice({
|
||||||
name: "auth",
|
name: 'auth',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
logout: (state) => {
|
logout: (state) => {
|
||||||
state.jwt = null;
|
state.jwt = null;
|
||||||
state.refreshToken = null;
|
state.refreshToken = null;
|
||||||
state.username = null;
|
state.username = null;
|
||||||
state.status = "idle";
|
state.status = 'idle';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
localStorage.removeItem("jwt");
|
localStorage.removeItem('jwt');
|
||||||
localStorage.removeItem("refreshToken");
|
localStorage.removeItem('refreshToken');
|
||||||
delete axios.defaults.headers.common['Authorization'];
|
delete axios.defaults.headers.common['Authorization'];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
// Регистрация
|
// Регистрация
|
||||||
builder.addCase(registerUser.pending, (state) => {
|
builder.addCase(registerUser.pending, (state) => {
|
||||||
state.status = "loading";
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(registerUser.fulfilled, (state, action: PayloadAction<{ jwt: string; refreshToken: string }>) => {
|
builder.addCase(
|
||||||
state.status = "successful";
|
registerUser.fulfilled,
|
||||||
|
(
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ jwt: string; refreshToken: string }>,
|
||||||
|
) => {
|
||||||
|
state.status = 'successful';
|
||||||
state.jwt = action.payload.jwt;
|
state.jwt = action.payload.jwt;
|
||||||
state.refreshToken = action.payload.refreshToken;
|
state.refreshToken = action.payload.refreshToken;
|
||||||
axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
|
axios.defaults.headers.common[
|
||||||
localStorage.setItem("jwt", action.payload.jwt);
|
'Authorization'
|
||||||
localStorage.setItem("refreshToken", action.payload.refreshToken);
|
] = `Bearer ${action.payload.jwt}`;
|
||||||
});
|
localStorage.setItem('jwt', action.payload.jwt);
|
||||||
builder.addCase(registerUser.rejected, (state, action: PayloadAction<any>) => {
|
localStorage.setItem(
|
||||||
state.status = "failed";
|
'refreshToken',
|
||||||
|
action.payload.refreshToken,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
builder.addCase(
|
||||||
|
registerUser.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.status = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Логин
|
// Логин
|
||||||
builder.addCase(loginUser.pending, (state) => {
|
builder.addCase(loginUser.pending, (state) => {
|
||||||
state.status = "loading";
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(loginUser.fulfilled, (state, action: PayloadAction<{ jwt: string; refreshToken: string }>) => {
|
builder.addCase(
|
||||||
state.status = "successful";
|
loginUser.fulfilled,
|
||||||
|
(
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ jwt: string; refreshToken: string }>,
|
||||||
|
) => {
|
||||||
|
state.status = 'successful';
|
||||||
state.jwt = action.payload.jwt;
|
state.jwt = action.payload.jwt;
|
||||||
state.refreshToken = action.payload.refreshToken;
|
state.refreshToken = action.payload.refreshToken;
|
||||||
axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
|
axios.defaults.headers.common[
|
||||||
localStorage.setItem("jwt", action.payload.jwt);
|
'Authorization'
|
||||||
localStorage.setItem("refreshToken", action.payload.refreshToken);
|
] = `Bearer ${action.payload.jwt}`;
|
||||||
});
|
localStorage.setItem('jwt', action.payload.jwt);
|
||||||
builder.addCase(loginUser.rejected, (state, action: PayloadAction<any>) => {
|
localStorage.setItem(
|
||||||
state.status = "failed";
|
'refreshToken',
|
||||||
|
action.payload.refreshToken,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
builder.addCase(
|
||||||
|
loginUser.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.status = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Обновление токена
|
// Обновление токена
|
||||||
builder.addCase(refreshToken.pending, (state) => {
|
builder.addCase(refreshToken.pending, (state) => {
|
||||||
state.status = "loading";
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(refreshToken.fulfilled, (state, action: PayloadAction<{ username: string }>) => {
|
builder.addCase(
|
||||||
state.status = "successful";
|
refreshToken.fulfilled,
|
||||||
|
(state, action: PayloadAction<{ username: string }>) => {
|
||||||
|
state.status = 'successful';
|
||||||
state.username = action.payload.username;
|
state.username = action.payload.username;
|
||||||
});
|
},
|
||||||
builder.addCase(refreshToken.rejected, (state, action: PayloadAction<any>) => {
|
);
|
||||||
state.status = "failed";
|
builder.addCase(
|
||||||
|
refreshToken.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.status = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Получение информации о пользователе
|
// Получение информации о пользователе
|
||||||
builder.addCase(fetchWhoAmI.pending, (state) => {
|
builder.addCase(fetchWhoAmI.pending, (state) => {
|
||||||
state.status = "loading";
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(fetchWhoAmI.fulfilled, (state, action: PayloadAction<{ username: string }>) => {
|
builder.addCase(
|
||||||
state.status = "successful";
|
fetchWhoAmI.fulfilled,
|
||||||
|
(state, action: PayloadAction<{ username: string }>) => {
|
||||||
|
state.status = 'successful';
|
||||||
state.username = action.payload.username;
|
state.username = action.payload.username;
|
||||||
});
|
},
|
||||||
builder.addCase(fetchWhoAmI.rejected, (state, action: PayloadAction<any>) => {
|
);
|
||||||
state.status = "failed";
|
builder.addCase(
|
||||||
|
fetchWhoAmI.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.status = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Загрузка токенов из localStorage
|
// Загрузка токенов из localStorage
|
||||||
builder.addCase(loadTokensFromLocalStorage.fulfilled, (state, action: PayloadAction<{ jwt: string | null; refreshToken: string | null }>) => {
|
builder.addCase(
|
||||||
|
loadTokensFromLocalStorage.fulfilled,
|
||||||
|
(
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
jwt: string | null;
|
||||||
|
refreshToken: string | null;
|
||||||
|
}>,
|
||||||
|
) => {
|
||||||
state.jwt = action.payload.jwt;
|
state.jwt = action.payload.jwt;
|
||||||
state.refreshToken = action.payload.refreshToken;
|
state.refreshToken = action.payload.refreshToken;
|
||||||
if (action.payload.jwt) {
|
if (action.payload.jwt) {
|
||||||
axios.defaults.headers.common['Authorization'] = `Bearer ${action.payload.jwt}`;
|
axios.defaults.headers.common[
|
||||||
|
'Authorization'
|
||||||
|
] = `Bearer ${action.payload.jwt}`;
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import axios from "../../axios";
|
import axios from '../../axios';
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// Типы
|
// Типы
|
||||||
@@ -42,7 +42,7 @@ interface ContestsResponse {
|
|||||||
export interface CreateContestBody {
|
export interface CreateContestBody {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
scheduleType: "FixedWindow" | "Flexible";
|
scheduleType: 'FixedWindow' | 'Flexible';
|
||||||
startsAt: string;
|
startsAt: string;
|
||||||
endsAt: string;
|
endsAt: string;
|
||||||
availableFrom: string | null;
|
availableFrom: string | null;
|
||||||
@@ -63,7 +63,7 @@ interface ContestsState {
|
|||||||
contests: Contest[];
|
contests: Contest[];
|
||||||
selectedContest: Contest | null;
|
selectedContest: Contest | null;
|
||||||
hasNextPage: boolean;
|
hasNextPage: boolean;
|
||||||
status: "idle" | "loading" | "successful" | "failed";
|
status: 'idle' | 'loading' | 'successful' | 'failed';
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ const initialState: ContestsState = {
|
|||||||
contests: [],
|
contests: [],
|
||||||
selectedContest: null,
|
selectedContest: null,
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
status: "idle",
|
status: 'idle',
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,47 +81,60 @@ const initialState: ContestsState = {
|
|||||||
|
|
||||||
// Получение списка контестов
|
// Получение списка контестов
|
||||||
export const fetchContests = createAsyncThunk(
|
export const fetchContests = createAsyncThunk(
|
||||||
"contests/fetchAll",
|
'contests/fetchAll',
|
||||||
async (
|
async (
|
||||||
params: { page?: number; pageSize?: number; groupId?: number | null } = {},
|
params: {
|
||||||
{ rejectWithValue }
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
groupId?: number | null;
|
||||||
|
} = {},
|
||||||
|
{ rejectWithValue },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const { page = 0, pageSize = 10, groupId } = params;
|
const { page = 0, pageSize = 10, groupId } = params;
|
||||||
const response = await axios.get<ContestsResponse>("/contests", {
|
const response = await axios.get<ContestsResponse>('/contests', {
|
||||||
params: { page, pageSize, groupId },
|
params: { page, pageSize, groupId },
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Failed to fetch contests");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Failed to fetch contests',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Получение одного контеста по ID
|
// Получение одного контеста по ID
|
||||||
export const fetchContestById = createAsyncThunk(
|
export const fetchContestById = createAsyncThunk(
|
||||||
"contests/fetchById",
|
'contests/fetchById',
|
||||||
async (id: number, { rejectWithValue }) => {
|
async (id: number, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get<Contest>(`/contests/${id}`);
|
const response = await axios.get<Contest>(`/contests/${id}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Failed to fetch contest");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Failed to fetch contest',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Создание нового контеста
|
// Создание нового контеста
|
||||||
export const createContest = createAsyncThunk(
|
export const createContest = createAsyncThunk(
|
||||||
"contests/create",
|
'contests/create',
|
||||||
async (contestData: CreateContestBody, { rejectWithValue }) => {
|
async (contestData: CreateContestBody, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<Contest>("/contests", contestData);
|
const response = await axios.post<Contest>(
|
||||||
|
'/contests',
|
||||||
|
contestData,
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Failed to create contest");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Failed to create contest',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
@@ -129,7 +142,7 @@ export const createContest = createAsyncThunk(
|
|||||||
// =====================
|
// =====================
|
||||||
|
|
||||||
const contestsSlice = createSlice({
|
const contestsSlice = createSlice({
|
||||||
name: "contests",
|
name: 'contests',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
clearSelectedContest: (state) => {
|
clearSelectedContest: (state) => {
|
||||||
@@ -139,46 +152,64 @@ const contestsSlice = createSlice({
|
|||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
// fetchContests
|
// fetchContests
|
||||||
builder.addCase(fetchContests.pending, (state) => {
|
builder.addCase(fetchContests.pending, (state) => {
|
||||||
state.status = "loading";
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(fetchContests.fulfilled, (state, action: PayloadAction<ContestsResponse>) => {
|
builder.addCase(
|
||||||
state.status = "successful";
|
fetchContests.fulfilled,
|
||||||
|
(state, action: PayloadAction<ContestsResponse>) => {
|
||||||
|
state.status = 'successful';
|
||||||
state.contests = action.payload.contests;
|
state.contests = action.payload.contests;
|
||||||
state.hasNextPage = action.payload.hasNextPage;
|
state.hasNextPage = action.payload.hasNextPage;
|
||||||
});
|
},
|
||||||
builder.addCase(fetchContests.rejected, (state, action: PayloadAction<any>) => {
|
);
|
||||||
state.status = "failed";
|
builder.addCase(
|
||||||
|
fetchContests.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.status = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// fetchContestById
|
// fetchContestById
|
||||||
builder.addCase(fetchContestById.pending, (state) => {
|
builder.addCase(fetchContestById.pending, (state) => {
|
||||||
state.status = "loading";
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(fetchContestById.fulfilled, (state, action: PayloadAction<Contest>) => {
|
builder.addCase(
|
||||||
state.status = "successful";
|
fetchContestById.fulfilled,
|
||||||
|
(state, action: PayloadAction<Contest>) => {
|
||||||
|
state.status = 'successful';
|
||||||
state.selectedContest = action.payload;
|
state.selectedContest = action.payload;
|
||||||
});
|
},
|
||||||
builder.addCase(fetchContestById.rejected, (state, action: PayloadAction<any>) => {
|
);
|
||||||
state.status = "failed";
|
builder.addCase(
|
||||||
|
fetchContestById.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.status = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// createContest
|
// createContest
|
||||||
builder.addCase(createContest.pending, (state) => {
|
builder.addCase(createContest.pending, (state) => {
|
||||||
state.status = "loading";
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(createContest.fulfilled, (state, action: PayloadAction<Contest>) => {
|
builder.addCase(
|
||||||
state.status = "successful";
|
createContest.fulfilled,
|
||||||
|
(state, action: PayloadAction<Contest>) => {
|
||||||
|
state.status = 'successful';
|
||||||
state.contests.unshift(action.payload);
|
state.contests.unshift(action.payload);
|
||||||
});
|
},
|
||||||
builder.addCase(createContest.rejected, (state, action: PayloadAction<any>) => {
|
);
|
||||||
state.status = "failed";
|
builder.addCase(
|
||||||
|
createContest.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.status = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import axios from "../../axios";
|
import axios from '../../axios';
|
||||||
|
|
||||||
// ─── Типы ────────────────────────────────────────────
|
// ─── Типы ────────────────────────────────────────────
|
||||||
|
|
||||||
type Status = "idle" | "loading" | "successful" | "failed";
|
type Status = 'idle' | 'loading' | 'successful' | 'failed';
|
||||||
|
|
||||||
export interface GroupMember {
|
export interface GroupMember {
|
||||||
userId: number;
|
userId: number;
|
||||||
@@ -38,124 +38,148 @@ const initialState: GroupsState = {
|
|||||||
groups: [],
|
groups: [],
|
||||||
currentGroup: null,
|
currentGroup: null,
|
||||||
statuses: {
|
statuses: {
|
||||||
create: "idle",
|
create: 'idle',
|
||||||
update: "idle",
|
update: 'idle',
|
||||||
delete: "idle",
|
delete: 'idle',
|
||||||
fetchMy: "idle",
|
fetchMy: 'idle',
|
||||||
fetchById: "idle",
|
fetchById: 'idle',
|
||||||
addMember: "idle",
|
addMember: 'idle',
|
||||||
removeMember: "idle",
|
removeMember: 'idle',
|
||||||
},
|
},
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// ─── Async Thunks ─────────────────────────────────────
|
// ─── Async Thunks ─────────────────────────────────────
|
||||||
|
|
||||||
// POST /groups
|
// POST /groups
|
||||||
export const createGroup = createAsyncThunk(
|
export const createGroup = createAsyncThunk(
|
||||||
"groups/createGroup",
|
'groups/createGroup',
|
||||||
async (
|
async (
|
||||||
{ name, description }: { name: string; description: string },
|
{ name, description }: { name: string; description: string },
|
||||||
{ rejectWithValue }
|
{ rejectWithValue },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post("/groups", { name, description });
|
const response = await axios.post('/groups', { name, description });
|
||||||
return response.data as Group;
|
return response.data as Group;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Ошибка при создании группы");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Ошибка при создании группы',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// PUT /groups/{groupId}
|
// PUT /groups/{groupId}
|
||||||
export const updateGroup = createAsyncThunk(
|
export const updateGroup = createAsyncThunk(
|
||||||
"groups/updateGroup",
|
'groups/updateGroup',
|
||||||
async (
|
async (
|
||||||
{ groupId, name, description }: { groupId: number; name: string; description: string },
|
{
|
||||||
{ rejectWithValue }
|
groupId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
}: { groupId: number; name: string; description: string },
|
||||||
|
{ rejectWithValue },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.put(`/groups/${groupId}`, { name, description });
|
const response = await axios.put(`/groups/${groupId}`, {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
});
|
||||||
return response.data as Group;
|
return response.data as Group;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Ошибка при обновлении группы");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Ошибка при обновлении группы',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// DELETE /groups/{groupId}
|
// DELETE /groups/{groupId}
|
||||||
export const deleteGroup = createAsyncThunk(
|
export const deleteGroup = createAsyncThunk(
|
||||||
"groups/deleteGroup",
|
'groups/deleteGroup',
|
||||||
async (groupId: number, { rejectWithValue }) => {
|
async (groupId: number, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/groups/${groupId}`);
|
await axios.delete(`/groups/${groupId}`);
|
||||||
return groupId;
|
return groupId;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Ошибка при удалении группы");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Ошибка при удалении группы',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// GET /groups/my
|
// GET /groups/my
|
||||||
export const fetchMyGroups = createAsyncThunk(
|
export const fetchMyGroups = createAsyncThunk(
|
||||||
"groups/fetchMyGroups",
|
'groups/fetchMyGroups',
|
||||||
async (_, { rejectWithValue }) => {
|
async (_, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get("/groups/my");
|
const response = await axios.get('/groups/my');
|
||||||
return response.data.groups as Group[];
|
return response.data.groups as Group[];
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Ошибка при получении групп");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Ошибка при получении групп',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// GET /groups/{groupId}
|
// GET /groups/{groupId}
|
||||||
export const fetchGroupById = createAsyncThunk(
|
export const fetchGroupById = createAsyncThunk(
|
||||||
"groups/fetchGroupById",
|
'groups/fetchGroupById',
|
||||||
async (groupId: number, { rejectWithValue }) => {
|
async (groupId: number, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/groups/${groupId}`);
|
const response = await axios.get(`/groups/${groupId}`);
|
||||||
return response.data as Group;
|
return response.data as Group;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Ошибка при получении группы");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Ошибка при получении группы',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// POST /groups/members
|
// POST /groups/members
|
||||||
export const addGroupMember = createAsyncThunk(
|
export const addGroupMember = createAsyncThunk(
|
||||||
"groups/addGroupMember",
|
'groups/addGroupMember',
|
||||||
async ({ userId, role }: { userId: number; role: string }, { rejectWithValue }) => {
|
async (
|
||||||
|
{ userId, role }: { userId: number; role: string },
|
||||||
|
{ rejectWithValue },
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
await axios.post("/groups/members", { userId, role });
|
await axios.post('/groups/members', { userId, role });
|
||||||
return { userId, role };
|
return { userId, role };
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Ошибка при добавлении участника");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message ||
|
||||||
|
'Ошибка при добавлении участника',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// DELETE /groups/{groupId}/members/{memberId}
|
// DELETE /groups/{groupId}/members/{memberId}
|
||||||
export const removeGroupMember = createAsyncThunk(
|
export const removeGroupMember = createAsyncThunk(
|
||||||
"groups/removeGroupMember",
|
'groups/removeGroupMember',
|
||||||
async (
|
async (
|
||||||
{ groupId, memberId }: { groupId: number; memberId: number },
|
{ groupId, memberId }: { groupId: number; memberId: number },
|
||||||
{ rejectWithValue }
|
{ rejectWithValue },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/groups/${groupId}/members/${memberId}`);
|
await axios.delete(`/groups/${groupId}/members/${memberId}`);
|
||||||
return { groupId, memberId };
|
return { groupId, memberId };
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Ошибка при удалении участника");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Ошибка при удалении участника',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ─── Slice ────────────────────────────────────────────
|
// ─── Slice ────────────────────────────────────────────
|
||||||
|
|
||||||
const groupsSlice = createSlice({
|
const groupsSlice = createSlice({
|
||||||
name: "groups",
|
name: 'groups',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
clearCurrentGroup: (state) => {
|
clearCurrentGroup: (state) => {
|
||||||
@@ -165,117 +189,160 @@ const groupsSlice = createSlice({
|
|||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
// ─── CREATE GROUP ───
|
// ─── CREATE GROUP ───
|
||||||
builder.addCase(createGroup.pending, (state) => {
|
builder.addCase(createGroup.pending, (state) => {
|
||||||
state.statuses.create = "loading";
|
state.statuses.create = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(createGroup.fulfilled, (state, action: PayloadAction<Group>) => {
|
builder.addCase(
|
||||||
state.statuses.create = "successful";
|
createGroup.fulfilled,
|
||||||
|
(state, action: PayloadAction<Group>) => {
|
||||||
|
state.statuses.create = 'successful';
|
||||||
state.groups.push(action.payload);
|
state.groups.push(action.payload);
|
||||||
});
|
},
|
||||||
builder.addCase(createGroup.rejected, (state, action: PayloadAction<any>) => {
|
);
|
||||||
state.statuses.create = "failed";
|
builder.addCase(
|
||||||
|
createGroup.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.statuses.create = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ─── UPDATE GROUP ───
|
// ─── UPDATE GROUP ───
|
||||||
builder.addCase(updateGroup.pending, (state) => {
|
builder.addCase(updateGroup.pending, (state) => {
|
||||||
state.statuses.update = "loading";
|
state.statuses.update = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(updateGroup.fulfilled, (state, action: PayloadAction<Group>) => {
|
builder.addCase(
|
||||||
state.statuses.update = "successful";
|
updateGroup.fulfilled,
|
||||||
const index = state.groups.findIndex((g) => g.id === action.payload.id);
|
(state, action: PayloadAction<Group>) => {
|
||||||
|
state.statuses.update = 'successful';
|
||||||
|
const index = state.groups.findIndex(
|
||||||
|
(g) => g.id === action.payload.id,
|
||||||
|
);
|
||||||
if (index !== -1) state.groups[index] = action.payload;
|
if (index !== -1) state.groups[index] = action.payload;
|
||||||
if (state.currentGroup?.id === action.payload.id) {
|
if (state.currentGroup?.id === action.payload.id) {
|
||||||
state.currentGroup = action.payload;
|
state.currentGroup = action.payload;
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
builder.addCase(updateGroup.rejected, (state, action: PayloadAction<any>) => {
|
);
|
||||||
state.statuses.update = "failed";
|
builder.addCase(
|
||||||
|
updateGroup.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.statuses.update = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
// ─── DELETE GROUP ───
|
// ─── DELETE GROUP ───
|
||||||
builder.addCase(deleteGroup.pending, (state) => {
|
builder.addCase(deleteGroup.pending, (state) => {
|
||||||
state.statuses.delete = "loading";
|
state.statuses.delete = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(deleteGroup.fulfilled, (state, action: PayloadAction<number>) => {
|
builder.addCase(
|
||||||
state.statuses.delete = "successful";
|
deleteGroup.fulfilled,
|
||||||
state.groups = state.groups.filter((g) => g.id !== action.payload);
|
(state, action: PayloadAction<number>) => {
|
||||||
if (state.currentGroup?.id === action.payload) state.currentGroup = null;
|
state.statuses.delete = 'successful';
|
||||||
});
|
state.groups = state.groups.filter(
|
||||||
builder.addCase(deleteGroup.rejected, (state, action: PayloadAction<any>) => {
|
(g) => g.id !== action.payload,
|
||||||
state.statuses.delete = "failed";
|
);
|
||||||
|
if (state.currentGroup?.id === action.payload)
|
||||||
|
state.currentGroup = null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
builder.addCase(
|
||||||
|
deleteGroup.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.statuses.delete = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ─── FETCH MY GROUPS ───
|
// ─── FETCH MY GROUPS ───
|
||||||
builder.addCase(fetchMyGroups.pending, (state) => {
|
builder.addCase(fetchMyGroups.pending, (state) => {
|
||||||
state.statuses.fetchMy = "loading";
|
state.statuses.fetchMy = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(fetchMyGroups.fulfilled, (state, action: PayloadAction<Group[]>) => {
|
builder.addCase(
|
||||||
state.statuses.fetchMy = "successful";
|
fetchMyGroups.fulfilled,
|
||||||
|
(state, action: PayloadAction<Group[]>) => {
|
||||||
|
state.statuses.fetchMy = 'successful';
|
||||||
state.groups = action.payload;
|
state.groups = action.payload;
|
||||||
});
|
},
|
||||||
builder.addCase(fetchMyGroups.rejected, (state, action: PayloadAction<any>) => {
|
);
|
||||||
state.statuses.fetchMy = "failed";
|
builder.addCase(
|
||||||
|
fetchMyGroups.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.statuses.fetchMy = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ─── FETCH GROUP BY ID ───
|
// ─── FETCH GROUP BY ID ───
|
||||||
builder.addCase(fetchGroupById.pending, (state) => {
|
builder.addCase(fetchGroupById.pending, (state) => {
|
||||||
state.statuses.fetchById = "loading";
|
state.statuses.fetchById = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(fetchGroupById.fulfilled, (state, action: PayloadAction<Group>) => {
|
builder.addCase(
|
||||||
state.statuses.fetchById = "successful";
|
fetchGroupById.fulfilled,
|
||||||
|
(state, action: PayloadAction<Group>) => {
|
||||||
|
state.statuses.fetchById = 'successful';
|
||||||
state.currentGroup = action.payload;
|
state.currentGroup = action.payload;
|
||||||
});
|
},
|
||||||
builder.addCase(fetchGroupById.rejected, (state, action: PayloadAction<any>) => {
|
);
|
||||||
state.statuses.fetchById = "failed";
|
builder.addCase(
|
||||||
|
fetchGroupById.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.statuses.fetchById = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ─── ADD MEMBER ───
|
// ─── ADD MEMBER ───
|
||||||
builder.addCase(addGroupMember.pending, (state) => {
|
builder.addCase(addGroupMember.pending, (state) => {
|
||||||
state.statuses.addMember = "loading";
|
state.statuses.addMember = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(addGroupMember.fulfilled, (state) => {
|
builder.addCase(addGroupMember.fulfilled, (state) => {
|
||||||
state.statuses.addMember = "successful";
|
state.statuses.addMember = 'successful';
|
||||||
});
|
});
|
||||||
builder.addCase(addGroupMember.rejected, (state, action: PayloadAction<any>) => {
|
builder.addCase(
|
||||||
state.statuses.addMember = "failed";
|
addGroupMember.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.statuses.addMember = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ─── REMOVE MEMBER ───
|
// ─── REMOVE MEMBER ───
|
||||||
builder.addCase(removeGroupMember.pending, (state) => {
|
builder.addCase(removeGroupMember.pending, (state) => {
|
||||||
state.statuses.removeMember = "loading";
|
state.statuses.removeMember = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(removeGroupMember.fulfilled, (state, action: PayloadAction<{ groupId: number; memberId: number }>) => {
|
builder.addCase(
|
||||||
state.statuses.removeMember = "successful";
|
removeGroupMember.fulfilled,
|
||||||
if (state.currentGroup && state.currentGroup.id === action.payload.groupId) {
|
(
|
||||||
state.currentGroup.members = state.currentGroup.members.filter(
|
state,
|
||||||
(m) => m.userId !== action.payload.memberId
|
action: PayloadAction<{ groupId: number; memberId: number }>,
|
||||||
|
) => {
|
||||||
|
state.statuses.removeMember = 'successful';
|
||||||
|
if (
|
||||||
|
state.currentGroup &&
|
||||||
|
state.currentGroup.id === action.payload.groupId
|
||||||
|
) {
|
||||||
|
state.currentGroup.members =
|
||||||
|
state.currentGroup.members.filter(
|
||||||
|
(m) => m.userId !== action.payload.memberId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
builder.addCase(removeGroupMember.rejected, (state, action: PayloadAction<any>) => {
|
);
|
||||||
state.statuses.removeMember = "failed";
|
builder.addCase(
|
||||||
|
removeGroupMember.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.statuses.removeMember = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
import { createSlice, PayloadAction} from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
// Типы данных
|
// Типы данных
|
||||||
interface StorState {
|
interface StorState {
|
||||||
menu: {
|
menu: {
|
||||||
activePage: string;
|
activePage: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Инициализация состояния
|
// Инициализация состояния
|
||||||
const initialState: StorState = {
|
const initialState: StorState = {
|
||||||
menu: {
|
menu: {
|
||||||
activePage: "",
|
activePage: '',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Slice
|
// Slice
|
||||||
const storeSlice = createSlice({
|
const storeSlice = createSlice({
|
||||||
name: "store",
|
name: 'store',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setMenuActivePage: (state, activePage: PayloadAction<string>) => {
|
setMenuActivePage: (state, activePage: PayloadAction<string>) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import axios from "../../axios";
|
import axios from '../../axios';
|
||||||
|
|
||||||
// Типы данных
|
// Типы данных
|
||||||
export interface Submit {
|
export interface Submit {
|
||||||
@@ -39,7 +39,7 @@ interface SubmitState {
|
|||||||
submits: Submit[];
|
submits: Submit[];
|
||||||
submitsById: Record<number, MissionSubmit[]>; // ✅ добавлено
|
submitsById: Record<number, MissionSubmit[]>; // ✅ добавлено
|
||||||
currentSubmit?: Submit;
|
currentSubmit?: Submit;
|
||||||
status: "idle" | "loading" | "successful" | "failed";
|
status: 'idle' | 'loading' | 'successful' | 'failed';
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,70 +48,81 @@ const initialState: SubmitState = {
|
|||||||
submits: [],
|
submits: [],
|
||||||
submitsById: {}, // ✅ инициализация
|
submitsById: {}, // ✅ инициализация
|
||||||
currentSubmit: undefined,
|
currentSubmit: undefined,
|
||||||
status: "idle",
|
status: 'idle',
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// AsyncThunk: Отправка решения
|
// AsyncThunk: Отправка решения
|
||||||
export const submitMission = createAsyncThunk(
|
export const submitMission = createAsyncThunk(
|
||||||
"submit/submitMission",
|
'submit/submitMission',
|
||||||
async (submitData: Submit, { rejectWithValue }) => {
|
async (submitData: Submit, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post("/submits", submitData);
|
const response = await axios.post('/submits', submitData);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Submit failed");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Submit failed',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// AsyncThunk: Получить все свои отправки
|
// AsyncThunk: Получить все свои отправки
|
||||||
export const fetchMySubmits = createAsyncThunk(
|
export const fetchMySubmits = createAsyncThunk(
|
||||||
"submit/fetchMySubmits",
|
'submit/fetchMySubmits',
|
||||||
async (_, { rejectWithValue }) => {
|
async (_, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get("/submits/my");
|
const response = await axios.get('/submits/my');
|
||||||
return response.data as Submit[];
|
return response.data as Submit[];
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Failed to fetch submits");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Failed to fetch submits',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// AsyncThunk: Получить конкретную отправку по ID
|
// AsyncThunk: Получить конкретную отправку по ID
|
||||||
export const fetchSubmitById = createAsyncThunk(
|
export const fetchSubmitById = createAsyncThunk(
|
||||||
"submit/fetchSubmitById",
|
'submit/fetchSubmitById',
|
||||||
async (id: number, { rejectWithValue }) => {
|
async (id: number, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/submits/${id}`);
|
const response = await axios.get(`/submits/${id}`);
|
||||||
return response.data as Submit;
|
return response.data as Submit;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Failed to fetch submit");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message || 'Failed to fetch submit',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// ✅ AsyncThunk: Получить отправки для конкретной миссии (новая структура)
|
// ✅ AsyncThunk: Получить отправки для конкретной миссии (новая структура)
|
||||||
export const fetchMySubmitsByMission = createAsyncThunk(
|
export const fetchMySubmitsByMission = createAsyncThunk(
|
||||||
"submit/fetchMySubmitsByMission",
|
'submit/fetchMySubmitsByMission',
|
||||||
async (missionId: number, { rejectWithValue }) => {
|
async (missionId: number, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/submits/my/mission/${missionId}`);
|
const response = await axios.get(
|
||||||
|
`/submits/my/mission/${missionId}`,
|
||||||
|
);
|
||||||
return { missionId, data: response.data as MissionSubmit[] };
|
return { missionId, data: response.data as MissionSubmit[] };
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return rejectWithValue(err.response?.data?.message || "Failed to fetch mission submits");
|
return rejectWithValue(
|
||||||
}
|
err.response?.data?.message ||
|
||||||
|
'Failed to fetch mission submits',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Slice
|
// Slice
|
||||||
const submitSlice = createSlice({
|
const submitSlice = createSlice({
|
||||||
name: "submit",
|
name: 'submit',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
clearCurrentSubmit: (state) => {
|
clearCurrentSubmit: (state) => {
|
||||||
state.currentSubmit = undefined;
|
state.currentSubmit = undefined;
|
||||||
state.status = "idle";
|
state.status = 'idle';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
},
|
},
|
||||||
clearSubmitsByMission: (state, action: PayloadAction<number>) => {
|
clearSubmitsByMission: (state, action: PayloadAction<number>) => {
|
||||||
@@ -121,64 +132,93 @@ const submitSlice = createSlice({
|
|||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
// Отправка решения
|
// Отправка решения
|
||||||
builder.addCase(submitMission.pending, (state) => {
|
builder.addCase(submitMission.pending, (state) => {
|
||||||
state.status = "loading";
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(submitMission.fulfilled, (state, action: PayloadAction<Submit>) => {
|
builder.addCase(
|
||||||
state.status = "successful";
|
submitMission.fulfilled,
|
||||||
|
(state, action: PayloadAction<Submit>) => {
|
||||||
|
state.status = 'successful';
|
||||||
state.submits.push(action.payload);
|
state.submits.push(action.payload);
|
||||||
});
|
},
|
||||||
builder.addCase(submitMission.rejected, (state, action: PayloadAction<any>) => {
|
);
|
||||||
state.status = "failed";
|
builder.addCase(
|
||||||
|
submitMission.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.status = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Получить все свои отправки
|
// Получить все свои отправки
|
||||||
builder.addCase(fetchMySubmits.pending, (state) => {
|
builder.addCase(fetchMySubmits.pending, (state) => {
|
||||||
state.status = "loading";
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(fetchMySubmits.fulfilled, (state, action: PayloadAction<Submit[]>) => {
|
builder.addCase(
|
||||||
state.status = "successful";
|
fetchMySubmits.fulfilled,
|
||||||
|
(state, action: PayloadAction<Submit[]>) => {
|
||||||
|
state.status = 'successful';
|
||||||
state.submits = action.payload;
|
state.submits = action.payload;
|
||||||
});
|
},
|
||||||
builder.addCase(fetchMySubmits.rejected, (state, action: PayloadAction<any>) => {
|
);
|
||||||
state.status = "failed";
|
builder.addCase(
|
||||||
|
fetchMySubmits.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.status = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Получить отправку по ID
|
// Получить отправку по ID
|
||||||
builder.addCase(fetchSubmitById.pending, (state) => {
|
builder.addCase(fetchSubmitById.pending, (state) => {
|
||||||
state.status = "loading";
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(fetchSubmitById.fulfilled, (state, action: PayloadAction<Submit>) => {
|
builder.addCase(
|
||||||
state.status = "successful";
|
fetchSubmitById.fulfilled,
|
||||||
|
(state, action: PayloadAction<Submit>) => {
|
||||||
|
state.status = 'successful';
|
||||||
state.currentSubmit = action.payload;
|
state.currentSubmit = action.payload;
|
||||||
});
|
},
|
||||||
builder.addCase(fetchSubmitById.rejected, (state, action: PayloadAction<any>) => {
|
);
|
||||||
state.status = "failed";
|
builder.addCase(
|
||||||
|
fetchSubmitById.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.status = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ✅ Получить отправки по миссии
|
// ✅ Получить отправки по миссии
|
||||||
builder.addCase(fetchMySubmitsByMission.pending, (state) => {
|
builder.addCase(fetchMySubmitsByMission.pending, (state) => {
|
||||||
state.status = "loading";
|
state.status = 'loading';
|
||||||
state.error = null;
|
state.error = null;
|
||||||
});
|
});
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
fetchMySubmitsByMission.fulfilled,
|
fetchMySubmitsByMission.fulfilled,
|
||||||
(state, action: PayloadAction<{ missionId: number; data: MissionSubmit[] }>) => {
|
(
|
||||||
state.status = "successful";
|
state,
|
||||||
state.submitsById[action.payload.missionId] = action.payload.data;
|
action: PayloadAction<{
|
||||||
}
|
missionId: number;
|
||||||
|
data: MissionSubmit[];
|
||||||
|
}>,
|
||||||
|
) => {
|
||||||
|
state.status = 'successful';
|
||||||
|
state.submitsById[action.payload.missionId] =
|
||||||
|
action.payload.data;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
builder.addCase(fetchMySubmitsByMission.rejected, (state, action: PayloadAction<any>) => {
|
builder.addCase(
|
||||||
state.status = "failed";
|
fetchMySubmitsByMission.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.status = 'failed';
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { clearCurrentSubmit, clearSubmitsByMission } = submitSlice.actions;
|
export const { clearCurrentSubmit, clearSubmitsByMission } =
|
||||||
|
submitSlice.actions;
|
||||||
export const submitReducer = submitSlice.reducer;
|
export const submitReducer = submitSlice.reducer;
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { configureStore } from "@reduxjs/toolkit";
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import { authReducer } from "./slices/auth";
|
import { authReducer } from './slices/auth';
|
||||||
import { storeReducer } from "./slices/store";
|
import { storeReducer } from './slices/store';
|
||||||
import { missionsReducer } from "./slices/missions";
|
import { missionsReducer } from './slices/missions';
|
||||||
import { submitReducer } from "./slices/submit";
|
import { submitReducer } from './slices/submit';
|
||||||
import { contestsReducer } from "./slices/contests";
|
import { contestsReducer } from './slices/contests';
|
||||||
import { groupsReducer } from "./slices/groups";
|
import { groupsReducer } from './slices/groups';
|
||||||
|
|
||||||
|
|
||||||
// использование
|
// использование
|
||||||
// import { useAppDispatch, useAppSelector } from '../redux/hooks';
|
// import { useAppDispatch, useAppSelector } from '../redux/hooks';
|
||||||
@@ -15,7 +14,6 @@ import { groupsReducer } from "./slices/groups";
|
|||||||
// const dispatch = useAppDispatch();
|
// const dispatch = useAppDispatch();
|
||||||
// const user = useAppSelector((state) => state.user);
|
// const user = useAppSelector((state) => state.user);
|
||||||
|
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
//user: userReducer,
|
//user: userReducer,
|
||||||
|
|||||||
@@ -2,14 +2,13 @@
|
|||||||
@import 'tailwindcss/components';
|
@import 'tailwindcss/components';
|
||||||
@import 'tailwindcss/utilities';
|
@import 'tailwindcss/utilities';
|
||||||
|
|
||||||
@import "./latex-container.css";
|
@import './latex-container.css';
|
||||||
|
|
||||||
* {
|
* {
|
||||||
-webkit-tap-highlight-color: transparent; /* Отключаем выделение синим при тапе на телефоне*/
|
-webkit-tap-highlight-color: transparent; /* Отключаем выделение синим при тапе на телефоне*/
|
||||||
/* outline: 1px solid green; */
|
/* outline: 1px solid green; */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -40,7 +39,6 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Общий контейнер полосы прокрутки */
|
/* Общий контейнер полосы прокрутки */
|
||||||
.thin-scrollbar::-webkit-scrollbar {
|
.thin-scrollbar::-webkit-scrollbar {
|
||||||
width: 4px; /* ширина вертикального */
|
width: 4px; /* ширина вертикального */
|
||||||
@@ -58,7 +56,6 @@ body {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Общий контейнер полосы прокрутки */
|
/* Общий контейнер полосы прокрутки */
|
||||||
.medium-scrollbar::-webkit-scrollbar {
|
.medium-scrollbar::-webkit-scrollbar {
|
||||||
width: 8px; /* ширина вертикального */
|
width: 8px; /* ширина вертикального */
|
||||||
@@ -76,8 +73,6 @@ body {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Общий контейнер полосы прокрутки */
|
/* Общий контейнер полосы прокрутки */
|
||||||
.thin-dark-scrollbar::-webkit-scrollbar {
|
.thin-dark-scrollbar::-webkit-scrollbar {
|
||||||
width: 4px; /* ширина вертикального */
|
width: 4px; /* ширина вертикального */
|
||||||
@@ -95,9 +90,6 @@ body {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
.latex-container p {
|
.latex-container p {
|
||||||
text-align: justify; /* выравнивание по ширине */
|
text-align: justify; /* выравнивание по ширине */
|
||||||
text-justify: inter-word;
|
text-justify: inter-word;
|
||||||
@@ -11,7 +10,7 @@
|
|||||||
padding-left: 1.5em; /* отступ для нумерации */
|
padding-left: 1.5em; /* отступ для нумерации */
|
||||||
margin: 0.5em 0; /* небольшой отступ сверху и снизу */
|
margin: 0.5em 0; /* небольшой отступ сверху и снизу */
|
||||||
line-height: 1.5; /* удобный межстрочный интервал */
|
line-height: 1.5; /* удобный межстрочный интервал */
|
||||||
font-family: "Inter", sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,8 +18,7 @@
|
|||||||
margin-bottom: 0.4em; /* расстояние между пунктами */
|
margin-bottom: 0.4em; /* расстояние между пунктами */
|
||||||
}
|
}
|
||||||
|
|
||||||
.latex-container .section-title{
|
.latex-container .section-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
@import 'tailwindcss/base';
|
@import 'tailwindcss/base';
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root[data-theme~="dark"] {
|
:root[data-theme~='dark'] {
|
||||||
--color-liquid-brightmain: #00DBD9;
|
--color-liquid-brightmain: #00dbd9;
|
||||||
--color-liquid-darkmain: #075867;
|
--color-liquid-darkmain: #075867;
|
||||||
--color-liquid-darker: #141515;
|
--color-liquid-darker: #141515;
|
||||||
--color-liquid-background: #202222;
|
--color-liquid-background: #202222;
|
||||||
--color-liquid-lighter: #2A2E2F;
|
--color-liquid-lighter: #2a2e2f;
|
||||||
--color-liquid-white: #EDF6F7;
|
--color-liquid-white: #edf6f7;
|
||||||
--color-liquid-red: #F13E5F;
|
--color-liquid-red: #f13e5f;
|
||||||
--color-liquid-green: #10BE59;
|
--color-liquid-green: #10be59;
|
||||||
--color-liquid-light: #576466;
|
--color-liquid-light: #576466;
|
||||||
--color-liquid-orange: #FF951B;
|
--color-liquid-orange: #ff951b;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--color-liquid-brightmain: #00DBD9;
|
--color-liquid-brightmain: #00dbd9;
|
||||||
--color-liquid-darkmain: #075867;
|
--color-liquid-darkmain: #075867;
|
||||||
--color-liquid-darker: #141515;
|
--color-liquid-darker: #141515;
|
||||||
--color-liquid-background: #202222;
|
--color-liquid-background: #202222;
|
||||||
--color-liquid-lighter: #2A2E2F;
|
--color-liquid-lighter: #2a2e2f;
|
||||||
--color-liquid-white: #EDF6F7;
|
--color-liquid-white: #edf6f7;
|
||||||
--color-liquid-red: #F13E5F;
|
--color-liquid-red: #f13e5f;
|
||||||
--color-liquid-green: #10BE59;
|
--color-liquid-green: #10be59;
|
||||||
--color-liquid-light: #576466;
|
--color-liquid-light: #576466;
|
||||||
--color-liquid-orange: #FF951B;
|
--color-liquid-orange: #ff951b;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from 'react';
|
||||||
import axios from "../../axios";
|
import axios from '../../axios';
|
||||||
import "highlight.js/styles/github-dark.css";
|
import 'highlight.js/styles/github-dark.css';
|
||||||
|
|
||||||
import MarkdownPreview from "./MarckDownPreview";
|
|
||||||
|
|
||||||
|
import MarkdownPreview from './MarckDownPreview';
|
||||||
|
|
||||||
interface MarkdownEditorProps {
|
interface MarkdownEditorProps {
|
||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MarkdownEditor: FC<MarkdownEditorProps> = ({ defaultValue, onChange }) => {
|
const MarkdownEditor: FC<MarkdownEditorProps> = ({
|
||||||
const [markdown, setMarkdown] = useState<string>(defaultValue || `# 🌙 Добро пожаловать в Markdown-редактор
|
defaultValue,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const [markdown, setMarkdown] = useState<string>(
|
||||||
|
defaultValue ||
|
||||||
|
`# 🌙 Добро пожаловать в Markdown-редактор
|
||||||
|
|
||||||
Добро пожаловать в **Markdown-редактор**!
|
Добро пожаловать в **Markdown-редактор**!
|
||||||
Здесь ты можешь писать в формате Markdown и видеть результат **в реальном времени** 👇
|
Здесь ты можешь писать в формате Markdown и видеть результат **в реальном времени** 👇
|
||||||
@@ -205,34 +209,42 @@ print(greet("Мир"))
|
|||||||
|
|
||||||
**🖤 Конец демонстрации. Спасибо, что используешь Markdown-редактор!**
|
**🖤 Конец демонстрации. Спасибо, что используешь Markdown-редактор!**
|
||||||
|
|
||||||
`);
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onChange(markdown);
|
onChange(markdown);
|
||||||
}, [markdown]);
|
}, [markdown]);
|
||||||
|
|
||||||
// Обработчик вставки
|
// Обработчик вставки
|
||||||
const handlePaste = async (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
const handlePaste = async (
|
||||||
|
e: React.ClipboardEvent<HTMLTextAreaElement>,
|
||||||
|
) => {
|
||||||
const items = e.clipboardData.items;
|
const items = e.clipboardData.items;
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.type.startsWith("image/")) {
|
if (item.type.startsWith('image/')) {
|
||||||
e.preventDefault(); // предотвращаем вставку картинки как текста
|
e.preventDefault(); // предотвращаем вставку картинки как текста
|
||||||
|
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append('file', file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post("/media/upload", formData, {
|
const response = await axios.post(
|
||||||
headers: { "Content-Type": "multipart/form-data" },
|
'/media/upload',
|
||||||
});
|
formData,
|
||||||
|
{
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const imageUrl = response.data.url;
|
const imageUrl = response.data.url;
|
||||||
// Вставляем ссылку на картинку в текст
|
// Вставляем ссылку на картинку в текст
|
||||||
const cursorPos = (e.target as HTMLTextAreaElement).selectionStart;
|
const cursorPos = (e.target as HTMLTextAreaElement)
|
||||||
|
.selectionStart;
|
||||||
const newText =
|
const newText =
|
||||||
markdown.slice(0, cursorPos) +
|
markdown.slice(0, cursorPos) +
|
||||||
`<img src=\"${imageUrl}\" alt=\"img\"/>` +
|
`<img src=\"${imageUrl}\" alt=\"img\"/>` +
|
||||||
@@ -240,7 +252,7 @@ print(greet("Мир"))
|
|||||||
|
|
||||||
setMarkdown(newText);
|
setMarkdown(newText);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Ошибка загрузки изображения:", err);
|
console.error('Ошибка загрузки изображения:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,15 +263,22 @@ print(greet("Мир"))
|
|||||||
{/* Предпросмотр */}
|
{/* Предпросмотр */}
|
||||||
<div className="overflow-y-auto min-h-0 overflow-hidden">
|
<div className="overflow-y-auto min-h-0 overflow-hidden">
|
||||||
<div className="p-4 border-r border-gray-700 flex flex-col h-full">
|
<div className="p-4 border-r border-gray-700 flex flex-col h-full">
|
||||||
<h2 className="text-lg font-semibold mb-3 text-gray-100">👀 Предпросмотр</h2>
|
<h2 className="text-lg font-semibold mb-3 text-gray-100">
|
||||||
<MarkdownPreview content={markdown} className="h-[calc(100%-40px)]"/>
|
👀 Предпросмотр
|
||||||
|
</h2>
|
||||||
|
<MarkdownPreview
|
||||||
|
content={markdown}
|
||||||
|
className="h-[calc(100%-40px)]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Редактор */}
|
{/* Редактор */}
|
||||||
<div className="overflow-y-auto min-h-0 overflow-hidden">
|
<div className="overflow-y-auto min-h-0 overflow-hidden">
|
||||||
<div className="p-4 border-r border-gray-700 flex flex-col h-full">
|
<div className="p-4 border-r border-gray-700 flex flex-col h-full">
|
||||||
<h2 className="text-lg font-semibold mb-3 text-gray-100">📝 Редактор</h2>
|
<h2 className="text-lg font-semibold mb-3 text-gray-100">
|
||||||
|
📝 Редактор
|
||||||
|
</h2>
|
||||||
<textarea
|
<textarea
|
||||||
value={markdown}
|
value={markdown}
|
||||||
onChange={(e) => setMarkdown(e.target.value)}
|
onChange={(e) => setMarkdown(e.target.value)}
|
||||||
|
|||||||
@@ -1,29 +1,39 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { arrowLeft } from "../../assets/icons/header";
|
import { arrowLeft } from '../../assets/icons/header';
|
||||||
import { Logo } from "../../assets/logos";
|
import { Logo } from '../../assets/logos';
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
backUrl?: string;
|
backUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Header: React.FC<HeaderProps> = ({ backUrl = '/home/articles' }) => {
|
||||||
const Header: React.FC<HeaderProps> = ({
|
|
||||||
backUrl="/home/articles",
|
|
||||||
}) => {
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
<header className="w-full h-[60px] flex items-center px-4 gap-[20px]">
|
<header className="w-full h-[60px] flex items-center px-4 gap-[20px]">
|
||||||
<img src={Logo} alt="Logo" className="h-[28px] w-auto cursor-pointer" onClick={() => { navigate("/home") }} />
|
<img
|
||||||
|
src={Logo}
|
||||||
|
alt="Logo"
|
||||||
|
className="h-[28px] w-auto cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/home');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<img src={arrowLeft} alt="back" className="h-[24px] w-[24px] cursor-pointer" onClick={() => { navigate(backUrl) }} />
|
<img
|
||||||
|
src={arrowLeft}
|
||||||
|
alt="back"
|
||||||
|
className="h-[24px] w-[24px] cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
navigate(backUrl);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* <div className="flex gap-[10px]">
|
{/* <div className="flex gap-[10px]">
|
||||||
<img src={chevroneLeft} alt="back" className="h-[24px] w-[24px] cursor-pointer" onClick={() => { navigate(`/mission/${missionId - 1}`) }} />
|
<img src={chevroneLeft} alt="back" className="h-[24px] w-[24px] cursor-pointer" onClick={() => { navigate(`/mission/${missionId - 1}`) }} />
|
||||||
<span>{missionId}</span>
|
<span>{missionId}</span>
|
||||||
<img src={chevroneRight} alt="back" className="h-[24px] w-[24px] cursor-pointer" onClick={() => { navigate(`/mission/${missionId + 1}`) }} />
|
<img src={chevroneRight} alt="back" className="h-[24px] w-[24px] cursor-pointer" onClick={() => { navigate(`/mission/${missionId + 1}`) }} />
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { FC } from "react";
|
import { FC } from 'react';
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from 'remark-gfm';
|
||||||
import rehypeHighlight from "rehype-highlight";
|
import rehypeHighlight from 'rehype-highlight';
|
||||||
import rehypeRaw from "rehype-raw";
|
import rehypeRaw from 'rehype-raw';
|
||||||
import rehypeSanitize from "rehype-sanitize";
|
import rehypeSanitize from 'rehype-sanitize';
|
||||||
import "highlight.js/styles/github-dark.css";
|
import 'highlight.js/styles/github-dark.css';
|
||||||
|
|
||||||
import { defaultSchema } from "hast-util-sanitize";
|
import { defaultSchema } from 'hast-util-sanitize';
|
||||||
import { cn } from "../../lib/cn";
|
import { cn } from '../../lib/cn';
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
...defaultSchema,
|
...defaultSchema,
|
||||||
@@ -15,9 +15,9 @@ const schema = {
|
|||||||
...defaultSchema.attributes,
|
...defaultSchema.attributes,
|
||||||
div: [
|
div: [
|
||||||
...(defaultSchema.attributes?.div || []),
|
...(defaultSchema.attributes?.div || []),
|
||||||
["style"] // разрешаем атрибут style на div
|
['style'], // разрешаем атрибут style на div
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
interface MarkdownPreviewProps {
|
interface MarkdownPreviewProps {
|
||||||
@@ -25,13 +25,25 @@ interface MarkdownPreviewProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MarkdownPreview: FC<MarkdownPreviewProps> = ({ content, className="" }) => {
|
const MarkdownPreview: FC<MarkdownPreviewProps> = ({
|
||||||
|
content,
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex-1 bg-[#161b22] rounded-lg shadow-lg p-6", className)}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex-1 bg-[#161b22] rounded-lg shadow-lg p-6',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="prose prose-invert max-w-none h-full overflow-auto pr-4 medium-scrollbar">
|
<div className="prose prose-invert max-w-none h-full overflow-auto pr-4 medium-scrollbar">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
rehypePlugins={[rehypeRaw, [rehypeSanitize, schema], rehypeHighlight]}
|
rehypePlugins={[
|
||||||
|
rehypeRaw,
|
||||||
|
[rehypeSanitize, schema],
|
||||||
|
rehypeHighlight,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cn } from "../../../lib/cn";
|
import { cn } from '../../../lib/cn';
|
||||||
|
|
||||||
export interface ArticleItemProps {
|
export interface ArticleItemProps {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -6,17 +6,17 @@ export interface ArticleItemProps {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ArticleItem: React.FC<ArticleItemProps> = ({
|
const ArticleItem: React.FC<ArticleItemProps> = ({ id, name, tags }) => {
|
||||||
id, name, tags
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("w-full relative rounded-[10px] text-liquid-white mb-[20px]",
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-full relative rounded-[10px] text-liquid-white mb-[20px]',
|
||||||
// type == "first" ? "bg-liquid-lighter" : "bg-liquid-background",
|
// type == "first" ? "bg-liquid-lighter" : "bg-liquid-background",
|
||||||
"gap-[20px] px-[20px] py-[10px] box-border ",
|
'gap-[20px] px-[20px] py-[10px] box-border ',
|
||||||
"border-b-[1px] border-b-liquid-lighter",
|
'border-b-[1px] border-b-liquid-lighter',
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
<div className="h-[23px] flex ">
|
<div className="h-[23px] flex ">
|
||||||
|
|
||||||
<div className="text-[18px] font-bold w-[60px] mr-[20px] flex items-center">
|
<div className="text-[18px] font-bold w-[60px] mr-[20px] flex items-center">
|
||||||
#{id}
|
#{id}
|
||||||
</div>
|
</div>
|
||||||
@@ -25,15 +25,18 @@ const ArticleItem: React.FC<ArticleItemProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[14px] flex text-liquid-light gap-[10px] mt-[10px]">
|
<div className="text-[14px] flex text-liquid-light gap-[10px] mt-[10px]">
|
||||||
{tags.map((v, i) =>
|
{tags.map((v, i) => (
|
||||||
<div key={i} className={cn(
|
<div
|
||||||
"rounded-full px-[16px] py-[8px] bg-liquid-lighter",
|
key={i}
|
||||||
v == "Sertificated" && "text-liquid-green")}>
|
className={cn(
|
||||||
|
'rounded-full px-[16px] py-[8px] bg-liquid-lighter',
|
||||||
|
v == 'Sertificated' && 'text-liquid-green',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{v}
|
{v}
|
||||||
</div>
|
</div>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from 'react';
|
||||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||||
import { useAppDispatch } from "../../../redux/hooks";
|
import { useAppDispatch } from '../../../redux/hooks';
|
||||||
import ArticleItem from "./ArticleItem";
|
import ArticleItem from './ArticleItem';
|
||||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
||||||
export interface Article {
|
export interface Article {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -12,159 +11,152 @@ export interface Article {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const Articles = () => {
|
const Articles = () => {
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const articles: Article[] = [
|
const articles: Article[] = [
|
||||||
{
|
{
|
||||||
"id": 1,
|
id: 1,
|
||||||
"name": "Todo List App",
|
name: 'Todo List App',
|
||||||
"tags": ["Sertificated", "state", "list"],
|
tags: ['Sertificated', 'state', 'list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
id: 2,
|
||||||
"name": "Search Filter Component",
|
name: 'Search Filter Component',
|
||||||
"tags": ["filter", "props", "hooks"],
|
tags: ['filter', 'props', 'hooks'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
id: 3,
|
||||||
"name": "User Card List",
|
name: 'User Card List',
|
||||||
"tags": ["components", "props", "array"],
|
tags: ['components', 'props', 'array'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
id: 4,
|
||||||
"name": "Theme Switcher",
|
name: 'Theme Switcher',
|
||||||
"tags": ["Sertificated", "theme", "hooks"],
|
tags: ['Sertificated', 'theme', 'hooks'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
id: 2,
|
||||||
"name": "Search Filter Component",
|
name: 'Search Filter Component',
|
||||||
"tags": ["filter", "props", "hooks"],
|
tags: ['filter', 'props', 'hooks'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
id: 3,
|
||||||
"name": "User Card List",
|
name: 'User Card List',
|
||||||
"tags": ["components", "props", "array"],
|
tags: ['components', 'props', 'array'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
id: 4,
|
||||||
"name": "Theme Switcher",
|
name: 'Theme Switcher',
|
||||||
"tags": ["Sertificated", "theme", "hooks"],
|
tags: ['Sertificated', 'theme', 'hooks'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
id: 2,
|
||||||
"name": "Search Filter Component",
|
name: 'Search Filter Component',
|
||||||
"tags": ["filter", "props", "hooks"],
|
tags: ['filter', 'props', 'hooks'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
id: 3,
|
||||||
"name": "User Card List",
|
name: 'User Card List',
|
||||||
"tags": ["components", "props", "array"],
|
tags: ['components', 'props', 'array'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
id: 4,
|
||||||
"name": "Theme Switcher",
|
name: 'Theme Switcher',
|
||||||
"tags": ["Sertificated", "theme", "hooks"],
|
tags: ['Sertificated', 'theme', 'hooks'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
id: 2,
|
||||||
"name": "Search Filter Component",
|
name: 'Search Filter Component',
|
||||||
"tags": ["filter", "props", "hooks"],
|
tags: ['filter', 'props', 'hooks'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
id: 3,
|
||||||
"name": "User Card List",
|
name: 'User Card List',
|
||||||
"tags": ["components", "props", "array"],
|
tags: ['components', 'props', 'array'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
id: 4,
|
||||||
"name": "Theme Switcher",
|
name: 'Theme Switcher',
|
||||||
"tags": ["Sertificated", "theme", "hooks"],
|
tags: ['Sertificated', 'theme', 'hooks'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
id: 2,
|
||||||
"name": "Search Filter Component",
|
name: 'Search Filter Component',
|
||||||
"tags": ["filter", "props", "hooks"],
|
tags: ['filter', 'props', 'hooks'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
id: 3,
|
||||||
"name": "User Card List",
|
name: 'User Card List',
|
||||||
"tags": ["components", "props", "array"],
|
tags: ['components', 'props', 'array'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
id: 4,
|
||||||
"name": "Theme Switcher",
|
name: 'Theme Switcher',
|
||||||
"tags": ["Sertificated", "theme", "hooks"],
|
tags: ['Sertificated', 'theme', 'hooks'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
id: 2,
|
||||||
"name": "Search Filter Component",
|
name: 'Search Filter Component',
|
||||||
"tags": ["filter", "props", "hooks"],
|
tags: ['filter', 'props', 'hooks'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
id: 3,
|
||||||
"name": "User Card List",
|
name: 'User Card List',
|
||||||
"tags": ["components", "props", "array"],
|
tags: ['components', 'props', 'array'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
id: 4,
|
||||||
"name": "Theme Switcher",
|
name: 'Theme Switcher',
|
||||||
"tags": ["Sertificated", "theme", "hooks"],
|
tags: ['Sertificated', 'theme', 'hooks'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
id: 2,
|
||||||
"name": "Search Filter Component",
|
name: 'Search Filter Component',
|
||||||
"tags": ["filter", "props", "hooks"],
|
tags: ['filter', 'props', 'hooks'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
id: 3,
|
||||||
"name": "User Card List",
|
name: 'User Card List',
|
||||||
"tags": ["components", "props", "array"],
|
tags: ['components', 'props', 'array'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
id: 4,
|
||||||
"name": "Theme Switcher",
|
name: 'Theme Switcher',
|
||||||
"tags": ["Sertificated", "theme", "hooks"],
|
tags: ['Sertificated', 'theme', 'hooks'],
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(setMenuActivePage("articles"))
|
dispatch(setMenuActivePage('articles'));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className=" h-full w-full box-border p-[20px] pt-[20px]">
|
<div className=" h-full w-full box-border p-[20px] pt-[20px]">
|
||||||
<div className="h-full box-border">
|
<div className="h-full box-border">
|
||||||
|
|
||||||
<div className="relative flex items-center mb-[20px]">
|
<div className="relative flex items-center mb-[20px]">
|
||||||
<div className="h-[50px] text-[40px] font-bold text-liquid-white flex items-center">
|
<div className="h-[50px] text-[40px] font-bold text-liquid-white flex items-center">
|
||||||
Статьи
|
Статьи
|
||||||
</div>
|
</div>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
onClick={() => {navigate("/article/create")}}
|
onClick={() => {
|
||||||
|
navigate('/article/create');
|
||||||
|
}}
|
||||||
text="Создать статью"
|
text="Создать статью"
|
||||||
className="absolute right-0"
|
className="absolute right-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-liquid-lighter h-[50px] mb-[20px]">
|
<div className="bg-liquid-lighter h-[50px] mb-[20px]"></div>
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
{articles.map((v, i) => (
|
{articles.map((v, i) => (
|
||||||
<ArticleItem key={i} {...v} />
|
<ArticleItem key={i} {...v} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>pages</div>
|
||||||
<div>
|
|
||||||
pages
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,37 +1,36 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
import { PrimaryButton } from "../../../components/button/PrimaryButton";
|
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||||
import { Input } from "../../../components/input/Input";
|
import { Input } from '../../../components/input/Input';
|
||||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { loginUser } from "../../../redux/slices/auth";
|
import { loginUser } from '../../../redux/slices/auth';
|
||||||
// import { cn } from "../../../lib/cn";
|
// import { cn } from "../../../lib/cn";
|
||||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||||
import { Balloon } from "../../../assets/icons/auth";
|
import { Balloon } from '../../../assets/icons/auth';
|
||||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||||
import { googleLogo } from "../../../assets/icons/input";
|
import { googleLogo } from '../../../assets/icons/input';
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [username, setUsername] = useState<string>("");
|
const [username, setUsername] = useState<string>('');
|
||||||
const [password, setPassword] = useState<string>("");
|
const [password, setPassword] = useState<string>('');
|
||||||
const [submitClicked, setSubmitClicked] = useState<boolean>(false);
|
const [submitClicked, setSubmitClicked] = useState<boolean>(false);
|
||||||
|
|
||||||
const { status, jwt } = useAppSelector((state) => state.auth);
|
const { status, jwt } = useAppSelector((state) => state.auth);
|
||||||
|
|
||||||
|
|
||||||
// const [err, setErr] = useState<string>("");
|
// const [err, setErr] = useState<string>("");
|
||||||
|
|
||||||
// После успешного логина
|
// После успешного логина
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(setMenuActivePage("account"))
|
dispatch(setMenuActivePage('account'));
|
||||||
console.log(submitClicked);
|
console.log(submitClicked);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
navigate("/home/offices"); // или другая страница после входа
|
navigate('/home/offices'); // или другая страница после входа
|
||||||
}
|
}
|
||||||
}, [jwt]);
|
}, [jwt]);
|
||||||
|
|
||||||
@@ -60,51 +59,72 @@ const Login = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
<Input name="login" autocomplete="login" className="mt-[10px]" type="text" label="Логин" onChange={(v) => { setUsername(v) }} placeholder="login" />
|
name="login"
|
||||||
<Input name="password" autocomplete="password" className="mt-[10px]" type="password" label="Пароль" onChange={(v) => { setPassword(v) }} placeholder="abCD1234" />
|
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]">
|
<div className="flex justify-end mt-[10px]">
|
||||||
<Link
|
<Link
|
||||||
to={""}
|
to={''}
|
||||||
className={"text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline "}>
|
className={
|
||||||
|
'text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline '
|
||||||
|
}
|
||||||
|
>
|
||||||
Забыли пароль?
|
Забыли пароль?
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="mt-[10px]">
|
<div className="mt-[10px]">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
className="w-full mb-[8px]"
|
className="w-full mb-[8px]"
|
||||||
onClick={handleLogin}
|
onClick={handleLogin}
|
||||||
text={status === "loading" ? "Вход..." : "Вход"}
|
text={status === 'loading' ? 'Вход...' : 'Вход'}
|
||||||
disabled={status === "loading"}
|
disabled={status === 'loading'}
|
||||||
/>
|
/>
|
||||||
<SecondaryButton
|
<SecondaryButton className="w-full" onClick={() => {}}>
|
||||||
className="w-full"
|
|
||||||
onClick={() => { }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<img src={googleLogo} className="h-[24px] w-[24px] mr-[15px]" />
|
<img
|
||||||
|
src={googleLogo}
|
||||||
|
className="h-[24px] w-[24px] mr-[15px]"
|
||||||
|
/>
|
||||||
Вход с Google
|
Вход с Google
|
||||||
</div>
|
</div>
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className="flex justify-center mt-[10px]">
|
<div className="flex justify-center mt-[10px]">
|
||||||
<span>
|
<span>
|
||||||
Нет аккаунта? <Link
|
Нет аккаунта?{' '}
|
||||||
to={"/home/register"}
|
<Link
|
||||||
className={"text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline "}>
|
to={'/home/register'}
|
||||||
|
className={
|
||||||
|
'text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline '
|
||||||
|
}
|
||||||
|
>
|
||||||
Регистрация
|
Регистрация
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from 'react';
|
||||||
import { PrimaryButton } from "../../../components/button/PrimaryButton";
|
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||||
import { Input } from "../../../components/input/Input";
|
import { Input } from '../../../components/input/Input';
|
||||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { registerUser } from "../../../redux/slices/auth";
|
import { registerUser } from '../../../redux/slices/auth';
|
||||||
// import { cn } from "../../../lib/cn";
|
// import { cn } from "../../../lib/cn";
|
||||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||||
import { Balloon } from "../../../assets/icons/auth";
|
import { Balloon } from '../../../assets/icons/auth';
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from 'react-router-dom';
|
||||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||||
import { Checkbox } from "../../../components/checkbox/Checkbox";
|
import { Checkbox } from '../../../components/checkbox/Checkbox';
|
||||||
import { googleLogo } from "../../../assets/icons/input";
|
import { googleLogo } from '../../../assets/icons/input';
|
||||||
|
|
||||||
|
|
||||||
const Register = () => {
|
const Register = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [username, setUsername] = useState<string>("");
|
const [username, setUsername] = useState<string>('');
|
||||||
const [email, setEmail] = useState<string>("");
|
const [email, setEmail] = useState<string>('');
|
||||||
const [password, setPassword] = useState<string>("");
|
const [password, setPassword] = useState<string>('');
|
||||||
const [confirmPassword, setConfirmPassword] = useState<string>("");
|
const [confirmPassword, setConfirmPassword] = useState<string>('');
|
||||||
const [submitClicked, setSubmitClicked] = useState<boolean>(false);
|
const [submitClicked, setSubmitClicked] = useState<boolean>(false);
|
||||||
|
|
||||||
const { status, jwt } = useAppSelector((state) => state.auth);
|
const { status, jwt } = useAppSelector((state) => state.auth);
|
||||||
@@ -28,12 +27,12 @@ const Register = () => {
|
|||||||
// После успешной регистрации — переход в систему
|
// После успешной регистрации — переход в систему
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(setMenuActivePage("account"))
|
dispatch(setMenuActivePage('account'));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
navigate("/home");
|
navigate('/home');
|
||||||
}
|
}
|
||||||
console.log(submitClicked);
|
console.log(submitClicked);
|
||||||
}, [jwt]);
|
}, [jwt]);
|
||||||
@@ -63,61 +62,105 @@ const Register = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
<Input name="email" autocomplete="email" className="mt-[10px]" type="email" label="Почта" onChange={(v) => {setEmail(v)}} placeholder="example@gmail.com" />
|
name="email"
|
||||||
<Input name="login" autocomplete="login" className="mt-[10px]" type="text" label="Логин пользователя" onChange={(v) => {setUsername(v)}} placeholder="login" />
|
autocomplete="email"
|
||||||
<Input name="password" autocomplete="password" className="mt-[10px]" type="password" label="Пароль" onChange={(v) => {setPassword(v)}} placeholder="abCD1234" />
|
className="mt-[10px]"
|
||||||
<Input name="confirm-password" autocomplete="confirm-password" className="mt-[10px]" type="password" label="Повторите пароль" onChange={(v) => {setConfirmPassword(v)}} placeholder="abCD1234" />
|
type="email"
|
||||||
|
label="Почта"
|
||||||
|
onChange={(v) => {
|
||||||
|
setEmail(v);
|
||||||
|
}}
|
||||||
|
placeholder="example@gmail.com"
|
||||||
|
/>
|
||||||
|
<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="confirm-password"
|
||||||
|
autocomplete="confirm-password"
|
||||||
|
className="mt-[10px]"
|
||||||
|
type="password"
|
||||||
|
label="Повторите пароль"
|
||||||
|
onChange={(v) => {
|
||||||
|
setConfirmPassword(v);
|
||||||
|
}}
|
||||||
|
placeholder="abCD1234"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className=" flex items-center mt-[10px] h-[24px]">
|
<div className=" flex items-center mt-[10px] h-[24px]">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
onChange={(value: boolean) => { value; }}
|
onChange={(value: boolean) => {
|
||||||
|
value;
|
||||||
|
}}
|
||||||
className="p-0 w-fit m-[2.75px]"
|
className="p-0 w-fit m-[2.75px]"
|
||||||
size="md"
|
size="md"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="default" />
|
variant="default"
|
||||||
|
/>
|
||||||
<span className="text-[14px] font-medium text-liquid-light h-[18px] ml-[10px]">
|
<span className="text-[14px] font-medium text-liquid-light h-[18px] ml-[10px]">
|
||||||
Я принимаю <Link
|
Я принимаю{' '}
|
||||||
to={"/home"}
|
<Link to={'/home'} className={' underline'}>
|
||||||
className={" underline"}
|
|
||||||
>
|
|
||||||
политику конфиденциальности
|
политику конфиденциальности
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-[10px]">
|
<div className="mt-[10px]">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
className="w-full mb-[8px]"
|
className="w-full mb-[8px]"
|
||||||
onClick={() => handleRegister()}
|
onClick={() => handleRegister()}
|
||||||
text={status === "loading" ? "Регистрация..." : "Регистрация"}
|
text={
|
||||||
disabled={status === "loading"}
|
status === 'loading'
|
||||||
|
? 'Регистрация...'
|
||||||
|
: 'Регистрация'
|
||||||
|
}
|
||||||
|
disabled={status === 'loading'}
|
||||||
/>
|
/>
|
||||||
<SecondaryButton
|
<SecondaryButton className="w-full" onClick={() => {}}>
|
||||||
className="w-full"
|
|
||||||
onClick={() => { }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<img src={googleLogo} className="h-[24px] w-[24px] mr-[15px]" />
|
<img
|
||||||
|
src={googleLogo}
|
||||||
|
className="h-[24px] w-[24px] mr-[15px]"
|
||||||
|
/>
|
||||||
Регистрация с Google
|
Регистрация с Google
|
||||||
</div>
|
</div>
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="flex justify-center mt-[10px]">
|
<div className="flex justify-center mt-[10px]">
|
||||||
<span>
|
<span>
|
||||||
Уже есть аккаунт? <Link
|
Уже есть аккаунт?{' '}
|
||||||
to={"/home/login"}
|
<Link
|
||||||
className={"text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline "}>
|
to={'/home/login'}
|
||||||
|
className={
|
||||||
|
'text-liquid-brightmain text-[16px] h-[20px] transition-all hover:underline '
|
||||||
|
}
|
||||||
|
>
|
||||||
Авторизация
|
Авторизация
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import { cn } from "../../../lib/cn";
|
import { cn } from '../../../lib/cn';
|
||||||
import { Account } from "../../../assets/icons/auth";
|
import { Account } from '../../../assets/icons/auth';
|
||||||
import { PrimaryButton } from "../../../components/button/PrimaryButton";
|
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||||
import { ReverseButton } from "../../../components/button/ReverseButton";
|
import { ReverseButton } from '../../../components/button/ReverseButton';
|
||||||
|
|
||||||
export interface ContestItemProps {
|
export interface ContestItemProps {
|
||||||
name: string;
|
name: string;
|
||||||
startAt: string;
|
startAt: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
members: number;
|
members: number;
|
||||||
statusRegister: "reg" | "nonreg";
|
statusRegister: 'reg' | 'nonreg';
|
||||||
type: "first" | "second";
|
type: 'first' | 'second';
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(dateString: string): string {
|
function formatDate(dateString: string): string {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
|
|
||||||
const day = date.getDate().toString().padStart(2, "0");
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
|
|
||||||
const hours = date.getHours().toString().padStart(2, "0");
|
const hours = date.getHours().toString().padStart(2, '0');
|
||||||
const minutes = date.getMinutes().toString().padStart(2, "0");
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
|
||||||
return `${day}/${month}/${year}\n${hours}:${minutes}`;
|
return `${day}/${month}/${year}\n${hours}:${minutes}`;
|
||||||
}
|
}
|
||||||
@@ -32,9 +32,10 @@ function formatWaitTime(ms: number): string {
|
|||||||
|
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
const remainder = days % 10;
|
const remainder = days % 10;
|
||||||
let suffix = "дней";
|
let suffix = 'дней';
|
||||||
if (remainder === 1 && days !== 11) suffix = "день";
|
if (remainder === 1 && days !== 11) suffix = 'день';
|
||||||
else if (remainder >= 2 && remainder <= 4 && (days < 10 || days > 20)) suffix = "дня";
|
else if (remainder >= 2 && remainder <= 4 && (days < 10 || days > 20))
|
||||||
|
suffix = 'дня';
|
||||||
return `${days} ${suffix}`;
|
return `${days} ${suffix}`;
|
||||||
} else if (hours > 0) {
|
} else if (hours > 0) {
|
||||||
const mins = minutes % 60;
|
const mins = minutes % 60;
|
||||||
@@ -45,21 +46,29 @@ function formatWaitTime(ms: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ContestItem: React.FC<ContestItemProps> = ({
|
const ContestItem: React.FC<ContestItemProps> = ({
|
||||||
name, startAt, duration, members, statusRegister, type
|
name,
|
||||||
|
startAt,
|
||||||
|
duration,
|
||||||
|
members,
|
||||||
|
statusRegister,
|
||||||
|
type,
|
||||||
}) => {
|
}) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const waitTime = new Date(startAt).getTime() - now.getTime();
|
const waitTime = new Date(startAt).getTime() - now.getTime();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("w-full box-border relative rounded-[10px] px-[20px] py-[10px] text-liquid-white text-[16px] leading-[20px]",
|
<div
|
||||||
waitTime <= 0 ? "grid grid-cols-6" : "grid grid-cols-7",
|
className={cn(
|
||||||
"items-center font-bold text-liquid-white",
|
'w-full box-border relative rounded-[10px] px-[20px] py-[10px] text-liquid-white text-[16px] leading-[20px]',
|
||||||
type == "first" ? " bg-liquid-lighter" : " bg-liquid-background"
|
waitTime <= 0 ? 'grid grid-cols-6' : 'grid grid-cols-7',
|
||||||
)}>
|
'items-center font-bold text-liquid-white',
|
||||||
<div className="text-left font-bold text-[18px]">
|
type == 'first'
|
||||||
{name}
|
? ' bg-liquid-lighter'
|
||||||
</div>
|
: ' bg-liquid-background',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="text-left font-bold text-[18px]">{name}</div>
|
||||||
<div className="text-center text-liquid-brightmain font-normal ">
|
<div className="text-center text-liquid-brightmain font-normal ">
|
||||||
{/* {authors.map((v, i) => <p key={i}>{v}</p>)} */}
|
{/* {authors.map((v, i) => <p key={i}>{v}</p>)} */}
|
||||||
valavshonok
|
valavshonok
|
||||||
@@ -67,29 +76,29 @@ const ContestItem: React.FC<ContestItemProps> = ({
|
|||||||
<div className="text-center text-nowrap whitespace-pre-line">
|
<div className="text-center text-nowrap whitespace-pre-line">
|
||||||
{formatDate(startAt)}
|
{formatDate(startAt)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">{formatWaitTime(duration)}</div>
|
||||||
{formatWaitTime(duration)}
|
{waitTime > 0 && (
|
||||||
</div>
|
|
||||||
{
|
|
||||||
waitTime > 0 &&
|
|
||||||
<div className="text-center whitespace-pre-line ">
|
<div className="text-center whitespace-pre-line ">
|
||||||
|
{'До начала\n' + formatWaitTime(waitTime)}
|
||||||
{"До начала\n" + formatWaitTime(waitTime)}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
<div className="items-center justify-center flex gap-[10px] flex-row w-full">
|
<div className="items-center justify-center flex gap-[10px] flex-row w-full">
|
||||||
<div>{members}</div>
|
<div>{members}</div>
|
||||||
<img src={Account} className="h-[24px] w-[24px]"/>
|
<img src={Account} className="h-[24px] w-[24px]" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
{
|
{statusRegister == 'reg' ? (
|
||||||
statusRegister == "reg" ?
|
<>
|
||||||
<> <PrimaryButton onClick={() => {}} text="Регистрация"/></>
|
{' '}
|
||||||
:
|
<PrimaryButton onClick={() => {}} text="Регистрация" />
|
||||||
<> <ReverseButton onClick={() => {}} text="Вы записаны"/></>
|
</>
|
||||||
}
|
) : (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<ReverseButton onClick={() => {}} text="Вы записаны" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from 'react';
|
||||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||||
import { cn } from "../../../lib/cn";
|
import { cn } from '../../../lib/cn';
|
||||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||||
import ContestsBlock from "./ContestsBlock";
|
import ContestsBlock from './ContestsBlock';
|
||||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||||
import { fetchContests } from "../../../redux/slices/contests";
|
import { fetchContests } from '../../../redux/slices/contests';
|
||||||
|
|
||||||
const Contests = () => {
|
const Contests = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@@ -17,12 +17,14 @@ const Contests = () => {
|
|||||||
|
|
||||||
// При загрузке страницы — выставляем активную вкладку и подгружаем контесты
|
// При загрузке страницы — выставляем активную вкладку и подгружаем контесты
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(setMenuActivePage("contests"));
|
dispatch(setMenuActivePage('contests'));
|
||||||
dispatch(fetchContests({}));
|
dispatch(fetchContests({}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (loading == "loading") {
|
if (loading == 'loading') {
|
||||||
return <div className="text-liquid-white p-4">Загрузка контестов...</div>;
|
return (
|
||||||
|
<div className="text-liquid-white p-4">Загрузка контестов...</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -33,7 +35,11 @@ const Contests = () => {
|
|||||||
<div className="h-full w-[calc(100%+250px)] box-border p-[20px] pt-[20p]">
|
<div className="h-full w-[calc(100%+250px)] box-border p-[20px] pt-[20p]">
|
||||||
<div className="h-full box-border">
|
<div className="h-full box-border">
|
||||||
<div className="relative flex items-center mb-[20px]">
|
<div className="relative flex items-center mb-[20px]">
|
||||||
<div className={cn("h-[50px] text-[40px] font-bold text-liquid-white flex items-center")}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'h-[50px] text-[40px] font-bold text-liquid-white flex items-center',
|
||||||
|
)}
|
||||||
|
>
|
||||||
Контесты
|
Контесты
|
||||||
</div>
|
</div>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
@@ -49,8 +55,7 @@ const Contests = () => {
|
|||||||
className="mb-[20px]"
|
className="mb-[20px]"
|
||||||
title="Текущие"
|
title="Текущие"
|
||||||
contests={contests.filter((contest) => {
|
contests={contests.filter((contest) => {
|
||||||
const endTime =
|
const endTime = new Date(contest.endsAt).getTime();
|
||||||
new Date(contest.endsAt).getTime()
|
|
||||||
return endTime >= now.getTime();
|
return endTime >= now.getTime();
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -59,8 +64,7 @@ const Contests = () => {
|
|||||||
className="mb-[20px]"
|
className="mb-[20px]"
|
||||||
title="Прошедшие"
|
title="Прошедшие"
|
||||||
contests={contests.filter((contest) => {
|
contests={contests.filter((contest) => {
|
||||||
const endTime =
|
const endTime = new Date(contest.endsAt).getTime();
|
||||||
new Date(contest.endsAt).getTime()
|
|
||||||
return endTime < now.getTime();
|
return endTime < now.getTime();
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { useState, FC } from "react";
|
import { useState, FC } from 'react';
|
||||||
import { cn } from "../../../lib/cn";
|
import { cn } from '../../../lib/cn';
|
||||||
import { ChevroneDown } from "../../../assets/icons/groups";
|
import { ChevroneDown } from '../../../assets/icons/groups';
|
||||||
import ContestItem from "./ContestItem";
|
import ContestItem from './ContestItem';
|
||||||
import { Contest } from "../../../redux/slices/contests";
|
import { Contest } from '../../../redux/slices/contests';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface ContestsBlockProps {
|
interface ContestsBlockProps {
|
||||||
contests: Contest[];
|
contests: Contest[];
|
||||||
@@ -13,46 +10,61 @@ interface ContestsBlockProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ContestsBlock: FC<ContestsBlockProps> = ({
|
||||||
const ContestsBlock: FC<ContestsBlockProps> = ({ contests, title, className }) => {
|
contests,
|
||||||
|
title,
|
||||||
|
className,
|
||||||
const [active, setActive] = useState<boolean>(title != "Скрытые");
|
}) => {
|
||||||
|
const [active, setActive] = useState<boolean>(title != 'Скрытые');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div
|
||||||
<div className={cn(" border-b-[1px] border-b-liquid-lighter rounded-[10px]",
|
className={cn(
|
||||||
className
|
' 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"
|
>
|
||||||
|
<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={() => {
|
onClick={() => {
|
||||||
setActive(!active)
|
setActive(!active);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
<img src={ChevroneDown} className={cn("transition-all duration-300",
|
<img
|
||||||
active && "rotate-180"
|
src={ChevroneDown}
|
||||||
)} />
|
className={cn(
|
||||||
|
'transition-all duration-300',
|
||||||
|
active && 'rotate-180',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={cn(" grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-300",
|
<div
|
||||||
active && "grid-rows-[1fr] opacity-100"
|
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="overflow-hidden">
|
||||||
<div className="pb-[10px] pt-[20px]">
|
<div className="pb-[10px] pt-[20px]">
|
||||||
{
|
{contests.map((v, i) => (
|
||||||
contests.map((v, i) => <ContestItem
|
<ContestItem
|
||||||
key={i}
|
key={i}
|
||||||
name={v.name}
|
name={v.name}
|
||||||
startAt={v.startsAt}
|
startAt={v.startsAt}
|
||||||
statusRegister={"reg"}
|
statusRegister={'reg'}
|
||||||
duration={new Date(v.endsAt).getTime() - new Date(v.startsAt).getTime()}
|
duration={
|
||||||
members={v.members.length}
|
new Date(v.endsAt).getTime() -
|
||||||
type={i % 2 ? "second" : "first"} />)
|
new Date(v.startsAt).getTime()
|
||||||
}
|
}
|
||||||
|
members={v.members.length}
|
||||||
|
type={i % 2 ? 'second' : 'first'}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FC } from "react";
|
import { FC } from 'react';
|
||||||
import { cn } from "../../../lib/cn";
|
import { cn } from '../../../lib/cn';
|
||||||
import { useParams, Navigate } from "react-router-dom";
|
import { useParams, Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
interface GroupsBlockProps {}
|
interface GroupsBlockProps {}
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ const Group: FC<GroupsBlockProps> = () => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-b-[1px] border-b-liquid-lighter rounded-[10px]"
|
'border-b-[1px] border-b-liquid-lighter rounded-[10px]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{groupIdNumber}
|
{groupIdNumber}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { cn } from "../../../lib/cn";
|
import { cn } from '../../../lib/cn';
|
||||||
import { Book, UserAdd, Edit, EyeClosed, EyeOpen } from "../../../assets/icons/groups";
|
import {
|
||||||
import { useNavigate } from "react-router-dom";
|
Book,
|
||||||
import { GroupUpdate } from "./Groups";
|
UserAdd,
|
||||||
|
Edit,
|
||||||
|
EyeClosed,
|
||||||
|
EyeOpen,
|
||||||
|
} from '../../../assets/icons/groups';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { GroupUpdate } from './Groups';
|
||||||
|
|
||||||
export interface GroupItemProps {
|
export interface GroupItemProps {
|
||||||
id: number;
|
id: number;
|
||||||
role: "menager" | "member" | "owner" | "viewer";
|
role: 'menager' | 'member' | 'owner' | 'viewer';
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -13,61 +19,64 @@ export interface GroupItemProps {
|
|||||||
setUpdateGroup: (value: GroupUpdate) => void;
|
setUpdateGroup: (value: GroupUpdate) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface IconComponentProps {
|
interface IconComponentProps {
|
||||||
src: string;
|
src: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IconComponent: React.FC<IconComponentProps> = ({
|
const IconComponent: React.FC<IconComponentProps> = ({ src, onClick }) => {
|
||||||
src,
|
return (
|
||||||
onClick
|
<img
|
||||||
}) => {
|
|
||||||
|
|
||||||
return <img
|
|
||||||
src={src}
|
src={src}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (onClick)
|
if (onClick) onClick();
|
||||||
onClick();
|
|
||||||
}}
|
}}
|
||||||
className="hover:bg-liquid-light rounded-[5px] cursor-pointer transition-all duration-300"
|
className="hover:bg-liquid-light rounded-[5px] cursor-pointer transition-all duration-300"
|
||||||
/>
|
/>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const GroupItem: React.FC<GroupItemProps> = ({
|
const GroupItem: React.FC<GroupItemProps> = ({
|
||||||
id, name, visible, role, description, setUpdateGroup, setUpdateActive
|
id,
|
||||||
|
name,
|
||||||
|
visible,
|
||||||
|
role,
|
||||||
|
description,
|
||||||
|
setUpdateGroup,
|
||||||
|
setUpdateActive,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("w-full h-[120px] box-border relative rounded-[10px] p-[10px] text-liquid-white bg-liquid-lighter cursor-pointer",
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-full h-[120px] box-border relative rounded-[10px] p-[10px] text-liquid-white bg-liquid-lighter cursor-pointer',
|
||||||
)}
|
)}
|
||||||
onClick={() => navigate(`/group/${id}`)}
|
onClick={() => navigate(`/group/${id}`)}
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-[100px,1fr] gap-[20px]">
|
<div className="grid grid-cols-[100px,1fr] gap-[20px]">
|
||||||
<img src={Book} className="bg-liquid-brightmain rounded-[10px]"/>
|
<img
|
||||||
|
src={Book}
|
||||||
|
className="bg-liquid-brightmain rounded-[10px]"
|
||||||
|
/>
|
||||||
<div className="grid grid-flow-row grid-rows-[1fr,24px]">
|
<div className="grid grid-flow-row grid-rows-[1fr,24px]">
|
||||||
<div className="text-[18px] font-bold">
|
<div className="text-[18px] font-bold">{name}</div>
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
<div className=" flex gap-[10px]">
|
<div className=" flex gap-[10px]">
|
||||||
{
|
{(role == 'menager' || role == 'owner') && (
|
||||||
(role == "menager" || role == "owner") && <IconComponent src={UserAdd}/>
|
<IconComponent src={UserAdd} />
|
||||||
}
|
)}
|
||||||
{
|
{(role == 'menager' || role == 'owner') && (
|
||||||
(role == "menager" || role == "owner") && <IconComponent src={Edit} onClick={() => {
|
<IconComponent
|
||||||
|
src={Edit}
|
||||||
setUpdateGroup({id, name, description });
|
onClick={() => {
|
||||||
|
setUpdateGroup({ id, name, description });
|
||||||
setUpdateActive(true);
|
setUpdateActive(true);
|
||||||
}} />
|
}}
|
||||||
}
|
/>
|
||||||
{
|
)}
|
||||||
visible == false && <IconComponent src={EyeOpen} />
|
{visible == false && <IconComponent src={EyeOpen} />}
|
||||||
}
|
{visible == true && <IconComponent src={EyeClosed} />}
|
||||||
{
|
|
||||||
visible == true && <IconComponent src={EyeClosed} />
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||||
import { cn } from "../../../lib/cn";
|
import { cn } from '../../../lib/cn';
|
||||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||||
import GroupsBlock from "./GroupsBlock";
|
import GroupsBlock from './GroupsBlock';
|
||||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||||
import { fetchMyGroups } from "../../../redux/slices/groups";
|
import { fetchMyGroups } from '../../../redux/slices/groups';
|
||||||
import ModalCreate from "./ModalCreate";
|
import ModalCreate from './ModalCreate';
|
||||||
import ModalUpdate from "./ModalUpdate";
|
import ModalUpdate from './ModalUpdate';
|
||||||
|
|
||||||
export interface GroupUpdate {
|
export interface GroupUpdate {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -17,11 +17,14 @@ export interface GroupUpdate {
|
|||||||
const Groups = () => {
|
const Groups = () => {
|
||||||
const [modalActive, setModalActive] = useState<boolean>(false);
|
const [modalActive, setModalActive] = useState<boolean>(false);
|
||||||
const [modelUpdateActive, setModalUpdateActive] = useState<boolean>(false);
|
const [modelUpdateActive, setModalUpdateActive] = useState<boolean>(false);
|
||||||
const [updateGroup, setUpdateGroup] = useState<GroupUpdate>({ id: 0, name: "", description: "" });
|
const [updateGroup, setUpdateGroup] = useState<GroupUpdate>({
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
|
||||||
// Берём группы из стора
|
// Берём группы из стора
|
||||||
const groups = useAppSelector((store) => store.groups.groups);
|
const groups = useAppSelector((store) => store.groups.groups);
|
||||||
|
|
||||||
@@ -29,8 +32,8 @@ const Groups = () => {
|
|||||||
const currentUserName = useAppSelector((store) => store.auth.username);
|
const currentUserName = useAppSelector((store) => store.auth.username);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(setMenuActivePage("groups"));
|
dispatch(setMenuActivePage('groups'));
|
||||||
dispatch(fetchMyGroups())
|
dispatch(fetchMyGroups());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
// Разделяем группы
|
// Разделяем группы
|
||||||
@@ -44,17 +47,23 @@ const Groups = () => {
|
|||||||
const hidden: typeof groups = []; // пока пустые, без логики
|
const hidden: typeof groups = []; // пока пустые, без логики
|
||||||
|
|
||||||
groups.forEach((group) => {
|
groups.forEach((group) => {
|
||||||
const me = group.members.find((m) => m.username === currentUserName);
|
const me = group.members.find(
|
||||||
|
(m) => m.username === currentUserName,
|
||||||
|
);
|
||||||
if (!me) return;
|
if (!me) return;
|
||||||
|
|
||||||
if (me.role === "Administrator") {
|
if (me.role === 'Administrator') {
|
||||||
managed.push(group);
|
managed.push(group);
|
||||||
} else {
|
} else {
|
||||||
current.push(group);
|
current.push(group);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { managedGroups: managed, currentGroups: current, hiddenGroups: hidden };
|
return {
|
||||||
|
managedGroups: managed,
|
||||||
|
currentGroups: current,
|
||||||
|
hiddenGroups: hidden,
|
||||||
|
};
|
||||||
}, [groups, currentUserName]);
|
}, [groups, currentUserName]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -63,13 +72,15 @@ const Groups = () => {
|
|||||||
<div className="relative flex items-center mb-[20px]">
|
<div className="relative flex items-center mb-[20px]">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-[50px] text-[40px] font-bold text-liquid-white flex items-center"
|
'h-[50px] text-[40px] font-bold text-liquid-white flex items-center',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Группы
|
Группы
|
||||||
</div>
|
</div>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
onClick={() => { setModalActive(true); }}
|
onClick={() => {
|
||||||
|
setModalActive(true);
|
||||||
|
}}
|
||||||
text="Создать группу"
|
text="Создать группу"
|
||||||
className="absolute right-0"
|
className="absolute right-0"
|
||||||
/>
|
/>
|
||||||
@@ -83,7 +94,6 @@ const Groups = () => {
|
|||||||
groups={managedGroups}
|
groups={managedGroups}
|
||||||
setUpdateActive={setModalUpdateActive}
|
setUpdateActive={setModalUpdateActive}
|
||||||
setUpdateGroup={setUpdateGroup}
|
setUpdateGroup={setUpdateGroup}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
<GroupsBlock
|
<GroupsBlock
|
||||||
className="mb-[20px]"
|
className="mb-[20px]"
|
||||||
@@ -101,7 +111,6 @@ const Groups = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<ModalCreate setActive={setModalActive} active={modalActive} />
|
<ModalCreate setActive={setModalActive} active={modalActive} />
|
||||||
<ModalUpdate
|
<ModalUpdate
|
||||||
setActive={setModalUpdateActive}
|
setActive={setModalUpdateActive}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useState, FC } from "react";
|
import { useState, FC } from 'react';
|
||||||
import GroupItem from "./GroupItem";
|
import GroupItem from './GroupItem';
|
||||||
import { cn } from "../../../lib/cn";
|
import { cn } from '../../../lib/cn';
|
||||||
import { ChevroneDown } from "../../../assets/icons/groups";
|
import { ChevroneDown } from '../../../assets/icons/groups';
|
||||||
import { Group } from "../../../redux/slices/groups";
|
import { Group } from '../../../redux/slices/groups';
|
||||||
import { GroupUpdate } from "./Groups";
|
import { GroupUpdate } from './Groups';
|
||||||
|
|
||||||
interface GroupsBlockProps {
|
interface GroupsBlockProps {
|
||||||
groups: Group[];
|
groups: Group[];
|
||||||
@@ -13,46 +13,60 @@ interface GroupsBlockProps {
|
|||||||
setUpdateGroup: (value: GroupUpdate) => void;
|
setUpdateGroup: (value: GroupUpdate) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GroupsBlock: FC<GroupsBlockProps> = ({
|
||||||
const GroupsBlock: FC<GroupsBlockProps> = ({ groups, title, className, setUpdateActive, setUpdateGroup }) => {
|
groups,
|
||||||
|
title,
|
||||||
|
className,
|
||||||
const [active, setActive] = useState<boolean>(title != "Скрытые");
|
setUpdateActive,
|
||||||
|
setUpdateGroup,
|
||||||
|
}) => {
|
||||||
|
const [active, setActive] = useState<boolean>(title != 'Скрытые');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div
|
||||||
<div className={cn(" border-b-[1px] border-b-liquid-lighter rounded-[10px]",
|
className={cn(
|
||||||
className
|
' 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"
|
>
|
||||||
|
<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={() => {
|
onClick={() => {
|
||||||
setActive(!active)
|
setActive(!active);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
<img src={ChevroneDown} className={cn("transition-all duration-300",
|
<img
|
||||||
active && "rotate-180"
|
src={ChevroneDown}
|
||||||
)}/>
|
className={cn(
|
||||||
|
'transition-all duration-300',
|
||||||
|
active && 'rotate-180',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={cn(" grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-300",
|
<div
|
||||||
active && "grid-rows-[1fr] opacity-100"
|
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="overflow-hidden">
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-[20px] pt-[20px] pb-[20px] box-border">
|
<div className="grid grid-cols-3 gap-[20px] pt-[20px] pb-[20px] box-border">
|
||||||
{
|
{groups.map((v, i) => (
|
||||||
groups.map((v, i) => <GroupItem
|
<GroupItem
|
||||||
key={i}
|
key={i}
|
||||||
id={v.id}
|
id={v.id}
|
||||||
visible={true}
|
visible={true}
|
||||||
description={v.description}
|
description={v.description}
|
||||||
setUpdateActive={setUpdateActive}
|
setUpdateActive={setUpdateActive}
|
||||||
setUpdateGroup={setUpdateGroup}
|
setUpdateGroup={setUpdateGroup}
|
||||||
role={"owner"}
|
role={'owner'}
|
||||||
name={v.name}/>)
|
name={v.name}
|
||||||
}
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { Modal } from "../../../components/modal/Modal";
|
import { Modal } from '../../../components/modal/Modal';
|
||||||
import { PrimaryButton } from "../../../components/button/PrimaryButton";
|
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||||
import { Input } from "../../../components/input/Input";
|
import { Input } from '../../../components/input/Input';
|
||||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||||
import { createGroup } from "../../../redux/slices/groups";
|
import { createGroup } from '../../../redux/slices/groups';
|
||||||
|
|
||||||
interface ModalCreateProps {
|
interface ModalCreateProps {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
@@ -12,27 +12,63 @@ interface ModalCreateProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
|
const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
|
||||||
const [name, setName] = useState<string>("");
|
const [name, setName] = useState<string>('');
|
||||||
const [description, setDescription] = useState<string>("");
|
const [description, setDescription] = useState<string>('');
|
||||||
const status = useAppSelector((state) => state.groups.statuses.create);
|
const status = useAppSelector((state) => state.groups.statuses.create);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status == "successful") {
|
if (status == 'successful') {
|
||||||
setActive(false);
|
setActive(false);
|
||||||
}
|
}
|
||||||
}, [status]);
|
}, [status]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white" onOpenChange={setActive} open={active} backdrop="blur" >
|
<Modal
|
||||||
|
className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white"
|
||||||
|
onOpenChange={setActive}
|
||||||
|
open={active}
|
||||||
|
backdrop="blur"
|
||||||
|
>
|
||||||
<div className="w-[500px]">
|
<div className="w-[500px]">
|
||||||
<div className="font-bold text-[30px]">Создать группу</div>
|
<div className="font-bold text-[30px]">Создать группу</div>
|
||||||
<Input name="name" autocomplete="name" className="mt-[10px]" type="text" label="Название" onChange={(v) => { setName(v) }} placeholder="login" />
|
<Input
|
||||||
<Input name="description" autocomplete="description" className="mt-[10px]" type="text" label="Описание" onChange={(v) => { setDescription(v) }} placeholder="login" />
|
name="name"
|
||||||
|
autocomplete="name"
|
||||||
|
className="mt-[10px]"
|
||||||
|
type="text"
|
||||||
|
label="Название"
|
||||||
|
onChange={(v) => {
|
||||||
|
setName(v);
|
||||||
|
}}
|
||||||
|
placeholder="login"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name="description"
|
||||||
|
autocomplete="description"
|
||||||
|
className="mt-[10px]"
|
||||||
|
type="text"
|
||||||
|
label="Описание"
|
||||||
|
onChange={(v) => {
|
||||||
|
setDescription(v);
|
||||||
|
}}
|
||||||
|
placeholder="login"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
|
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
|
||||||
<PrimaryButton onClick={() => { dispatch(createGroup({ name, description })) }} text="Создать" disabled={status == "loading"} />
|
<PrimaryButton
|
||||||
<SecondaryButton onClick={() => { setActive(false); }} text="Отмена" />
|
onClick={() => {
|
||||||
|
dispatch(createGroup({ name, description }));
|
||||||
|
}}
|
||||||
|
text="Создать"
|
||||||
|
disabled={status == 'loading'}
|
||||||
|
/>
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={() => {
|
||||||
|
setActive(false);
|
||||||
|
}}
|
||||||
|
text="Отмена"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -40,4 +76,3 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ModalCreate;
|
export default ModalCreate;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { Modal } from "../../../components/modal/Modal";
|
import { Modal } from '../../../components/modal/Modal';
|
||||||
import { PrimaryButton } from "../../../components/button/PrimaryButton";
|
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||||
import { Input } from "../../../components/input/Input";
|
import { Input } from '../../../components/input/Input';
|
||||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||||
import { deleteGroup, updateGroup } from "../../../redux/slices/groups";
|
import { deleteGroup, updateGroup } from '../../../redux/slices/groups';
|
||||||
|
|
||||||
interface ModalUpdateProps {
|
interface ModalUpdateProps {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
@@ -14,36 +14,95 @@ interface ModalUpdateProps {
|
|||||||
groupDescription: string;
|
groupDescription: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalUpdate: FC<ModalUpdateProps> = ({ active, setActive, groupName, groupId, groupDescription }) => {
|
const ModalUpdate: FC<ModalUpdateProps> = ({
|
||||||
const [name, setName] = useState<string>("");
|
active,
|
||||||
const [description, setDescription] = useState<string>("");
|
setActive,
|
||||||
const statusUpdate = useAppSelector((state) => state.groups.statuses.update);
|
groupName,
|
||||||
const statusDelete = useAppSelector((state) => state.groups.statuses.delete);
|
groupId,
|
||||||
|
groupDescription,
|
||||||
|
}) => {
|
||||||
|
const [name, setName] = useState<string>('');
|
||||||
|
const [description, setDescription] = useState<string>('');
|
||||||
|
const statusUpdate = useAppSelector(
|
||||||
|
(state) => state.groups.statuses.update,
|
||||||
|
);
|
||||||
|
const statusDelete = useAppSelector(
|
||||||
|
(state) => state.groups.statuses.delete,
|
||||||
|
);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (statusUpdate == "successful"){
|
if (statusUpdate == 'successful') {
|
||||||
setActive(false);
|
setActive(false);
|
||||||
}
|
}
|
||||||
}, [statusUpdate]);
|
}, [statusUpdate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (statusDelete == "successful"){
|
if (statusDelete == 'successful') {
|
||||||
setActive(false);
|
setActive(false);
|
||||||
}
|
}
|
||||||
}, [statusDelete]);
|
}, [statusDelete]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white" onOpenChange={setActive} open={active} backdrop="blur" >
|
<Modal
|
||||||
|
className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white"
|
||||||
|
onOpenChange={setActive}
|
||||||
|
open={active}
|
||||||
|
backdrop="blur"
|
||||||
|
>
|
||||||
<div className="w-[500px]">
|
<div className="w-[500px]">
|
||||||
<div className="font-bold text-[30px]">Изменить группу {groupName} #{groupId}</div>
|
<div className="font-bold text-[30px]">
|
||||||
<Input name="name" autocomplete="name" className="mt-[10px]" type="text" label="Новое название" defaultState={groupName} onChange={(v) => { setName(v)}} placeholder="login"/>
|
Изменить группу {groupName} #{groupId}
|
||||||
<Input name="description" autocomplete="description" className="mt-[10px]" type="text" label="Описание" onChange={(v) => { setDescription(v)}} placeholder="login" defaultState={groupDescription}/>
|
</div>
|
||||||
|
<Input
|
||||||
|
name="name"
|
||||||
|
autocomplete="name"
|
||||||
|
className="mt-[10px]"
|
||||||
|
type="text"
|
||||||
|
label="Новое название"
|
||||||
|
defaultState={groupName}
|
||||||
|
onChange={(v) => {
|
||||||
|
setName(v);
|
||||||
|
}}
|
||||||
|
placeholder="login"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name="description"
|
||||||
|
autocomplete="description"
|
||||||
|
className="mt-[10px]"
|
||||||
|
type="text"
|
||||||
|
label="Описание"
|
||||||
|
onChange={(v) => {
|
||||||
|
setDescription(v);
|
||||||
|
}}
|
||||||
|
placeholder="login"
|
||||||
|
defaultState={groupDescription}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
|
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
|
||||||
<PrimaryButton onClick={() => {dispatch(deleteGroup(groupId))}} text="Удалить" disabled={statusDelete=="loading"} color="error"/>
|
<PrimaryButton
|
||||||
<PrimaryButton onClick={() => {dispatch(updateGroup({name, description, groupId}))}} text="Обновить" disabled={statusUpdate=="loading"}/>
|
onClick={() => {
|
||||||
<SecondaryButton onClick={() => {setActive(false);}} text="Отмена" />
|
dispatch(deleteGroup(groupId));
|
||||||
|
}}
|
||||||
|
text="Удалить"
|
||||||
|
disabled={statusDelete == 'loading'}
|
||||||
|
color="error"
|
||||||
|
/>
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(
|
||||||
|
updateGroup({ name, description, groupId }),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
text="Обновить"
|
||||||
|
disabled={statusUpdate == 'loading'}
|
||||||
|
/>
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={() => {
|
||||||
|
setActive(false);
|
||||||
|
}}
|
||||||
|
text="Отмена"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -51,4 +110,3 @@ const ModalUpdate: FC<ModalUpdateProps> = ({ active, setActive, groupName, group
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ModalUpdate;
|
export default ModalUpdate;
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,43 @@
|
|||||||
import { Logo } from "../../../assets/logos";
|
import { Logo } from '../../../assets/logos';
|
||||||
import {Account, Clipboard, Cup, Home, Openbook, Users} from "../../../assets/icons/menu";
|
import {
|
||||||
import MenuItem from "./MenuItem";
|
Account,
|
||||||
import { useAppSelector } from "../../../redux/hooks";
|
Clipboard,
|
||||||
|
Cup,
|
||||||
|
Home,
|
||||||
|
Openbook,
|
||||||
|
Users,
|
||||||
|
} from '../../../assets/icons/menu';
|
||||||
|
import MenuItem from './MenuItem';
|
||||||
|
import { useAppSelector } from '../../../redux/hooks';
|
||||||
|
|
||||||
const Menu = () => {
|
const Menu = () => {
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{text: "Главная", href: "/home", icon: Home, page: "home" },
|
{ text: 'Главная', href: '/home', icon: Home, page: 'home' },
|
||||||
{text: "Задачи", href: "/home/missions", icon: Clipboard, page: "missions" },
|
{
|
||||||
{text: "Статьи", href: "/home/articles", icon: Openbook, page: "articles" },
|
text: 'Задачи',
|
||||||
{text: "Группы", href: "/home/groups", icon: Users, page: "groups" },
|
href: '/home/missions',
|
||||||
{text: "Контесты", href: "/home/contests", icon: Cup, page: "contests" },
|
icon: Clipboard,
|
||||||
{text: "Аккаунт", href: "/home/account", icon: Account, page: "account" },
|
page: 'missions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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);
|
const activePage = useAppSelector((state) => state.store.menu.activePage);
|
||||||
|
|
||||||
@@ -19,7 +46,14 @@ const Menu = () => {
|
|||||||
<img src={Logo} className="w-[173px]" />
|
<img src={Logo} className="w-[173px]" />
|
||||||
<div className="">
|
<div className="">
|
||||||
{menuItems.map((v, i) => (
|
{menuItems.map((v, i) => (
|
||||||
<MenuItem key={i} icon={v.icon} text={v.text} href={v.href} active={v.page == activePage} page={v.page}/>
|
<MenuItem
|
||||||
|
key={i}
|
||||||
|
icon={v.icon}
|
||||||
|
text={v.text}
|
||||||
|
href={v.href}
|
||||||
|
active={v.page == activePage}
|
||||||
|
page={v.page}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from 'react-router-dom';
|
||||||
import { useAppDispatch } from "../../../redux/hooks";
|
import { useAppDispatch } from '../../../redux/hooks';
|
||||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||||
|
|
||||||
interface MenuItemProps {
|
interface MenuItemProps {
|
||||||
icon: string; // SVG или любой JSX
|
icon: string; // SVG или любой JSX
|
||||||
@@ -11,7 +11,13 @@ interface MenuItemProps {
|
|||||||
active?: boolean; // необязательный, по умолчанию false
|
active?: boolean; // необязательный, по умолчанию false
|
||||||
}
|
}
|
||||||
|
|
||||||
const MenuItem: React.FC<MenuItemProps> = ({ icon, text = "", href = "", active = false, page = "" }) => {
|
const MenuItem: React.FC<MenuItemProps> = ({
|
||||||
|
icon,
|
||||||
|
text = '',
|
||||||
|
href = '',
|
||||||
|
active = false,
|
||||||
|
page = '',
|
||||||
|
}) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -21,12 +27,13 @@ const MenuItem: React.FC<MenuItemProps> = ({ icon, text = "", href = "", active
|
|||||||
flex items-center gap-3 p-[16px] rounded-[10px\] h-[40px] text-[18px] font-bold
|
flex items-center gap-3 p-[16px] rounded-[10px\] h-[40px] text-[18px] font-bold
|
||||||
transition-all duration-300 text-liquid-white mt-[20px]
|
transition-all duration-300 text-liquid-white mt-[20px]
|
||||||
active:scale-95
|
active:scale-95
|
||||||
${active ? "bg-liquid-darkmain hover:bg-liquid-lighter hover:ring-[1px] hover:ring-liquid-darkmain hover:ring-inset"
|
${
|
||||||
: " hover:bg-liquid-lighter"}
|
active
|
||||||
`}
|
? 'bg-liquid-darkmain hover:bg-liquid-lighter hover:ring-[1px] hover:ring-liquid-darkmain hover:ring-inset'
|
||||||
onClick={
|
: ' hover:bg-liquid-lighter'
|
||||||
() => dispatch(setMenuActivePage(page))
|
|
||||||
}
|
}
|
||||||
|
`}
|
||||||
|
onClick={() => dispatch(setMenuActivePage(page))}
|
||||||
>
|
>
|
||||||
<img src={icon} />
|
<img src={icon} />
|
||||||
<span>{text}</span>
|
<span>{text}</span>
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { cn } from "../../../lib/cn";
|
import { cn } from '../../../lib/cn';
|
||||||
import { IconError, IconSuccess } from "../../../assets/icons/missions";
|
import { IconError, IconSuccess } from '../../../assets/icons/missions';
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
export interface MissionItemProps {
|
export interface MissionItemProps {
|
||||||
id: number;
|
id: number;
|
||||||
authorId: number;
|
authorId: number;
|
||||||
name: string;
|
name: string;
|
||||||
difficulty: "Easy" | "Medium" | "Hard";
|
difficulty: 'Easy' | 'Medium' | 'Hard';
|
||||||
tags: string[];
|
tags: string[];
|
||||||
timeLimit: number;
|
timeLimit: number;
|
||||||
memoryLimit: number;
|
memoryLimit: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
type: "first" | "second";
|
type: 'first' | 'second';
|
||||||
status: "empty" | "success" | "error";
|
status: 'empty' | 'success' | 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatMilliseconds(ms: number): string {
|
export function formatMilliseconds(ms: number): string {
|
||||||
@@ -28,44 +28,51 @@ export function formatBytesToMB(bytes: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MissionItem: React.FC<MissionItemProps> = ({
|
const MissionItem: React.FC<MissionItemProps> = ({
|
||||||
id, name, difficulty, timeLimit, memoryLimit, type, status
|
id,
|
||||||
|
name,
|
||||||
|
difficulty,
|
||||||
|
timeLimit,
|
||||||
|
memoryLimit,
|
||||||
|
type,
|
||||||
|
status,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("h-[44px] w-full relative rounded-[10px] text-liquid-white",
|
<div
|
||||||
type == "first" ? "bg-liquid-lighter" : "bg-liquid-background",
|
className={cn(
|
||||||
"grid grid-cols-[80px,1fr,1fr,60px,24px] grid-flow-col gap-[20px] px-[20px] box-border items-center",
|
'h-[44px] w-full relative rounded-[10px] text-liquid-white',
|
||||||
status == "error" && "border-l-[11px] border-l-liquid-red pl-[9px]",
|
type == 'first' ? 'bg-liquid-lighter' : 'bg-liquid-background',
|
||||||
status == "success" && "border-l-[11px] border-l-liquid-green pl-[9px]",
|
'grid grid-cols-[80px,1fr,1fr,60px,24px] grid-flow-col gap-[20px] px-[20px] box-border items-center',
|
||||||
"cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300",
|
status == 'error' &&
|
||||||
|
'border-l-[11px] border-l-liquid-red pl-[9px]',
|
||||||
|
status == 'success' &&
|
||||||
|
'border-l-[11px] border-l-liquid-green pl-[9px]',
|
||||||
|
'cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300',
|
||||||
)}
|
)}
|
||||||
onClick={() => {navigate(`/mission/${id}`)}}
|
onClick={() => {
|
||||||
|
navigate(`/mission/${id}`);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-[18px] font-bold">
|
<div className="text-[18px] font-bold">#{id}</div>
|
||||||
#{id}
|
<div className="text-[18px] font-bold">{name}</div>
|
||||||
</div>
|
|
||||||
<div className="text-[18px] font-bold">
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
<div className="text-[12px] text-right">
|
<div className="text-[12px] text-right">
|
||||||
стандартный ввод/вывод {formatMilliseconds(timeLimit)}, {formatBytesToMB(memoryLimit)}
|
стандартный ввод/вывод {formatMilliseconds(timeLimit)},{' '}
|
||||||
|
{formatBytesToMB(memoryLimit)}
|
||||||
</div>
|
</div>
|
||||||
<div className={cn(
|
<div
|
||||||
"text-center text-[18px]",
|
className={cn(
|
||||||
difficulty == "Hard" && "text-liquid-red",
|
'text-center text-[18px]',
|
||||||
difficulty == "Medium" && "text-liquid-orange",
|
difficulty == 'Hard' && 'text-liquid-red',
|
||||||
difficulty == "Easy" && "text-liquid-green",
|
difficulty == 'Medium' && 'text-liquid-orange',
|
||||||
)}>
|
difficulty == 'Easy' && 'text-liquid-green',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{difficulty}
|
{difficulty}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[24px] w-[24px]">
|
<div className="h-[24px] w-[24px]">
|
||||||
{
|
{status == 'error' && <img src={IconError} />}
|
||||||
status == "error" && <img src={IconError}/>
|
{status == 'success' && <img src={IconSuccess} />}
|
||||||
}
|
|
||||||
{
|
|
||||||
status == "success" && <img src={IconSuccess}/>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
import MissionItem from "./MissionItem";
|
import MissionItem from './MissionItem';
|
||||||
import { SecondaryButton } from "../../../components/button/SecondaryButton";
|
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react';
|
||||||
import { setMenuActivePage } from "../../../redux/slices/store";
|
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { fetchMissions } from "../../../redux/slices/missions";
|
import { fetchMissions } from '../../../redux/slices/missions';
|
||||||
import ModalCreate from "./ModalCreate";
|
import ModalCreate from './ModalCreate';
|
||||||
|
|
||||||
|
|
||||||
export interface Mission {
|
export interface Mission {
|
||||||
id: number;
|
id: number;
|
||||||
authorId: number;
|
authorId: number;
|
||||||
name: string;
|
name: string;
|
||||||
difficulty: "Easy" | "Medium" | "Hard";
|
difficulty: 'Easy' | 'Medium' | 'Hard';
|
||||||
tags: string[];
|
tags: string[];
|
||||||
timeLimit: number;
|
timeLimit: number;
|
||||||
memoryLimit: number;
|
memoryLimit: number;
|
||||||
@@ -21,60 +20,60 @@ export interface Mission {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Missions = () => {
|
const Missions = () => {
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [modalActive, setModalActive] = useState<boolean>(false);
|
const [modalActive, setModalActive] = useState<boolean>(false);
|
||||||
|
|
||||||
const missions = useAppSelector((state) => state.missions.missions);
|
const missions = useAppSelector((state) => state.missions.missions);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(setMenuActivePage("missions"))
|
dispatch(setMenuActivePage('missions'));
|
||||||
dispatch(fetchMissions({}))
|
dispatch(fetchMissions({}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className=" h-full w-full box-border p-[20px] pt-[20px]">
|
<div className=" h-full w-full box-border p-[20px] pt-[20px]">
|
||||||
<div className="h-full box-border">
|
<div className="h-full box-border">
|
||||||
|
|
||||||
<div className="relative flex items-center mb-[20px]">
|
<div className="relative flex items-center mb-[20px]">
|
||||||
<div className="h-[50px] text-[40px] font-bold text-liquid-white flex items-center">
|
<div className="h-[50px] text-[40px] font-bold text-liquid-white flex items-center">
|
||||||
Задачи
|
Задачи
|
||||||
</div>
|
</div>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
onClick={() => {setModalActive(true)}}
|
onClick={() => {
|
||||||
|
setModalActive(true);
|
||||||
|
}}
|
||||||
text="Добавить задачу"
|
text="Добавить задачу"
|
||||||
className="absolute right-0"
|
className="absolute right-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-liquid-lighter h-[50px] mb-[20px]">
|
<div className="bg-liquid-lighter h-[50px] mb-[20px]"></div>
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
{missions.map((v, i) => (
|
{missions.map((v, i) => (
|
||||||
<MissionItem
|
<MissionItem
|
||||||
key={i}
|
key={i}
|
||||||
id={v.id}
|
id={v.id}
|
||||||
authorId={v.authorId}
|
authorId={v.authorId}
|
||||||
name={v.name}
|
name={v.name}
|
||||||
difficulty={"Easy"}
|
difficulty={'Easy'}
|
||||||
tags={v.tags}
|
tags={v.tags}
|
||||||
timeLimit={1000}
|
timeLimit={1000}
|
||||||
memoryLimit={256 * 1024 * 1024}
|
memoryLimit={256 * 1024 * 1024}
|
||||||
createdAt={v.createdAt}
|
createdAt={v.createdAt}
|
||||||
updatedAt={v.updatedAt}
|
updatedAt={v.updatedAt}
|
||||||
type={i % 2 == 0 ? "first" : "second"}
|
type={i % 2 == 0 ? 'first' : 'second'}
|
||||||
status={i == 0 || i == 3 || i == 7 ? "success" : (i == 2 || i == 4 || i == 9 ? "error" : "empty")}/>
|
status={
|
||||||
|
i == 0 || i == 3 || i == 7
|
||||||
|
? 'success'
|
||||||
|
: i == 2 || i == 4 || i == 9
|
||||||
|
? 'error'
|
||||||
|
: 'empty'
|
||||||
|
}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>pages</div>
|
||||||
<div>
|
|
||||||
pages
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ModalCreate setActive={setModalActive} active={modalActive} />
|
<ModalCreate setActive={setModalActive} active={modalActive} />
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
|
|||||||
}
|
}
|
||||||
}, [status]);
|
}, [status]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(setMissionsStatus({ key: 'upload', status: 'idle' }));
|
||||||
|
}, [active]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white"
|
className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white"
|
||||||
@@ -152,6 +156,8 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
|
|||||||
text="Отмена"
|
text="Отмена"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{status == 'failed' && <div>error</div>}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from 'react';
|
||||||
import Editor from "@monaco-editor/react";
|
import Editor from '@monaco-editor/react';
|
||||||
import { upload } from "../../../assets/icons/input";
|
import { upload } from '../../../assets/icons/input';
|
||||||
import { cn } from "../../../lib/cn";
|
import { cn } from '../../../lib/cn';
|
||||||
import { DropDownList } from "../../../components/drop-down-list/DropDownList";
|
import { DropDownList } from '../../../components/drop-down-list/DropDownList';
|
||||||
|
|
||||||
const languageMap: Record<string, string> = {
|
const languageMap: Record<string, string> = {
|
||||||
c: "cpp",
|
c: 'cpp',
|
||||||
"C++": "cpp",
|
'C++': 'cpp',
|
||||||
java: "java",
|
java: 'java',
|
||||||
python: "python",
|
python: 'python',
|
||||||
pascal: "pascal",
|
pascal: 'pascal',
|
||||||
kotlin: "kotlin",
|
kotlin: 'kotlin',
|
||||||
csharp: "csharp"
|
csharp: 'csharp',
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface CodeEditorProps {
|
export interface CodeEditorProps {
|
||||||
@@ -19,28 +19,30 @@ export interface CodeEditorProps {
|
|||||||
onChangeLanguage: (value: string) => void;
|
onChangeLanguage: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CodeEditor: React.FC<CodeEditorProps> = ({onChange, onChangeLanguage}) => {
|
const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||||
const [language, setLanguage] = useState<string>("C++");
|
onChange,
|
||||||
const [code, setCode] = useState<string>("");
|
onChangeLanguage,
|
||||||
|
}) => {
|
||||||
|
const [language, setLanguage] = useState<string>('C++');
|
||||||
|
const [code, setCode] = useState<string>('');
|
||||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||||
|
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{ value: "c", text: "C" },
|
{ value: 'c', text: 'C' },
|
||||||
{ value: "C++", text: "C++" },
|
{ value: 'C++', text: 'C++' },
|
||||||
{ value: "java", text: "Java" },
|
{ value: 'java', text: 'Java' },
|
||||||
{ value: "python", text: "Python" },
|
{ value: 'python', text: 'Python' },
|
||||||
{ value: "pascal", text: "Pascal" },
|
{ value: 'pascal', text: 'Pascal' },
|
||||||
{ value: "kotlin", text: "Kotlin" },
|
{ value: 'kotlin', text: 'Kotlin' },
|
||||||
{ value: "csharp", text: "C#" },
|
{ value: 'csharp', text: 'C#' },
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onChange(code);
|
onChange(code);
|
||||||
}, [code])
|
}, [code]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onChangeLanguage(language);
|
onChangeLanguage(language);
|
||||||
}, [language])
|
}, [language]);
|
||||||
|
|
||||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
@@ -49,10 +51,10 @@ const CodeEditor: React.FC<CodeEditorProps> = ({onChange, onChangeLanguage}) =>
|
|||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
const text = event.target?.result;
|
const text = event.target?.result;
|
||||||
if (typeof text === "string") setCode(text);
|
if (typeof text === 'string') setCode(text);
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
e.target.value = "";
|
e.target.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent<HTMLLabelElement>) => {
|
const handleDrop = (e: React.DragEvent<HTMLLabelElement>) => {
|
||||||
@@ -64,7 +66,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({onChange, onChangeLanguage}) =>
|
|||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
const text = event.target?.result;
|
const text = event.target?.result;
|
||||||
if (typeof text === "string") setCode(text);
|
if (typeof text === 'string') setCode(text);
|
||||||
};
|
};
|
||||||
reader.readAsText(droppedFile);
|
reader.readAsText(droppedFile);
|
||||||
};
|
};
|
||||||
@@ -88,11 +90,18 @@ const CodeEditor: React.FC<CodeEditorProps> = ({onChange, onChangeLanguage}) =>
|
|||||||
{/* Панель выбора языка и загрузки файла */}
|
{/* Панель выбора языка и загрузки файла */}
|
||||||
<div className="flex items-center justify-between py-3 ">
|
<div className="flex items-center justify-between py-3 ">
|
||||||
<div className="flex items-center gap-[20px]">
|
<div className="flex items-center gap-[20px]">
|
||||||
<DropDownList items={items} onChange={(v) => { setLanguage(v) }} defaultState={{ value: "C++", text: "C++" }}/>
|
<DropDownList
|
||||||
|
items={items}
|
||||||
|
onChange={(v) => {
|
||||||
|
setLanguage(v);
|
||||||
|
}}
|
||||||
|
defaultState={{ value: 'C++', text: 'C++' }}
|
||||||
|
/>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
className={cn("h-[40px] w-[250px] rounded-[10px] px-[16px] relative flex items-center cursor-pointer transition-all bg-liquid-lighter outline-dashed outline-[2px] outline-transparent active:scale-[95%]",
|
className={cn(
|
||||||
isDragging && "outline-blue-500 "
|
'h-[40px] w-[250px] rounded-[10px] px-[16px] relative flex items-center cursor-pointer transition-all bg-liquid-lighter outline-dashed outline-[2px] outline-transparent active:scale-[95%]',
|
||||||
|
isDragging && 'outline-blue-500 ',
|
||||||
)}
|
)}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
@@ -100,9 +109,12 @@ const CodeEditor: React.FC<CodeEditorProps> = ({onChange, onChangeLanguage}) =>
|
|||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
>
|
>
|
||||||
<span className="text-[18px] text-liquid-white font-bold pointer-events-none">
|
<span className="text-[18px] text-liquid-white font-bold pointer-events-none">
|
||||||
{"Загрузить решение"}
|
{'Загрузить решение'}
|
||||||
</span>
|
</span>
|
||||||
<img src={upload} className="absolute right-[16px] pointer-events-none" />
|
<img
|
||||||
|
src={upload}
|
||||||
|
className="absolute right-[16px] pointer-events-none"
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
onChange={(e) => handleFileUpload(e)}
|
onChange={(e) => handleFileUpload(e)}
|
||||||
@@ -119,7 +131,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({onChange, onChangeLanguage}) =>
|
|||||||
height="100%"
|
height="100%"
|
||||||
language={languageMap[language]}
|
language={languageMap[language]}
|
||||||
value={code}
|
value={code}
|
||||||
onChange={(value) => setCode(value ?? "")}
|
onChange={(value) => setCode(value ?? '')}
|
||||||
theme="vs-dark"
|
theme="vs-dark"
|
||||||
options={{
|
options={{
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -130,7 +142,7 @@ const CodeEditor: React.FC<CodeEditorProps> = ({onChange, onChangeLanguage}) =>
|
|||||||
tabSize: 4,
|
tabSize: 4,
|
||||||
insertSpaces: true,
|
insertSpaces: true,
|
||||||
detectIndentation: false,
|
detectIndentation: false,
|
||||||
autoIndent: "full",
|
autoIndent: 'full',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,28 +1,57 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { chevroneLeft, chevroneRight, arrowLeft } from "../../../assets/icons/header";
|
import {
|
||||||
import { Logo } from "../../../assets/logos";
|
chevroneLeft,
|
||||||
import { useNavigate } from "react-router-dom";
|
chevroneRight,
|
||||||
|
arrowLeft,
|
||||||
|
} from '../../../assets/icons/header';
|
||||||
|
import { Logo } from '../../../assets/logos';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
missionId: number;
|
missionId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Header: React.FC<HeaderProps> = ({
|
const Header: React.FC<HeaderProps> = ({ missionId }) => {
|
||||||
missionId
|
|
||||||
}) => {
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
<header className="w-full h-[60px] flex items-center px-4 gap-[20px]">
|
<header className="w-full h-[60px] flex items-center px-4 gap-[20px]">
|
||||||
<img src={Logo} alt="Logo" className="h-[28px] w-auto cursor-pointer" onClick={() => { navigate("/home") }} />
|
<img
|
||||||
|
src={Logo}
|
||||||
|
alt="Logo"
|
||||||
|
className="h-[28px] w-auto cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/home');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<img src={arrowLeft} alt="back" className="h-[24px] w-[24px] cursor-pointer" onClick={() => { navigate("/home/missions") }} />
|
<img
|
||||||
|
src={arrowLeft}
|
||||||
|
alt="back"
|
||||||
|
className="h-[24px] w-[24px] cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/home/missions');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex gap-[10px]">
|
<div className="flex gap-[10px]">
|
||||||
<img src={chevroneLeft} alt="back" className="h-[24px] w-[24px] cursor-pointer" onClick={() => { navigate(`/mission/${missionId - 1}`) }} />
|
<img
|
||||||
|
src={chevroneLeft}
|
||||||
|
alt="back"
|
||||||
|
className="h-[24px] w-[24px] cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/mission/${missionId - 1}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<span>{missionId}</span>
|
<span>{missionId}</span>
|
||||||
<img src={chevroneRight} alt="back" className="h-[24px] w-[24px] cursor-pointer" onClick={() => { navigate(`/mission/${missionId + 1}`) }} />
|
<img
|
||||||
|
src={chevroneRight}
|
||||||
|
alt="back"
|
||||||
|
className="h-[24px] w-[24px] cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/mission/${missionId + 1}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -35,19 +35,27 @@ const loadMathJax = () => {
|
|||||||
|
|
||||||
(window as any).MathJax = {
|
(window as any).MathJax = {
|
||||||
tex: {
|
tex: {
|
||||||
inlineMath: [["$$$", "$$$"]],
|
inlineMath: [['$$$', '$$$']],
|
||||||
displayMath: [["$$$$$$", "$$$$$$"]],
|
displayMath: [['$$$$$$', '$$$$$$']],
|
||||||
processEscapes: true,
|
processEscapes: true,
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
skipHtmlTags: ["script", "noscript", "style", "textarea", "pre", "code"],
|
skipHtmlTags: [
|
||||||
|
'script',
|
||||||
|
'noscript',
|
||||||
|
'style',
|
||||||
|
'textarea',
|
||||||
|
'pre',
|
||||||
|
'code',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
startup: { typeset: false },
|
startup: { typeset: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
const script = document.createElement("script");
|
const script = document.createElement('script');
|
||||||
script.id = "mathjax-script";
|
script.id = 'mathjax-script';
|
||||||
script.src = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js";
|
script.src =
|
||||||
|
'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js';
|
||||||
script.async = true;
|
script.async = true;
|
||||||
|
|
||||||
script.onload = () => {
|
script.onload = () => {
|
||||||
@@ -61,15 +69,19 @@ const loadMathJax = () => {
|
|||||||
return mathJaxPromise;
|
return mathJaxPromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
const replaceImages = (html: string, latex: string, mediaFiles?: MediaFile[]) => {
|
const replaceImages = (
|
||||||
|
html: string,
|
||||||
|
latex: string,
|
||||||
|
mediaFiles?: MediaFile[],
|
||||||
|
) => {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(html, "text/html");
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
|
||||||
const latexImageNames = Array.from(latex.matchAll(/\\includegraphics\{(.+?)\}/g)).map(
|
const latexImageNames = Array.from(
|
||||||
(match) => match[1]
|
latex.matchAll(/\\includegraphics\{(.+?)\}/g),
|
||||||
);
|
).map((match) => match[1]);
|
||||||
|
|
||||||
const imgs = doc.querySelectorAll<HTMLImageElement>("img.tex-graphics");
|
const imgs = doc.querySelectorAll<HTMLImageElement>('img.tex-graphics');
|
||||||
|
|
||||||
imgs.forEach((img, idx) => {
|
imgs.forEach((img, idx) => {
|
||||||
const imageName = latexImageNames[idx];
|
const imageName = latexImageNames[idx];
|
||||||
@@ -81,7 +93,11 @@ const replaceImages = (html: string, latex: string, mediaFiles?: MediaFile[]) =>
|
|||||||
return doc.body.innerHTML;
|
return doc.body.innerHTML;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LaTextContainer: React.FC<LaTextContainerProps> = ({ html, latex, mediaFiles }) => {
|
const LaTextContainer: React.FC<LaTextContainerProps> = ({
|
||||||
|
html,
|
||||||
|
latex,
|
||||||
|
mediaFiles,
|
||||||
|
}) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [processedHtml, setProcessedHtml] = useState<string>(html);
|
const [processedHtml, setProcessedHtml] = useState<string>(html);
|
||||||
|
|
||||||
@@ -94,7 +110,9 @@ const LaTextContainer: React.FC<LaTextContainerProps> = ({ html, latex, mediaFil
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const renderMath = () => {
|
const renderMath = () => {
|
||||||
if (containerRef.current && window.MathJax?.typesetPromise) {
|
if (containerRef.current && window.MathJax?.typesetPromise) {
|
||||||
window.MathJax.typesetPromise([containerRef.current]).catch(console.error);
|
window.MathJax.typesetPromise([containerRef.current]).catch(
|
||||||
|
console.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import SubmissionItem from "./SubmissionItem";
|
import SubmissionItem from './SubmissionItem';
|
||||||
import { useAppSelector } from "../../../redux/hooks";
|
import { useAppSelector } from '../../../redux/hooks';
|
||||||
import { FC, useEffect } from "react";
|
import { FC, useEffect } from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface Mission {
|
export interface Mission {
|
||||||
id: number;
|
id: number;
|
||||||
authorId: number;
|
authorId: number;
|
||||||
name: string;
|
name: string;
|
||||||
difficulty: "Easy" | "Medium" | "Hard";
|
difficulty: 'Easy' | 'Medium' | 'Hard';
|
||||||
tags: string[];
|
tags: string[];
|
||||||
timeLimit: number;
|
timeLimit: number;
|
||||||
memoryLimit: number;
|
memoryLimit: number;
|
||||||
@@ -16,39 +14,45 @@ export interface Mission {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MissionSubmissionsProps{
|
interface MissionSubmissionsProps {
|
||||||
missionId: number;
|
missionId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MissionSubmissions: FC<MissionSubmissionsProps> = ({missionId}) => {
|
const MissionSubmissions: FC<MissionSubmissionsProps> = ({ missionId }) => {
|
||||||
const submissions = useAppSelector((state) => state.submin.submitsById[missionId]);
|
const submissions = useAppSelector(
|
||||||
|
(state) => state.submin.submitsById[missionId],
|
||||||
useEffect(() => {
|
);
|
||||||
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
useEffect(() => {}, []);
|
||||||
|
|
||||||
const checkStatus = (status: string) => {
|
const checkStatus = (status: string) => {
|
||||||
if (status == "IncorrectAnswer")
|
if (status == 'IncorrectAnswer') return 'wronganswer';
|
||||||
return "wronganswer";
|
if (status == 'TimeLimitError') return 'timelimit';
|
||||||
if (status == "TimeLimitError")
|
|
||||||
return "timelimit";
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full box-border overflow-y-scroll overflow-x-hidden thin-scrollbar pr-[10px]">
|
<div className="h-full w-full box-border overflow-y-scroll overflow-x-hidden thin-scrollbar pr-[10px]">
|
||||||
|
{submissions &&
|
||||||
|
submissions.map((v, i) => (
|
||||||
{submissions && submissions.map((v, i) => (
|
|
||||||
<SubmissionItem
|
<SubmissionItem
|
||||||
key={i}
|
key={i}
|
||||||
id={v.id}
|
id={v.id}
|
||||||
language={v.solution.language}
|
language={v.solution.language}
|
||||||
time={v.solution.time}
|
time={v.solution.time}
|
||||||
verdict={v.solution.testerMessage?.includes("Compilation failed") ? "Compilation failed" : v.solution.testerMessage}
|
verdict={
|
||||||
type={i % 2 ? "second" : "first"}
|
v.solution.testerMessage?.includes(
|
||||||
status={v.solution.testerMessage == "All tests passed" ? "success" : checkStatus(v.solution.testerErrorCode)}
|
'Compilation failed',
|
||||||
|
)
|
||||||
|
? 'Compilation failed'
|
||||||
|
: v.solution.testerMessage
|
||||||
|
}
|
||||||
|
type={i % 2 ? 'second' : 'first'}
|
||||||
|
status={
|
||||||
|
v.solution.testerMessage == 'All tests passed'
|
||||||
|
? 'success'
|
||||||
|
: checkStatus(v.solution.testerErrorCode)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import React, { FC } from "react";
|
import React, { FC } from 'react';
|
||||||
import { cn } from "../../../lib/cn";
|
import { cn } from '../../../lib/cn';
|
||||||
import LaTextContainer from "./LaTextContainer";
|
import LaTextContainer from './LaTextContainer';
|
||||||
import { CopyIcon } from "../../../assets/icons/missions";
|
import { CopyIcon } from '../../../assets/icons/missions';
|
||||||
// import FullLatexRenderer from "./FullLatexRenderer";
|
// import FullLatexRenderer from "./FullLatexRenderer";
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface CopyableDivPropd {
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
interface CopyableDivPropd{
|
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,9 +16,9 @@ const CopyableDiv: FC<CopyableDivPropd> = ({ content }) => {
|
|||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(content);
|
await navigator.clipboard.writeText(content);
|
||||||
alert("Скопировано!");
|
alert('Скопировано!');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Ошибка копирования:", err);
|
console.error('Ошибка копирования:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,22 +30,18 @@ const CopyableDiv: FC<CopyableDivPropd> = ({ content }) => {
|
|||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
|
||||||
|
|
||||||
<img
|
<img
|
||||||
src={CopyIcon}
|
src={CopyIcon}
|
||||||
alt="copy"
|
alt="copy"
|
||||||
className={cn("absolute top-2 right-2 w-6 h-6 cursor-pointer opacity-0 transition-all duration-300 hover:h-7 hover:w-7 hover:top-[6px] hover:right-[6px]",
|
className={cn(
|
||||||
hovered && " opacity-100"
|
'absolute top-2 right-2 w-6 h-6 cursor-pointer opacity-0 transition-all duration-300 hover:h-7 hover:w-7 hover:top-[6px] hover:right-[6px]',
|
||||||
|
hovered && ' opacity-100',
|
||||||
)}
|
)}
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface StatementData {
|
export interface StatementData {
|
||||||
id?: number;
|
id?: number;
|
||||||
@@ -66,9 +60,9 @@ export interface StatementData {
|
|||||||
|
|
||||||
function extractDivByClass(html: string, className: string): string {
|
function extractDivByClass(html: string, className: string): string {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(html, "text/html");
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
const div = doc.querySelector(`div.${className}`);
|
const div = doc.querySelector(`div.${className}`);
|
||||||
return div ? div.outerHTML : "";
|
return div ? div.outerHTML : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const Statement: React.FC<StatementData> = ({
|
const Statement: React.FC<StatementData> = ({
|
||||||
@@ -77,62 +71,109 @@ const Statement: React.FC<StatementData> = ({
|
|||||||
tags,
|
tags,
|
||||||
timeLimit = 1000,
|
timeLimit = 1000,
|
||||||
memoryLimit = 256 * 1024 * 1024,
|
memoryLimit = 256 * 1024 * 1024,
|
||||||
legend = "",
|
legend = '',
|
||||||
input = "",
|
input = '',
|
||||||
output = "",
|
output = '',
|
||||||
sampleTests = [],
|
sampleTests = [],
|
||||||
notes = "",
|
notes = '',
|
||||||
html = "",
|
html = '',
|
||||||
mediaFiles,
|
mediaFiles,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full h-full medium-scrollbar pl-[20px] pr-[12px] gap-[20px] text-liquid-white overflow-y-scroll thin-dark-scrollbar [scrollbar-gutter:stable]">
|
<div className="flex flex-col w-full h-full medium-scrollbar pl-[20px] pr-[12px] gap-[20px] text-liquid-white overflow-y-scroll thin-dark-scrollbar [scrollbar-gutter:stable]">
|
||||||
<div>
|
<div>
|
||||||
<p className="h-[50px] text-[40px] font-bold text-liquid-white">{name}</p>
|
<p className="h-[50px] text-[40px] font-bold text-liquid-white">
|
||||||
<p className="h-[23px] text-[18px] font-bold text-liquid-light">Задача #{id}</p>
|
{name}
|
||||||
|
</p>
|
||||||
|
<p className="h-[23px] text-[18px] font-bold text-liquid-light">
|
||||||
|
Задача #{id}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-[10px] w-full flex-wrap">
|
<div className="flex gap-[10px] w-full flex-wrap">
|
||||||
{tags && tags.map((v, i) => <div key={i} className="px-[16px] py-[8px] rounded-full bg-liquid-lighter ">{v}</div>)}
|
{tags &&
|
||||||
|
tags.map((v, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="px-[16px] py-[8px] rounded-full bg-liquid-lighter "
|
||||||
|
>
|
||||||
|
{v}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="text-liquid-white h-[20px] text-[18px] font-bold"><span className="text-liquid-light">ограничение по времени на тест:</span> {timeLimit / 1000} секунда</p>
|
<p className="text-liquid-white h-[20px] text-[18px] font-bold">
|
||||||
<p className="text-liquid-white h-[20px] text-[18px] font-bold"><span className="text-liquid-light">ограничение по памяти на тест:</span> {memoryLimit / 1024 / 1024} мегабайт</p>
|
<span className="text-liquid-light">
|
||||||
<p className="text-liquid-white h-[20px] text-[18px] font-bold"><span className="text-liquid-light">ввод:</span> стандартный ввод</p>
|
ограничение по времени на тест:
|
||||||
<p className="text-liquid-white h-[20px] text-[18px] font-bold"><span className="text-liquid-light">вывод:</span> стандартный вывод</p>
|
</span>{' '}
|
||||||
|
{timeLimit / 1000} секунда
|
||||||
|
</p>
|
||||||
|
<p className="text-liquid-white h-[20px] text-[18px] font-bold">
|
||||||
|
<span className="text-liquid-light">
|
||||||
|
ограничение по памяти на тест:
|
||||||
|
</span>{' '}
|
||||||
|
{memoryLimit / 1024 / 1024} мегабайт
|
||||||
|
</p>
|
||||||
|
<p className="text-liquid-white h-[20px] text-[18px] font-bold">
|
||||||
|
<span className="text-liquid-light">ввод:</span> стандартный
|
||||||
|
ввод
|
||||||
|
</p>
|
||||||
|
<p className="text-liquid-white h-[20px] text-[18px] font-bold">
|
||||||
|
<span className="text-liquid-light">вывод:</span>{' '}
|
||||||
|
стандартный вывод
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-[10px] mt-[20px]">
|
<div className="flex flex-col gap-[10px] mt-[20px]">
|
||||||
<LaTextContainer html={extractDivByClass(html, "legend")} latex={legend} mediaFiles={mediaFiles}/>
|
<LaTextContainer
|
||||||
|
html={extractDivByClass(html, 'legend')}
|
||||||
|
latex={legend}
|
||||||
|
mediaFiles={mediaFiles}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-[10px]">
|
<div className="flex flex-col gap-[10px]">
|
||||||
<LaTextContainer html={extractDivByClass(html, "input-specification")} latex={input} mediaFiles={mediaFiles}/>
|
<LaTextContainer
|
||||||
|
html={extractDivByClass(html, 'input-specification')}
|
||||||
|
latex={input}
|
||||||
|
mediaFiles={mediaFiles}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-[10px]">
|
<div className="flex flex-col gap-[10px]">
|
||||||
<LaTextContainer html={extractDivByClass(html, "output-specification")} latex={output} mediaFiles={mediaFiles}/>
|
<LaTextContainer
|
||||||
|
html={extractDivByClass(html, 'output-specification')}
|
||||||
|
latex={output}
|
||||||
|
mediaFiles={mediaFiles}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-[10px]">
|
<div className="flex flex-col gap-[10px]">
|
||||||
<div className="text-[18px] font-bold">{sampleTests.length == 1 ? "Пример" : "Примеры"}</div>
|
<div className="text-[18px] font-bold">
|
||||||
|
{sampleTests.length == 1 ? 'Пример' : 'Примеры'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sampleTests.map((v, i) => (
|
||||||
{sampleTests.map((v, i) =>
|
|
||||||
<div key={i} className="flex flex-col gap-[10px]">
|
<div key={i} className="flex flex-col gap-[10px]">
|
||||||
<div className="text-[14px] font-bold">Входные данные</div>
|
<div className="text-[14px] font-bold">
|
||||||
<CopyableDiv content={v.input}/>
|
Входные данные
|
||||||
<div className="text-[14px] font-bold">Выходные данные</div>
|
|
||||||
<CopyableDiv content={v.output}/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<CopyableDiv content={v.input} />
|
||||||
|
<div className="text-[14px] font-bold">
|
||||||
|
Выходные данные
|
||||||
|
</div>
|
||||||
|
<CopyableDiv content={v.output} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-[10px]">
|
<div className="flex flex-col gap-[10px]">
|
||||||
<LaTextContainer html={extractDivByClass(html, "note")} latex={notes} mediaFiles={mediaFiles}/>
|
<LaTextContainer
|
||||||
|
html={extractDivByClass(html, 'note')}
|
||||||
|
latex={notes}
|
||||||
|
mediaFiles={mediaFiles}
|
||||||
|
/>
|
||||||
<div>Автор: Jacks</div>
|
<div>Автор: Jacks</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cn } from "../../../lib/cn";
|
import { cn } from '../../../lib/cn';
|
||||||
// import { IconError, IconSuccess } from "../../../assets/icons/missions";
|
// import { IconError, IconSuccess } from "../../../assets/icons/missions";
|
||||||
// import { useNavigate } from "react-router-dom";
|
// import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
@@ -7,8 +7,8 @@ export interface SubmissionItemProps {
|
|||||||
language: string;
|
language: string;
|
||||||
time: string;
|
time: string;
|
||||||
verdict: string;
|
verdict: string;
|
||||||
type: "first" | "second";
|
type: 'first' | 'second';
|
||||||
status?: "success" | "wronganswer" | "timelimit";
|
status?: 'success' | 'wronganswer' | 'timelimit';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatMilliseconds(ms: number): string {
|
export function formatMilliseconds(ms: number): string {
|
||||||
@@ -25,12 +25,12 @@ export function formatBytesToMB(bytes: number): string {
|
|||||||
function formatDate(dateString: string): string {
|
function formatDate(dateString: string): string {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
|
|
||||||
const day = date.getDate().toString().padStart(2, "0");
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
|
|
||||||
const hours = date.getHours().toString().padStart(2, "0");
|
const hours = date.getHours().toString().padStart(2, '0');
|
||||||
const minutes = date.getMinutes().toString().padStart(2, "0");
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
|
||||||
return `${day}/${month}/${year}\n${hours}:${minutes}`;
|
return `${day}/${month}/${year}\n${hours}:${minutes}`;
|
||||||
}
|
}
|
||||||
@@ -46,30 +46,34 @@ const SubmissionItem: React.FC<SubmissionItemProps> = ({
|
|||||||
// const navigate = useNavigate();
|
// const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(" w-full relative rounded-[10px] text-liquid-white",
|
<div
|
||||||
type == "first" ? "bg-liquid-lighter" : "bg-liquid-background",
|
className={cn(
|
||||||
"grid grid-cols-[80px,1fr,1fr,2fr] grid-flow-col gap-[20px] px-[20px] box-border items-center",
|
' w-full relative rounded-[10px] text-liquid-white',
|
||||||
status == "wronganswer" && "border-l-[11px] border-l-liquid-red pl-[9px]",
|
type == 'first' ? 'bg-liquid-lighter' : 'bg-liquid-background',
|
||||||
status == "timelimit" && "border-l-[11px] border-l-liquid-orange pl-[9px]",
|
'grid grid-cols-[80px,1fr,1fr,2fr] grid-flow-col gap-[20px] px-[20px] box-border items-center',
|
||||||
status == "success" && "border-l-[11px] border-l-liquid-green pl-[9px]",
|
status == 'wronganswer' &&
|
||||||
"cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300",
|
'border-l-[11px] border-l-liquid-red pl-[9px]',
|
||||||
|
status == 'timelimit' &&
|
||||||
|
'border-l-[11px] border-l-liquid-orange pl-[9px]',
|
||||||
|
status == 'success' &&
|
||||||
|
'border-l-[11px] border-l-liquid-green pl-[9px]',
|
||||||
|
'cursor-pointer brightness-100 hover:brightness-125 transition-all duration-300',
|
||||||
)}
|
)}
|
||||||
onClick={() => { }}
|
onClick={() => {}}
|
||||||
>
|
>
|
||||||
<div className="text-[18px] font-bold">
|
<div className="text-[18px] font-bold">#{id}</div>
|
||||||
#{id}
|
|
||||||
</div>
|
|
||||||
<div className="text-[18px] font-bold text-center">
|
<div className="text-[18px] font-bold text-center">
|
||||||
{formatDate(time)}
|
{formatDate(time)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[18px] font-bold text-center">
|
<div className="text-[18px] font-bold text-center">{language}</div>
|
||||||
{language}
|
<div
|
||||||
</div>
|
className={cn(
|
||||||
<div className={cn("text-[18px] font-bold text-center",
|
'text-[18px] font-bold text-center',
|
||||||
status == "wronganswer" && "text-liquid-red",
|
status == 'wronganswer' && 'text-liquid-red',
|
||||||
status == "timelimit" && "text-liquid-orange",
|
status == 'timelimit' && 'text-liquid-orange',
|
||||||
status == "success" && "text-liquid-green",
|
status == 'success' && 'text-liquid-green',
|
||||||
)} >
|
)}
|
||||||
|
>
|
||||||
{verdict}
|
{verdict}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user