Add files via upload
This commit is contained in:
31
src/assets/svg/arrow.tsx
Normal file
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
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
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
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
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
1
src/components/SeoHead/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './SeoHead';
|
||||
79
src/constants/app.ts
Normal file
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
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
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
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
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
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
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
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
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
1
src/layouts/ContentLayout/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './ContentLayout';
|
||||
13
src/layouts/RootLayout/RootLayout.tsx
Normal file
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
1
src/layouts/RootLayout/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './RootLayout';
|
||||
39
src/pages/_app.tsx
Normal file
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
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
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
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
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
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
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
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
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
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
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
50
src/ui/Error/Error.module.css
Normal file
@@ -0,0 +1,50 @@
|
||||
.errorContainer {
|
||||
/* background: ; */
|
||||
border-radius: 4px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 1200;
|
||||
margin: 0 auto;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0 10px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.errorTitle {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
margin-top: 10rem;
|
||||
}
|
||||
|
||||
.statusCodeText {
|
||||
font-size: 16px;
|
||||
margin-top: 19px;
|
||||
}
|
||||
|
||||
.link {
|
||||
font-size: 16px;
|
||||
transition: opacity 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
|
||||
margin-top: 19px;
|
||||
color: var(--gray);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
opacity: 0.7;
|
||||
transition: 'opacity 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms';
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.errorContainer {
|
||||
padding: '0 5px';
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.errorContainer {
|
||||
padding: '0 5px';
|
||||
}
|
||||
}
|
||||
34
src/ui/Error/Error.tsx
Normal file
34
src/ui/Error/Error.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
import { ELinkPath } from '@enums/enums';
|
||||
|
||||
import Link from '@ui/Link';
|
||||
|
||||
import style from './Error.module.css';
|
||||
|
||||
type ErrorProps = {
|
||||
statusCode?: number;
|
||||
errorText: string;
|
||||
goHome?: boolean;
|
||||
};
|
||||
|
||||
const Error: FC<ErrorProps> = ({ statusCode, errorText, goHome }) => (
|
||||
<div className={style.errorContainer}>
|
||||
{errorText && (
|
||||
<h1 className={style.errorTitle}>
|
||||
{errorText}
|
||||
</h1>
|
||||
)}
|
||||
|
||||
{ statusCode && <h2 className={style.statusCodeText}>
|
||||
{statusCode && `Ошибка ${statusCode}`}
|
||||
</h2>
|
||||
}
|
||||
|
||||
{
|
||||
goHome ? <Link path={ELinkPath.home} className={style.link}>Вернуться на главную?</Link> : <></>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Error;
|
||||
1
src/ui/Error/index.ts
Normal file
1
src/ui/Error/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Error';
|
||||
98
src/ui/HorizontalScroll/HorizontalScroll.module.css
Normal file
98
src/ui/HorizontalScroll/HorizontalScroll.module.css
Normal file
@@ -0,0 +1,98 @@
|
||||
.outerContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.horizontalScroll {
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
will-change: transform;
|
||||
white-space: nowrap;
|
||||
overflow: auto;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-ms-overflow-style: none;
|
||||
/* IE и Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.horizontalScroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.horizontalScrollСontent {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.horizontalScrollСontent>* {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.horizontalScrollItem {
|
||||
flex: 0 0 auto;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
.prevButton,
|
||||
.nextButton {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 24px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
/* Добавлено для позиционирования поверх контента */
|
||||
}
|
||||
|
||||
.nextButton svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.prevButton {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.nextButton {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(10px);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.bounce {
|
||||
animation: bounce 0.3s ease-out;
|
||||
}
|
||||
301
src/ui/HorizontalScroll/HorizontalScroll.tsx
Normal file
301
src/ui/HorizontalScroll/HorizontalScroll.tsx
Normal file
@@ -0,0 +1,301 @@
|
||||
import {
|
||||
FC,
|
||||
useRef,
|
||||
useState,
|
||||
useEffect,
|
||||
ReactNode,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
TouchEvent,
|
||||
} from 'react';
|
||||
|
||||
import ArrowSVG from '@assets/svg/arrow';
|
||||
|
||||
import style from './HorizontalScroll.module.css';
|
||||
|
||||
type HorizontalScrollProps = {
|
||||
children: ReactNode[];
|
||||
};
|
||||
|
||||
const HorizontalScroll: FC<HorizontalScrollProps> = ({ children }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const isMouseDown = useRef(false);
|
||||
const startX = useRef(0);
|
||||
const scrollLeft = useRef(0);
|
||||
const [showPrevButton, setShowPrevButton] = useState(false);
|
||||
const [showNextButton, setShowNextButton] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [bounceOffset, setBounceOffset] = useState(0);
|
||||
const [bounceDirection, setBounceDirection] = useState(0);
|
||||
// const [snapPosition, setSnapPosition] = useState(0);
|
||||
const scrollWalkFactor = 1;
|
||||
const bounceDecayRate = 0.9;
|
||||
const bounceThreshold = 0.01;
|
||||
|
||||
const handleMouseDown = (e: MouseEvent<HTMLDivElement>) => {
|
||||
isMouseDown.current = true;
|
||||
startX.current = e.pageX - (containerRef.current?.offsetLeft || 0);
|
||||
scrollLeft.current = containerRef.current?.scrollLeft || 0;
|
||||
setIsDragging(true);
|
||||
containerRef.current?.classList.add(style.grabbing);
|
||||
};
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
if (!isMouseDown.current) return;
|
||||
e.preventDefault();
|
||||
const x = e.pageX - (containerRef.current?.offsetLeft || 0);
|
||||
const walk = (x - startX.current) * scrollWalkFactor;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (containerRef.current && contentRef.current) {
|
||||
const targetScrollLeft = scrollLeft.current - walk;
|
||||
const maxScrollLeft = contentRef.current.scrollWidth - containerRef.current.clientWidth;
|
||||
|
||||
let newBounceOffset = 0;
|
||||
let newBounceDirection = 0;
|
||||
|
||||
if (targetScrollLeft < 0) {
|
||||
newBounceOffset = Math.abs(targetScrollLeft) * 0.1;
|
||||
newBounceDirection = 1;
|
||||
} else if (targetScrollLeft > maxScrollLeft) {
|
||||
newBounceOffset = (targetScrollLeft - maxScrollLeft) * 0.1;
|
||||
newBounceDirection = -1;
|
||||
}
|
||||
|
||||
setBounceOffset(newBounceOffset);
|
||||
setBounceDirection(newBounceDirection);
|
||||
|
||||
// const firstChild = contentRef.current.firstElementChild;
|
||||
// const itemWidth = firstChild instanceof HTMLElement ? firstChild.offsetWidth : 0;
|
||||
// const newSnapPosition = Math.round(targetScrollLeft / itemWidth) * itemWidth;
|
||||
// setSnapPosition(newSnapPosition);
|
||||
|
||||
containerRef.current.scrollTo({
|
||||
left: Math.max(0, Math.min(targetScrollLeft, maxScrollLeft)),
|
||||
behavior: 'auto',
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
if (isDragging) {
|
||||
isMouseDown.current = false;
|
||||
setIsDragging(false);
|
||||
containerRef.current?.classList.remove(style.grabbing);
|
||||
containerRef.current?.classList.add(style.grab);
|
||||
|
||||
// if (containerRef.current && contentRef.current) {
|
||||
// containerRef.current.scrollTo({
|
||||
// left: snapPosition,
|
||||
// behavior: 'smooth',
|
||||
// });
|
||||
// }
|
||||
|
||||
setBounceDirection(0);
|
||||
setBounceOffset(0);
|
||||
}
|
||||
}, [isDragging]);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
if (isMouseDown.current) {
|
||||
isMouseDown.current = false;
|
||||
setIsDragging(false);
|
||||
containerRef.current?.classList.remove(style.grabbing);
|
||||
containerRef.current?.classList.add(style.grab);
|
||||
setBounceDirection(0);
|
||||
setBounceOffset(0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePrevClick = () => {
|
||||
if (containerRef.current && contentRef.current) {
|
||||
const firstChild = contentRef.current.firstElementChild;
|
||||
const itemWidth = firstChild instanceof HTMLElement ? firstChild.offsetWidth : 0;
|
||||
containerRef.current.scrollTo({
|
||||
left: containerRef.current.scrollLeft - itemWidth,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleNextClick = () => {
|
||||
if (containerRef.current && contentRef.current) {
|
||||
const firstChild = contentRef.current.firstElementChild;
|
||||
const itemWidth = firstChild instanceof HTMLElement ? firstChild.offsetWidth : 0;
|
||||
containerRef.current.scrollTo({
|
||||
left: containerRef.current.scrollLeft + itemWidth,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (containerRef.current) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const { scrollWidth, clientWidth, scrollLeft } = containerRef.current;
|
||||
|
||||
if (scrollWidth <= clientWidth) {
|
||||
setShowPrevButton(false);
|
||||
setShowNextButton(false);
|
||||
} else {
|
||||
setShowPrevButton(scrollLeft > 0);
|
||||
setShowNextButton(scrollLeft < scrollWidth - clientWidth - 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchStart = (e: TouchEvent<HTMLDivElement>) => {
|
||||
isMouseDown.current = true;
|
||||
startX.current = e.touches[0].pageX - (containerRef.current?.offsetLeft || 0);
|
||||
scrollLeft.current = containerRef.current?.scrollLeft || 0;
|
||||
setIsDragging(true);
|
||||
containerRef.current?.classList.add(style.grabbing);
|
||||
};
|
||||
|
||||
const handleTouchMove = useCallback(
|
||||
(e: TouchEvent<HTMLDivElement>) => {
|
||||
if (!isMouseDown.current) return;
|
||||
e.preventDefault();
|
||||
const x = e.touches[0].pageX - (containerRef.current?.offsetLeft || 0);
|
||||
const walk = (x - startX.current) * scrollWalkFactor;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (containerRef.current && contentRef.current) {
|
||||
const targetScrollLeft = scrollLeft.current - walk;
|
||||
const maxScrollLeft = contentRef.current.scrollWidth - containerRef.current.clientWidth;
|
||||
|
||||
let newBounceOffset = 0;
|
||||
let newBounceDirection = 0;
|
||||
|
||||
if (targetScrollLeft < 0) {
|
||||
newBounceOffset = Math.abs(targetScrollLeft) * 0.1;
|
||||
newBounceDirection = 1;
|
||||
} else if (targetScrollLeft > maxScrollLeft) {
|
||||
newBounceOffset = (targetScrollLeft - maxScrollLeft) * 0.1;
|
||||
newBounceDirection = -1;
|
||||
}
|
||||
|
||||
setBounceOffset(newBounceOffset);
|
||||
setBounceDirection(newBounceDirection);
|
||||
|
||||
// const firstChild = contentRef.current.firstElementChild;
|
||||
// const itemWidth = firstChild instanceof HTMLElement ? firstChild.offsetWidth : 0;
|
||||
// const newSnapPosition = Math.round(targetScrollLeft / itemWidth) * itemWidth;
|
||||
// setSnapPosition(newSnapPosition);
|
||||
|
||||
containerRef.current.scrollTo({
|
||||
left: Math.max(0, Math.min(targetScrollLeft, maxScrollLeft)),
|
||||
behavior: 'auto',
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleTouchEnd = useCallback(() => {
|
||||
if (isDragging) {
|
||||
isMouseDown.current = false;
|
||||
setIsDragging(false);
|
||||
containerRef.current?.classList.remove(style.grabbing);
|
||||
containerRef.current?.classList.add(style.grab);
|
||||
|
||||
// if (containerRef.current && contentRef.current) {
|
||||
// containerRef.current.scrollTo({
|
||||
// left: snapPosition,
|
||||
// behavior: 'smooth',
|
||||
// });
|
||||
// }
|
||||
|
||||
setBounceDirection(0);
|
||||
setBounceOffset(0);
|
||||
}
|
||||
}, [isDragging]);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
containerRef.current.addEventListener('scroll', handleScroll);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (containerRef.current) {
|
||||
containerRef.current.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
};
|
||||
}, [containerRef.current, contentRef.current]);
|
||||
|
||||
useEffect(() => {
|
||||
let bounceAnimationFrame: number | null = null;
|
||||
|
||||
const bounceAnimation = () => {
|
||||
if (containerRef.current && Math.abs(bounceOffset) > bounceThreshold) {
|
||||
setBounceOffset((prevOffset) => prevOffset * bounceDecayRate);
|
||||
bounceAnimationFrame = requestAnimationFrame(bounceAnimation);
|
||||
} else {
|
||||
setBounceOffset(0);
|
||||
setBounceDirection(0);
|
||||
bounceAnimationFrame = null;
|
||||
}
|
||||
};
|
||||
|
||||
if (bounceOffset !== 0) {
|
||||
bounceAnimationFrame = requestAnimationFrame(bounceAnimation);
|
||||
} else if (bounceAnimationFrame) {
|
||||
cancelAnimationFrame(bounceAnimationFrame);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (bounceAnimationFrame) {
|
||||
cancelAnimationFrame(bounceAnimationFrame);
|
||||
}
|
||||
};
|
||||
}, [bounceOffset, bounceDecayRate, bounceThreshold]);
|
||||
|
||||
return (
|
||||
<div className={style.outerContainer}>
|
||||
{showPrevButton && (
|
||||
<button className={style.prevButton} onClick={handlePrevClick}>
|
||||
<ArrowSVG />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
className={style.horizontalScroll}
|
||||
style={{
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
transform: `translateX(${bounceDirection * bounceOffset}px)`,
|
||||
transition: 'transform 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<div ref={contentRef} className={style.horizontalScrollСontent}>
|
||||
{children.map((child, index) => (
|
||||
<div key={index} className={style.horizontalScrollItem}>
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showNextButton && (
|
||||
<button className={style.nextButton} onClick={handleNextClick}>
|
||||
<ArrowSVG />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HorizontalScroll;
|
||||
1
src/ui/HorizontalScroll/index.ts
Normal file
1
src/ui/HorizontalScroll/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './HorizontalScroll';
|
||||
3
src/ui/Link/Link.module.css
Normal file
3
src/ui/Link/Link.module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.link {
|
||||
text-decoration: 'none',
|
||||
}
|
||||
54
src/ui/Link/Link.tsx
Normal file
54
src/ui/Link/Link.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
CSSProperties, FC, HTMLAttributeAnchorTarget, ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import NextLink from 'next/link';
|
||||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import styles from './Link.module.css';
|
||||
|
||||
type LinkProps = {
|
||||
path: string;
|
||||
query?: { [id: string]: string } | string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
onClick?: ((e: any) => void) | undefined;
|
||||
scroll?: boolean;
|
||||
draggable?: boolean;
|
||||
attributeTitle?: string;
|
||||
shallow?: boolean;
|
||||
style?: CSSProperties;
|
||||
target?: HTMLAttributeAnchorTarget;
|
||||
};
|
||||
|
||||
const Link: FC<LinkProps> = ({
|
||||
path,
|
||||
query,
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
scroll = true,
|
||||
draggable = false,
|
||||
attributeTitle,
|
||||
shallow,
|
||||
style,
|
||||
target,
|
||||
}) => {
|
||||
const currentStyles = clsx(styles.link, className);
|
||||
|
||||
return <NextLink href={{ pathname: path, query }} scroll={scroll} shallow={shallow} >
|
||||
<a
|
||||
className={currentStyles}
|
||||
onClick={onClick}
|
||||
draggable={draggable}
|
||||
title={attributeTitle}
|
||||
style={style}
|
||||
target={target}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
</NextLink>;
|
||||
};
|
||||
|
||||
export default Link;
|
||||
1
src/ui/Link/index.ts
Normal file
1
src/ui/Link/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Link';
|
||||
27
src/ui/Tabbar/TabBar.module.css
Normal file
27
src/ui/Tabbar/TabBar.module.css
Normal file
@@ -0,0 +1,27 @@
|
||||
.tabBar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabBarNav {
|
||||
position: relative;
|
||||
display: flex;
|
||||
z-index: 2;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabContainer {
|
||||
min-height: 100px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: var(--red);
|
||||
color: var(--white);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
57
src/ui/Tabbar/TabBar.tsx
Normal file
57
src/ui/Tabbar/TabBar.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
/* eslint-disable @typescript-eslint/no-shadow */
|
||||
import React from 'react';
|
||||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import style from './TabBar.module.css';
|
||||
import { TabBarItemProps } from './tabbarItem/TabBarItem';
|
||||
import TabBarNav from './TabBarNav';
|
||||
|
||||
interface TabBarProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
activeTab: string;
|
||||
setActiveTab: (label: string) => void;
|
||||
}
|
||||
|
||||
const TabBar: React.FC<TabBarProps> = ({
|
||||
children, className = '', activeTab, setActiveTab,
|
||||
}) => {
|
||||
const getChildrenLabels = (children: React.ReactNode): string[] => React.Children.toArray(children)
|
||||
.filter(React.isValidElement)
|
||||
.map((child) => (child.props as any).label);
|
||||
|
||||
const handleChangeActiveTab = (label: string) => {
|
||||
setActiveTab(label);
|
||||
};
|
||||
|
||||
const renderTabs = () => {
|
||||
const childrenLabels = getChildrenLabels(children);
|
||||
|
||||
return childrenLabels.map((navLabel) => (
|
||||
<TabBarNav
|
||||
key={navLabel}
|
||||
navLabel={navLabel}
|
||||
className={clsx({ [style.active]: activeTab === navLabel })}
|
||||
onChangeActiveTab={handleChangeActiveTab}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const classes = clsx(style.tabBar, className);
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={style.tabBarNav}>{renderTabs()}</div>
|
||||
<div className={style.tabContainer}>
|
||||
{React.Children.map(
|
||||
children,
|
||||
(child) => (React.isValidElement(child) ? React.cloneElement(child, { activeTab } as TabBarItemProps) : null),
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabBar;
|
||||
11
src/ui/Tabbar/TabBarNav.module.css
Normal file
11
src/ui/Tabbar/TabBarNav.module.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.navItem {
|
||||
flex: 1;
|
||||
border: none;
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
31
src/ui/Tabbar/TabBarNav.tsx
Normal file
31
src/ui/Tabbar/TabBarNav.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import style from './TabBarNav.module.css';
|
||||
|
||||
interface TabBarNavProps {
|
||||
navLabel?: string;
|
||||
className?: string;
|
||||
onChangeActiveTab: (label: string) => void;
|
||||
}
|
||||
|
||||
const TabBarNav: React.FC<TabBarNavProps> = ({
|
||||
navLabel = 'Tab',
|
||||
className = '',
|
||||
onChangeActiveTab,
|
||||
}) => {
|
||||
const classes = clsx(className, style.navItem);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={classes}
|
||||
onClick={() => onChangeActiveTab(navLabel)}
|
||||
>
|
||||
{navLabel}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabBarNav;
|
||||
1
src/ui/Tabbar/index.ts
Normal file
1
src/ui/Tabbar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './TabBar';
|
||||
11
src/ui/Tabbar/tabbarItem/TabBarItem.module.css
Normal file
11
src/ui/Tabbar/tabbarItem/TabBarItem.module.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.tabBarItem {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabBarItem.active {
|
||||
height: auto;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
31
src/ui/Tabbar/tabbarItem/TabBarItem.tsx
Normal file
31
src/ui/Tabbar/tabbarItem/TabBarItem.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import style from './TabBarItem.module.css';
|
||||
|
||||
export interface TabBarItemProps {
|
||||
children?: React.ReactNode;
|
||||
label: string;
|
||||
activeTab?: string;
|
||||
}
|
||||
|
||||
const TabBarItem: React.FC<TabBarItemProps> = ({
|
||||
children,
|
||||
label,
|
||||
activeTab,
|
||||
...attrs
|
||||
}) => {
|
||||
const classes = clsx(
|
||||
style.tabBarItem,
|
||||
{ [style.active]: activeTab === label },
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classes} {...attrs}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabBarItem;
|
||||
1
src/ui/Tabbar/tabbarItem/index.ts
Normal file
1
src/ui/Tabbar/tabbarItem/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './TabBarItem';
|
||||
26
src/ui/Ticker/Ticker.module.css
Normal file
26
src/ui/Ticker/Ticker.module.css
Normal file
@@ -0,0 +1,26 @@
|
||||
.ticker {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tickerContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
animation: ticker 1s linear infinite;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
@keyframes ticker {
|
||||
0% {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
left: -122px;
|
||||
}
|
||||
}
|
||||
18
src/ui/Ticker/Ticker.tsx
Normal file
18
src/ui/Ticker/Ticker.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { FC, ReactNode } from 'react';
|
||||
|
||||
import styles from './Ticker.module.css';
|
||||
|
||||
type TickerProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const Ticker: FC<TickerProps> = ({ children }) => (
|
||||
<div className={styles.ticker}>
|
||||
|
||||
<div className={styles.tickerContent}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Ticker;
|
||||
1
src/ui/Ticker/index.ts
Normal file
1
src/ui/Ticker/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Ticker';
|
||||
62
src/ui/modal/Modal.module.css
Normal file
62
src/ui/modal/Modal.module.css
Normal file
@@ -0,0 +1,62 @@
|
||||
.modal {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: var(--blackTransparent);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: all 200ms ease;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.modalActive {
|
||||
transition: all 200ms ease;
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
background-color: var(--white);
|
||||
max-width: 360px;
|
||||
width: 100%;
|
||||
z-index: 300;
|
||||
min-height: 320px;
|
||||
min-width: 320px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modalContentHead {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modalTitle {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modalCloseIcon {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
transition: all 200ms ease;
|
||||
}
|
||||
|
||||
.modalCloseIcon:hover {
|
||||
opacity: 0.6;
|
||||
transition: all 200ms ease;
|
||||
}
|
||||
|
||||
.modalTransparent {
|
||||
background-color: transparent;
|
||||
padding: 5px;
|
||||
}
|
||||
70
src/ui/modal/Modal.tsx
Normal file
70
src/ui/modal/Modal.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
FC, ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import CloseSVG from '@assets/svg/close';
|
||||
|
||||
import style from './Modal.module.css';
|
||||
|
||||
type ModalProps = {
|
||||
title?: string;
|
||||
showCloseButton?: boolean;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
setOpen: (isOpen: boolean) => void;
|
||||
isOpen: boolean;
|
||||
noBody?: boolean;
|
||||
};
|
||||
|
||||
const Modal: FC<ModalProps> = ({
|
||||
title,
|
||||
showCloseButton,
|
||||
children,
|
||||
className,
|
||||
setOpen,
|
||||
isOpen,
|
||||
noBody,
|
||||
}) => {
|
||||
const onClose = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<div className={clsx(
|
||||
style.modal,
|
||||
{
|
||||
[style.modalActive]: isOpen,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
style.modalContent,
|
||||
{
|
||||
[style.modalTransparent]: noBody,
|
||||
},
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{
|
||||
!noBody && <div>
|
||||
<div className={style.modalContentHead}>
|
||||
{
|
||||
title && <span className={style.modalTitle}>{title}</span>
|
||||
}
|
||||
|
||||
{
|
||||
showCloseButton && <span className={style.modalCloseIcon} onClick={onClose}><CloseSVG /></span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
1
src/ui/modal/index.ts
Normal file
1
src/ui/modal/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Modal';
|
||||
11
src/utils/scrollToElement.ts
Normal file
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;
|
||||
Reference in New Issue
Block a user