Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/emcd-swap/documents/terms.pdf
Binary file not shown.
140 changes: 140 additions & 0 deletions src/apps/emcd-swap/api/coinsApi.tsx
Original file line number Diff line number Diff line change
@@ -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<Record<string, string>>
}

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<ICoin[], void>({
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<any, any>({
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<any, any>({
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<any, { swapID: string; status: number }>({
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'
};
},
}),
Comment on lines +90 to +100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Define response type for getSwapStatus endpoint

Using any for the response type reduces type safety. Define and use a proper interface.

+interface SwapStatusResponse {
+  status: number;
+  // Add other properties based on the API response
+}

-    getSwapStatus: builder.query<any, { swapID: string; status: number }>({
+    getSwapStatus: builder.query<SwapStatusResponse, { swapID: string; status: number }>({
🤖 Prompt for AI Agents
In src/apps/emcd-swap/api/coinsApi.tsx around lines 90 to 100, the getSwapStatus
query uses 'any' as the response type, which reduces type safety. Define a
proper TypeScript interface representing the expected response structure from
this endpoint and replace 'any' with this interface in the query definition to
improve type safety and code clarity.

getSwap: builder.query<any, { swapID: string }>({
query: ({ swapID }) => `swap/${swapID}`,
}),
Comment on lines +101 to +103
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling to getSwap endpoint

This endpoint is missing error transformation logic that all other endpoints have.

    getSwap: builder.query<any, { swapID: string }>({
      query: ({ swapID }) => `swap/${swapID}`,
+      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'
+        };
+      },
    }),

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/apps/emcd-swap/api/coinsApi.tsx around lines 101 to 103, the getSwap
endpoint lacks error transformation logic present in other endpoints. Add an
error handling or transformErrorResponse function to the getSwap builder.query
configuration to properly handle and transform errors returned from the API,
ensuring consistent error processing across all endpoints.

createUser: builder.mutation<any, any>({
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<any, any>({
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;
Binary file added src/apps/emcd-swap/assets/cancelled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/apps/emcd-swap/assets/error.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/apps/emcd-swap/assets/success.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 69 additions & 0 deletions src/apps/emcd-swap/components/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve onClick event handler typing

The current onClick type is () => void, but it should accept the React mouse event parameter for compatibility with form handling and event prevention.

-  onClick?: () => void;
+  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
🤖 Prompt for AI Agents
In src/apps/emcd-swap/components/Button/Button.tsx at line 8, update the onClick
prop type from a parameterless function to one that accepts a React mouse event
parameter. Change the type to (event: React.MouseEvent<HTMLButtonElement>) =>
void to ensure proper typing for event handling and compatibility with form
events and event prevention.

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 ''
}
Comment on lines +44 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve disabled state handling

When disabled is true, the getType() function returns an empty string, but there's no specific styling applied for the disabled state. This could confuse users as there's no visual indication that the button is disabled.

  const getType = () => {
    const classes = []

    if (disabled) {
-     return ''
+     return 'bg-gray-400 cursor-not-allowed opacity-60'
    }

    // rest of the function remains the same
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (disabled) {
return ''
}
if (disabled) {
return 'bg-gray-400 cursor-not-allowed opacity-60'
}
🤖 Prompt for AI Agents
In src/apps/emcd-swap/components/Button/Button.tsx around lines 44 to 46, the
getType() function returns an empty string when disabled is true but does not
apply any specific styling for the disabled state. Update the function to return
a distinct type or class name for the disabled state and ensure the button
component applies corresponding styles to visually indicate it is disabled.


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 (
<button onClick={onClick} type={buttonType} className={`w-full text-color-1 rounded-sm text-sm outline-none border font-medium border-transparent ${getSize()} ${getType()} ${className}`}>
{children}
</button>
Comment on lines +63 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add accessibility attributes to the button

The button component is missing important accessibility attributes that would improve usability for users with disabilities.

  return (
    <button 
      onClick={onClick} 
      type={buttonType} 
+     disabled={!!disabled}
+     aria-disabled={!!disabled}
      className={`w-full text-color-1 rounded-sm text-sm outline-none border font-medium border-transparent ${getSize()} ${getType()} ${className || ''}`}>
      {children}
    </button>
  );
🤖 Prompt for AI Agents
In src/apps/emcd-swap/components/Button/Button.tsx around lines 63 to 65, the
button element lacks accessibility attributes. Add appropriate ARIA attributes
such as aria-label or aria-labelledby to describe the button's purpose, and
ensure the button has a clear accessible name. Also verify that the button's
role and keyboard interaction are properly supported to improve usability for
users with disabilities.

);
};

export default Button;
17 changes: 17 additions & 0 deletions src/apps/emcd-swap/components/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={`p-5 border w-full bg-bg-7/70 rounded-5 ${valid ? 'border-bg-5' : 'border-error'} ${className} `}>
{ children }
</div>
);
};

export default Card;
30 changes: 30 additions & 0 deletions src/apps/emcd-swap/components/DefaultInput/DefaultInput.tsx
Original file line number Diff line number Diff line change
@@ -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<DefaultInputProps> = ({ value, onChange, placeholder }) => {
const [stateValue, setStateValue] = useState('');

useEffect(() => {
if (value) {
setStateValue(value)
}
}, [value]);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setStateValue(e.target.value);
onChange(e.target.value);
}

return (
<div className='w-full'>
<input placeholder={placeholder} value={stateValue} aria-label={placeholder || "Default input"} onChange={handleChange} className='w-full bg-bg-6 p-4 text-color-1 border border-bg-2 hover:border-brand-hover focus:border-brand-active text-sm outline-none rounded-sm transition-all' />
</div>
);
};

export default DefaultInput;
51 changes: 51 additions & 0 deletions src/apps/emcd-swap/components/FAQ/FAQ.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="faq-container max-w-4xl mx-auto p-4 mt-5">
{faqList.map((item, index) => (
<FAQItem key={index} question={item.question} answer={item.answer} />
))}
</div>
);
};

export default FAQList;
45 changes: 45 additions & 0 deletions src/apps/emcd-swap/components/FAQ/components/FAQItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, { useState } from 'react';

interface FAQItemProps {
question: string;
answer: string;
}

const FAQItem: React.FC<FAQItemProps> = ({ question, answer }) => {
const [isOpen, setIsOpen] = useState(false);

const toggle = () => {
setIsOpen(!isOpen);
};

return (
<div className="faq-item mb-4 border-b border-bg-35 pb-4">
<button
onClick={toggle}
className="flex justify-between items-center cursor-pointer"
>
<div className='text-sm text-color-1'>
{question}
</div>

<div className="ml-2">
<svg
className={`w-4 h-4 transition-transform ${isOpen ? 'transform rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>

</button>

<div className={`text-xs text-color-3 transition-all overflow-hidden ${isOpen ? 'h-auto' : 'h-0 opacity-0'}`}>
{answer}
</div>
</div>
);
};

export default FAQItem;
Loading