Add files via upload
This commit is contained in:
50
src/ui/Error/Error.module.css
Normal file
50
src/ui/Error/Error.module.css
Normal 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
34
src/ui/Error/Error.tsx
Normal 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
1
src/ui/Error/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Error';
|
||||
98
src/ui/HorizontalScroll/HorizontalScroll.module.css
Normal file
98
src/ui/HorizontalScroll/HorizontalScroll.module.css
Normal 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;
|
||||
}
|
||||
301
src/ui/HorizontalScroll/HorizontalScroll.tsx
Normal file
301
src/ui/HorizontalScroll/HorizontalScroll.tsx
Normal 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;
|
||||
1
src/ui/HorizontalScroll/index.ts
Normal file
1
src/ui/HorizontalScroll/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './HorizontalScroll';
|
||||
3
src/ui/Link/Link.module.css
Normal file
3
src/ui/Link/Link.module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.link {
|
||||
text-decoration: 'none',
|
||||
}
|
||||
54
src/ui/Link/Link.tsx
Normal file
54
src/ui/Link/Link.tsx
Normal 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
1
src/ui/Link/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Link';
|
||||
27
src/ui/Tabbar/TabBar.module.css
Normal file
27
src/ui/Tabbar/TabBar.module.css
Normal 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
57
src/ui/Tabbar/TabBar.tsx
Normal 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;
|
||||
11
src/ui/Tabbar/TabBarNav.module.css
Normal file
11
src/ui/Tabbar/TabBarNav.module.css
Normal 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;
|
||||
}
|
||||
31
src/ui/Tabbar/TabBarNav.tsx
Normal file
31
src/ui/Tabbar/TabBarNav.tsx
Normal 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
1
src/ui/Tabbar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './TabBar';
|
||||
11
src/ui/Tabbar/tabbarItem/TabBarItem.module.css
Normal file
11
src/ui/Tabbar/tabbarItem/TabBarItem.module.css
Normal 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;
|
||||
}
|
||||
31
src/ui/Tabbar/tabbarItem/TabBarItem.tsx
Normal file
31
src/ui/Tabbar/tabbarItem/TabBarItem.tsx
Normal 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;
|
||||
1
src/ui/Tabbar/tabbarItem/index.ts
Normal file
1
src/ui/Tabbar/tabbarItem/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './TabBarItem';
|
||||
26
src/ui/Ticker/Ticker.module.css
Normal file
26
src/ui/Ticker/Ticker.module.css
Normal 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
18
src/ui/Ticker/Ticker.tsx
Normal 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
1
src/ui/Ticker/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Ticker';
|
||||
62
src/ui/modal/Modal.module.css
Normal file
62
src/ui/modal/Modal.module.css
Normal 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
70
src/ui/modal/Modal.tsx
Normal 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
1
src/ui/modal/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Modal';
|
||||
Reference in New Issue
Block a user