missions and filter
This commit is contained in:
3
src/assets/icons/filters/filters-active.svg
Normal file
3
src/assets/icons/filters/filters-active.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M19 2H5C4.20435 2 3.44129 2.31607 2.87868 2.87868C2.31607 3.44129 2 4.20435 2 5V6.17C1.99986 6.58294 2.08497 6.99147 2.25 7.37V7.43C2.39128 7.75097 2.59139 8.04266 2.84 8.29L9 14.41V21C8.99966 21.1699 9.04264 21.3372 9.12487 21.4859C9.20711 21.6346 9.32589 21.7599 9.47 21.85C9.62914 21.9486 9.81277 22.0006 10 22C10.1565 21.9991 10.3107 21.9614 10.45 21.89L14.45 19.89C14.6149 19.8069 14.7536 19.6798 14.8507 19.5227C14.9478 19.3656 14.9994 19.1847 15 19V14.41L21.12 8.29C21.3686 8.04266 21.5687 7.75097 21.71 7.43V7.37C21.8888 6.99443 21.9876 6.58578 22 6.17V5C22 4.20435 21.6839 3.44129 21.1213 2.87868C20.5587 2.31607 19.7956 2 19 2ZM13.29 13.29C13.1973 13.3834 13.124 13.4943 13.0742 13.6161C13.0245 13.7379 12.9992 13.8684 13 14V18.38L11 19.38V14C11.0008 13.8684 10.9755 13.7379 10.9258 13.6161C10.876 13.4943 10.8027 13.3834 10.71 13.29L5.41 8H18.59L13.29 13.29ZM20 6H4V5C4 4.73478 4.10536 4.48043 4.29289 4.29289C4.48043 4.10536 4.73478 4 5 4H19C19.2652 4 19.5196 4.10536 19.7071 4.29289C19.8946 4.48043 20 4.73478 20 5V6Z" fill="#00DBD9"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
3
src/assets/icons/filters/filters.svg
Normal file
3
src/assets/icons/filters/filters.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M19 2H5C4.20435 2 3.44129 2.31607 2.87868 2.87868C2.31607 3.44129 2 4.20435 2 5V6.17C1.99986 6.58294 2.08497 6.99147 2.25 7.37V7.43C2.39128 7.75097 2.59139 8.04266 2.84 8.29L9 14.41V21C8.99966 21.1699 9.04264 21.3372 9.12487 21.4859C9.20711 21.6346 9.32589 21.7599 9.47 21.85C9.62914 21.9486 9.81277 22.0006 10 22C10.1565 21.9991 10.3107 21.9614 10.45 21.89L14.45 19.89C14.6149 19.8069 14.7536 19.6798 14.8507 19.5227C14.9478 19.3656 14.9994 19.1847 15 19V14.41L21.12 8.29C21.3686 8.04266 21.5687 7.75097 21.71 7.43V7.37C21.8888 6.99443 21.9876 6.58578 22 6.17V5C22 4.20435 21.6839 3.44129 21.1213 2.87868C20.5587 2.31607 19.7956 2 19 2ZM13.29 13.29C13.1973 13.3834 13.124 13.4943 13.0742 13.6161C13.0245 13.7379 12.9992 13.8684 13 14V18.38L11 19.38V14C11.0008 13.8684 10.9755 13.7379 10.9258 13.6161C10.876 13.4943 10.8027 13.3834 10.71 13.29L5.41 8H18.59L13.29 13.29ZM20 6H4V5C4 4.73478 4.10536 4.48043 4.29289 4.29289C4.48043 4.10536 4.73478 4 5 4H19C19.2652 4 19.5196 4.10536 19.7071 4.29289C19.8946 4.48043 20 4.73478 20 5V6Z" fill="#576466"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
6
src/assets/icons/filters/index.ts
Normal file
6
src/assets/icons/filters/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import FilterActive from './filters-active.svg';
|
||||||
|
import Filter from './filters.svg';
|
||||||
|
import Sort from './sort.svg';
|
||||||
|
import SortActive from './sort-active.svg';
|
||||||
|
|
||||||
|
export { Filter, FilterActive, Sort, SortActive };
|
||||||
3
src/assets/icons/filters/sort-active.svg
Normal file
3
src/assets/icons/filters/sort-active.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13.4415 6.62732H22M13.4415 11.4421H19.5547M13.4415 16.2569H17.1094M5.80564 6V18M5.80564 18L2 14.3317M5.80564 18L9.7566 14.3317" stroke="#00DBD9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 324 B |
3
src/assets/icons/filters/sort.svg
Normal file
3
src/assets/icons/filters/sort.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13.4415 6.62732H22M13.4415 11.4421H19.5547M13.4415 16.2569H17.1094M5.80564 6V18M5.80564 18L2 14.3317M5.80564 18L9.7566 14.3317" stroke="#576466" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 324 B |
@@ -5,7 +5,7 @@ interface ButtonProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
text?: string;
|
text?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
onClick: () => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
color?: 'primary' | 'secondary' | 'error' | 'warning' | 'success';
|
color?: 'primary' | 'secondary' | 'error' | 'warning' | 'success';
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,9 @@ export const PrimaryButton: React.FC<ButtonProps> = ({
|
|||||||
disabled && 'pointer-events-none',
|
disabled && 'pointer-events-none',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Основной контейнер, */}
|
{/* Основной контейнер, */}
|
||||||
<div
|
<div
|
||||||
@@ -60,10 +63,8 @@ export const PrimaryButton: React.FC<ButtonProps> = ({
|
|||||||
'[&:focus-visible+*]:outline-liquid-brightmain',
|
'[&:focus-visible+*]:outline-liquid-brightmain',
|
||||||
)}
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={(
|
onClick={() => {
|
||||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
onClick();
|
||||||
) => {
|
|
||||||
onClick(e);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ interface ButtonProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
text?: string;
|
text?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
onClick: () => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
color?: 'primary' | 'secondary' | 'error' | 'warning' | 'success';
|
color?: 'primary' | 'secondary' | 'error' | 'warning' | 'success';
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,9 @@ export const ReverseButton: React.FC<ButtonProps> = ({
|
|||||||
disabled && 'pointer-events-none',
|
disabled && 'pointer-events-none',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Основной контейнер, */}
|
{/* Основной контейнер, */}
|
||||||
<div
|
<div
|
||||||
@@ -61,10 +64,8 @@ export const ReverseButton: React.FC<ButtonProps> = ({
|
|||||||
'[&:focus-visible+*]:outline-liquid-brightmain',
|
'[&:focus-visible+*]:outline-liquid-brightmain',
|
||||||
)}
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={(
|
onClick={() => {
|
||||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
onClick();
|
||||||
) => {
|
|
||||||
onClick(e);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ interface ButtonProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
text?: string;
|
text?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
onClick: () => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +23,9 @@ export const SecondaryButton: React.FC<ButtonProps> = ({
|
|||||||
disabled && 'pointer-events-none',
|
disabled && 'pointer-events-none',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* Основной контейнер, */}
|
{/* Основной контейнер, */}
|
||||||
<div
|
<div
|
||||||
@@ -41,8 +44,8 @@ export const SecondaryButton: React.FC<ButtonProps> = ({
|
|||||||
'[&:focus-visible+*]:outline-liquid-brightmain',
|
'[&:focus-visible+*]:outline-liquid-brightmain',
|
||||||
)}
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={(e) => {
|
onClick={() => {
|
||||||
onClick(e);
|
onClick();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
124
src/components/drop-down-list/Filter.tsx
Normal file
124
src/components/drop-down-list/Filter.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../../lib/cn';
|
||||||
|
import { checkMark, chevroneDropDownList } from '../../assets/icons/input';
|
||||||
|
import { useClickOutside } from '../../hooks/useClickOutside';
|
||||||
|
|
||||||
|
export interface FilterItem {
|
||||||
|
text: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
onChange: (state: string[]) => void; // теперь массив выбранных значений
|
||||||
|
defaultState?: FilterItem[];
|
||||||
|
items: FilterItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Filter: React.FC<FilterProps> = ({
|
||||||
|
className = '',
|
||||||
|
onChange,
|
||||||
|
defaultState = [],
|
||||||
|
items = [{ text: '', value: '' }],
|
||||||
|
}) => {
|
||||||
|
if (items.length === 0) items.push({ text: '', value: '' });
|
||||||
|
|
||||||
|
const [selectedValues, setSelectedValues] =
|
||||||
|
React.useState<FilterItem[]>(defaultState);
|
||||||
|
const [active, setActive] = React.useState<boolean>(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
onChange(selectedValues.map((v) => v.value));
|
||||||
|
}, [selectedValues]);
|
||||||
|
|
||||||
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
useClickOutside(ref, () => setActive(false));
|
||||||
|
|
||||||
|
const toggleItem = (item: FilterItem) => {
|
||||||
|
const exists = selectedValues.some((v) => v.value === item.value);
|
||||||
|
if (exists) {
|
||||||
|
setSelectedValues(
|
||||||
|
selectedValues.filter((v) => v.value !== item.value),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedValues([...selectedValues, item]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayText =
|
||||||
|
selectedValues.length > 0
|
||||||
|
? selectedValues.map((v) => v.text).join(', ')
|
||||||
|
: 'Выберите...';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('relative', className)} ref={ref}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center h-[40px] rounded-[10px] bg-liquid-lighter px-[16px] w-[220px]',
|
||||||
|
'text-[16px] font-bold cursor-pointer select-none',
|
||||||
|
'transition-all active:scale-95 duration-300 overflow-hidden whitespace-nowrap text-ellipsis',
|
||||||
|
)}
|
||||||
|
onClick={() => setActive(!active)}
|
||||||
|
title={displayText}
|
||||||
|
>
|
||||||
|
{displayText}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src={chevroneDropDownList}
|
||||||
|
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
|
||||||
|
className={cn(
|
||||||
|
'absolute rounded-[10px] bg-liquid-lighter w-[220px] left-0 top-[48px] z-50 transition-all duration-300',
|
||||||
|
'grid overflow-hidden',
|
||||||
|
active
|
||||||
|
? 'grid-rows-[1fr] opacity-100'
|
||||||
|
: 'grid-rows-[0fr] opacity-0',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="overflow-hidden p-[8px]">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'overflow-y-scroll max-h-[220px] thin-scrollbar pr-[8px]',
|
||||||
|
'grid grid-cols-2 gap-[4px]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{items.map((v, i) => {
|
||||||
|
const isSelected = selectedValues.some(
|
||||||
|
(sel) => sel.value === v.value,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={cn(
|
||||||
|
'cursor-pointer h-[36px] relative transition-all duration-300 flex items-center pl-[8px] pr-[24px] rounded-[6px]',
|
||||||
|
'text-[14px] font-medium select-none',
|
||||||
|
'hover:bg-liquid-background',
|
||||||
|
isSelected &&
|
||||||
|
'bg-liquid-background font-semibold',
|
||||||
|
)}
|
||||||
|
onClick={() => toggleItem(v)}
|
||||||
|
>
|
||||||
|
{v.text}
|
||||||
|
|
||||||
|
{isSelected && (
|
||||||
|
<img
|
||||||
|
src={checkMark}
|
||||||
|
className="absolute right-[6px] w-[16px] h-[16px]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
127
src/components/drop-down-list/Sorter.tsx
Normal file
127
src/components/drop-down-list/Sorter.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../../lib/cn';
|
||||||
|
import { checkMark } from '../../assets/icons/input';
|
||||||
|
import { useClickOutside } from '../../hooks/useClickOutside';
|
||||||
|
import { Sort, SortActive } from '../../assets/icons/filters';
|
||||||
|
|
||||||
|
export interface SorterItem {
|
||||||
|
text: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SorterProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
className?: string;
|
||||||
|
onChange: (state: string) => void;
|
||||||
|
defaultState?: SorterItem;
|
||||||
|
items: SorterItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Sorter: React.FC<SorterProps> = ({
|
||||||
|
// disabled = false,
|
||||||
|
className = '',
|
||||||
|
onChange,
|
||||||
|
defaultState,
|
||||||
|
items = [{ text: '', value: '' }],
|
||||||
|
}) => {
|
||||||
|
if (items.length == 0) items.push({ text: '', value: '' });
|
||||||
|
|
||||||
|
const [value, setValue] = React.useState<SorterItem>(
|
||||||
|
defaultState != undefined ? defaultState : items[0],
|
||||||
|
);
|
||||||
|
const [active, setActive] = React.useState<boolean>(false);
|
||||||
|
|
||||||
|
React.useEffect(() => onChange(value.value), [value]);
|
||||||
|
|
||||||
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useClickOutside(ref, () => {
|
||||||
|
setActive(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('relative', className)} ref={ref}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'grid items-center h-[40px] rounded-full bg-liquid-lighter grid-cols-[40px]',
|
||||||
|
'text-[18px] font-bold cursor-pointer select-none',
|
||||||
|
'transitin-all active:scale-95 duration-300 overflow-hidden',
|
||||||
|
active && ' grid-cols-[1fr]',
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setActive(!active);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'text-liquid-brightmain pl-[40px] pr-[16px]',
|
||||||
|
active && '',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
{value.text}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-[24px] w-[24px] bg-red-500"></div>
|
||||||
|
<img
|
||||||
|
src={Sort}
|
||||||
|
className={cn(
|
||||||
|
' absolute right-[16px] h-[24px] w-[24px] top-[8px] left-[8px] rotate-0 transition-all duration-300 pointer-events-none',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={SortActive}
|
||||||
|
className={cn(
|
||||||
|
' absolute right-[16px] h-[24px] w-[24px] top-[8px] left-[8px] rotate-0 transition-all duration-300 pointer-events-none opacity-0',
|
||||||
|
active && ' opacity-100',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
' absolute rounded-[10px] bg-liquid-lighter w-[220px] left-0 top-[48px] z-50 transition-all duration-300',
|
||||||
|
'grid overflow-hidden',
|
||||||
|
active
|
||||||
|
? 'grid-rows-[1fr] opacity-100'
|
||||||
|
: 'grid-rows-[0fr] opacity-0',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className=" overflow-hidden p-[8px]">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
' overflow-y-scroll max-h-[200px] thin-scrollbar pr-[8px] ',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{items.map((v, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={cn(
|
||||||
|
'cursor-pointer h-[36px] relative transition-all duration-300',
|
||||||
|
i + 1 != items.length &&
|
||||||
|
'border-b-liquid-light border-b-[1px]',
|
||||||
|
'text-[16px] font-medium cursor-pointer select-none flex items-center pl-[8px]',
|
||||||
|
'hover:bg-liquid-background',
|
||||||
|
'first:rounded-t-[6px] last:rounded-b-[6px]',
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setValue(v);
|
||||||
|
setActive(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{v.text}
|
||||||
|
|
||||||
|
{v.text == value.text && (
|
||||||
|
<img
|
||||||
|
src={checkMark}
|
||||||
|
className=" absolute right-[8px]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,7 +4,6 @@ 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 {
|
import {
|
||||||
createContest,
|
|
||||||
CreateContestBody,
|
CreateContestBody,
|
||||||
deleteContest,
|
deleteContest,
|
||||||
fetchContestById,
|
fetchContestById,
|
||||||
@@ -17,7 +16,6 @@ import { Navigate, useNavigate } from 'react-router-dom';
|
|||||||
import { fetchMissionById } from '../redux/slices/missions';
|
import { fetchMissionById } from '../redux/slices/missions';
|
||||||
import { ReverseButton } from '../components/button/ReverseButton';
|
import { ReverseButton } from '../components/button/ReverseButton';
|
||||||
|
|
||||||
|
|
||||||
interface Mission {
|
interface Mission {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -27,7 +25,6 @@ interface Mission {
|
|||||||
* Страница создания / редактирования контеста
|
* Страница создания / редактирования контеста
|
||||||
*/
|
*/
|
||||||
const ContestEditor = () => {
|
const ContestEditor = () => {
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -36,21 +33,16 @@ const ContestEditor = () => {
|
|||||||
const contestId = Number(query.get('contestId') ?? undefined);
|
const contestId = Number(query.get('contestId') ?? undefined);
|
||||||
const refactor = !!contestId;
|
const refactor = !!contestId;
|
||||||
|
|
||||||
if (!refactor){
|
if (!refactor) {
|
||||||
return <Navigate to="/home/account/acontest" />
|
return <Navigate to="/home/account/acontest" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const status = useAppSelector(
|
const status = useAppSelector(
|
||||||
(state) => state.contests.createContest.status,
|
(state) => state.contests.createContest.status,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const [missionIdInput, setMissionIdInput] = useState<string>('');
|
const [missionIdInput, setMissionIdInput] = useState<string>('');
|
||||||
|
|
||||||
|
|
||||||
const [contest, setContest] = useState<CreateContestBody>({
|
const [contest, setContest] = useState<CreateContestBody>({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
@@ -67,9 +59,12 @@ const ContestEditor = () => {
|
|||||||
|
|
||||||
const [missions, setMissions] = useState<Mission[]>([]);
|
const [missions, setMissions] = useState<Mission[]>([]);
|
||||||
|
|
||||||
|
const statusDelete = useAppSelector(
|
||||||
const statusDelete = useAppSelector((state) => state.contests.deleteContest.status)
|
(state) => state.contests.deleteContest.status,
|
||||||
const statusUpdate = useAppSelector((state) => state.contests.updateContest.status);
|
);
|
||||||
|
const statusUpdate = useAppSelector(
|
||||||
|
(state) => state.contests.updateContest.status,
|
||||||
|
);
|
||||||
|
|
||||||
const { contest: contestById, status: contestByIdstatus } = useAppSelector(
|
const { contest: contestById, status: contestByIdstatus } = useAppSelector(
|
||||||
(state) => state.contests.fetchContestById,
|
(state) => state.contests.fetchContestById,
|
||||||
@@ -85,15 +80,13 @@ const ContestEditor = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateContest = () => {
|
const handleUpdateContest = () => {
|
||||||
dispatch(updateContest({...contest, contestId}));
|
dispatch(updateContest({ ...contest, contestId }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteContest = () => {
|
const handleDeleteContest = () => {
|
||||||
dispatch(deleteContest(contestId));
|
dispatch(deleteContest(contestId));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const addMission = () => {
|
const addMission = () => {
|
||||||
const id = Number(missionIdInput.trim());
|
const id = Number(missionIdInput.trim());
|
||||||
if (!id || contest.missionIds?.includes(id)) return;
|
if (!id || contest.missionIds?.includes(id)) return;
|
||||||
@@ -121,19 +114,22 @@ const ContestEditor = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (statusDelete == "successful"){
|
if (statusDelete == 'successful') {
|
||||||
dispatch(setContestStatus({key: "deleteContest", status: "idle"}))
|
dispatch(
|
||||||
navigate('/home/account/contests')
|
setContestStatus({ key: 'deleteContest', status: 'idle' }),
|
||||||
|
);
|
||||||
|
navigate('/home/account/contests');
|
||||||
}
|
}
|
||||||
}, [statusDelete])
|
}, [statusDelete]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (statusUpdate == "successful"){
|
if (statusUpdate == 'successful') {
|
||||||
dispatch(setContestStatus({key: "updateContest", status: "idle"}))
|
dispatch(
|
||||||
navigate('/home/account/contests')
|
setContestStatus({ key: 'updateContest', status: 'idle' }),
|
||||||
|
);
|
||||||
|
navigate('/home/account/contests');
|
||||||
}
|
}
|
||||||
}, [statusUpdate])
|
}, [statusUpdate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (refactor) {
|
if (refactor) {
|
||||||
@@ -146,8 +142,10 @@ const ContestEditor = () => {
|
|||||||
setContest({
|
setContest({
|
||||||
...contestById,
|
...contestById,
|
||||||
// groupIds: contestById.groups.map(group => group.groupId),
|
// groupIds: contestById.groups.map(group => group.groupId),
|
||||||
missionIds: contestById.missions?.map(mission => mission.id),
|
missionIds: contestById.missions?.map((mission) => mission.id),
|
||||||
articleIds: contestById.articles?.map(article => article.articleId),
|
articleIds: contestById.articles?.map(
|
||||||
|
(article) => article.articleId,
|
||||||
|
),
|
||||||
visibility: 'Public',
|
visibility: 'Public',
|
||||||
scheduleType: 'AlwaysOpen',
|
scheduleType: 'AlwaysOpen',
|
||||||
});
|
});
|
||||||
@@ -300,19 +298,17 @@ const ContestEditor = () => {
|
|||||||
|
|
||||||
{/* Кнопки */}
|
{/* Кнопки */}
|
||||||
<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
|
||||||
<PrimaryButton
|
onClick={handleUpdateContest}
|
||||||
onClick={handleUpdateContest}
|
text="Сохранить"
|
||||||
text="Сохранить"
|
disabled={status === 'loading'}
|
||||||
disabled={status === 'loading'}
|
/>
|
||||||
/>
|
<ReverseButton
|
||||||
<ReverseButton
|
color="error"
|
||||||
color="error"
|
onClick={handleDeleteContest}
|
||||||
onClick={handleDeleteContest}
|
text="Удалить"
|
||||||
text="Удалить"
|
disabled={statusDelete === 'loading'}
|
||||||
disabled={statusDelete === 'loading'}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,8 +51,10 @@ const Home = () => {
|
|||||||
<p>{jwt}</p>
|
<p>{jwt}</p>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (jwt)
|
if (jwt) {
|
||||||
navigator.clipboard.writeText(jwt);
|
navigator.clipboard.writeText(jwt);
|
||||||
|
alert(jwt);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
text="скопировать токен"
|
text="скопировать токен"
|
||||||
className="pt-[20px]"
|
className="pt-[20px]"
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export interface Mission {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
timeLimit: number;
|
||||||
|
memoryLimit: number;
|
||||||
statements?: Statement[];
|
statements?: Statement[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@ interface MissionsState {
|
|||||||
fetchList: Status;
|
fetchList: Status;
|
||||||
fetchById: Status;
|
fetchById: Status;
|
||||||
upload: Status;
|
upload: Status;
|
||||||
|
fetchMy: Status;
|
||||||
};
|
};
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
@@ -45,6 +48,7 @@ const initialState: MissionsState = {
|
|||||||
fetchList: 'idle',
|
fetchList: 'idle',
|
||||||
fetchById: 'idle',
|
fetchById: 'idle',
|
||||||
upload: 'idle',
|
upload: 'idle',
|
||||||
|
fetchMy: 'idle',
|
||||||
},
|
},
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
@@ -90,6 +94,22 @@ export const fetchMissionById = createAsyncThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ✅ GET /missions/my
|
||||||
|
export const fetchMyMissions = createAsyncThunk(
|
||||||
|
'missions/fetchMyMissions',
|
||||||
|
async (_, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/missions/my');
|
||||||
|
return response.data as Mission[]; // массив миссий пользователя
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
err.response?.data?.message ||
|
||||||
|
'Ошибка при получении моих миссий',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// POST /missions/upload
|
// POST /missions/upload
|
||||||
export const uploadMission = createAsyncThunk(
|
export const uploadMission = createAsyncThunk(
|
||||||
'missions/uploadMission',
|
'missions/uploadMission',
|
||||||
@@ -189,6 +209,26 @@ const missionsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ✅ FETCH MY MISSIONS ───
|
||||||
|
builder.addCase(fetchMyMissions.pending, (state) => {
|
||||||
|
state.statuses.fetchMy = 'loading';
|
||||||
|
state.error = null;
|
||||||
|
});
|
||||||
|
builder.addCase(
|
||||||
|
fetchMyMissions.fulfilled,
|
||||||
|
(state, action: PayloadAction<Mission[]>) => {
|
||||||
|
state.statuses.fetchMy = 'successful';
|
||||||
|
state.missions = action.payload;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
builder.addCase(
|
||||||
|
fetchMyMissions.rejected,
|
||||||
|
(state, action: PayloadAction<any>) => {
|
||||||
|
state.statuses.fetchMy = 'failed';
|
||||||
|
state.error = action.payload;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ─── UPLOAD MISSION ───
|
// ─── UPLOAD MISSION ───
|
||||||
builder.addCase(uploadMission.pending, (state) => {
|
builder.addCase(uploadMission.pending, (state) => {
|
||||||
state.statuses.upload = 'loading';
|
state.statuses.upload = 'loading';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import AccountMenu from './AccoutMenu';
|
import AccountMenu from './AccoutMenu';
|
||||||
import RightPanel from './RightPanel';
|
import RightPanel from './RightPanel';
|
||||||
import MissionsBlock from './missions/MissionsBlock';
|
import Missions from './missions/Missions';
|
||||||
import Contests from './contests/Contests';
|
import Contests from './contests/Contests';
|
||||||
import ArticlesBlock from './articles/ArticlesBlock';
|
import ArticlesBlock from './articles/ArticlesBlock';
|
||||||
import { useAppDispatch } from '../../../redux/hooks';
|
import { useAppDispatch } from '../../../redux/hooks';
|
||||||
@@ -24,10 +24,7 @@ const Account = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="h-full min-h-0 overflow-y-scroll medium-scrollbar flex flex-col gap-[20px] ">
|
<div className="h-full min-h-0 overflow-y-scroll medium-scrollbar flex flex-col gap-[20px] ">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route path="missions" element={<Missions />} />
|
||||||
path="missions"
|
|
||||||
element={<MissionsBlock />}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="articles"
|
path="articles"
|
||||||
element={<ArticlesBlock />}
|
element={<ArticlesBlock />}
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ const Contests = () => {
|
|||||||
const myContestsState = useAppSelector(
|
const myContestsState = useAppSelector(
|
||||||
(state) => state.contests.fetchMyContests,
|
(state) => state.contests.fetchMyContests,
|
||||||
);
|
);
|
||||||
const regContestsState = useAppSelector(
|
|
||||||
(state) => state.contests.fetchRegisteredContests,
|
|
||||||
);
|
|
||||||
|
|
||||||
// При загрузке страницы — выставляем вкладку и подгружаем контесты
|
// При загрузке страницы — выставляем вкладку и подгружаем контесты
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
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 { ReverseButton } from '../../../../components/button/ReverseButton';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Edit } from '../../../../assets/icons/input';
|
import { Edit } from '../../../../assets/icons/input';
|
||||||
|
|
||||||
@@ -57,10 +55,6 @@ const ContestItem: React.FC<ContestItemProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
const waitTime = new Date(startAt).getTime() - now.getTime();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -98,22 +98,12 @@ const ContestItem: React.FC<ContestItemProps> = ({
|
|||||||
{statusRegister == 'reg' ? (
|
{statusRegister == 'reg' ? (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
<PrimaryButton
|
<PrimaryButton onClick={() => {}} text="Регистрация" />
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
text="Регистрация"
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
<ReverseButton
|
<ReverseButton onClick={() => {}} text="Вы записаны" />
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
text="Вы записаны"
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
109
src/views/home/account/missions/Missions.tsx
Normal file
109
src/views/home/account/missions/Missions.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { FC, useEffect } from 'react';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
|
||||||
|
import { setMenuActiveProfilePage } from '../../../../redux/slices/store';
|
||||||
|
import { cn } from '../../../../lib/cn';
|
||||||
|
import MissionsBlock from './MissionsBlock';
|
||||||
|
import {
|
||||||
|
fetchMyMissions,
|
||||||
|
setMissionsStatus,
|
||||||
|
} from '../../../../redux/slices/missions';
|
||||||
|
|
||||||
|
interface ItemProps {
|
||||||
|
count: number;
|
||||||
|
totalCount: number;
|
||||||
|
title: string;
|
||||||
|
color?: 'default' | 'red' | 'green' | 'orange';
|
||||||
|
}
|
||||||
|
|
||||||
|
const Item: FC<ItemProps> = ({
|
||||||
|
count,
|
||||||
|
totalCount,
|
||||||
|
title,
|
||||||
|
color = 'default',
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-row rounded-full bg-liquid-lighter px-[16px] py-[8px] gap-[10px] text-[14px]',
|
||||||
|
color == 'default' && 'text-liquid-light',
|
||||||
|
color == 'red' && 'text-liquid-red',
|
||||||
|
color == 'green' && 'text-liquid-green',
|
||||||
|
color == 'orange' && 'text-liquid-orange',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{count}/{totalCount}
|
||||||
|
</div>
|
||||||
|
<div>{title}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Missions = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const missions = useAppSelector((state) => state.missions.missions);
|
||||||
|
const status = useAppSelector((state) => state.missions.statuses.fetchMy);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(setMenuActiveProfilePage('missions'));
|
||||||
|
dispatch(fetchMyMissions());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(setMissionsStatus({ key: 'fetchMy', status: 'idle' }));
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full relative overflow-y-scroll medium-scrollbar">
|
||||||
|
<div className="w-full flex flex-col">
|
||||||
|
<div className="p-[20px] flex flex-col gap-[20px]">
|
||||||
|
<div className="text-[24px] font-bold text-liquid-white">
|
||||||
|
Решенные задачи
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-between items-start">
|
||||||
|
<div className="flex gap-[10px]">
|
||||||
|
<Item count={14} totalCount={123} title="Задачи" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-[20px]">
|
||||||
|
<Item
|
||||||
|
count={14}
|
||||||
|
totalCount={123}
|
||||||
|
title="Easy"
|
||||||
|
color="green"
|
||||||
|
/>
|
||||||
|
<Item
|
||||||
|
count={14}
|
||||||
|
totalCount={123}
|
||||||
|
title="Medium"
|
||||||
|
color="orange"
|
||||||
|
/>
|
||||||
|
<Item
|
||||||
|
count={14}
|
||||||
|
totalCount={123}
|
||||||
|
title="Hard"
|
||||||
|
color="red"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-[24px] font-bold text-liquid-white">
|
||||||
|
Компетенции
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-[10px]">
|
||||||
|
<Item count={14} totalCount={123} title="Массивы" />
|
||||||
|
<Item count={14} totalCount={123} title="Списки" />
|
||||||
|
<Item count={14} totalCount={123} title="Стэк" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-[20px]">
|
||||||
|
<MissionsBlock
|
||||||
|
missions={missions ?? []}
|
||||||
|
title="Мои миссии"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Missions;
|
||||||
@@ -1,66 +1,71 @@
|
|||||||
import { FC, useEffect } from "react";
|
import { useState, FC } from 'react';
|
||||||
import { useAppDispatch } from "../../../../redux/hooks";
|
import { cn } from '../../../../lib/cn';
|
||||||
import { setMenuActiveProfilePage } from "../../../../redux/slices/store";
|
import { ChevroneDown } from '../../../../assets/icons/groups';
|
||||||
import { cn } from "../../../../lib/cn";
|
import MyMissionItem from './MyMissionItem';
|
||||||
|
import { Mission } from '../../../../redux/slices/missions';
|
||||||
|
|
||||||
|
interface MissionsBlockProps {
|
||||||
interface ItemProps {
|
missions: Mission[];
|
||||||
count: number;
|
|
||||||
totalCount: number;
|
|
||||||
title: string;
|
title: string;
|
||||||
color?: "default" | "red" | "green" | "orange";
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Item: FC<ItemProps> = ({count, totalCount, title, color = "default"}) => {
|
const MissionsBlock: FC<MissionsBlockProps> = ({
|
||||||
|
missions,
|
||||||
|
title,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
const [active, setActive] = useState<boolean>(true);
|
||||||
|
|
||||||
return <div className={cn("flex flex-row rounded-full bg-liquid-lighter px-[16px] py-[8px] gap-[10px] text-[14px]",
|
return (
|
||||||
color == "default" && "text-liquid-light",
|
<div
|
||||||
color == "red" && "text-liquid-red",
|
className={cn(
|
||||||
color == "green" && "text-liquid-green",
|
' border-b-[1px] border-b-liquid-lighter rounded-[10px]',
|
||||||
color == "orange" && "text-liquid-orange",
|
className,
|
||||||
)}>
|
)}
|
||||||
<div>{count}/{totalCount}</div>
|
>
|
||||||
<div>{title}</div>
|
<div
|
||||||
</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',
|
||||||
const MissionsBlock = () => {
|
)}
|
||||||
const dispatch = useAppDispatch();
|
onClick={() => {
|
||||||
|
setActive(!active);
|
||||||
useEffect(() => {
|
}}
|
||||||
dispatch(setMenuActiveProfilePage("missions"));
|
>
|
||||||
}, []);
|
<span>{title}</span>
|
||||||
|
<img
|
||||||
return (
|
src={ChevroneDown}
|
||||||
<div className="h-full w-full relative overflow-y-scroll medium-scrollbar">
|
className={cn(
|
||||||
<div className="w-full flex flex-col">
|
'transition-all duration-300',
|
||||||
<div className="p-[20px] flex flex-col gap-[20px]">
|
active && 'rotate-180',
|
||||||
<div className="text-[24px] font-bold text-liquid-white">Решенные задачи</div>
|
)}
|
||||||
<div className="flex flex-row justify-between items-start">
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex gap-[10px]">
|
<div
|
||||||
<Item count={14} totalCount={123} title="Задачи"/>
|
className={cn(
|
||||||
</div>
|
' grid grid-flow-row grid-rows-[0fr] opacity-0 transition-all duration-300',
|
||||||
<div className="flex gap-[20px]">
|
active && 'grid-rows-[1fr] opacity-100',
|
||||||
<Item count={14} totalCount={123} title="Easy" color="green"/>
|
)}
|
||||||
<Item count={14} totalCount={123} title="Medium" color="orange"/>
|
>
|
||||||
<Item count={14} totalCount={123} title="Hard" color="red"/>
|
<div className="overflow-hidden">
|
||||||
</div>
|
<div className="pb-[10px] pt-[20px]">
|
||||||
|
{missions.map((v, i) => (
|
||||||
</div>
|
<MyMissionItem
|
||||||
<div className="text-[24px] font-bold text-liquid-white">Компетенции</div>
|
key={i}
|
||||||
|
id={v.id}
|
||||||
<div className="flex flex-wrap gap-[10px]">
|
name={v.name}
|
||||||
<Item count={14} totalCount={123} title="Массивы"/>
|
timeLimit={v.timeLimit}
|
||||||
<Item count={14} totalCount={123} title="Списки"/>
|
memoryLimit={v.memoryLimit}
|
||||||
<Item count={14} totalCount={123} title="Стэк"/>
|
difficulty={v.difficulty}
|
||||||
</div>
|
type={i % 2 ? 'second' : 'first'}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>Недавиние задачи</div>
|
);
|
||||||
<div>Мои задачи</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MissionsBlock;
|
export default MissionsBlock;
|
||||||
|
|||||||
90
src/views/home/account/missions/MyMissionItem.tsx
Normal file
90
src/views/home/account/missions/MyMissionItem.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { cn } from '../../../../lib/cn';
|
||||||
|
import { IconError, IconSuccess } from '../../../../assets/icons/missions';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Edit } from '../../../../assets/icons/input';
|
||||||
|
|
||||||
|
export interface MissionItemProps {
|
||||||
|
id: number;
|
||||||
|
authorId?: number;
|
||||||
|
name: string;
|
||||||
|
difficulty: number;
|
||||||
|
tags?: string[];
|
||||||
|
timeLimit?: number;
|
||||||
|
memoryLimit?: number;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
type?: 'first' | 'second';
|
||||||
|
status?: 'empty' | 'success' | 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMilliseconds(ms: number): string {
|
||||||
|
const rounded = Math.round(ms) / 1000;
|
||||||
|
const formatted = rounded.toString().replace(/\.?0+$/, '');
|
||||||
|
return `${formatted} c`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBytesToMB(bytes: number): string {
|
||||||
|
const megabytes = Math.floor(bytes / (1024 * 1024));
|
||||||
|
return `${megabytes} МБ`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MissionItem: React.FC<MissionItemProps> = ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
difficulty,
|
||||||
|
timeLimit = 1000,
|
||||||
|
memoryLimit = 256 * 1024 * 1024,
|
||||||
|
type,
|
||||||
|
status,
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const difficultyItems = ['Easy', 'Medium', 'Hard'];
|
||||||
|
const difficultyString =
|
||||||
|
difficultyItems[Math.min(Math.max(0, difficulty - 1), 2)];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'min-h-[44px] w-full relative rounded-[10px] text-liquid-white py-[8px]',
|
||||||
|
type == 'first' ? 'bg-liquid-lighter' : 'bg-liquid-background',
|
||||||
|
'grid grid-cols-[80px,2fr,3fr,60px,24px] grid-flow-col gap-[20px] px-[20px] box-border items-center',
|
||||||
|
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}?back=/home/account/missions`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-[18px] font-bold">#{id}</div>
|
||||||
|
<div className="text-[18px] font-bold">{name}</div>
|
||||||
|
<div className="text-[12px] text-right">
|
||||||
|
стандартный ввод/вывод {formatMilliseconds(timeLimit)},{' '}
|
||||||
|
{formatBytesToMB(memoryLimit)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'text-center text-[18px]',
|
||||||
|
difficultyString == 'Hard' && 'text-liquid-red',
|
||||||
|
difficultyString == 'Medium' && 'text-liquid-orange',
|
||||||
|
difficultyString == 'Easy' && 'text-liquid-green',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{difficultyString}
|
||||||
|
</div>
|
||||||
|
<div className="h-[24px] w-[24px]">
|
||||||
|
<img
|
||||||
|
src={Edit}
|
||||||
|
className="hover:bg-liquid-light rounded-[8px] transition-all duration-300"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MissionItem;
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||||
import { Navigate, Route, Routes, useNavigate, useParams } from 'react-router-dom';
|
import { Navigate, Route, Routes, useParams } from 'react-router-dom';
|
||||||
import { fetchContestById } from '../../../redux/slices/contests';
|
import { fetchContestById } from '../../../redux/slices/contests';
|
||||||
import ContestMissions from './Missions';
|
import ContestMissions from './Missions';
|
||||||
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
|
||||||
import Submissions from './Submissions';
|
import Submissions from './Submissions';
|
||||||
|
|
||||||
export interface Article {
|
export interface Article {
|
||||||
@@ -14,7 +13,6 @@ export interface Article {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Contest = () => {
|
const Contest = () => {
|
||||||
const navigate = useNavigate();
|
|
||||||
const { contestId } = useParams<{ contestId: string }>();
|
const { contestId } = useParams<{ contestId: string }>();
|
||||||
const contestIdNumber =
|
const contestIdNumber =
|
||||||
contestId && /^\d+$/.test(contestId) ? parseInt(contestId, 10) : null;
|
contestId && /^\d+$/.test(contestId) ? parseInt(contestId, 10) : null;
|
||||||
@@ -22,8 +20,9 @@ const Contest = () => {
|
|||||||
return <Navigate to="/home/contests" replace />;
|
return <Navigate to="/home/contests" replace />;
|
||||||
}
|
}
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const contest = useAppSelector((state) => state.contests.fetchContestById.contest);
|
const contest = useAppSelector(
|
||||||
|
(state) => state.contests.fetchContestById.contest,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(setMenuActivePage('contest'));
|
dispatch(setMenuActivePage('contest'));
|
||||||
@@ -34,19 +33,17 @@ const Contest = () => {
|
|||||||
}, [contestIdNumber]);
|
}, [contestIdNumber]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full h-full'>
|
<div className="w-full h-full">
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="submissions"
|
path="submissions"
|
||||||
element={<Submissions contest={contest}/>}
|
element={<Submissions contest={contest} />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="*"
|
path="*"
|
||||||
element={<ContestMissions contest={contest}/>}
|
element={<ContestMissions contest={contest} />}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ const ContestItem: React.FC<ContestItemProps> = ({
|
|||||||
: ' bg-liquid-background',
|
: ' bg-liquid-background',
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
console.log(456);
|
||||||
navigate(`/contest/${id}`);
|
navigate(`/contest/${id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -99,8 +100,8 @@ const ContestItem: React.FC<ContestItemProps> = ({
|
|||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
onClick={(e) => {
|
onClick={() => {
|
||||||
e.stopPropagation();
|
console.log(123);
|
||||||
}}
|
}}
|
||||||
text="Регистрация"
|
text="Регистрация"
|
||||||
/>
|
/>
|
||||||
@@ -108,12 +109,7 @@ const ContestItem: React.FC<ContestItemProps> = ({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
<ReverseButton
|
<ReverseButton onClick={() => {}} text="Вы записаны" />
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
text="Вы записаны"
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
48
src/views/home/missions/Filter.tsx
Normal file
48
src/views/home/missions/Filter.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Filter, FilterItem } from '../../../components/drop-down-list/Filter';
|
||||||
|
import { Sorter } from '../../../components/drop-down-list/Sorter';
|
||||||
|
|
||||||
|
const Filters = () => {
|
||||||
|
const items: FilterItem[] = [
|
||||||
|
{ text: 'React', value: 'react' },
|
||||||
|
{ text: 'Vue', value: 'vue' },
|
||||||
|
{ text: 'Angular', value: 'angular' },
|
||||||
|
{ text: 'Svelte', value: 'svelte' },
|
||||||
|
{ text: 'Next.js', value: 'next' },
|
||||||
|
{ text: 'Nuxt', value: 'nuxt' },
|
||||||
|
{ text: 'Solid', value: 'solid' },
|
||||||
|
{ text: 'Qwik', value: 'qwik' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className=" h-[50px] mb-[20px] flex">
|
||||||
|
<div></div>
|
||||||
|
|
||||||
|
<Sorter
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
value: '1',
|
||||||
|
text: 'Сложность',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '2',
|
||||||
|
text: 'Дата создания',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '3',
|
||||||
|
text: 'ID',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(v) => console.log(v)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <Filter
|
||||||
|
items={items}
|
||||||
|
defaultState={[items[0], items[3]]} // начальные выбранные элементы
|
||||||
|
onChange={(values) => console.log(values)} // обработчик изменения
|
||||||
|
className="w-[240px]"
|
||||||
|
/> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Filters;
|
||||||
@@ -5,6 +5,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { setMenuActivePage } from '../../../redux/slices/store';
|
import { setMenuActivePage } from '../../../redux/slices/store';
|
||||||
import { fetchMissions } from '../../../redux/slices/missions';
|
import { fetchMissions } from '../../../redux/slices/missions';
|
||||||
import ModalCreate from './ModalCreate';
|
import ModalCreate from './ModalCreate';
|
||||||
|
import Filters from './Filter';
|
||||||
|
|
||||||
export interface Mission {
|
export interface Mission {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -45,7 +46,7 @@ const Missions = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-liquid-lighter h-[50px] mb-[20px]"></div>
|
<Filters />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{missions.map((v, i) => (
|
{missions.map((v, i) => (
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/app.tsx","./src/axios.ts","./src/main.tsx","./src/vite-env.d.ts","./src/assets/icons/account/index.ts","./src/assets/icons/auth/index.ts","./src/assets/icons/groups/index.ts","./src/assets/icons/header/index.ts","./src/assets/icons/input/index.ts","./src/assets/icons/menu/index.ts","./src/assets/icons/missions/index.ts","./src/assets/logos/index.ts","./src/components/button/primarybutton.tsx","./src/components/button/reversebutton.tsx","./src/components/button/secondarybutton.tsx","./src/components/checkbox/checkbox.tsx","./src/components/drop-down-list/dropdownlist.tsx","./src/components/input/daterangeinput.tsx","./src/components/input/input.tsx","./src/components/modal/modal.tsx","./src/components/router/protectedroute.tsx","./src/components/switch/switch.tsx","./src/config/colors.ts","./src/hooks/useclickoutside.ts","./src/hooks/usequery.ts","./src/lib/cn.ts","./src/pages/article.tsx","./src/pages/articleeditor.tsx","./src/pages/home.tsx","./src/pages/mission.tsx","./src/redux/hooks.ts","./src/redux/store.ts","./src/redux/slices/account.ts","./src/redux/slices/articles.ts","./src/redux/slices/auth.ts","./src/redux/slices/contests.ts","./src/redux/slices/groups.ts","./src/redux/slices/missions.ts","./src/redux/slices/store.ts","./src/redux/slices/submit.ts","./src/views/article/header.tsx","./src/views/articleeditor/editor.tsx","./src/views/articleeditor/header.tsx","./src/views/articleeditor/marckdownpreview.tsx","./src/views/home/account/account.tsx","./src/views/home/account/accoutmenu.tsx","./src/views/home/account/articlesblock.tsx","./src/views/home/account/contestsblock.tsx","./src/views/home/account/missionsblock.tsx","./src/views/home/account/rightpanel.tsx","./src/views/home/articles/articleitem.tsx","./src/views/home/articles/articles.tsx","./src/views/home/auth/login.tsx","./src/views/home/auth/register.tsx","./src/views/home/contest/contest.tsx","./src/views/home/contest/missionitem.tsx","./src/views/home/contest/missions.tsx","./src/views/home/contest/submissions.tsx","./src/views/home/contests/contestitem.tsx","./src/views/home/contests/contests.tsx","./src/views/home/contests/contestsblock.tsx","./src/views/home/contests/modalcreate.tsx","./src/views/home/groups/group.tsx","./src/views/home/groups/groupitem.tsx","./src/views/home/groups/groups.tsx","./src/views/home/groups/groupsblock.tsx","./src/views/home/groups/modalcreate.tsx","./src/views/home/groups/modalupdate.tsx","./src/views/home/menu/menu.tsx","./src/views/home/menu/menuitem.tsx","./src/views/home/missions/missionitem.tsx","./src/views/home/missions/missions.tsx","./src/views/home/missions/modalcreate.tsx","./src/views/mission/codeeditor/codeeditor.tsx","./src/views/mission/statement/header.tsx","./src/views/mission/statement/latextcontainer.tsx","./src/views/mission/statement/missionsubmissions.tsx","./src/views/mission/statement/statement.tsx","./src/views/mission/statement/submissionitem.tsx","./src/views/mission/submission/submission.tsx"],"version":"5.6.2"}
|
{"root":["./src/app.tsx","./src/axios.ts","./src/main.tsx","./src/vite-env.d.ts","./src/assets/icons/account/index.ts","./src/assets/icons/auth/index.ts","./src/assets/icons/groups/index.ts","./src/assets/icons/header/index.ts","./src/assets/icons/input/index.ts","./src/assets/icons/menu/index.ts","./src/assets/icons/missions/index.ts","./src/assets/logos/index.ts","./src/components/button/primarybutton.tsx","./src/components/button/reversebutton.tsx","./src/components/button/secondarybutton.tsx","./src/components/checkbox/checkbox.tsx","./src/components/drop-down-list/dropdownlist.tsx","./src/components/input/daterangeinput.tsx","./src/components/input/input.tsx","./src/components/modal/modal.tsx","./src/components/router/protectedroute.tsx","./src/components/switch/switch.tsx","./src/config/colors.ts","./src/hooks/useclickoutside.ts","./src/hooks/usequery.ts","./src/lib/cn.ts","./src/pages/article.tsx","./src/pages/articleeditor.tsx","./src/pages/contesteditor.tsx","./src/pages/home.tsx","./src/pages/mission.tsx","./src/redux/hooks.ts","./src/redux/store.ts","./src/redux/slices/account.ts","./src/redux/slices/articles.ts","./src/redux/slices/auth.ts","./src/redux/slices/contests.ts","./src/redux/slices/groups.ts","./src/redux/slices/missions.ts","./src/redux/slices/store.ts","./src/redux/slices/submit.ts","./src/views/article/header.tsx","./src/views/articleeditor/editor.tsx","./src/views/articleeditor/header.tsx","./src/views/articleeditor/marckdownpreview.tsx","./src/views/home/account/account.tsx","./src/views/home/account/accoutmenu.tsx","./src/views/home/account/rightpanel.tsx","./src/views/home/account/articles/articlesblock.tsx","./src/views/home/account/contests/contests.tsx","./src/views/home/account/contests/contestsblock.tsx","./src/views/home/account/contests/mycontestitem.tsx","./src/views/home/account/contests/registercontestitem.tsx","./src/views/home/account/missions/missionsblock.tsx","./src/views/home/articles/articleitem.tsx","./src/views/home/articles/articles.tsx","./src/views/home/auth/login.tsx","./src/views/home/auth/register.tsx","./src/views/home/contest/contest.tsx","./src/views/home/contest/missionitem.tsx","./src/views/home/contest/missions.tsx","./src/views/home/contest/submissionitem.tsx","./src/views/home/contest/submissions.tsx","./src/views/home/contests/contestitem.tsx","./src/views/home/contests/contests.tsx","./src/views/home/contests/contestsblock.tsx","./src/views/home/contests/modalcreate.tsx","./src/views/home/groups/group.tsx","./src/views/home/groups/groupitem.tsx","./src/views/home/groups/groups.tsx","./src/views/home/groups/groupsblock.tsx","./src/views/home/groups/modalcreate.tsx","./src/views/home/groups/modalupdate.tsx","./src/views/home/menu/menu.tsx","./src/views/home/menu/menuitem.tsx","./src/views/home/missions/missionitem.tsx","./src/views/home/missions/missions.tsx","./src/views/home/missions/modalcreate.tsx","./src/views/mission/codeeditor/codeeditor.tsx","./src/views/mission/statement/header.tsx","./src/views/mission/statement/latextcontainer.tsx","./src/views/mission/statement/missionsubmissions.tsx","./src/views/mission/statement/statement.tsx","./src/views/mission/statement/submissionitem.tsx","./src/views/mission/submission/submission.tsx"],"version":"5.6.2"}
|
||||||
Reference in New Issue
Block a user