diff --git a/public/emcd-swap/documents/terms.pdf b/public/emcd-swap/documents/terms.pdf new file mode 100644 index 00000000..e8758cd2 Binary files /dev/null and b/public/emcd-swap/documents/terms.pdf differ diff --git a/src/apps/emcd-swap/api/coinsApi.tsx b/src/apps/emcd-swap/api/coinsApi.tsx new file mode 100644 index 00000000..6b353e5d --- /dev/null +++ b/src/apps/emcd-swap/api/coinsApi.tsx @@ -0,0 +1,140 @@ +/* eslint-disable no-console */ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; + +// store +import { addMiddleware } from '../../../store'; + +// Define types for the API responses and requests +interface ICoin { + title: string; + icon_url: string; + networks: Array> +} + +interface ErrorData { + message?: string; + [key: string]: any; +} + +interface EstimateParams { + coin_from: string; + coin_to: string; + amount: number; + network_from: string; + network_to: string; +} + +interface EstimateResponse { + amount_to: string; + rate: string; + // Add other properties as needed +} + + +export const emcdSwapApi = createApi({ + reducerPath: 'emcdSwapApi', + baseQuery: fetchBaseQuery({ + baseUrl: process.env.REACT_APP_EMCD_SWAP_API_URL || 'https://b2b-endpoint.dev-b2b.mytstnv.site', + prepareHeaders: (headers) => { + return headers; + }, + }), + endpoints: (builder) => ({ + getSwapCoins: builder.query({ + query: () => 'v2/swap/coins', + transformErrorResponse: (response: { status: number; data: ErrorData }) => { + console.error('API Error:', response); + return { + status: response.status, + data: response.data, + message: response.data?.message || 'An error occurred' + }; + }, + }), + getEstimate: builder.query({ + query: (params) => { + const searchParams = new URLSearchParams(); + + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + searchParams.append(key, String(value)); + } + }); + + return `swap/estimate?${searchParams.toString()}`; + }, + transformErrorResponse: (response: { status: number; data: ErrorData }) => { + console.error('API Error:', response); + return { + status: response.status, + data: response.data, + message: response.data?.message || 'An error occurred' + }; + }, + }), + createSwap: builder.mutation({ + query: (formData) => ({ + url: 'swap', + method: 'POST', + body: formData, + }), + transformErrorResponse: (response: { status: number; data: ErrorData }) => { + console.error('API Error:', response); + return { + status: response.status, + data: response.data, + message: response.data?.message || 'An error occurred' + }; + }, + }), + getSwapStatus: builder.query({ + query: ({ swapID, status }) => `swap/${swapID}/${status}`, + transformErrorResponse: (response: { status: number; data: ErrorData }) => { + console.error('API Error:', response); + return { + status: response.status, + data: response.data, + message: response.data?.message || 'An error occurred' + }; + }, + }), + getSwap: builder.query({ + query: ({ swapID }) => `swap/${swapID}`, + }), + createUser: builder.mutation({ + query: (formData) => ({ + url: 'swap/user', + method: 'POST', + body: formData, + }), + transformErrorResponse: (response: { status: number; data: ErrorData }) => { + console.error('API Error:', response); + return { + status: response.status, + data: response.data, + message: response.data?.message || 'An error occurred' + }; + }, + }), + + createTicket: builder.mutation({ + query: (formData) => ({ + url: 'swap/support/message', + method: 'POST', + body: formData, + }), + transformErrorResponse: (response: { status: number; data: ErrorData }) => { + console.error('API Error:', response); + return { + status: response.status, + data: response.data, + message: response.data?.message || 'An error occurred' + }; + }, + }), + }), +}); + +addMiddleware(emcdSwapApi); + +export const { useGetSwapCoinsQuery, useLazyGetEstimateQuery, useCreateSwapMutation, useCreateUserMutation, useLazyGetSwapQuery, useCreateTicketMutation, useLazyGetSwapStatusQuery } = emcdSwapApi; diff --git a/src/apps/emcd-swap/assets/cancelled.png b/src/apps/emcd-swap/assets/cancelled.png new file mode 100644 index 00000000..dc747e18 Binary files /dev/null and b/src/apps/emcd-swap/assets/cancelled.png differ diff --git a/src/apps/emcd-swap/assets/error.png b/src/apps/emcd-swap/assets/error.png new file mode 100644 index 00000000..1f356fd0 Binary files /dev/null and b/src/apps/emcd-swap/assets/error.png differ diff --git a/src/apps/emcd-swap/assets/success.png b/src/apps/emcd-swap/assets/success.png new file mode 100644 index 00000000..a40f8508 Binary files /dev/null and b/src/apps/emcd-swap/assets/success.png differ diff --git a/src/apps/emcd-swap/components/Button/Button.tsx b/src/apps/emcd-swap/components/Button/Button.tsx new file mode 100644 index 00000000..e26a51d6 --- /dev/null +++ b/src/apps/emcd-swap/components/Button/Button.tsx @@ -0,0 +1,69 @@ +import React, { ReactNode } from 'react'; + +interface ButtonProps { + children: ReactNode; + type?: 'shade' | 'main' | 'monochrome'; + size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + className?: string; + onClick?: () => void; + disabled?: boolean | null; + buttonType?: 'button' | 'submit' | 'reset'; +} + +const Button = ({ children, type, className, onClick, size = 'sm', disabled, buttonType = 'button' }: ButtonProps) => { + + const getSize = () => { + const classes = [] + + if (size === 'xs') { + classes.push('p-1') + } + + if (size === 'sm') { + classes.push('px-4 py-2') + } + + if (size === 'md') { + classes.push('p-4') + } + + if (size === 'lg') { + classes.push('px-6 py-3') + } + + if (size === 'xl') { + classes.push('px-8 py-4') + } + + return classes.join(' ') + } + + const getType = () => { + const classes = [] + + if (disabled) { + return '' + } + + if (type === 'shade') { + classes.push('bg-bg-5') + } + + if (type === 'main') { + classes.push('bg-brand') + } + + if (type === 'monochrome') { + classes.push('bg-transparent text-color-2 border border-color-3') + } + + return classes.join(' ') + } + return ( + + ); +}; + +export default Button; \ No newline at end of file diff --git a/src/apps/emcd-swap/components/Card/Card.tsx b/src/apps/emcd-swap/components/Card/Card.tsx new file mode 100644 index 00000000..736cff35 --- /dev/null +++ b/src/apps/emcd-swap/components/Card/Card.tsx @@ -0,0 +1,17 @@ +import React, { ReactNode } from 'react'; + +interface CardProps { + children: ReactNode; + className?: string; + valid?: boolean | null; +} + +const Card = ({ children, className, valid }:CardProps) => { + return ( +
+ { children } +
+ ); +}; + +export default Card; \ No newline at end of file diff --git a/src/apps/emcd-swap/components/DefaultInput/DefaultInput.tsx b/src/apps/emcd-swap/components/DefaultInput/DefaultInput.tsx new file mode 100644 index 00000000..e1b5c5a6 --- /dev/null +++ b/src/apps/emcd-swap/components/DefaultInput/DefaultInput.tsx @@ -0,0 +1,30 @@ +import React, { useEffect, useState } from 'react'; + +interface DefaultInputProps { + value: string | null; + onChange: (value: string | null) => void; + placeholder?: string; +} + +const DefaultInput:React.FC = ({ value, onChange, placeholder }) => { + const [stateValue, setStateValue] = useState(''); + + useEffect(() => { + if (value) { + setStateValue(value) + } + }, [value]); + + const handleChange = (e: React.ChangeEvent) => { + setStateValue(e.target.value); + onChange(e.target.value); + } + + return ( +
+ +
+ ); +}; + +export default DefaultInput; \ No newline at end of file diff --git a/src/apps/emcd-swap/components/FAQ/FAQ.tsx b/src/apps/emcd-swap/components/FAQ/FAQ.tsx new file mode 100644 index 00000000..5b374512 --- /dev/null +++ b/src/apps/emcd-swap/components/FAQ/FAQ.tsx @@ -0,0 +1,51 @@ +import React from 'react' + +import FAQItem from './components//FAQItem' + +interface FAQItemData { + question: string; + answer: string; +} + +const faqList: FAQItemData[] = [ + { + question: 'Что такое ESWAP?', + answer: 'ESWAP — это сервис для мгновенного обмена криптовалют. Он интегрирован с разными торговыми платформами, чтобы предложить лучший курс для обмена 500 монет без скрытых комиссий. Для ESWAP не нужно создавать аккаунт', + }, + { + question: 'Как быстро проходит обмен?', + answer: 'Обмен занимает от двух до двадцати минут — это зависит от загруженности блокчейна. Большинство обменов завершаются всего за несколько минут.', + }, + { + question: 'Как получить криптовалютный кошелек?', + answer: 'Адрес криптовалютного кошелька — это уникальная комбинация цифр и букв длиной от 26 до 35 символов. Обычно он выглядит так: 17bkZPLB4Wn6F347PLZBR34ijhzQDUFZ4ZC. Чтобы получить свой адрес, нужно создать горячий кошелек или купить холодный. Горячий кошелек можно получить бесплатно в EMCD, если создать аккаунт, а холодный купить, например, Ledger', + }, + { + question: 'Что такое мемо или тег?', + answer: 'Некоторые монеты, например TON, требуют ввода дополнительного идентификатора сделки при отправке или приеме крипты. Этот идентификатор называется мемо или тег. Если не ввести мемо или тег там, где он требуется, отправленные средства исчезнут', + }, + { + question: 'Что такое адрес кошелька получателя?', + answer: 'Если ты хочешь купить криптовалюту, тебе нужно отправить ее на определенный криптокошелек. У каждой монеты он свой. Адрес получателя — это твой кошелек, на который переводится криптовалюта после обмена', + }, + { + question: 'Как отменить транзакцию?', + answer: 'Транзакцию в блокчейне нельзя отменить, поэтому нужно тщательно проверять адрес кошелька, мемо или тег и другие данные перед отправкой криптовалюты.', + }, + { + question: 'Почему финальная сумма обмена отличается от первоначальной?', + answer: 'Обработка транзакций занимает некоторое время. Из-за высокой волатильности криптовалюты финальный курс обмена может отличаться как в положительную, так и в отрицательную сторону', + }, +] + +const FAQList: React.FC = () => { + return ( +
+ {faqList.map((item, index) => ( + + ))} +
+ ); +}; + +export default FAQList; diff --git a/src/apps/emcd-swap/components/FAQ/components/FAQItem.tsx b/src/apps/emcd-swap/components/FAQ/components/FAQItem.tsx new file mode 100644 index 00000000..1db15a06 --- /dev/null +++ b/src/apps/emcd-swap/components/FAQ/components/FAQItem.tsx @@ -0,0 +1,45 @@ +import React, { useState } from 'react'; + +interface FAQItemProps { + question: string; + answer: string; +} + +const FAQItem: React.FC = ({ question, answer }) => { + const [isOpen, setIsOpen] = useState(false); + + const toggle = () => { + setIsOpen(!isOpen); + }; + + return ( +
+ + +
+ {answer} +
+
+ ); +}; + +export default FAQItem; diff --git a/src/apps/emcd-swap/components/FormInput/FormInput.tsx b/src/apps/emcd-swap/components/FormInput/FormInput.tsx new file mode 100644 index 00000000..fab9e0f2 --- /dev/null +++ b/src/apps/emcd-swap/components/FormInput/FormInput.tsx @@ -0,0 +1,59 @@ +import React, { useEffect, useState } from 'react'; + +import LinearProgress from '@mui/material/LinearProgress'; + +interface FormInputProps { + value: string | null; + onChange: (value: string | null) => void; + loading?: boolean; + + valid?: boolean | null; + error?: string | null; +} + +const FormInput: React.FC = ({ + value, + onChange, + loading, + valid, + error, +}) => { + const [stateValue, setStateValue] = useState(''); + + useEffect(() => { + if (value !== null) { + setStateValue(value); + } else { + setStateValue(''); + } + }, [value]); + + const handleChange = (e: React.ChangeEvent) => { + setStateValue(e.target.value); + onChange(e.target.value); + }; + + return ( +
+ + + {loading ? ( +
+ +
+ ) : ( +
+ )} + {!valid &&
{error}
} +
+ ); +}; + +export default FormInput; \ No newline at end of file diff --git a/src/apps/emcd-swap/components/Header/Header.tsx b/src/apps/emcd-swap/components/Header/Header.tsx new file mode 100644 index 00000000..8328de5e --- /dev/null +++ b/src/apps/emcd-swap/components/Header/Header.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { useLocation } from 'react-router-dom'; + +import LogoIcon from '../icons/LogoIcon'; +import CloseIcon from '../icons/CloseIcon'; + +import { setCurrentView } from '../../reducer/emcdSwapSlice'; + +import { VIEW_TYPE } from '../../constants/views'; + +interface HeaderProps { + title?: string; + close?: boolean | null; + smallFAQ?: boolean | null; + onClose?: () => void; +} + +const Header: React.FC = ({ title, close, onClose, smallFAQ }) => { + const location = useLocation(); + const dispatch = useDispatch(); + + const handleLogoClick = () => { + const url = window.location.origin + location.pathname; + window.open(url, '_blank'); + }; + + const handleFAQ = () => { + dispatch(setCurrentView(VIEW_TYPE.FAQ)); + }; + + return ( +
+ {!close ? ( +
+ {' '} + {' '} +
+ ) : ( +
+ +
+ )} + +
{title}
+ +
+ {smallFAQ && ( +
+ ? +
+ )} +
RU
+
+
+ ); +}; + +export default Header; \ No newline at end of file diff --git a/src/apps/emcd-swap/components/Loader/Loader.tsx b/src/apps/emcd-swap/components/Loader/Loader.tsx new file mode 100644 index 00000000..771a1451 --- /dev/null +++ b/src/apps/emcd-swap/components/Loader/Loader.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const Loader: React.FC = () => { + const dotStyle = (delay: string) => ({ + animation: 'fade 1.5s infinite', + animationDelay: delay, + }); + + return ( +
+
+
+
+
+
+
+ ); +}; + +export default Loader; diff --git a/src/apps/emcd-swap/components/Modals/SupportModal.tsx b/src/apps/emcd-swap/components/Modals/SupportModal.tsx new file mode 100644 index 00000000..f14f0d4c --- /dev/null +++ b/src/apps/emcd-swap/components/Modals/SupportModal.tsx @@ -0,0 +1,134 @@ +import React, { useEffect, useRef, useState } from 'react'; + +import { useCreateTicketMutation } from '../../api/coinsApi'; + +import CloseIcon from '../icons/CloseIcon'; +import Loader from '../Loader/Loader'; + + +interface SupportModalProps { + onClose: () => void; +} +const SupportModal: React.FC = ({ onClose }) => { + const [createTicket, { isLoading, isSuccess }] = useCreateTicketMutation() + const modalRef = useRef(null) + const [formData, setFormData] = useState({ + name: '', + email: '', + text: '', + }) + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (modalRef.current && !modalRef.current.contains(event.target as Node)) { + onClose(); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [onClose]) + + useEffect(() => { + if (isSuccess) { + onClose() + + } + }, [isSuccess]) + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev: any) => ({ + ...prev, + [name]: value, + })); + } + + const handleSubmit = () => { + if (!formData.name || !formData.email || !formData.text || isLoading) { + return + } + + createTicket(formData) + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(); + } + }; + + return ( +
+
+
+ +
+ + + +
+
+
Имя
+ + +
+ +
+
Почта
+ + +
+ +
+
Обращение
+ +