diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..b9380e5 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..af5986d Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 0000000..3cf9aad --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/default_interface_1.webp b/public/images/default_interface_1.webp new file mode 100644 index 0000000..ce4a61c Binary files /dev/null and b/public/images/default_interface_1.webp differ diff --git a/public/images/default_interface_2.webp b/public/images/default_interface_2.webp new file mode 100644 index 0000000..7a6c9d3 Binary files /dev/null and b/public/images/default_interface_2.webp differ diff --git a/public/images/exclusive_interface_1.webp b/public/images/exclusive_interface_1.webp new file mode 100644 index 0000000..1895eeb Binary files /dev/null and b/public/images/exclusive_interface_1.webp differ diff --git a/public/images/exclusive_interface_2.webp b/public/images/exclusive_interface_2.webp new file mode 100644 index 0000000..3f8089e Binary files /dev/null and b/public/images/exclusive_interface_2.webp differ diff --git a/src/assets/svg/arrow.tsx b/src/assets/svg/arrow.tsx new file mode 100644 index 0000000..94d4196 --- /dev/null +++ b/src/assets/svg/arrow.tsx @@ -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 = ( + { + width = 30, + height = 30, + fill = EColor.white, + className, + }, +) => ( + + + +); + +export default ArrowSVG; diff --git a/src/assets/svg/close.tsx b/src/assets/svg/close.tsx new file mode 100644 index 0000000..da94506 --- /dev/null +++ b/src/assets/svg/close.tsx @@ -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 = ( + { + width = 25, + height = 25, + fill = EColor.black, + className, + }, +) => ( + + + + + +); + +export default CloseSVG; diff --git a/src/assets/svg/info.tsx b/src/assets/svg/info.tsx new file mode 100644 index 0000000..938bd74 --- /dev/null +++ b/src/assets/svg/info.tsx @@ -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 = ( + { + width = 20, + height = 20, + fill = EColor.white, + className, + }, +) => ( + + + + +); + +export default InfoSVG; diff --git a/src/assets/svg/logo.tsx b/src/assets/svg/logo.tsx new file mode 100644 index 0000000..3325154 --- /dev/null +++ b/src/assets/svg/logo.tsx @@ -0,0 +1,49 @@ +import { FC } from 'react'; + +import { SvgProps } from '@interfaces/svg'; + +/* eslint-disable max-len */ +const LogoSVG: FC = ({ + width = 150, + height = 150, + // fill = EColor.purple, + className, +}) => ( + + + + + + + + + + +); + +export default LogoSVG; diff --git a/src/components/SeoHead/SeoHead.tsx b/src/components/SeoHead/SeoHead.tsx new file mode 100644 index 0000000..36363ca --- /dev/null +++ b/src/components/SeoHead/SeoHead.tsx @@ -0,0 +1,51 @@ +import { FC } from 'react'; + +import Head from 'next/head'; + +type Tags = Array ; + +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) => ); + +const SeoHead: FC = ({ + tabTitle, + title, + canonical, + ogUrl, + description, + keywords, + imageSource, + videoTags, + bookTags, +}) => ( + {tabTitle} + + + + + + {canonical && } + {ogUrl && } + {getTags('video', videoTags)} + {getTags('book', bookTags)} + {imageSource && } + {imageSource && } + {keywords && } +); + +export default SeoHead; diff --git a/src/components/SeoHead/index.ts b/src/components/SeoHead/index.ts new file mode 100644 index 0000000..f7508b3 --- /dev/null +++ b/src/components/SeoHead/index.ts @@ -0,0 +1 @@ +export { default } from './SeoHead'; diff --git a/src/constants/app.ts b/src/constants/app.ts new file mode 100644 index 0000000..2731572 --- /dev/null +++ b/src/constants/app.ts @@ -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 = [ + { + 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: [], + }], + }, + ]; diff --git a/src/constants/common.ts b/src/constants/common.ts new file mode 100644 index 0000000..92d6c1d --- /dev/null +++ b/src/constants/common.ts @@ -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'; diff --git a/src/constants/error.ts b/src/constants/error.ts new file mode 100644 index 0000000..724ec03 --- /dev/null +++ b/src/constants/error.ts @@ -0,0 +1,3 @@ +/* eslint-disable max-len */ +export const COMMON_ERROR: string = 'Упс, что-то пошло не так, попробуйте зайти на другую страницу или обновить текущую'; +export const NOT_FOUND_ERROR: string = 'Похоже этой страницы не существует, попробуйте зайти на другую'; diff --git a/src/constants/seo.ts b/src/constants/seo.ts new file mode 100644 index 0000000..0e6bd0a --- /dev/null +++ b/src/constants/seo.ts @@ -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, андроид, анидаб онлайн для андроид, аниме, фандаб, русская озвучка, смотреть бесплатно, телефон, сматрфон, приложение'; diff --git a/src/enums/enums.ts b/src/enums/enums.ts new file mode 100644 index 0000000..2fdea01 --- /dev/null +++ b/src/enums/enums.ts @@ -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)', +} diff --git a/src/interfaces/common.ts b/src/interfaces/common.ts new file mode 100644 index 0000000..b220db8 --- /dev/null +++ b/src/interfaces/common.ts @@ -0,0 +1,15 @@ +export type AppChanges = { + date: string; + version: string; + download: string; + changes: Array; + supportAndroidVersion: string; + isCurrentVersion?: boolean; +}; + +export type AppChangelog = { + appName: string; + descrition: string; + images: Array; + changelogs: Array; +}; diff --git a/src/interfaces/svg.ts b/src/interfaces/svg.ts new file mode 100644 index 0000000..a0a22fb --- /dev/null +++ b/src/interfaces/svg.ts @@ -0,0 +1,6 @@ +export type SvgProps = { + width?: number; + height?: number; + fill?: string; + className?: string; +}; diff --git a/src/layouts/ContentLayout/ContentLayout.module.css b/src/layouts/ContentLayout/ContentLayout.module.css new file mode 100644 index 0000000..1230801 --- /dev/null +++ b/src/layouts/ContentLayout/ContentLayout.module.css @@ -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; + } +} \ No newline at end of file diff --git a/src/layouts/ContentLayout/ContentLayout.tsx b/src/layouts/ContentLayout/ContentLayout.tsx new file mode 100644 index 0000000..589e307 --- /dev/null +++ b/src/layouts/ContentLayout/ContentLayout.tsx @@ -0,0 +1,15 @@ +import { FC, ReactNode } from 'react'; + +import style from './ContentLayout.module.css'; + +type ContentLayoutProps = { + children: ReactNode; +}; + +const ContentLayout: FC = ({ children }) => ( +
+ {children} +
+); + +export default ContentLayout; diff --git a/src/layouts/ContentLayout/index.ts b/src/layouts/ContentLayout/index.ts new file mode 100644 index 0000000..bf3aba8 --- /dev/null +++ b/src/layouts/ContentLayout/index.ts @@ -0,0 +1 @@ +export { default } from './ContentLayout'; diff --git a/src/layouts/RootLayout/RootLayout.tsx b/src/layouts/RootLayout/RootLayout.tsx new file mode 100644 index 0000000..f022217 --- /dev/null +++ b/src/layouts/RootLayout/RootLayout.tsx @@ -0,0 +1,13 @@ +import { FC, ReactNode } from 'react'; + +type MainLayout = { + children: ReactNode; +}; + +const RootLayout: FC = ({ children }) => ( + <> + {children} + +); + +export default RootLayout; diff --git a/src/layouts/RootLayout/index.ts b/src/layouts/RootLayout/index.ts new file mode 100644 index 0000000..bcf4176 --- /dev/null +++ b/src/layouts/RootLayout/index.ts @@ -0,0 +1 @@ +export { default } from './RootLayout'; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx new file mode 100644 index 0000000..f5fc37d --- /dev/null +++ b/src/pages/_app.tsx @@ -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 ( + + + + + + + + + + + + + + ); +} + +export default MyApp; diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx new file mode 100644 index 0000000..cb5537a --- /dev/null +++ b/src/pages/_document.tsx @@ -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 ( + + + + + + + + +
+ + + + + + + + + ); +} diff --git a/src/pages/_error.tsx b/src/pages/_error.tsx new file mode 100644 index 0000000..f3cdbc4 --- /dev/null +++ b/src/pages/_error.tsx @@ -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 ( + + + + ); +}; + +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; diff --git a/src/pages/api/extension/episode/[releaseId].ts b/src/pages/api/extension/episode/[releaseId].ts new file mode 100644 index 0000000..43dacf5 --- /dev/null +++ b/src/pages/api/extension/episode/[releaseId].ts @@ -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(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; +} \ No newline at end of file diff --git a/src/pages/api/extension/profile/[userId].ts b/src/pages/api/extension/profile/[userId].ts new file mode 100644 index 0000000..3b97a85 --- /dev/null +++ b/src/pages/api/extension/profile/[userId].ts @@ -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, + // 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(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' }); + } +} diff --git a/src/pages/api/extension/release/[id].ts b/src/pages/api/extension/release/[id].ts new file mode 100644 index 0000000..fd528a7 --- /dev/null +++ b/src/pages/api/extension/release/[id].ts @@ -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(anixartAPI); + const shikiAnime = await axios.get( + `${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' }); + } +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx new file mode 100644 index 0000000..845c517 --- /dev/null +++ b/src/pages/index.tsx @@ -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(false); + const [activeTab, setActiveTab] = useState(APP_VERSIONS[0].appName); + const [isOpenPreviewImageModal, setPreviewImageModal] = useState(false); + const [changelogItems, setChangelogItems] = useState(null); + const [previewImage, setPreviewImage] = useState(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) => 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) => ( + + {`${appName} ${version}`} + + + + + {isCurrentVersion ? 'Активный' : 'Устаревший'} + + + + + {date} + + + + + + ); + + const getTable = useCallback(() => ( + + { + APP_VERSIONS.map(({ + appName, descrition, images, changelogs, + }) => ( +
+ + { + getImages(images) + } + +
+ +
+

{descrition}

+
+ +
+ + + + + + + + + + + { + changelogs.map((change) => ( + + {getTableRow({ appName, ...change })} + + )) + } +
ВерсияСтатусДатаИнформация
+
+
)) + } +
+ ), [activeTab]); + + return ( + + + + +
+
    + {changelogItems && changelogItems.changes.length > 0 && changelogItems.changes.map((change) =>
  • + {change} +
  • )} +
+ +
+ + {`${changelogItems?.appName} ${changelogItems?.version}`} + + + + {`Android ${changelogItems?.supportAndroidVersion}`} + +
+
+ +
+ + + Изображение приложения + + +
+
+ + +

ANIXART / MODE

+ + +
+
+ +
+

Версии приложения

+ +
+ {getTable()} +
+
+
+ ); +}; + +export default Main; diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..c434ecc --- /dev/null +++ b/src/styles/global.css @@ -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; +} \ No newline at end of file diff --git a/src/styles/normalize.css b/src/styles/normalize.css new file mode 100644 index 0000000..5971931 --- /dev/null +++ b/src/styles/normalize.css @@ -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; +} \ No newline at end of file diff --git a/src/styles/pages/homePage.module.css b/src/styles/pages/homePage.module.css new file mode 100644 index 0000000..b3320bb --- /dev/null +++ b/src/styles/pages/homePage.module.css @@ -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; + } +} diff --git a/src/styles/variables.css b/src/styles/variables.css new file mode 100644 index 0000000..3f7bd1f --- /dev/null +++ b/src/styles/variables.css @@ -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; +} \ No newline at end of file diff --git a/src/ui/Error/Error.module.css b/src/ui/Error/Error.module.css new file mode 100644 index 0000000..840158e --- /dev/null +++ b/src/ui/Error/Error.module.css @@ -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'; + } +} \ No newline at end of file diff --git a/src/ui/Error/Error.tsx b/src/ui/Error/Error.tsx new file mode 100644 index 0000000..f960e25 --- /dev/null +++ b/src/ui/Error/Error.tsx @@ -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 = ({ statusCode, errorText, goHome }) => ( +
+ {errorText && ( +

+ {errorText} +

+ )} + + { statusCode &&

+ {statusCode && `Ошибка ${statusCode}`} +

+ } + + { + goHome ? Вернуться на главную? : <> + } +
+); + +export default Error; diff --git a/src/ui/Error/index.ts b/src/ui/Error/index.ts new file mode 100644 index 0000000..3edca40 --- /dev/null +++ b/src/ui/Error/index.ts @@ -0,0 +1 @@ +export { default } from './Error'; diff --git a/src/ui/HorizontalScroll/HorizontalScroll.module.css b/src/ui/HorizontalScroll/HorizontalScroll.module.css new file mode 100644 index 0000000..9f6cc4c --- /dev/null +++ b/src/ui/HorizontalScroll/HorizontalScroll.module.css @@ -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; +} \ No newline at end of file diff --git a/src/ui/HorizontalScroll/HorizontalScroll.tsx b/src/ui/HorizontalScroll/HorizontalScroll.tsx new file mode 100644 index 0000000..a639b4b --- /dev/null +++ b/src/ui/HorizontalScroll/HorizontalScroll.tsx @@ -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 = ({ children }) => { + const containerRef = useRef(null); + const contentRef = useRef(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) => { + 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) => { + 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) => { + 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) => { + 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 ( +
+ {showPrevButton && ( + + )} + +
+
+ {children.map((child, index) => ( +
+ {child} +
+ ))} +
+
+ + {showNextButton && ( + + )} +
+ ); +}; + +export default HorizontalScroll; diff --git a/src/ui/HorizontalScroll/index.ts b/src/ui/HorizontalScroll/index.ts new file mode 100644 index 0000000..bd8ded6 --- /dev/null +++ b/src/ui/HorizontalScroll/index.ts @@ -0,0 +1 @@ +export { default } from './HorizontalScroll'; diff --git a/src/ui/Link/Link.module.css b/src/ui/Link/Link.module.css new file mode 100644 index 0000000..2a12086 --- /dev/null +++ b/src/ui/Link/Link.module.css @@ -0,0 +1,3 @@ +.link { + text-decoration: 'none', +} \ No newline at end of file diff --git a/src/ui/Link/Link.tsx b/src/ui/Link/Link.tsx new file mode 100644 index 0000000..efbf37c --- /dev/null +++ b/src/ui/Link/Link.tsx @@ -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 = ({ + path, + query, + children, + className, + onClick, + scroll = true, + draggable = false, + attributeTitle, + shallow, + style, + target, +}) => { + const currentStyles = clsx(styles.link, className); + + return + + {children} + + ; +}; + +export default Link; diff --git a/src/ui/Link/index.ts b/src/ui/Link/index.ts new file mode 100644 index 0000000..2410460 --- /dev/null +++ b/src/ui/Link/index.ts @@ -0,0 +1 @@ +export { default } from './Link'; diff --git a/src/ui/Tabbar/TabBar.module.css b/src/ui/Tabbar/TabBar.module.css new file mode 100644 index 0000000..1b89299 --- /dev/null +++ b/src/ui/Tabbar/TabBar.module.css @@ -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; +} \ No newline at end of file diff --git a/src/ui/Tabbar/TabBar.tsx b/src/ui/Tabbar/TabBar.tsx new file mode 100644 index 0000000..d13aa5f --- /dev/null +++ b/src/ui/Tabbar/TabBar.tsx @@ -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 = ({ + 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) => ( + + )); + }; + + const classes = clsx(style.tabBar, className); + + return ( +
+
{renderTabs()}
+
+ {React.Children.map( + children, + (child) => (React.isValidElement(child) ? React.cloneElement(child, { activeTab } as TabBarItemProps) : null), + ) + } +
+
+ ); +}; + +export default TabBar; diff --git a/src/ui/Tabbar/TabBarNav.module.css b/src/ui/Tabbar/TabBarNav.module.css new file mode 100644 index 0000000..60efbc2 --- /dev/null +++ b/src/ui/Tabbar/TabBarNav.module.css @@ -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; +} \ No newline at end of file diff --git a/src/ui/Tabbar/TabBarNav.tsx b/src/ui/Tabbar/TabBarNav.tsx new file mode 100644 index 0000000..897b795 --- /dev/null +++ b/src/ui/Tabbar/TabBarNav.tsx @@ -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 = ({ + navLabel = 'Tab', + className = '', + onChangeActiveTab, +}) => { + const classes = clsx(className, style.navItem); + + return ( + + ); +}; + +export default TabBarNav; diff --git a/src/ui/Tabbar/index.ts b/src/ui/Tabbar/index.ts new file mode 100644 index 0000000..3bfa291 --- /dev/null +++ b/src/ui/Tabbar/index.ts @@ -0,0 +1 @@ +export { default } from './TabBar'; diff --git a/src/ui/Tabbar/tabbarItem/TabBarItem.module.css b/src/ui/Tabbar/tabbarItem/TabBarItem.module.css new file mode 100644 index 0000000..69c867b --- /dev/null +++ b/src/ui/Tabbar/tabbarItem/TabBarItem.module.css @@ -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; +} \ No newline at end of file diff --git a/src/ui/Tabbar/tabbarItem/TabBarItem.tsx b/src/ui/Tabbar/tabbarItem/TabBarItem.tsx new file mode 100644 index 0000000..6dcadc6 --- /dev/null +++ b/src/ui/Tabbar/tabbarItem/TabBarItem.tsx @@ -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 = ({ + children, + label, + activeTab, + ...attrs +}) => { + const classes = clsx( + style.tabBarItem, + { [style.active]: activeTab === label }, + ); + + return ( +
+ {children} +
+ ); +}; + +export default TabBarItem; diff --git a/src/ui/Tabbar/tabbarItem/index.ts b/src/ui/Tabbar/tabbarItem/index.ts new file mode 100644 index 0000000..de5cbf3 --- /dev/null +++ b/src/ui/Tabbar/tabbarItem/index.ts @@ -0,0 +1 @@ +export { default } from './TabBarItem'; diff --git a/src/ui/Ticker/Ticker.module.css b/src/ui/Ticker/Ticker.module.css new file mode 100644 index 0000000..05a1a70 --- /dev/null +++ b/src/ui/Ticker/Ticker.module.css @@ -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; + } +} \ No newline at end of file diff --git a/src/ui/Ticker/Ticker.tsx b/src/ui/Ticker/Ticker.tsx new file mode 100644 index 0000000..700ed2c --- /dev/null +++ b/src/ui/Ticker/Ticker.tsx @@ -0,0 +1,18 @@ +import { FC, ReactNode } from 'react'; + +import styles from './Ticker.module.css'; + +type TickerProps = { + children: ReactNode; +}; + +const Ticker: FC = ({ children }) => ( +
+ +
+ {children} +
+
+); + +export default Ticker; diff --git a/src/ui/Ticker/index.ts b/src/ui/Ticker/index.ts new file mode 100644 index 0000000..81f57fa --- /dev/null +++ b/src/ui/Ticker/index.ts @@ -0,0 +1 @@ +export { default } from './Ticker'; diff --git a/src/ui/modal/Modal.module.css b/src/ui/modal/Modal.module.css new file mode 100644 index 0000000..9216b54 --- /dev/null +++ b/src/ui/modal/Modal.module.css @@ -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; +} \ No newline at end of file diff --git a/src/ui/modal/Modal.tsx b/src/ui/modal/Modal.tsx new file mode 100644 index 0000000..691b34d --- /dev/null +++ b/src/ui/modal/Modal.tsx @@ -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 = ({ + title, + showCloseButton, + children, + className, + setOpen, + isOpen, + noBody, +}) => { + const onClose = () => setOpen(false); + + return ( +
+
e.stopPropagation()} + > + { + !noBody &&
+
+ { + title && {title} + } + + { + showCloseButton && + } +
+
+ } + {children} +
+
+ ); +}; + +export default Modal; diff --git a/src/ui/modal/index.ts b/src/ui/modal/index.ts new file mode 100644 index 0000000..0690fec --- /dev/null +++ b/src/ui/modal/index.ts @@ -0,0 +1 @@ +export { default } from './Modal'; diff --git a/src/utils/scrollToElement.ts b/src/utils/scrollToElement.ts new file mode 100644 index 0000000..454d273 --- /dev/null +++ b/src/utils/scrollToElement.ts @@ -0,0 +1,11 @@ +const scrollToElement = (elementId: string) => { + const element = document.getElementById(elementId); + if (element) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + } +}; + +export default scrollToElement;