Add files via upload

This commit is contained in:
2024-07-17 14:41:27 +05:00
committed by GitHub
parent 3fef1624db
commit 0cb207bd7c
60 changed files with 2325 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
.errorContainer {
/* background: ; */
border-radius: 4px;
height: 100%;
display: flex;
align-items: center;
width: 100%;
max-width: 1200;
margin: 0 auto;
justify-content: center;
box-sizing: border-box;
padding: 0 10px;
flex-direction: column;
}
.errorTitle {
text-align: center;
font-size: 18px;
margin-top: 10rem;
}
.statusCodeText {
font-size: 16px;
margin-top: 19px;
}
.link {
font-size: 16px;
transition: opacity 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
margin-top: 19px;
color: var(--gray);
font-weight: 600;
}
.link:hover {
opacity: 0.7;
transition: 'opacity 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms';
}
@media (max-width: 700px) {
.errorContainer {
padding: '0 5px';
}
}
@media (max-width: 400px) {
.errorContainer {
padding: '0 5px';
}
}

34
src/ui/Error/Error.tsx Normal file
View File

@@ -0,0 +1,34 @@
import { FC } from 'react';
import { ELinkPath } from '@enums/enums';
import Link from '@ui/Link';
import style from './Error.module.css';
type ErrorProps = {
statusCode?: number;
errorText: string;
goHome?: boolean;
};
const Error: FC<ErrorProps> = ({ statusCode, errorText, goHome }) => (
<div className={style.errorContainer}>
{errorText && (
<h1 className={style.errorTitle}>
{errorText}
</h1>
)}
{ statusCode && <h2 className={style.statusCodeText}>
{statusCode && `Ошибка ${statusCode}`}
</h2>
}
{
goHome ? <Link path={ELinkPath.home} className={style.link}>Вернуться на главную?</Link> : <></>
}
</div>
);
export default Error;

1
src/ui/Error/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default } from './Error';

View File

@@ -0,0 +1,98 @@
.outerContainer {
position: relative;
width: 100%;
}
.horizontalScroll {
width: 100%;
user-select: none;
will-change: transform;
white-space: nowrap;
overflow: auto;
overflow-x: scroll;
overflow-y: hidden;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
-ms-overflow-style: none;
/* IE и Edge */
scrollbar-width: none;
/* Firefox */
position: relative;
}
.horizontalScroll::-webkit-scrollbar {
display: none;
}
.horizontalScrollСontent {
display: inline-flex;
}
.horizontalScrollСontent>* {
flex-shrink: 0;
}
.horizontalScrollItem {
flex: 0 0 auto;
scroll-snap-align: start;
}
.prevButton,
.nextButton {
position: absolute;
top: 50%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transform: translateY(-50%);
width: 45px;
height: 45px;
font-size: 24px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: none;
cursor: pointer;
z-index: 1;
/* Добавлено для позиционирования поверх контента */
}
.nextButton svg {
transform: rotate(180deg);
}
.prevButton {
left: 0;
}
.nextButton {
right: 0;
}
@keyframes bounce {
0% {
transform: translateX(0);
}
25% {
transform: translateX(-10px);
}
50% {
transform: translateX(10px);
}
75% {
transform: translateX(-5px);
}
100% {
transform: translateX(0);
}
}
.bounce {
animation: bounce 0.3s ease-out;
}

View File

@@ -0,0 +1,301 @@
import {
FC,
useRef,
useState,
useEffect,
ReactNode,
MouseEvent,
useCallback,
TouchEvent,
} from 'react';
import ArrowSVG from '@assets/svg/arrow';
import style from './HorizontalScroll.module.css';
type HorizontalScrollProps = {
children: ReactNode[];
};
const HorizontalScroll: FC<HorizontalScrollProps> = ({ children }) => {
const containerRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const isMouseDown = useRef(false);
const startX = useRef(0);
const scrollLeft = useRef(0);
const [showPrevButton, setShowPrevButton] = useState(false);
const [showNextButton, setShowNextButton] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [bounceOffset, setBounceOffset] = useState(0);
const [bounceDirection, setBounceDirection] = useState(0);
// const [snapPosition, setSnapPosition] = useState(0);
const scrollWalkFactor = 1;
const bounceDecayRate = 0.9;
const bounceThreshold = 0.01;
const handleMouseDown = (e: MouseEvent<HTMLDivElement>) => {
isMouseDown.current = true;
startX.current = e.pageX - (containerRef.current?.offsetLeft || 0);
scrollLeft.current = containerRef.current?.scrollLeft || 0;
setIsDragging(true);
containerRef.current?.classList.add(style.grabbing);
};
const handleMouseMove = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
if (!isMouseDown.current) return;
e.preventDefault();
const x = e.pageX - (containerRef.current?.offsetLeft || 0);
const walk = (x - startX.current) * scrollWalkFactor;
requestAnimationFrame(() => {
if (containerRef.current && contentRef.current) {
const targetScrollLeft = scrollLeft.current - walk;
const maxScrollLeft = contentRef.current.scrollWidth - containerRef.current.clientWidth;
let newBounceOffset = 0;
let newBounceDirection = 0;
if (targetScrollLeft < 0) {
newBounceOffset = Math.abs(targetScrollLeft) * 0.1;
newBounceDirection = 1;
} else if (targetScrollLeft > maxScrollLeft) {
newBounceOffset = (targetScrollLeft - maxScrollLeft) * 0.1;
newBounceDirection = -1;
}
setBounceOffset(newBounceOffset);
setBounceDirection(newBounceDirection);
// const firstChild = contentRef.current.firstElementChild;
// const itemWidth = firstChild instanceof HTMLElement ? firstChild.offsetWidth : 0;
// const newSnapPosition = Math.round(targetScrollLeft / itemWidth) * itemWidth;
// setSnapPosition(newSnapPosition);
containerRef.current.scrollTo({
left: Math.max(0, Math.min(targetScrollLeft, maxScrollLeft)),
behavior: 'auto',
});
}
});
},
[],
);
const handleMouseUp = useCallback(() => {
if (isDragging) {
isMouseDown.current = false;
setIsDragging(false);
containerRef.current?.classList.remove(style.grabbing);
containerRef.current?.classList.add(style.grab);
// if (containerRef.current && contentRef.current) {
// containerRef.current.scrollTo({
// left: snapPosition,
// behavior: 'smooth',
// });
// }
setBounceDirection(0);
setBounceOffset(0);
}
}, [isDragging]);
const handleMouseLeave = useCallback(() => {
if (isMouseDown.current) {
isMouseDown.current = false;
setIsDragging(false);
containerRef.current?.classList.remove(style.grabbing);
containerRef.current?.classList.add(style.grab);
setBounceDirection(0);
setBounceOffset(0);
}
}, []);
const handlePrevClick = () => {
if (containerRef.current && contentRef.current) {
const firstChild = contentRef.current.firstElementChild;
const itemWidth = firstChild instanceof HTMLElement ? firstChild.offsetWidth : 0;
containerRef.current.scrollTo({
left: containerRef.current.scrollLeft - itemWidth,
behavior: 'smooth',
});
}
};
const handleNextClick = () => {
if (containerRef.current && contentRef.current) {
const firstChild = contentRef.current.firstElementChild;
const itemWidth = firstChild instanceof HTMLElement ? firstChild.offsetWidth : 0;
containerRef.current.scrollTo({
left: containerRef.current.scrollLeft + itemWidth,
behavior: 'smooth',
});
}
};
const handleScroll = () => {
if (containerRef.current) {
// eslint-disable-next-line @typescript-eslint/no-shadow
const { scrollWidth, clientWidth, scrollLeft } = containerRef.current;
if (scrollWidth <= clientWidth) {
setShowPrevButton(false);
setShowNextButton(false);
} else {
setShowPrevButton(scrollLeft > 0);
setShowNextButton(scrollLeft < scrollWidth - clientWidth - 1);
}
}
};
const handleTouchStart = (e: TouchEvent<HTMLDivElement>) => {
isMouseDown.current = true;
startX.current = e.touches[0].pageX - (containerRef.current?.offsetLeft || 0);
scrollLeft.current = containerRef.current?.scrollLeft || 0;
setIsDragging(true);
containerRef.current?.classList.add(style.grabbing);
};
const handleTouchMove = useCallback(
(e: TouchEvent<HTMLDivElement>) => {
if (!isMouseDown.current) return;
e.preventDefault();
const x = e.touches[0].pageX - (containerRef.current?.offsetLeft || 0);
const walk = (x - startX.current) * scrollWalkFactor;
requestAnimationFrame(() => {
if (containerRef.current && contentRef.current) {
const targetScrollLeft = scrollLeft.current - walk;
const maxScrollLeft = contentRef.current.scrollWidth - containerRef.current.clientWidth;
let newBounceOffset = 0;
let newBounceDirection = 0;
if (targetScrollLeft < 0) {
newBounceOffset = Math.abs(targetScrollLeft) * 0.1;
newBounceDirection = 1;
} else if (targetScrollLeft > maxScrollLeft) {
newBounceOffset = (targetScrollLeft - maxScrollLeft) * 0.1;
newBounceDirection = -1;
}
setBounceOffset(newBounceOffset);
setBounceDirection(newBounceDirection);
// const firstChild = contentRef.current.firstElementChild;
// const itemWidth = firstChild instanceof HTMLElement ? firstChild.offsetWidth : 0;
// const newSnapPosition = Math.round(targetScrollLeft / itemWidth) * itemWidth;
// setSnapPosition(newSnapPosition);
containerRef.current.scrollTo({
left: Math.max(0, Math.min(targetScrollLeft, maxScrollLeft)),
behavior: 'auto',
});
}
});
},
[],
);
const handleTouchEnd = useCallback(() => {
if (isDragging) {
isMouseDown.current = false;
setIsDragging(false);
containerRef.current?.classList.remove(style.grabbing);
containerRef.current?.classList.add(style.grab);
// if (containerRef.current && contentRef.current) {
// containerRef.current.scrollTo({
// left: snapPosition,
// behavior: 'smooth',
// });
// }
setBounceDirection(0);
setBounceOffset(0);
}
}, [isDragging]);
useEffect(() => {
if (containerRef.current) {
containerRef.current.addEventListener('scroll', handleScroll);
}
return () => {
if (containerRef.current) {
containerRef.current.removeEventListener('scroll', handleScroll);
}
};
}, [containerRef.current, contentRef.current]);
useEffect(() => {
let bounceAnimationFrame: number | null = null;
const bounceAnimation = () => {
if (containerRef.current && Math.abs(bounceOffset) > bounceThreshold) {
setBounceOffset((prevOffset) => prevOffset * bounceDecayRate);
bounceAnimationFrame = requestAnimationFrame(bounceAnimation);
} else {
setBounceOffset(0);
setBounceDirection(0);
bounceAnimationFrame = null;
}
};
if (bounceOffset !== 0) {
bounceAnimationFrame = requestAnimationFrame(bounceAnimation);
} else if (bounceAnimationFrame) {
cancelAnimationFrame(bounceAnimationFrame);
}
return () => {
if (bounceAnimationFrame) {
cancelAnimationFrame(bounceAnimationFrame);
}
};
}, [bounceOffset, bounceDecayRate, bounceThreshold]);
return (
<div className={style.outerContainer}>
{showPrevButton && (
<button className={style.prevButton} onClick={handlePrevClick}>
<ArrowSVG />
</button>
)}
<div
ref={containerRef}
onMouseUp={handleMouseUp}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
className={style.horizontalScroll}
style={{
cursor: isDragging ? 'grabbing' : 'grab',
transform: `translateX(${bounceDirection * bounceOffset}px)`,
transition: 'transform 0.2s ease',
}}
>
<div ref={contentRef} className={style.horizontalScrollСontent}>
{children.map((child, index) => (
<div key={index} className={style.horizontalScrollItem}>
{child}
</div>
))}
</div>
</div>
{showNextButton && (
<button className={style.nextButton} onClick={handleNextClick}>
<ArrowSVG />
</button>
)}
</div>
);
};
export default HorizontalScroll;

View File

@@ -0,0 +1 @@
export { default } from './HorizontalScroll';

View File

@@ -0,0 +1,3 @@
.link {
text-decoration: 'none',
}

54
src/ui/Link/Link.tsx Normal file
View File

@@ -0,0 +1,54 @@
import {
CSSProperties, FC, HTMLAttributeAnchorTarget, ReactNode,
} from 'react';
import NextLink from 'next/link';
import clsx from 'clsx';
import styles from './Link.module.css';
type LinkProps = {
path: string;
query?: { [id: string]: string } | string;
children: ReactNode;
className?: string;
onClick?: ((e: any) => void) | undefined;
scroll?: boolean;
draggable?: boolean;
attributeTitle?: string;
shallow?: boolean;
style?: CSSProperties;
target?: HTMLAttributeAnchorTarget;
};
const Link: FC<LinkProps> = ({
path,
query,
children,
className,
onClick,
scroll = true,
draggable = false,
attributeTitle,
shallow,
style,
target,
}) => {
const currentStyles = clsx(styles.link, className);
return <NextLink href={{ pathname: path, query }} scroll={scroll} shallow={shallow} >
<a
className={currentStyles}
onClick={onClick}
draggable={draggable}
title={attributeTitle}
style={style}
target={target}
>
{children}
</a>
</NextLink>;
};
export default Link;

1
src/ui/Link/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default } from './Link';

View File

@@ -0,0 +1,27 @@
.tabBar {
width: 100%;
}
.tabBarNav {
position: relative;
display: flex;
z-index: 2;
border-radius: 12px;
overflow: hidden;
transition: all 0.3s ease;
width: 100%;
}
.tabContainer {
min-height: 100px;
width: 100%;
position: relative;
top: -1px;
margin-top: 30px;
}
.active {
background-color: var(--red);
color: var(--white);
transition: all 0.3s ease;
}

57
src/ui/Tabbar/TabBar.tsx Normal file
View File

@@ -0,0 +1,57 @@
/* eslint-disable @typescript-eslint/no-shadow */
import React from 'react';
import clsx from 'clsx';
import style from './TabBar.module.css';
import { TabBarItemProps } from './tabbarItem/TabBarItem';
import TabBarNav from './TabBarNav';
interface TabBarProps {
children: React.ReactNode;
className?: string;
activeTab: string;
setActiveTab: (label: string) => void;
}
const TabBar: React.FC<TabBarProps> = ({
children, className = '', activeTab, setActiveTab,
}) => {
const getChildrenLabels = (children: React.ReactNode): string[] => React.Children.toArray(children)
.filter(React.isValidElement)
.map((child) => (child.props as any).label);
const handleChangeActiveTab = (label: string) => {
setActiveTab(label);
};
const renderTabs = () => {
const childrenLabels = getChildrenLabels(children);
return childrenLabels.map((navLabel) => (
<TabBarNav
key={navLabel}
navLabel={navLabel}
className={clsx({ [style.active]: activeTab === navLabel })}
onChangeActiveTab={handleChangeActiveTab}
/>
));
};
const classes = clsx(style.tabBar, className);
return (
<div className={classes}>
<div className={style.tabBarNav}>{renderTabs()}</div>
<div className={style.tabContainer}>
{React.Children.map(
children,
(child) => (React.isValidElement(child) ? React.cloneElement(child, { activeTab } as TabBarItemProps) : null),
)
}
</div>
</div>
);
};
export default TabBar;

View File

@@ -0,0 +1,11 @@
.navItem {
flex: 1;
border: none;
padding: 10px 15px;
cursor: pointer;
overflow: hidden;
outline: none;
text-transform: uppercase;
font-weight: 600;
transition: all 0.3s ease;
}

View File

@@ -0,0 +1,31 @@
import React from 'react';
import clsx from 'clsx';
import style from './TabBarNav.module.css';
interface TabBarNavProps {
navLabel?: string;
className?: string;
onChangeActiveTab: (label: string) => void;
}
const TabBarNav: React.FC<TabBarNavProps> = ({
navLabel = 'Tab',
className = '',
onChangeActiveTab,
}) => {
const classes = clsx(className, style.navItem);
return (
<button
type="button"
className={classes}
onClick={() => onChangeActiveTab(navLabel)}
>
{navLabel}
</button>
);
};
export default TabBarNav;

1
src/ui/Tabbar/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default } from './TabBar';

View File

@@ -0,0 +1,11 @@
.tabBarItem {
height: 0;
overflow: hidden;
transition: all 0.3s ease;
width: 100%;
}
.tabBarItem.active {
height: auto;
transition: all 0.3s ease;
}

View File

@@ -0,0 +1,31 @@
import React from 'react';
import clsx from 'clsx';
import style from './TabBarItem.module.css';
export interface TabBarItemProps {
children?: React.ReactNode;
label: string;
activeTab?: string;
}
const TabBarItem: React.FC<TabBarItemProps> = ({
children,
label,
activeTab,
...attrs
}) => {
const classes = clsx(
style.tabBarItem,
{ [style.active]: activeTab === label },
);
return (
<div className={classes} {...attrs}>
{children}
</div>
);
};
export default TabBarItem;

View File

@@ -0,0 +1 @@
export { default } from './TabBarItem';

View File

@@ -0,0 +1,26 @@
.ticker {
overflow: hidden;
position: relative;
width: 100%;
height: 100%;
}
.tickerContent {
display: flex;
align-items: center;
animation: ticker 1s linear infinite;
position: absolute;
top: 0;
left: 0;
bottom: 0;
}
@keyframes ticker {
0% {
left: 0;
}
100% {
left: -122px;
}
}

18
src/ui/Ticker/Ticker.tsx Normal file
View File

@@ -0,0 +1,18 @@
import { FC, ReactNode } from 'react';
import styles from './Ticker.module.css';
type TickerProps = {
children: ReactNode;
};
const Ticker: FC<TickerProps> = ({ children }) => (
<div className={styles.ticker}>
<div className={styles.tickerContent}>
{children}
</div>
</div>
);
export default Ticker;

1
src/ui/Ticker/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default } from './Ticker';

View File

@@ -0,0 +1,62 @@
.modal {
width: 100vw;
height: 100vh;
background-color: var(--blackTransparent);
position: fixed;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 200ms ease;
z-index: 100;
pointer-events: none;
}
.modalActive {
transition: all 200ms ease;
opacity: 1;
pointer-events: all;
}
.modalContent {
padding: 20px;
border-radius: 12px;
background-color: var(--white);
max-width: 360px;
width: 100%;
z-index: 300;
min-height: 320px;
min-width: 320px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modalContentHead {
display: flex;
align-items: center;
justify-content: space-between;
}
.modalTitle {
font-size: 18px;
font-weight: 600;
}
.modalCloseIcon {
cursor: pointer;
opacity: 1;
transition: all 200ms ease;
}
.modalCloseIcon:hover {
opacity: 0.6;
transition: all 200ms ease;
}
.modalTransparent {
background-color: transparent;
padding: 5px;
}

70
src/ui/modal/Modal.tsx Normal file
View File

@@ -0,0 +1,70 @@
import {
FC, ReactNode,
} from 'react';
import clsx from 'clsx';
import CloseSVG from '@assets/svg/close';
import style from './Modal.module.css';
type ModalProps = {
title?: string;
showCloseButton?: boolean;
children: ReactNode;
className?: string;
setOpen: (isOpen: boolean) => void;
isOpen: boolean;
noBody?: boolean;
};
const Modal: FC<ModalProps> = ({
title,
showCloseButton,
children,
className,
setOpen,
isOpen,
noBody,
}) => {
const onClose = () => setOpen(false);
return (
<div className={clsx(
style.modal,
{
[style.modalActive]: isOpen,
},
className,
)}
onClick={onClose}
>
<div
className={clsx(
style.modalContent,
{
[style.modalTransparent]: noBody,
},
)}
onClick={(e) => e.stopPropagation()}
>
{
!noBody && <div>
<div className={style.modalContentHead}>
{
title && <span className={style.modalTitle}>{title}</span>
}
{
showCloseButton && <span className={style.modalCloseIcon} onClick={onClose}><CloseSVG /></span>
}
</div>
</div>
}
{children}
</div>
</div>
);
};
export default Modal;

1
src/ui/modal/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default } from './Modal';