Add files via upload
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './SeoHead';
|
||||
@@ -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: [],
|
||||
}],
|
||||
},
|
||||
];
|
||||
@@ -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';
|
||||
@@ -0,0 +1,3 @@
|
||||
/* eslint-disable max-len */
|
||||
export const COMMON_ERROR: string = 'Упс, что-то пошло не так, попробуйте зайти на другую страницу или обновить текущую';
|
||||
export const NOT_FOUND_ERROR: string = 'Похоже этой страницы не существует, попробуйте зайти на другую';
|
||||
@@ -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, андроид, анидаб онлайн для андроид, аниме, фандаб, русская озвучка, смотреть бесплатно, телефон, сматрфон, приложение';
|
||||
@@ -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)',
|
||||
}
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export type SvgProps = {
|
||||
width?: number;
|
||||
height?: number;
|
||||
fill?: string;
|
||||
className?: string;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ContentLayout';
|
||||
@@ -0,0 +1,13 @@
|
||||
import { FC, ReactNode } from 'react';
|
||||
|
||||
type MainLayout = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const RootLayout: FC<MainLayout> = ({ children }) => (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
export default RootLayout;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './RootLayout';
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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' });
|
||||
}
|
||||
}
|
||||
@@ -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' });
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
Vendored
+210
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './Error';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './HorizontalScroll';
|
||||
@@ -0,0 +1,3 @@
|
||||
.link {
|
||||
text-decoration: 'none',
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './Link';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './TabBar';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './TabBarItem';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './Ticker';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './Modal';
|
||||
@@ -0,0 +1,11 @@
|
||||
const scrollToElement = (elementId: string) => {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default scrollToElement;
|
||||
Reference in New Issue
Block a user