From 918667304341610273a652a040369a3aedaa8524 Mon Sep 17 00:00:00 2001 From: Lanskikh Date: Wed, 10 Jul 2024 14:54:16 +0500 Subject: [PATCH] fixes, todo contacts form and animations --- .env | 1 + package.json | 5 + src/api/contactsFormInstance.ts | 7 + src/components/Layout/Navbar.tsx | 20 +- src/components/Main/Availables.tsx | 44 +-- src/components/Main/ContactsForm.tsx | 203 ++++++++++++ src/components/Main/Efficiency.tsx | 48 ++- src/components/Main/Events.tsx | 6 +- .../Main/ProductTabs/ForTeachingTab.tsx | 68 ++++ .../Main/ProductTabs/SimulatorsTab.tsx | 150 +++++++++ .../Main/ProductTabs/TrainingsTab.tsx | 72 +++++ src/components/Main/Products.tsx | 296 +----------------- src/components/Main/Teaching.tsx | 2 +- src/components/Main/Trainings.tsx | 27 +- src/components/icons/AstreskIcon.tsx | 16 + src/components/icons/CheckGradientIcon.tsx | 37 +++ src/components/icons/Close2Icon.tsx | 20 ++ src/components/icons/LoaderIcon.tsx | 35 +++ src/components/icons/SendIcon.tsx | 24 ++ src/index.css | 4 +- src/ui/AppearanceText.tsx | 46 ++- src/ui/Button.tsx | 40 +++ yarn.lock | 60 +++- 23 files changed, 851 insertions(+), 380 deletions(-) create mode 100644 .env create mode 100644 src/api/contactsFormInstance.ts create mode 100644 src/components/Main/ContactsForm.tsx create mode 100644 src/components/Main/ProductTabs/ForTeachingTab.tsx create mode 100644 src/components/Main/ProductTabs/SimulatorsTab.tsx create mode 100644 src/components/Main/ProductTabs/TrainingsTab.tsx create mode 100644 src/components/icons/AstreskIcon.tsx create mode 100644 src/components/icons/CheckGradientIcon.tsx create mode 100644 src/components/icons/Close2Icon.tsx create mode 100644 src/components/icons/LoaderIcon.tsx create mode 100644 src/components/icons/SendIcon.tsx create mode 100644 src/ui/Button.tsx diff --git a/.env b/.env new file mode 100644 index 0000000..ee00db5 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +PUBLIC_API_URL=https://graff.estate/api \ No newline at end of file diff --git a/package.json b/package.json index 84b1eef..480dd3c 100644 --- a/package.json +++ b/package.json @@ -11,16 +11,21 @@ }, "dependencies": { "framer-motion": "^11.2.14", + "ky": "^1.4.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-input-mask": "^2.0.4", "react-router-dom": "^6.23.1", "react-router-hash-link": "^2.4.3", "react-swipeable": "^7.0.1", + "usehooks-ts": "^3.1.0", "zustand": "^4.5.4" }, "devDependencies": { + "@types/node": "^20.14.10", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@types/react-input-mask": "^3.0.5", "@types/react-router-hash-link": "^2.4.9", "@typescript-eslint/eslint-plugin": "^7.13.1", "@typescript-eslint/parser": "^7.13.1", diff --git a/src/api/contactsFormInstance.ts b/src/api/contactsFormInstance.ts new file mode 100644 index 0000000..4747829 --- /dev/null +++ b/src/api/contactsFormInstance.ts @@ -0,0 +1,7 @@ +import ky from 'ky'; + +const api = ky.extend({ + prefixUrl: process.env.PUBLIC_API_URL, +}); + +export default api; diff --git a/src/components/Layout/Navbar.tsx b/src/components/Layout/Navbar.tsx index b338c32..833ce58 100644 --- a/src/components/Layout/Navbar.tsx +++ b/src/components/Layout/Navbar.tsx @@ -1,13 +1,21 @@ -import { PropsWithChildren, useState } from 'react'; +import { PropsWithChildren, useRef, useState } from 'react'; import { NavLink } from '../../ui/NavLink'; import { Link } from 'react-router-dom'; import { Lang, useLang } from '../../store/language'; import { HashLink } from 'react-router-hash-link'; +import { useOnClickOutside } from 'usehooks-ts'; export function Navbar() { const [menuOpen, setMenuOpen] = useState(false); const { value: lang } = useLang(); + const menuRef = useRef(null); + const menuBtnRef = useRef(null); + useOnClickOutside( + [menuRef, menuBtnRef], + () => setMenuOpen(false), + ); + return ( <> {menuOpen && (
setMenuOpen(false)} className={ 'absolute z-50 w-full min-[1350px]:hidden tablet:max-[1350px]:max-w-[340px] right-0 tablet:border-l border-b border-[#3D425C]' + @@ -108,8 +118,14 @@ function ChooseLang({ function LangToggler({ lang }: { lang: Lang }) { const [open, setOpen] = useState(false); + const langTogglerRef = useRef(null); + useOnClickOutside(langTogglerRef, () => setOpen(false)); + return ( -
+
diff --git a/src/components/Main/ContactsForm.tsx b/src/components/Main/ContactsForm.tsx new file mode 100644 index 0000000..0f96d8a --- /dev/null +++ b/src/components/Main/ContactsForm.tsx @@ -0,0 +1,203 @@ +import { ChangeEvent, FormEvent, useState } from 'react'; +import api from '../../api/contactsFormInstance'; +import { Close2Icon } from '../icons/Close2Icon'; +import { AsteriskIcon } from '../icons/AstreskIcon'; +import InputMask from 'react-input-mask'; +import Button from '../../ui/Button'; +import LoaderIcon from '../icons/LoaderIcon'; +import SendIcon from '../icons/SendIcon'; +import CheckGradientIcon from '../icons/CheckGradientIcon'; + +function ContactsForm() { + const [name, setName] = useState(''); + const [phone, setPhone] = useState(''); + const [email, setEmail] = useState(''); + const [description, setDescription] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isSend, setIsSend] = useState(false); + + function handleSubmit(e: FormEvent) { + e.preventDefault(); + + sendMail(); + } + + async function sendMail() { + setIsLoading(true); + + try { + await api + .post('mail', { + json: { + fullname: name, + phone, + email, + request: description, + }, + }) + .json(); + + setIsSend(true); + setIsLoading(false); + } catch (error) { + setIsLoading(false); + if (error instanceof Error) { + alert(error.message); + } + } + } + + return ( + <> + {!isSend ? ( +
+
+

+ Свяжитесь с нами +

+ +
+
+
+
+ setName(e.target.value)} + className="feedback-field bg-transparent border border-[#3D425C] rounded-none sm:pt-12 sm:pb-4 sm:px-4 pt-8 pb-3 px-3 outline-none outline-1 -outline-offset-1 focus:outline-[#D375FF] transition-all w-full" + /> +

+ Имя + +

+
+ +
+ ) => + setPhone(e.target.value) + } + className={[ + 'feedback-field bg-transparent border rounded-none border-t-0 border-[#3D425C] sm:pt-12 sm:pb-4 sm:px-4 pt-8 pb-3 px-3 outline-none outline-1 -outline-offset-1 focus:outline-[#D375FF] transition-all w-full', + ].join(' ')} + /> +

+ Телефон + +

+
+ +
+ setEmail(e.target.value)} + className="feedback-field bg-transparent border rounded-none border-t-0 border-[#3D425C] sm:pt-12 sm:pb-4 sm:px-4 pt-8 pb-3 px-3 outline-none outline-1 -outline-offset-1 focus:outline-[#D375FF] transition-all w-full" + /> +

+ Email + +

+
+ +
+ +
+ +
+
+ Нажимая кнопку отправить, вы принимаете{' '} + + условия использования + {' '} + и{' '} + + политику конфиденциальности + +
+
+
+
+
+ +
+

+

+ Звездочкой отмечены обязательные +
+ для заполнения поля +

+
+
+
+ + +
+
+ ) : ( +
+
+

+ Заявка отправлена + +

+ +
+ +
+

+ Спасибо за подачу заявки! +

+ +

+ Мы ценим ваш интерес к нашей компании и в ближайшее время свяжемся + с вами для уточнения деталей проекта. +

+
+
+ )} + + ); +} + +export default ContactsForm; diff --git a/src/components/Main/Efficiency.tsx b/src/components/Main/Efficiency.tsx index 83d4036..821b4c9 100644 --- a/src/components/Main/Efficiency.tsx +++ b/src/components/Main/Efficiency.tsx @@ -1,20 +1,5 @@ import { MiniTitle } from '../../ui/MiniTitle'; -import AppearanceText from '../../ui/AppearanceText'; - -const splits = [ - 'В тренажере человек ', - 'принимает решения ', - 'так же, ', - 'как в реальном мире, ', - 'активируя ', - 'те же нейронные ', - 'цепочки в мозгу. ', - 'Это позволяет ', - 'добиться ', - 'реальной ', - 'производительности ', - 'в работе. ', -]; +import { AppearanceText } from '../../ui/AppearanceText'; export function Effeciency() { return ( @@ -39,20 +24,23 @@ export function Effeciency() { title={'готовность к опасным ситуациямние выше на'} /> -

- {splits.map((text, index) => ( - 3 ? 60 : 100} - duration={3 + index} - /> - ))} -

+ diff --git a/src/components/Main/Events.tsx b/src/components/Main/Events.tsx index 6edd484..a0c5e2f 100644 --- a/src/components/Main/Events.tsx +++ b/src/components/Main/Events.tsx @@ -11,7 +11,7 @@ export function Events() {
-
+
Макет кабины машиниста «Иволга» на выставке ВДНХ @@ -31,11 +31,11 @@ export function Events() {
-
+
Победа на BuildUP 2023 в номинации IT
-
+
Транспортное и специальное тренажеростроение — 2023 diff --git a/src/components/Main/ProductTabs/ForTeachingTab.tsx b/src/components/Main/ProductTabs/ForTeachingTab.tsx new file mode 100644 index 0000000..3705b3e --- /dev/null +++ b/src/components/Main/ProductTabs/ForTeachingTab.tsx @@ -0,0 +1,68 @@ +export function ForTeachingTab() { + return ( +
+
+

+ Интерактивные тренажеры для учебных заведений +

+ +
+
+
+ +
+
+ +

+ cоздание обучающих VR систем +

+
+

+ Проведение виртуальных практических работ, создание учебных + мастерских и стендов +

+
+
+
+ +
+
+ +

+ cоздание VR лабораторий +

+
+

+ Тренажер для проведения лабораториных работ позволит избежать + поломки оборудования, а также экономить на расходных средствах +

+
+
+
+

+ Оснащение учебных классов и центров всем необходимым для современного + обучения под «ключ» +

+
+ ); +} diff --git a/src/components/Main/ProductTabs/SimulatorsTab.tsx b/src/components/Main/ProductTabs/SimulatorsTab.tsx new file mode 100644 index 0000000..b031f8c --- /dev/null +++ b/src/components/Main/ProductTabs/SimulatorsTab.tsx @@ -0,0 +1,150 @@ +import { useEffect, useReducer, useState } from 'react'; +import { useWindowWidth } from '../../../hooks/useWindowWidth'; +import { useSwipeable } from 'react-swipeable'; + +export function SimulatorsTab() { + const width = useWindowWidth(); + const [slide, setSlide] = useState(0); + const [sliderOffset, setSliderOffset] = useState(-width + 80); + + const [order, dispatch] = useReducer( + (state: string[], action: string) => { + switch (action) { + case 'prev': + setSliderOffset(-(width - 80) * 2); + return [state[state.length - 3], ...state.slice(0, -1)]; + case 'next': + setSliderOffset(prev => prev + width - 80); + return [...state.slice(1), state[2]]; + default: + return state; + } + }, + [ + 'src/assets/rzhd2.png', + 'src/assets/train.png', + 'src/assets/dispatcher.png', + 'src/assets/winda.png', + 'src/assets/rzhd.png', + 'src/assets/rzhd2.png', + 'src/assets/train.png', + ], + ); + + useEffect(() => { + setSliderOffset(-width + 80); + }, [order, slide, width]); + + const handlers = useSwipeable({ + onSwipedLeft: () => { + dispatch('next'); + setSlide(prev => (prev === order.length - 3 ? 0 : prev + 1)); + }, + onSwipedRight: () => { + dispatch('prev'); + setSlide(prev => (prev === 0 ? order.length - 3 : prev - 1)); + }, + trackMouse: true, + preventScrollOnSwipe: true, + touchEventOptions: { passive: true }, + }); + + return ( +
+
+
+

+ Интерактивные симуляторы управления техникой +

+
    + + + + + + +
+
+

+ В основу симуляторов заложена математическая модель, полностью + соответствующая работе настоящего оборудования +

+

+ модель позволяет производить расчеты характеристик работы, + отслеживать безопасность работы устройств и симулировать + внештатные ситуации. +

+
+
+
+
+
+ {width < 640 ? ( + order.map((src, index) => ( + + )) + ) : ( + <> + + + + + + + )} +
+
+

+ модель позволяет производить расчеты характеристик работы, + отслеживать безопасность работы устройств и симулировать внештатные + ситуации. +

+
+
+
+ ); +} + +function SimulatorsItem({ text }: { text: string }) { + return ( +
  • + {text} +
  • + ); +} diff --git a/src/components/Main/ProductTabs/TrainingsTab.tsx b/src/components/Main/ProductTabs/TrainingsTab.tsx new file mode 100644 index 0000000..9a7ae6a --- /dev/null +++ b/src/components/Main/ProductTabs/TrainingsTab.tsx @@ -0,0 +1,72 @@ +export function TrainingsTab() { + return ( +
    +
    +
    +
    +

    + Промышленные тренажеры виртуальной реальности +

    +

    + Может быть еще какой-нибудь небольшой текст, а то мне не хватает + для балланса. Ну если не будет, то как-нибудь переживем +

    + +
    +
    +
    + + + +
    +
    +
    + ); +} + +function TeachingItem({ + title, + text, + iconType, +}: { + title: string; + text: string; + iconType: 'danger' | 'service' | 'safety'; +}) { + return ( +
    + +
    +

    + + {title} +

    +

    {text}

    +
    +
    + ); +} diff --git a/src/components/Main/Products.tsx b/src/components/Main/Products.tsx index 8b03a18..1f701b9 100644 --- a/src/components/Main/Products.tsx +++ b/src/components/Main/Products.tsx @@ -1,8 +1,9 @@ -import { useEffect, useReducer, useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import { MiniTitle } from '../../ui/MiniTitle'; import { Title } from '../../ui/Title'; -import { useWindowWidth } from '../../hooks/useWindowWidth'; -import { useSwipeable } from 'react-swipeable'; +import { ForTeachingTab } from './ProductTabs/ForTeachingTab'; +import { SimulatorsTab } from './ProductTabs/SimulatorsTab'; +import { TrainingsTab } from './ProductTabs/TrainingsTab'; export function Products() { const [curTab, setCurTab] = useState(0); @@ -97,292 +98,3 @@ function TabButton({ ); } - -function TeachingItem({ - title, - text, - iconType, -}: { - title: string; - text: string; - iconType: 'danger' | 'service' | 'safety'; -}) { - return ( -
    - -
    -

    - - {title} -

    -

    {text}

    -
    -
    - ); -} - -function TrainingsTab() { - return ( -
    -
    -
    -
    -

    - Промышленные тренажеры виртуальной реальности -

    -

    - Может быть еще какой-нибудь небольшой текст, а то мне не хватает - для балланса. Ну если не будет, то как-нибудь переживем -

    - -
    -
    -
    - - - -
    -
    -
    - ); -} - -function SimulatorsTab() { - const width = useWindowWidth(); - const [slide, setSlide] = useState(0); - const [sliderOffset, setSliderOffset] = useState(-width + 80); - - const [order, dispatch] = useReducer( - (state: string[], action: string) => { - switch (action) { - case 'prev': - setSliderOffset(-(width - 80) * 2); - return [state[state.length - 3], ...state.slice(0, -1)]; - case 'next': - setSliderOffset(prev => prev + width - 80); - return [...state.slice(1), state[2]]; - default: - return state; - } - }, - [ - 'src/assets/rzhd2.png', - 'src/assets/train.png', - 'src/assets/dispatcher.png', - 'src/assets/winda.png', - 'src/assets/rzhd.png', - 'src/assets/rzhd2.png', - 'src/assets/train.png', - ], - ); - - useEffect(() => { - setSliderOffset(-width + 80); - }, [order, slide, width]); - - const handlers = useSwipeable({ - onSwipedLeft: () => { - dispatch('next'); - setSlide(prev => (prev === order.length - 3 ? 0 : prev + 1)); - }, - onSwipedRight: () => { - dispatch('prev'); - setSlide(prev => (prev === 0 ? order.length - 3 : prev - 1)); - }, - trackMouse: true, - preventScrollOnSwipe: true, - touchEventOptions: { passive: true }, - }); - - return ( -
    -
    -
    -

    - Интерактивные симуляторы управления техникой -

    -
      - - - - - - -
    -
    -

    - В основу симуляторов заложена математическая модель, полностью - соответствующая работе настоящего оборудования -

    -

    - модель позволяет производить расчеты характеристик работы, - отслеживать безопасность работы устройств и симулировать - внештатные ситуации. -

    -
    -
    -
    -
    -
    - {width < 640 ? ( - order.map((src, index) => ( - - )) - ) : ( - <> - - - - - - - )} -
    -
    -

    - модель позволяет производить расчеты характеристик работы, - отслеживать безопасность работы устройств и симулировать внештатные - ситуации. -

    -
    -
    -
    - ); -} - -function SimulatorsItem({ text }: { text: string }) { - return ( -
  • - {text} -
  • - ); -} - -function ForTeachingTab() { - return ( -
    -
    -

    - Интерактивные тренажеры для учебных заведений -

    - -
    -
    -
    - -
    -
    - -

    - cоздание обучающих VR систем -

    -
    -

    - Проведение виртуальных практических работ, создание учебных - мастерских и стендов -

    -
    -
    -
    - -
    -
    - -

    - cоздание VR лабораторий -

    -
    -

    - Тренажер для проведения лабораториных работ позволит избежать - поломки оборудования, а также экономить на расходных средствах -

    -
    -
    -
    -

    - Оснащение учебных классов и центров всем необходимым для современного - обучения под «ключ» -

    -
    - ); -} diff --git a/src/components/Main/Teaching.tsx b/src/components/Main/Teaching.tsx index e428155..21edbdd 100644 --- a/src/components/Main/Teaching.tsx +++ b/src/components/Main/Teaching.tsx @@ -4,7 +4,7 @@ import { Title } from '../../ui/Title'; export function Teaching() { return (
    - + <Title className="mobile:max-desktop:hidden mb-8 desktop:sticky top-14 h-fit"> <span className="bg-text-gradient bg-gradient-to-r from-[#798FFF] to-[#D375FF]" style={{ diff --git a/src/components/Main/Trainings.tsx b/src/components/Main/Trainings.tsx index 0b54437..1c9e2ed 100644 --- a/src/components/Main/Trainings.tsx +++ b/src/components/Main/Trainings.tsx @@ -1,4 +1,6 @@ +import { useHover } from 'usehooks-ts'; import { Title } from '../../ui/Title'; +import { useRef } from 'react'; export function Trainings() { return ( @@ -20,7 +22,7 @@ export function Trainings() { </span> , основываясь на специфике вашего тренировочного процесса -
    +
    (null); + const hovered = useHover(ref); + return ( -
    -
    +
    +

    {title}

    @@ -68,7 +76,18 @@ function TrainingsFeature({
    - + {hovered && ( + + )} +

    {order}

    + + + ); +} diff --git a/src/components/icons/CheckGradientIcon.tsx b/src/components/icons/CheckGradientIcon.tsx new file mode 100644 index 0000000..55681d4 --- /dev/null +++ b/src/components/icons/CheckGradientIcon.tsx @@ -0,0 +1,37 @@ +function CheckGradientIcon({ className = '' }: { className?: string }) { + return ( + + + + + + + + + + + + ); +} + +export default CheckGradientIcon; diff --git a/src/components/icons/Close2Icon.tsx b/src/components/icons/Close2Icon.tsx new file mode 100644 index 0000000..887dee8 --- /dev/null +++ b/src/components/icons/Close2Icon.tsx @@ -0,0 +1,20 @@ +export function Close2Icon({ className = '' }: { className?: string }) { + return ( + + + + ); +} diff --git a/src/components/icons/LoaderIcon.tsx b/src/components/icons/LoaderIcon.tsx new file mode 100644 index 0000000..974d4f1 --- /dev/null +++ b/src/components/icons/LoaderIcon.tsx @@ -0,0 +1,35 @@ +function LoaderIcon({ className = '' }: { className?: string }) { + return ( + + + + + + + + + + + + ); +} + +export default LoaderIcon; diff --git a/src/components/icons/SendIcon.tsx b/src/components/icons/SendIcon.tsx new file mode 100644 index 0000000..10dae78 --- /dev/null +++ b/src/components/icons/SendIcon.tsx @@ -0,0 +1,24 @@ +function SendIcon({ className = '' }: { className?: string }) { + return ( + + + + + + ); +} + +export default SendIcon; diff --git a/src/index.css b/src/index.css index c30eeac..a72d535 100644 --- a/src/index.css +++ b/src/index.css @@ -10,10 +10,10 @@ body { @layer components { .h1 { - @apply -tracking-[.02em] leading-[90%] desktop-figma:text-[clamp(96px,6vw,112px)] desktop:max-desktop-figma:text-[clamp(76px,76px+(100vw-1024px)/576*20,96px)] tablet-figma:max-desktop:text-[clamp(64px,64px+(100vw-768px)/256*16,80px)] tablet:max-tablet-figma:text-[clamp(56px,56px+(100vw-640px)/128*8,64px)] mobile:max-tablet:text-[clamp(40px,40px+(100vw-360px)/280*4,44px)]; + @apply -tracking-[.02em] leading-[90%] desktop-figma:text-[clamp(96px,6vw,128px)] desktop:max-desktop-figma:text-[clamp(76px,76px+(100vw-1024px)/576*20,96px)] tablet-figma:max-desktop:text-[clamp(64px,64px+(100vw-768px)/256*16,80px)] tablet:max-tablet-figma:text-[clamp(56px,56px+(100vw-640px)/128*8,64px)] mobile:max-tablet:text-[clamp(40px,40px+(100vw-360px)/280*4,44px)]; } .h2 { - @apply -tracking-[.02em] desktop:leading-[90%] mobile:max-desktop:leading-[100%] desktop-figma:text-[clamp(64px,4vw,72px)] desktop:max-desktop-figma:text-[clamp(56px,56px+(100vw-1024px)/576*8,64px)] tablet-figma:max-desktop:text-[clamp(40px,40px+(100vw-768px)/256*12,52px)] tablet:max-tablet-figma:text-[clamp(32px,32px+(1000vw-640px)/128*8,40px)] mobile:max-tablet:text-[clamp(28px,28px+(100vw-360px)/280*4,32px)]; + @apply -tracking-[.02em] desktop:leading-[90%] mobile:max-desktop:leading-[100%] desktop-figma:text-[clamp(64px,4vw,80px)] desktop:max-desktop-figma:text-[clamp(56px,56px+(100vw-1024px)/576*8,64px)] tablet-figma:max-desktop:text-[clamp(40px,40px+(100vw-768px)/256*12,52px)] tablet:max-tablet-figma:text-[clamp(32px,32px+(1000vw-640px)/128*8,40px)] mobile:max-tablet:text-[clamp(28px,28px+(100vw-360px)/280*4,32px)]; } .h3 { diff --git a/src/ui/AppearanceText.tsx b/src/ui/AppearanceText.tsx index 33ae792..ff5aa1f 100644 --- a/src/ui/AppearanceText.tsx +++ b/src/ui/AppearanceText.tsx @@ -1,28 +1,40 @@ -import { useInView, motion } from 'framer-motion'; -import { useRef } from 'react'; +import { motion, useScroll, useMotionValueEvent } from 'framer-motion'; +import { useRef, useState } from 'react'; -export default function AppearanceText({ - text, - opacity, - duration, -}: { - text: string; - opacity: number; - duration: number; -}) { +function AppearanceItem({ text }: { text: string }) { const ref = useRef(null); - const isInView = useInView(ref); + const [isShowed, setIsShowed] = useState(false); + + const { scrollY } = useScroll(); + + useMotionValueEvent(scrollY, 'change', latest => { + setIsShowed( + latest >= (ref.current?.offsetTop ?? 0) - (window.innerHeight / 3) * 2, + ); + }); return ( {text} ); } + +export function AppearanceText({ + splits, + className = '', +}: { + splits: string[]; + className?: string; +}) { + return ( +

    + {splits.map(text => ( + + ))} +

    + ); +} diff --git a/src/ui/Button.tsx b/src/ui/Button.tsx new file mode 100644 index 0000000..fc1e7cc --- /dev/null +++ b/src/ui/Button.tsx @@ -0,0 +1,40 @@ +import { ReactNode } from 'react'; + +interface ButtonProps { + children: ReactNode; + icon?: JSX.Element; + color?: 'primary' | 'secondary'; + width?: 'fit' | 'full'; + disabled?: boolean; + className?: string; + onClick?: () => void; +} + +function Button({ + children, + color = 'primary', + icon, + width = 'fit', + disabled = false, + className, + onClick, +}: ButtonProps) { + return ( + + ); +} + +export default Button; diff --git a/yarn.lock b/yarn.lock index b33f520..abb11bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -581,6 +581,13 @@ resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== +"@types/node@^20.14.10": + version "20.14.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.10.tgz#a1a218290f1b6428682e3af044785e5874db469a" + integrity sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ== + dependencies: + undici-types "~5.26.4" + "@types/prop-types@*": version "15.7.12" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" @@ -593,6 +600,13 @@ dependencies: "@types/react" "*" +"@types/react-input-mask@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/react-input-mask/-/react-input-mask-3.0.5.tgz#9fbe9a984b3299419a6071dbf697ac2cae2abd2d" + integrity sha512-vQ1x6ykwjDrDrJZq1zw5/uQ+nqGHUV6bWscsVZJ/qsNwNXWxZm7KRBHLJ5k6TQt3MHjhpoYHzPH6FwjVSZODHA== + dependencies: + "@types/react" "*" + "@types/react-router-dom@^5.3.0": version "5.3.3" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" @@ -1384,6 +1398,13 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -1488,6 +1509,11 @@ keyv@^4.5.3: dependencies: json-buffer "3.0.1" +ky@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ky/-/ky-1.4.0.tgz#68b4a71eccfb4177199fe6ee2d5041b50bb41931" + integrity sha512-tPhhoGUiEiU/WXR4rt8klIoLdnTtyu+9jVKHd/wauEjYud32jyn63mzKWQweaQrHWxBQtYoVtdcEnYX1LosnFQ== + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -1518,12 +1544,17 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -1811,6 +1842,14 @@ react-dom@^18.3.1: loose-envify "^1.1.0" scheduler "^0.23.2" +react-input-mask@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-input-mask/-/react-input-mask-2.0.4.tgz#9ade5cf8196f4a856dbf010820fe75a795f3eb14" + integrity sha512-1hwzMr/aO9tXfiroiVCx5EtKohKwLk/NT8QlJXHQ4N+yJJFyUuMT+zfTpLBwX/lK3PkuMlievIffncpMZ3HGRQ== + dependencies: + invariant "^2.2.4" + warning "^4.0.2" + react-is@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -2152,6 +2191,11 @@ typescript@^5.2.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.2.tgz#c26f023cb0054e657ce04f72583ea2d85f8d0507" integrity sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + update-browserslist-db@^1.0.16: version "1.0.16" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz#f6d489ed90fb2f07d67784eb3f53d7891f736356" @@ -2172,6 +2216,13 @@ use-sync-external-store@1.2.0: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== +usehooks-ts@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-3.1.0.tgz#156119f36efc85f1b1952616c02580f140950eca" + integrity sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw== + dependencies: + lodash.debounce "^4.0.8" + util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -2188,6 +2239,13 @@ vite@^5.3.1: optionalDependencies: fsevents "~2.3.3" +warning@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"