Add files via upload
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
4
public/icon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="512" height="512" rx="40" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||||
|
<path d="M81.5828 153C96.3584 153 109.252 157.765 120.26 167.294C131.56 176.347 140.251 190.165 146.335 208.747C152.71 226.853 155.896 249.485 155.896 276.644H112.873C112.873 260.444 111.714 247.103 109.396 236.621C107.368 225.662 104.037 217.562 99.4005 212.321C94.7652 207.079 88.6811 204.459 81.1482 204.459C69.849 204.459 61.7368 210.415 56.8115 222.326C51.8862 234.238 49.4236 252.344 49.4236 276.644H6.3999C6.3999 249.962 9.44199 227.568 15.5262 209.462C21.6103 190.879 30.302 176.824 41.6011 167.294C52.9003 157.765 66.2275 153 81.5828 153ZM330.748 276.644C330.748 295.702 328.865 312.618 325.099 327.388C321.622 342.158 316.552 354.786 309.888 365.268C303.515 375.274 295.692 382.897 286.421 388.138C277.44 393.38 267.154 396 255.565 396C244.846 396 234.995 393.38 226.014 388.138C217.033 382.897 209.21 375.274 202.546 365.268C195.882 354.786 190.667 342.158 186.901 327.388C183.424 312.618 181.541 295.702 181.252 276.644H224.275C224.275 291.414 225.434 304.042 227.752 314.524C230.07 324.529 233.547 332.152 238.182 337.394C242.817 342.158 248.901 344.541 256.434 344.541C263.677 344.541 269.617 342.158 274.252 337.394C278.888 332.152 282.22 324.529 284.248 314.524C286.566 304.042 287.724 291.414 287.724 276.644H330.748ZM431.286 153C446.062 153 458.954 157.765 469.964 167.294C481.263 176.347 489.955 190.165 496.039 208.747C502.413 226.853 505.6 249.485 505.6 276.644H462.576C462.576 260.444 461.418 247.103 459.1 236.621C457.072 225.662 453.739 217.562 449.104 212.321C444.469 207.079 438.385 204.459 430.852 204.459C419.553 204.459 411.441 210.415 406.515 222.326C401.589 234.238 399.127 252.344 399.127 276.644H356.103C356.103 249.962 359.146 227.568 365.23 209.462C371.314 190.879 380.005 176.824 391.305 167.294C402.604 157.765 415.931 153 431.286 153Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/images/default_interface_1.webp
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
public/images/default_interface_2.webp
Normal file
|
After Width: | Height: | Size: 785 KiB |
BIN
public/images/exclusive_interface_1.webp
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
public/images/exclusive_interface_2.webp
Normal file
|
After Width: | Height: | Size: 909 KiB |
31
src/assets/svg/arrow.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/* eslint-disable max-len */
|
||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import { SvgProps } from '@interfaces/svg';
|
||||||
|
|
||||||
|
import { EColor } from '@enums/enums';
|
||||||
|
|
||||||
|
const ArrowSVG: FC<SvgProps> = (
|
||||||
|
{
|
||||||
|
width = 30,
|
||||||
|
height = 30,
|
||||||
|
fill = EColor.white,
|
||||||
|
className,
|
||||||
|
},
|
||||||
|
) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill={fill}
|
||||||
|
d="M14.29 5.707a1 1 0 00-1.415 0L7.988 10.6a2 2 0 000 2.828l4.89 4.89a1 1 0 001.415-1.414l-4.186-4.185a1 1 0 010-1.415l4.182-4.182a1 1 0 000-1.414z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ArrowSVG;
|
||||||
36
src/assets/svg/close.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/* eslint-disable max-len */
|
||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import { SvgProps } from '@interfaces/svg';
|
||||||
|
|
||||||
|
import { EColor } from '@enums/enums';
|
||||||
|
|
||||||
|
const CloseSVG: FC<SvgProps> = (
|
||||||
|
{
|
||||||
|
width = 25,
|
||||||
|
height = 25,
|
||||||
|
fill = EColor.black,
|
||||||
|
className,
|
||||||
|
},
|
||||||
|
) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
stroke={fill}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M18 18l-6-6m0 0L6 6m6 6l6-6m-6 6l-6 6"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default CloseSVG;
|
||||||
34
src/assets/svg/info.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/* eslint-disable max-len */
|
||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import { SvgProps } from '@interfaces/svg';
|
||||||
|
|
||||||
|
import { EColor } from '@enums/enums';
|
||||||
|
|
||||||
|
const InfoSVG: FC<SvgProps> = (
|
||||||
|
{
|
||||||
|
width = 20,
|
||||||
|
height = 20,
|
||||||
|
fill = EColor.white,
|
||||||
|
className,
|
||||||
|
},
|
||||||
|
) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
fill={fill}
|
||||||
|
enableBackground="new 0 0 330 330"
|
||||||
|
version="1.1"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
viewBox="0 0 330 330"
|
||||||
|
xmlSpace="preserve"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path d="M165 0C74.019 0 0 74.02 0 165.001 0 255.982 74.019 330 165 330s165-74.018 165-164.999S255.981 0 165 0zm0 300c-74.44 0-135-60.56-135-134.999S90.56 30 165 30s135 60.562 135 135.001C300 239.44 239.439 300 165 300z"></path>
|
||||||
|
<path d="M164.998 70c-11.026 0-19.996 8.976-19.996 20.009 0 11.023 8.97 19.991 19.996 19.991 11.026 0 19.996-8.968 19.996-19.991 0-11.033-8.97-20.009-19.996-20.009zM165 140c-8.284 0-15 6.716-15 15v90c0 8.284 6.716 15 15 15 8.284 0 15-6.716 15-15v-90c0-8.284-6.716-15-15-15z"></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default InfoSVG;
|
||||||
49
src/assets/svg/logo.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import { SvgProps } from '@interfaces/svg';
|
||||||
|
|
||||||
|
/* eslint-disable max-len */
|
||||||
|
const LogoSVG: FC<SvgProps> = ({
|
||||||
|
width = 150,
|
||||||
|
height = 150,
|
||||||
|
// fill = EColor.purple,
|
||||||
|
className,
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
enableBackground="new 0 0 512 512"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
xmlSpace="preserve"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<linearGradient
|
||||||
|
x1="25.276"
|
||||||
|
x2="487.471"
|
||||||
|
y1="285.368"
|
||||||
|
y2="285.368"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0" stopColor="#FF6060"></stop>
|
||||||
|
<stop offset="1" stopColor="#E23D3D"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
d="M457.65 185.2c-27.28-48.21-71.39-85.67-124.36-104.36-24.07-8.49-49.96-13.11-76.92-13.11-27.42 0-53.72 4.78-78.12 13.54-52.43 18.83-96.09 56.07-123.15 103.92-18.98 33.56-29.82 72.33-29.82 113.63 0 6.11.23 12.15.71 18.12 2.37 30.67 10.73 59.64 23.9 85.78 20.88 41.4 53.84 75.67 94.25 98.16 6.16 3.43 13.97.14 15.79-6.68 3.09-11.6 4.74-23.78 4.74-36.36 0-2.84-.08-5.67-.25-8.47-.51-8.58 8.48-14.42 16.05-10.36 22.38 12.01 47.96 18.82 75.14 18.82s52.76-6.81 75.14-18.82c7.57-4.06 16.56 1.78 16.05 10.36-.17 2.8-.25 5.63-.25 8.47 0 12.85 1.72 25.31 4.95 37.14 1.84 6.76 9.58 10.04 15.73 6.67 40.99-22.44 74.44-56.96 95.55-98.75a229.255 229.255 0 0023.99-85.98c.48-5.97.71-12.01.71-18.1-.01-41.29-10.85-80.06-29.83-113.62zM339.49 338.24c-14.82 31.43-46.82 53.18-83.89 53.18s-69.07-21.75-83.89-53.18c-5.69-12-8.86-25.41-8.86-39.57 0-5.16.42-10.22 1.24-15.15 5.38-32.76 27.92-59.73 58.08-71.39 10.37-4.02 21.64-6.21 33.44-6.21s23.06 2.2 33.44 6.21c30.36 11.73 52.99 38.97 58.18 72a93.93 93.93 0 011.13 14.53c-.01 14.16-3.18 27.58-8.87 39.58z"
|
||||||
|
className="st0"
|
||||||
|
fill={'#313131'}
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M140.38 77.46c-29.18 15.23-54.98 36.07-75.95 61.09-9.38 11.19-27.46 7.27-31.26-6.84-5.15-19.17-7.9-39.32-7.9-60.12 0-16.56 1.74-32.73 5.06-48.31 1.91-8.99 10.95-15.32 20.07-14.1 34.39 4.62 65.99 18.04 92.45 37.92 10.59 7.94 9.26 24.23-2.47 30.36zM487.47 71.59c0 21.34-2.89 41.99-8.3 61.6-3.89 14.09-21.99 17.95-31.29 6.68-21.17-25.61-47.36-46.92-77.07-62.42-11.74-6.12-13.02-22.42-2.43-30.37 26.88-20.19 59.04-33.71 94.05-38.12 9.06-1.14 18.04 5.18 19.95 14.11 3.34 15.64 5.09 31.88 5.09 48.52z"
|
||||||
|
className="st1"
|
||||||
|
fill={'#313131'}
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default LogoSVG;
|
||||||
51
src/components/SeoHead/SeoHead.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import Head from 'next/head';
|
||||||
|
|
||||||
|
type Tags = Array<string> ;
|
||||||
|
|
||||||
|
type SeoHeadProps = {
|
||||||
|
tabTitle: string;
|
||||||
|
title: string;
|
||||||
|
canonical?: string;
|
||||||
|
ogUrl?: string
|
||||||
|
description: string;
|
||||||
|
keywords?: string;
|
||||||
|
imageSource?: string;
|
||||||
|
videoTags?: Tags;
|
||||||
|
bookTags?: Tags;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTags = (tagName: 'video' | 'book', tags?: Tags) => tags && tags.length && tags.map((tag, i) => <meta
|
||||||
|
key={`${tag}-${i}`}
|
||||||
|
content={tag}
|
||||||
|
property={`${tagName}:tag`}
|
||||||
|
/>);
|
||||||
|
|
||||||
|
const SeoHead: FC<SeoHeadProps> = ({
|
||||||
|
tabTitle,
|
||||||
|
title,
|
||||||
|
canonical,
|
||||||
|
ogUrl,
|
||||||
|
description,
|
||||||
|
keywords,
|
||||||
|
imageSource,
|
||||||
|
videoTags,
|
||||||
|
bookTags,
|
||||||
|
}) => (<Head>
|
||||||
|
<title>{tabTitle}</title>
|
||||||
|
<meta content={title} property="og:title" />
|
||||||
|
<meta content={title} property="twitter:title" />
|
||||||
|
<meta content={description} name="og:description" />
|
||||||
|
<meta content={description} name="twitter:description" />
|
||||||
|
<meta content={description} name="description" />
|
||||||
|
{canonical && <link rel="canonical" href={canonical} />}
|
||||||
|
{ogUrl && <meta content={ogUrl} property="og:url" />}
|
||||||
|
{getTags('video', videoTags)}
|
||||||
|
{getTags('book', bookTags)}
|
||||||
|
{imageSource && <meta content={imageSource} property="og:image"/>}
|
||||||
|
{imageSource && <meta content={imageSource} property="twitter:image"/>}
|
||||||
|
{keywords && <meta content={keywords} name="Keywords" />}
|
||||||
|
</Head>);
|
||||||
|
|
||||||
|
export default SeoHead;
|
||||||
1
src/components/SeoHead/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './SeoHead';
|
||||||
79
src/constants/app.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { AppChangelog } from '@interfaces/common';
|
||||||
|
|
||||||
|
import {
|
||||||
|
default_img_1,
|
||||||
|
default_img_2,
|
||||||
|
exclusive_img_1,
|
||||||
|
exclusive_img_2,
|
||||||
|
} from '@constants/common';
|
||||||
|
|
||||||
|
export const APP_VERSIONS: Array<AppChangelog> = [
|
||||||
|
{
|
||||||
|
appName: 'Default',
|
||||||
|
descrition: 'Простой мод без каких-либо дополнительных функций.',
|
||||||
|
images: [
|
||||||
|
default_img_1,
|
||||||
|
default_img_2,
|
||||||
|
],
|
||||||
|
changelogs: [{
|
||||||
|
version: '1.0',
|
||||||
|
date: '20/05/24',
|
||||||
|
isCurrentVersion: true,
|
||||||
|
supportAndroidVersion: '9+',
|
||||||
|
download: 'https://github.com/seele-off/anixart/releases/download/anixart-default/Anixart-Default-v1.0-by-Seele.apk', // ссылка на apk
|
||||||
|
changes: [
|
||||||
|
'Без рекламы',
|
||||||
|
'Корона в профиле 👑',
|
||||||
|
'Добавлена тематическая иконка',
|
||||||
|
'Добавлено новое расширение "MD Seele v3.0"',
|
||||||
|
'Расширение предназначено для просмотра запрещенных аниме'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '1.0 Alpha',
|
||||||
|
supportAndroidVersion: '9+',
|
||||||
|
download: 'https://www.darknet.kz/download', // ссылка на apk
|
||||||
|
date: '18/05/22',
|
||||||
|
changes: [
|
||||||
|
'Adding Serach System',
|
||||||
|
'Adding Bypes',
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appName: 'Exclusive',
|
||||||
|
descrition: 'Эксклюзивный мод, особенностью этого мода является Monet Тема (Material You)',
|
||||||
|
images: [
|
||||||
|
exclusive_img_1,
|
||||||
|
exclusive_img_2
|
||||||
|
],
|
||||||
|
changelogs: [{
|
||||||
|
version: '1.0',
|
||||||
|
date: '20/05/24',
|
||||||
|
isCurrentVersion: true,
|
||||||
|
supportAndroidVersion: '12+',
|
||||||
|
download: 'https://github.com/seele-off/anixart/releases/download/anixart-exclusive/Anixart-Exclusive-v1.0-by-Seele.apk', // ссылка на apk
|
||||||
|
changes: [
|
||||||
|
'Без рекламы',
|
||||||
|
'Корона в профиле 👑',
|
||||||
|
'Monet Theme (Material You)',
|
||||||
|
'Добавлена тематическая иконка',
|
||||||
|
'Добавлено новое расширение "MD Seele v3.0"',
|
||||||
|
'Расширение предназначено для просмотра запрещенных аниме'
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appName: 'Amoled',
|
||||||
|
descrition: 'Анонс',
|
||||||
|
images: [],
|
||||||
|
changelogs: [{
|
||||||
|
version: '',
|
||||||
|
date: 'В разработке',
|
||||||
|
isCurrentVersion: true,
|
||||||
|
supportAndroidVersion: '9+',
|
||||||
|
download: '', // ссылка на apk
|
||||||
|
changes: [],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
];
|
||||||
4
src/constants/common.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const default_img_1 = '/images/default_interface_1.webp';
|
||||||
|
export const default_img_2 = '/images/default_interface_2.webp';
|
||||||
|
export const exclusive_img_1 = '/images/exclusive_interface_1.webp';
|
||||||
|
export const exclusive_img_2 = '/images/exclusive_interface_2.webp';
|
||||||
3
src/constants/error.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/* eslint-disable max-len */
|
||||||
|
export const COMMON_ERROR: string = 'Упс, что-то пошло не так, попробуйте зайти на другую страницу или обновить текущую';
|
||||||
|
export const NOT_FOUND_ERROR: string = 'Похоже этой страницы не существует, попробуйте зайти на другую';
|
||||||
10
src/constants/seo.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
/* eslint-disable max-len */
|
||||||
|
export const APP_NAME: string = 'Anixart - улучшенная версия';
|
||||||
|
export const SEO_TITLE: string = 'Anixart - улучшенная версия | скачать';
|
||||||
|
|
||||||
|
export const SEO_BASE_DESCRIPTION: string = 'Это мобильное приложение, которое поможет вам ознакомиться с самыми разнообразными работами японской мультипликации. Открывайте для себя новые произведения, составляйте списки просмотра, смотрите онлайн, участвуйте в обсуждениях и многое другое!';
|
||||||
|
|
||||||
|
export const SEO_DESCRIPTION: string = 'Anixart | улучшенная версия – это мобильное приложение, которое поможет вам ознакомиться с самыми разнообразными работами японской мультипликации. Открывайте для себя новые произведения, составляйте списки просмотра, смотрите онлайн, участвуйте в обсуждениях и многое другое!';
|
||||||
|
|
||||||
|
export const SEO_KEYWORD: string = 'аниксарт мод, anixart mod, скачать аниксарт мод, Аниксарт мод, Мод на аниксарт, Anixart Mod, mod anixart, аниксарт без рекламы, без реклама аниксарт, Anixart, anixart modding, аниксарт, Seele, Seele Anixart, strannik, alexstrannik, Strannik Anixart mod, скачать аниме, смотреть аниме, смотреть бесплатно, скачать бесплатно, аниме бесплатно, anixart, аниксарт, onedub, вандаб, anidub, anilibria, anistar, animevost, анидаб, анилибрия, анистар, анимевост, android, online, онлайн, anime, андроид, анидаб онлайн для андроид, аниме, фандаб, русская озвучка, смотреть бесплатно, телефон, сматрфон, приложение';
|
||||||
17
src/enums/enums.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export enum ELinkPath {
|
||||||
|
home = '/',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EScrollId {
|
||||||
|
download = 'download',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EColor {
|
||||||
|
white = '#fff',
|
||||||
|
black = '#0f0f0f',
|
||||||
|
lightBlack = 'rgb(38 38 38)',
|
||||||
|
lightGray = 'rgb(212 212 212)',
|
||||||
|
red = 'rgb(245 65 66)',
|
||||||
|
purple = 'rgb(145, 107, 227)',
|
||||||
|
purpleTransparent = 'rgba(145, 107, 227, 0.250)',
|
||||||
|
}
|
||||||
15
src/interfaces/common.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export type AppChanges = {
|
||||||
|
date: string;
|
||||||
|
version: string;
|
||||||
|
download: string;
|
||||||
|
changes: Array<string>;
|
||||||
|
supportAndroidVersion: string;
|
||||||
|
isCurrentVersion?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AppChangelog = {
|
||||||
|
appName: string;
|
||||||
|
descrition: string;
|
||||||
|
images: Array<string>;
|
||||||
|
changelogs: Array<AppChanges>;
|
||||||
|
};
|
||||||
6
src/interfaces/svg.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export type SvgProps = {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
fill?: string;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
21
src/layouts/ContentLayout/ContentLayout.module.css
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.contentWrapper {
|
||||||
|
max-width: var(--full-hd-width-screen);
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0rem 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 850px) {
|
||||||
|
.contentWrapper {
|
||||||
|
max-width: var(--full-hd-width-screen);
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0rem 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.contentWrapper {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/layouts/ContentLayout/ContentLayout.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { FC, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import style from './ContentLayout.module.css';
|
||||||
|
|
||||||
|
type ContentLayoutProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ContentLayout: FC<ContentLayoutProps> = ({ children }) => (
|
||||||
|
<main className={style.contentWrapper}>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ContentLayout;
|
||||||
1
src/layouts/ContentLayout/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './ContentLayout';
|
||||||
13
src/layouts/RootLayout/RootLayout.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { FC, ReactNode } from 'react';
|
||||||
|
|
||||||
|
type MainLayout = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RootLayout: FC<MainLayout> = ({ children }) => (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default RootLayout;
|
||||||
1
src/layouts/RootLayout/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './RootLayout';
|
||||||
39
src/pages/_app.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { AppProps } from 'next/app';
|
||||||
|
import Head from 'next/head';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EColor,
|
||||||
|
} from '@enums/enums';
|
||||||
|
|
||||||
|
import { APP_NAME } from '@constants/seo';
|
||||||
|
|
||||||
|
import RootLayout from '@layouts/RootLayout';
|
||||||
|
|
||||||
|
import '@styles/normalize.css';
|
||||||
|
import '@styles/variables.css';
|
||||||
|
import '@styles/global.css';
|
||||||
|
|
||||||
|
function MyApp({
|
||||||
|
Component,
|
||||||
|
pageProps,
|
||||||
|
}: AppProps) {
|
||||||
|
const themeForMeta = true ? EColor.white : EColor.black;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RootLayout>
|
||||||
|
<Head>
|
||||||
|
<meta charSet="UTF-8" />
|
||||||
|
<meta content="website" property="og:type" />
|
||||||
|
<meta name="theme-color" content={themeForMeta} />
|
||||||
|
<meta content={APP_NAME} name="twitter:site" />
|
||||||
|
<meta content={APP_NAME} property="og:site_name" />
|
||||||
|
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</RootLayout>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MyApp;
|
||||||
48
src/pages/_document.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/* eslint-disable max-len */
|
||||||
|
import {
|
||||||
|
Html, Head, Main, NextScript,
|
||||||
|
} from 'next/document';
|
||||||
|
import Script from 'next/script';
|
||||||
|
|
||||||
|
export default function MyDocument() {
|
||||||
|
return (
|
||||||
|
<Html lang="ru">
|
||||||
|
<Head>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
|
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
</Head>
|
||||||
|
<body>
|
||||||
|
<Main />
|
||||||
|
|
||||||
|
<NextScript />
|
||||||
|
|
||||||
|
<Script id="metrika-counter" strategy="afterInteractive">
|
||||||
|
{`(function(m,e,t,r,i,k,a){m[i]=m[i]function(){(m[i].a=m[i].a[]).push(arguments)};
|
||||||
|
m[i].l=1*new Date();
|
||||||
|
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
||||||
|
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
|
||||||
|
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
|
||||||
|
|
||||||
|
ym(97463564, "init", {
|
||||||
|
clickmap:true,
|
||||||
|
trackLinks:true,
|
||||||
|
accurateTrackBounce:true
|
||||||
|
});`
|
||||||
|
}
|
||||||
|
</Script>
|
||||||
|
|
||||||
|
<noscript>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src="https://mc.yandex.ru/watch/97463564"
|
||||||
|
style={{ position: 'absolute', left: '-9999px' }}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/pages/_error.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { NextPageContext } from 'next';
|
||||||
|
|
||||||
|
import { COMMON_ERROR, NOT_FOUND_ERROR } from '@constants/error';
|
||||||
|
|
||||||
|
import ErrorComponent from '@ui/Error';
|
||||||
|
|
||||||
|
import ContentLayout from '@layouts/ContentLayout';
|
||||||
|
|
||||||
|
const Error = ({ statusCode }: { statusCode: number }) => {
|
||||||
|
const errorText = statusCode === 404
|
||||||
|
? NOT_FOUND_ERROR
|
||||||
|
: COMMON_ERROR;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContentLayout>
|
||||||
|
<ErrorComponent statusCode={statusCode} errorText={errorText} goHome />
|
||||||
|
</ContentLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Error.getInitialProps = ({ res, err }: NextPageContext) => {
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
|
||||||
|
return { statusCode };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Error;
|
||||||
53
src/pages/api/extension/episode/[releaseId].ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface EpisodeResponse {
|
||||||
|
code: number;
|
||||||
|
types?: Array<{
|
||||||
|
'@id': number;
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
icon: string | null;
|
||||||
|
workers: string | null;
|
||||||
|
is_sub: boolean;
|
||||||
|
episodes_count: number;
|
||||||
|
view_count: number;
|
||||||
|
pinned: boolean;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { releaseId } = req.query;
|
||||||
|
const anixartAPI = `https://api.anixart.tv/episode/${releaseId}`;
|
||||||
|
const seeleAPI = `https://seele-off.github.io/anixart/extension/api/episode/${releaseId}.json`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const anixartRes = await axios.get<EpisodeResponse>(anixartAPI);
|
||||||
|
const anixartResData = anixartRes.data;
|
||||||
|
const modifyedData = modifyData(anixartRes.data);
|
||||||
|
|
||||||
|
if (!modifyedData.types || modifyedData.types.length === 0) {
|
||||||
|
const seeleRes = await axios.get(seeleAPI);
|
||||||
|
|
||||||
|
res.json({ is_blocked: true, ...modifyData(seeleRes.data) });
|
||||||
|
} else {
|
||||||
|
res.json(anixartResData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data from Anixart API:', error);
|
||||||
|
res.status(500).json({ message: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function modifyData(data: EpisodeResponse): EpisodeResponse {
|
||||||
|
if (data.types && data.types.length) {
|
||||||
|
data.types = data.types.map(type => {
|
||||||
|
return {
|
||||||
|
...type,
|
||||||
|
workers: 'Отображается благодаря расширению «MD Seele»' // Изменение значения workers на 'MD Sele'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
52
src/pages/api/extension/profile/[userId].ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
type Role = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfileResponse = {
|
||||||
|
code: number;
|
||||||
|
profile?: {
|
||||||
|
id: number;
|
||||||
|
is_verified: boolean;
|
||||||
|
is_sponsor: boolean;
|
||||||
|
is_sponsor_transferred: boolean;
|
||||||
|
sponsorshipExpires: number;
|
||||||
|
roles: Array<Role>,
|
||||||
|
// and other types
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { userId } = req.query;
|
||||||
|
const anixartAPI = `https://api.anixart.tv/profile/${userId}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const anixartRes = await axios.get<ProfileResponse>(anixartAPI);
|
||||||
|
|
||||||
|
const currentProfile = anixartRes.data.profile && userId === '790852'
|
||||||
|
? {
|
||||||
|
...anixartRes.data.profile,
|
||||||
|
is_verified: true,
|
||||||
|
is_sponsor: true,
|
||||||
|
is_sponsor_transferred: false,
|
||||||
|
sponsorshipExpires: Number.MAX_SAFE_INTEGER,
|
||||||
|
roles: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Разработчик мода",
|
||||||
|
color: "F04E4E"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: anixartRes.data.profile;
|
||||||
|
|
||||||
|
res.json({ ...anixartRes.data, profile: currentProfile });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data from Anixart API:', error);
|
||||||
|
res.status(500).json({ message: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/pages/api/extension/release/[id].ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface AnimeResponse {
|
||||||
|
code: number;
|
||||||
|
release: {
|
||||||
|
title_original: string;
|
||||||
|
grade: number;
|
||||||
|
};
|
||||||
|
// other types
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShikiResponse {
|
||||||
|
score: string;
|
||||||
|
// other types
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { id } = req.query;
|
||||||
|
const anixartAPI = `https://api.anixart.tv/release/${id}`;
|
||||||
|
const shikimoriAPI = `https://shikimori.one/api/animes`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<AnimeResponse>(anixartAPI);
|
||||||
|
const shikiAnime = await axios.get<ShikiResponse[]>(
|
||||||
|
`${shikimoriAPI}?search=${encodeURIComponent(data.release.title_original)}`
|
||||||
|
);
|
||||||
|
const shikiData = shikiAnime.data;
|
||||||
|
|
||||||
|
if (shikiData.length) {
|
||||||
|
res.status(200).json({
|
||||||
|
...data,
|
||||||
|
release: {
|
||||||
|
...data.release,
|
||||||
|
grade: Number(shikiData[0].score),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
res.status(200).json(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data from Anixart or Shikimori API:', error);
|
||||||
|
res.status(500).json({ message: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
242
src/pages/index.tsx
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
import React, {
|
||||||
|
FC, useCallback, useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { AppChanges } from '@interfaces/common';
|
||||||
|
|
||||||
|
import { EScrollId } from '@enums/enums';
|
||||||
|
|
||||||
|
import { APP_VERSIONS } from '@constants/app';
|
||||||
|
import {
|
||||||
|
SEO_DESCRIPTION, SEO_KEYWORD, SEO_TITLE,
|
||||||
|
} from '@constants/seo';
|
||||||
|
|
||||||
|
import HorizontalScroll from '@ui/HorizontalScroll';
|
||||||
|
import Link from '@ui/Link';
|
||||||
|
import Modal from '@ui/modal';
|
||||||
|
import TabBar from '@ui/Tabbar';
|
||||||
|
import TabBarItem from '@ui/Tabbar/tabbarItem';
|
||||||
|
import Ticker from '@ui/Ticker';
|
||||||
|
|
||||||
|
import SeoHead from '@components/SeoHead';
|
||||||
|
|
||||||
|
import ContentLayout from '@layouts/ContentLayout';
|
||||||
|
|
||||||
|
import InfoSVG from '@assets/svg/info';
|
||||||
|
import LogoSVG from '@assets/svg/logo';
|
||||||
|
|
||||||
|
import scrollToElement from '@utils/scrollToElement';
|
||||||
|
|
||||||
|
import style from '@styles/pages/homePage.module.css';
|
||||||
|
|
||||||
|
const fillArray = Array(3).fill(0).map((_, index) => index + 1);
|
||||||
|
|
||||||
|
type TableRowType = AppChanges & {
|
||||||
|
appName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Changes = {
|
||||||
|
appName: string;
|
||||||
|
version: string;
|
||||||
|
supportAndroidVersion: string;
|
||||||
|
changes: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const Main: FC = () => {
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
const [activeTab, setActiveTab] = useState<string>(APP_VERSIONS[0].appName);
|
||||||
|
const [isOpenPreviewImageModal, setPreviewImageModal] = useState<boolean>(false);
|
||||||
|
const [changelogItems, setChangelogItems] = useState<Changes | null>(null);
|
||||||
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const setTest = (t: string) => setActiveTab(t);
|
||||||
|
|
||||||
|
const onOpen = (isOpened: boolean) => {
|
||||||
|
setIsOpen(isOpened);
|
||||||
|
setChangelogItems(null);
|
||||||
|
setPreviewImage(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOpenPreviewImage = (isOpened: boolean) => {
|
||||||
|
setPreviewImageModal(isOpened);
|
||||||
|
setPreviewImage(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSetChangelogItems = (logs: Changes) => {
|
||||||
|
setChangelogItems(logs);
|
||||||
|
setIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPrewImage = (image: string) => {
|
||||||
|
setPreviewImage(image);
|
||||||
|
setPreviewImageModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImages = (images: string[]) => images.map((img, i) => <img
|
||||||
|
key={i}
|
||||||
|
onClick={() => setPrewImage(img)}
|
||||||
|
className={style.imagePreview}
|
||||||
|
style={{ marginRight: i + 1 === images.length ? 0 : 45 }}
|
||||||
|
src={img} alt="Изображение приложения"
|
||||||
|
/>);
|
||||||
|
|
||||||
|
const getTableRow = ({
|
||||||
|
appName, version, download, isCurrentVersion, date, changes, supportAndroidVersion,
|
||||||
|
}: TableRowType) => (<tr>
|
||||||
|
<th>
|
||||||
|
<Link className={style.downloadApplink} path={download} target="_blank">{`${appName} ${version}`}</Link>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th>
|
||||||
|
<span className={clsx(style.status, { [style.statusActive]: isCurrentVersion })}>
|
||||||
|
{isCurrentVersion ? 'Активный' : 'Устаревший'}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th>
|
||||||
|
{date}
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th>
|
||||||
|
<button className={style.appInfoButton} onClick={() => onSetChangelogItems({
|
||||||
|
changes, appName, version, supportAndroidVersion,
|
||||||
|
})}>
|
||||||
|
<div className={style.appInfoButtonIconWrapper}>
|
||||||
|
<InfoSVG />
|
||||||
|
<span className={style.appInfoButtonText}>Подробнее</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
</tr>);
|
||||||
|
|
||||||
|
const getTable = useCallback(() => (
|
||||||
|
<TabBar activeTab={activeTab} setActiveTab={setTest}>
|
||||||
|
{
|
||||||
|
APP_VERSIONS.map(({
|
||||||
|
appName, descrition, images, changelogs,
|
||||||
|
}) => (<TabBarItem key={appName} label={appName}>
|
||||||
|
<div className={style.imagesWrapper}>
|
||||||
|
<HorizontalScroll>
|
||||||
|
{
|
||||||
|
getImages(images)
|
||||||
|
}
|
||||||
|
</HorizontalScroll>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={style.appVersionDescriptionWrapper}>
|
||||||
|
<p className={style.appVersionDescription}>{descrition}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={style.changelogsList}>
|
||||||
|
<table className={style.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Версия</th>
|
||||||
|
<th>Статус</th>
|
||||||
|
<th>Дата</th>
|
||||||
|
<th>Информация</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
{
|
||||||
|
changelogs.map((change) => (
|
||||||
|
<tbody key={change.version}>
|
||||||
|
{getTableRow({ appName, ...change })}
|
||||||
|
</tbody>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</TabBarItem>))
|
||||||
|
}
|
||||||
|
</TabBar>
|
||||||
|
), [activeTab]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContentLayout>
|
||||||
|
<SeoHead
|
||||||
|
title={SEO_TITLE}
|
||||||
|
tabTitle={SEO_TITLE}
|
||||||
|
keywords={SEO_KEYWORD}
|
||||||
|
description={SEO_DESCRIPTION}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="Изменения"
|
||||||
|
showCloseButton
|
||||||
|
isOpen={isOpen}
|
||||||
|
setOpen={onOpen}
|
||||||
|
>
|
||||||
|
<div className={style.changesListWrapper}>
|
||||||
|
<ul className={style.changesList}>
|
||||||
|
{changelogItems && changelogItems.changes.length > 0 && changelogItems.changes.map((change) => <li
|
||||||
|
key={change}
|
||||||
|
className={style.changesListItem}
|
||||||
|
>
|
||||||
|
{change}
|
||||||
|
</li>)}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className={style.changesListBottomInfo}>
|
||||||
|
<span className={style.changesListBottomInfoName}>
|
||||||
|
{`${changelogItems?.appName} ${changelogItems?.version}`}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className={style.changesListBottomInfoSupport}>
|
||||||
|
{`Android ${changelogItems?.supportAndroidVersion}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
noBody={true}
|
||||||
|
isOpen={isOpenPreviewImageModal}
|
||||||
|
setOpen={onOpenPreviewImage}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={previewImage || ''}
|
||||||
|
className={style.appImage}
|
||||||
|
alt="Изображение приложения"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<section className={style.sectionWrapper}>
|
||||||
|
<div className={style.sectionContent}>
|
||||||
|
<LogoSVG />
|
||||||
|
|
||||||
|
<h1 className={style.appTitle}>ANIXART / MODE</h1>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={style.btn}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
scrollToElement('download');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ticker>
|
||||||
|
{
|
||||||
|
fillArray.map((item) => (
|
||||||
|
<p key={item} className={style.tickerItem}>Cкачать</p>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Ticker>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={style.sectionWrapper} id={EScrollId.download}>
|
||||||
|
<h2 className={style.sectionTitle}>Версии приложения</h2>
|
||||||
|
|
||||||
|
<div className={style.downloadContent}>
|
||||||
|
{getTable()}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</ContentLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Main;
|
||||||
35
src/styles/global.css
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table,
|
||||||
|
thead,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
tr,
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-collapse: inherit;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
background-color: var(--white);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
210
src/styles/normalize.css
vendored
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
html {
|
||||||
|
line-height: 1.15;
|
||||||
|
/* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin: 0.67em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
box-sizing: content-box;
|
||||||
|
/* 1 */
|
||||||
|
height: 0;
|
||||||
|
/* 1 */
|
||||||
|
overflow: visible;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: monospace, monospace;
|
||||||
|
/* 1 */
|
||||||
|
font-size: 1em;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
border-bottom: none;
|
||||||
|
/* 1 */
|
||||||
|
text-decoration: underline;
|
||||||
|
/* 2 */
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: monospace, monospace;
|
||||||
|
/* 1 */
|
||||||
|
font-size: 1em;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-size: 100%;
|
||||||
|
/* 1 */
|
||||||
|
line-height: 1.15;
|
||||||
|
/* 1 */
|
||||||
|
margin: 0;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
/* 1 */
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
/* 1 */
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type="button"],
|
||||||
|
[type="reset"],
|
||||||
|
[type="submit"] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
[type="button"]::-moz-focus-inner,
|
||||||
|
[type="reset"]::-moz-focus-inner,
|
||||||
|
[type="submit"]::-moz-focus-inner {
|
||||||
|
border-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:-moz-focusring,
|
||||||
|
[type="button"]:-moz-focusring,
|
||||||
|
[type="reset"]:-moz-focusring,
|
||||||
|
[type="submit"]:-moz-focusring {
|
||||||
|
outline: 1px dotted ButtonText;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
padding: 0.35em 0.75em 0.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* 1 */
|
||||||
|
color: inherit;
|
||||||
|
/* 2 */
|
||||||
|
display: table;
|
||||||
|
/* 1 */
|
||||||
|
max-width: 100%;
|
||||||
|
/* 1 */
|
||||||
|
padding: 0;
|
||||||
|
/* 3 */
|
||||||
|
white-space: normal;
|
||||||
|
/* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="checkbox"],
|
||||||
|
[type="radio"] {
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* 1 */
|
||||||
|
padding: 0;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="number"]::-webkit-inner-spin-button,
|
||||||
|
[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="search"] {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
/* 1 */
|
||||||
|
outline-offset: -2px;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
/* 1 */
|
||||||
|
font: inherit;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
details {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
265
src/styles/pages/homePage.module.css
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
.sectionWrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 95vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionContent {
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appTitle {
|
||||||
|
margin-top: 3rem;
|
||||||
|
font-size: 4rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
transition: all 200ms ease-in-out;
|
||||||
|
background-color: var(--redTransparent);
|
||||||
|
width: 12rem;
|
||||||
|
height: 64px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 40px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--red);
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: var(--red);
|
||||||
|
width: 13rem;
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tickerItem {
|
||||||
|
padding: 0 15px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: 'inherit';
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
margin-bottom: 3.5rem;
|
||||||
|
font-size: 4rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.appImage {
|
||||||
|
max-width: 350px;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 600px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imagePreview {
|
||||||
|
width: 170px;
|
||||||
|
height: 250px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imagesWrapper {
|
||||||
|
max-width: 900px;
|
||||||
|
margin-top: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelogsList {
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appVersionDescriptionWrapper {
|
||||||
|
padding: 15px;
|
||||||
|
background-color: var(--lightGray);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changesListWrapper {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-bottom: 35px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changesList {
|
||||||
|
padding: 10px 0px 0px 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changesListItem {
|
||||||
|
padding: 5px;
|
||||||
|
width: 100%;
|
||||||
|
word-wrap: break-word;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changesListBottomInfo {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changesListBottomInfoName {
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: var(--blue);
|
||||||
|
padding: 5px 10px;
|
||||||
|
color: var(--white);
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changesListBottomInfoSupport {
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background-color: var(--green);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadApplink {
|
||||||
|
color: var(--black);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 300ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadApplink:hover {
|
||||||
|
color: var(--red);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 300ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appVersionDescription {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: var(--lightGray);
|
||||||
|
padding: 5px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusActive {
|
||||||
|
background-color: var(--green);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusDev {
|
||||||
|
background-color: #FFF8E1;
|
||||||
|
color: #FFC107;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelogsListItem {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appInfoButton {
|
||||||
|
border: none;
|
||||||
|
background-color: var(--red);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: var(--white);
|
||||||
|
height: 30px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0 10px;
|
||||||
|
display: inline-block;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appInfoButton:hover {
|
||||||
|
transition: all 200ms ease;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appInfoButtonIconWrapper {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appInfoButtonText {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
padding: 0.75rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead tr th {
|
||||||
|
border-bottom: 1px solid var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadContent {
|
||||||
|
max-width: 820px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 850px) {
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appDescription {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin-top: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appTitle {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appDescription {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 465px) {
|
||||||
|
.table th {
|
||||||
|
padding: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appInfoButtonIconWrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appInfoButton {
|
||||||
|
height: 45px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/styles/variables.css
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
:root {
|
||||||
|
--white: #fff;
|
||||||
|
--black: #0f0f0f;
|
||||||
|
--blackTransparent: rgba(0, 0, 0, 0.300);
|
||||||
|
--lightBlack: rgb(38 38 38);
|
||||||
|
--gray: rgb(112, 112, 112);
|
||||||
|
--lightGray: rgb(240 240 240);
|
||||||
|
--red: rgb(245 65 66);
|
||||||
|
--redTransparent: rgba(245, 65, 66, 0.200);
|
||||||
|
--purple: rgb(145, 107, 227);
|
||||||
|
--purpleTransparent: rgba(145, 107, 227, 0.250);
|
||||||
|
--green: rgb(119 197 153);
|
||||||
|
--blue: rgb(95 200 248);
|
||||||
|
--full-hd-width-screen: 1920px;
|
||||||
|
}
|
||||||
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
@@ -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
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './Error';
|
||||||
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
@@ -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
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './HorizontalScroll';
|
||||||
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
@@ -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
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './Link';
|
||||||
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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './TabBar';
|
||||||
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
@@ -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
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './TabBarItem';
|
||||||
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
@@ -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
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './Ticker';
|
||||||
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
@@ -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
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './Modal';
|
||||||
11
src/utils/scrollToElement.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const scrollToElement = (elementId: string) => {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default scrollToElement;
|
||||||