├── .gitignore ├── .vscode └── settings.json ├── README.md ├── custom.d.ts ├── package-lock.json ├── package.json ├── public ├── index.html └── robots.txt ├── src ├── App.tsx ├── api │ ├── index.ts │ ├── methods │ │ └── index.ts │ └── utils │ │ └── index.ts ├── components │ ├── ActionText │ │ ├── ActionText.component.tsx │ │ ├── ActionText.module.css │ │ ├── ActionText.types.ts │ │ └── index.ts │ ├── AppTitle │ │ ├── AppTitle.component.tsx │ │ ├── AppTitle.module.css │ │ ├── AppTitle.types.ts │ │ └── index.ts │ ├── Avatar │ │ ├── Avatar.component.tsx │ │ ├── Avatar.module.css │ │ ├── Avatar.types.ts │ │ └── index.ts │ ├── Block │ │ ├── Block.component.tsx │ │ ├── Block.module.css │ │ ├── Block.types.ts │ │ └── index.ts │ ├── BlockHeader │ │ ├── BlockHeader.component.tsx │ │ ├── BlockHeader.module.css │ │ ├── BlockHeader.types.ts │ │ └── index.ts │ ├── Button │ │ ├── Button.component.tsx │ │ ├── Button.module.css │ │ ├── Button.types.ts │ │ └── index.ts │ ├── Cell │ │ ├── Cell.component.tsx │ │ ├── Cell.module.css │ │ ├── Cell.types.ts │ │ └── index.ts │ ├── ErrorBlock │ │ ├── ErrorBlock.component.tsx │ │ ├── ErrorBlock.module.css │ │ ├── ErrorBlock.types.ts │ │ └── index.ts │ ├── Filters │ │ ├── Filters.component.tsx │ │ ├── Filters.module.css │ │ ├── Filters.types.ts │ │ └── index.ts │ ├── Group │ │ ├── Group.component.tsx │ │ ├── Group.module.css │ │ ├── Group.types.ts │ │ └── index.ts │ ├── Input │ │ ├── Input.component.tsx │ │ ├── Input.module.css │ │ ├── Input.types.ts │ │ └── index.ts │ ├── Link │ │ ├── Link.component.tsx │ │ ├── Link.module.css │ │ ├── Link.types.ts │ │ └── index.ts │ ├── Panel │ │ ├── Panel.component.tsx │ │ ├── Panel.module.css │ │ ├── Panel.types.ts │ │ └── index.ts │ ├── PanelHeader │ │ ├── PanelHeader.component.tsx │ │ ├── PanelHeader.module.css │ │ ├── PanelHeader.types.ts │ │ └── index.ts │ ├── RichCell │ │ ├── RichCell.component.tsx │ │ ├── RichCell.module.css │ │ ├── RichCell.types.ts │ │ └── index.ts │ ├── Select │ │ ├── Select.component.tsx │ │ ├── Select.module.css │ │ ├── Select.types.ts │ │ └── index.ts │ ├── Separator │ │ ├── Separator.component.tsx │ │ ├── Separator.module.css │ │ ├── Separator.types.ts │ │ └── index.ts │ ├── Switch │ │ ├── Switch.component.tsx │ │ ├── Switch.module.css │ │ ├── Switch.types.ts │ │ └── index.ts │ ├── Text │ │ ├── Text.component.tsx │ │ ├── Text.types.ts │ │ └── index.ts │ └── index.ts ├── constants │ ├── index.ts │ └── menu.json ├── hooks │ └── useQuery.ts ├── i18next │ └── index.ts ├── icons │ ├── Arrow15Outline.svg │ ├── ArrowDown15Outline.svg │ ├── AstralyxLogo.svg │ ├── Back24Outline.svg │ ├── BankCard.svg │ ├── Bin18Outline.svg │ ├── BoxSend18Outline.svg │ ├── Burn24Outline.svg │ ├── Cart18Outline.svg │ ├── Chains20Outline.svg │ ├── Cheque24Outline.svg │ ├── Copy20Outline.svg │ ├── CopySuccess24Outline.svg │ ├── Date24Outline.svg │ ├── File24Outline.svg │ ├── Fire18Outline.svg │ ├── Get18Outline.svg │ ├── GoArrow24Outline.svg │ ├── History24Outline.svg │ ├── Info.svg │ ├── Invoice24Outline.svg │ ├── Logo.svg │ ├── LogoOutline.svg │ ├── LogoQR40Outline.svg │ ├── Menu24Outline.svg │ ├── MinusSmall.svg │ ├── Picture24Outline.svg │ ├── PlusSmall.svg │ ├── Program24Outline.svg │ ├── QRCopy17Outline.svg │ ├── Receipt18Outline.svg │ ├── Receive18Outline.svg │ ├── Receive24Outline.svg │ ├── Search17Outline.svg │ ├── Security24Outline.svg │ ├── Send18Outline.svg │ ├── Send24Outline.svg │ ├── Settings24Outline.svg │ ├── Star24.svg │ ├── Star24Outline.svg │ ├── Swap24Outline.svg │ ├── Switch15Outline.svg │ ├── Switcher.svg │ ├── Switching24Outline.svg │ ├── Text24Outline.svg │ ├── USAFlag.svg │ ├── cross.svg │ └── russianFlag.svg ├── images │ └── ton.jpeg ├── index.tsx ├── lotties │ └── loading-plane.json ├── panels │ ├── ErrorDeposit │ │ ├── ErrorDeposit.module.css │ │ └── index.tsx │ ├── History │ │ ├── History.module.css │ │ ├── History.panel.tsx │ │ └── index.ts │ ├── Home │ │ ├── Home.module.css │ │ ├── Home.panel.tsx │ │ └── index.ts │ ├── Load │ │ ├── Load.module.css │ │ ├── Load.panel.tsx │ │ └── index.ts │ ├── Menu │ │ ├── Menu.module.css │ │ ├── Menu.panel.tsx │ │ └── index.ts │ ├── MenuExpanded │ │ ├── MenuExpanded.module.css │ │ ├── MenuExpanded.panel.tsx │ │ └── index.ts │ ├── Nft │ │ ├── Nft.module.css │ │ ├── Nft.panel.tsx │ │ ├── NftDetail.panel.module.css │ │ ├── NftDetail.panel.tsx │ │ └── index.ts │ ├── PurchaseTonPage │ │ ├── PurchaseFiatSelect.panel.tsx │ │ ├── PurchaseTonFirstStep.panel.tsx │ │ ├── PurchaseTonPage.module.css │ │ ├── PurchaseTonPage.panel.tsx │ │ └── index.ts │ ├── Receive │ │ ├── Receive.module.css │ │ ├── Receive.panel.tsx │ │ └── index.ts │ ├── SelectTransfer │ │ ├── SelectTransfer.panel.tsx │ │ └── index.ts │ ├── SellNft │ │ ├── SellNft.tsx │ │ ├── SellNftCurrencies.tsx │ │ └── index.ts │ ├── SellNftSuccess │ │ ├── SellNftSuccess.tsx │ │ └── index.ts │ ├── Send │ │ ├── Send.panel.tsx │ │ └── index.ts │ ├── SendNft │ │ ├── SendNft.tsx │ │ └── index.ts │ ├── SendNftSuccessPanel │ │ ├── SendNftSuccessPanel.tsx │ │ └── index.ts │ ├── SendSuccess │ │ ├── SendSuccess.panel.tsx │ │ └── index.ts │ ├── Settings │ │ ├── SelectCurrency.tsx │ │ ├── SelectLanguage.tsx │ │ ├── Settings.module.css │ │ ├── Settings.panel.tsx │ │ └── index.ts │ ├── Swap │ │ ├── Swap.module.css │ │ ├── Swap.panel.tsx │ │ ├── SwapSelect.panel.tsx │ │ └── index.ts │ ├── Trading │ │ ├── Trading.module.css │ │ ├── TradingPanel.tsx │ │ ├── TradingSelect.panel.tsx │ │ ├── TradingSuccess.tsx │ │ └── index.ts │ └── index.ts ├── providers │ ├── ExchangePairContextProvider.tsx │ ├── JetTokensContextProvider.tsx │ ├── PurchaseTonContextProvider.tsx │ └── SwapDataContextProvider.tsx ├── react-app-env.d.ts ├── router │ ├── constants.ts │ └── index.tsx ├── store │ ├── constants │ │ └── index.ts │ ├── index.ts │ └── reducers │ │ ├── index.ts │ │ └── user │ │ ├── index.ts │ │ ├── user.selectors.ts │ │ └── user.slice.ts ├── styles │ └── global.css ├── types.d.ts └── utils │ └── index.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "[xml]": { 4 | "editor.defaultFormatter": "redhat.vscode-xml" 5 | } 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.css" { 2 | interface IClassNames { 3 | [className: string]: string; 4 | } 5 | const classNames: IClassNames; 6 | export = classNames; 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xjet-tg", 3 | "version": "0.1.0", 4 | "homepage": "https://webapp.xjet.app/", 5 | "private": true, 6 | "dependencies": { 7 | "@amplitude/analytics-browser": "^2.4.0", 8 | "@reduxjs/toolkit": "^1.9.1", 9 | "@testing-library/jest-dom": "^5.16.5", 10 | "@testing-library/react": "^13.4.0", 11 | "@testing-library/user-event": "^13.5.0", 12 | "@vercel/analytics": "^1.1.3", 13 | "@vercel/speed-insights": "^1.0.9", 14 | "antd": "^5.4.0", 15 | "axios": "^1.2.3", 16 | "classnames": "^2.3.2", 17 | "i18next": "^22.5.0", 18 | "lottie-react": "^2.4.0", 19 | "qr-code-styling": "^1.6.0-rc.1", 20 | "qrcode": "^1.5.1", 21 | "qrcode.react": "^3.1.0", 22 | "react": "^18.2.0", 23 | "react-content-loader": "^6.2.0", 24 | "react-dom": "^18.2.0", 25 | "react-i18next": "^12.3.1", 26 | "react-number-format": "^5.2.2", 27 | "react-qr-code": "^2.0.11", 28 | "react-qrcode-logo": "^2.8.0", 29 | "react-redux": "^8.0.5", 30 | "react-router-dom": "^6.7.0", 31 | "react-safe": "^1.3.0", 32 | "react-scripts": "5.0.1", 33 | "swiper": "^11.1.0", 34 | "tweetnacl": "^1.0.3", 35 | "tweetnacl-util": "^0.15.1", 36 | "web-vitals": "^2.1.4" 37 | }, 38 | "scripts": { 39 | "predeploy": "npm run build", 40 | "deploy": "gh-pages -d build", 41 | "start": "react-scripts start", 42 | "build": "GENERATE_SOURCEMAP=false react-scripts build", 43 | "test": "react-scripts test", 44 | "eject": "react-scripts eject" 45 | }, 46 | "eslintConfig": { 47 | "extends": [ 48 | "react-app", 49 | "react-app/jest" 50 | ] 51 | }, 52 | "browserslist": { 53 | "production": [ 54 | ">0.2%", 55 | "not dead", 56 | "not op_mini all" 57 | ], 58 | "development": [ 59 | "last 1 chrome version", 60 | "last 1 firefox version", 61 | "last 1 safari version" 62 | ] 63 | }, 64 | "devDependencies": { 65 | "@types/jest": "^27.5.2", 66 | "@types/node": "^16.18.11", 67 | "@types/qrcode": "^1.5.0", 68 | "@types/react": "^18.0.27", 69 | "@types/react-dom": "^18.0.10", 70 | "gh-pages": "^5.0.0", 71 | "typescript": "^4.9.4" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | xJet wallet 11 | 12 | 13 | 14 | 15 | 16 | 27 | 28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { RouterProvider } from "react-router-dom"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | import "./i18next"; 7 | import { router } from "./router"; 8 | import { balanceCheckWatcher } from "./api"; 9 | import { SwapDataContextProvider } from "./providers/SwapDataContextProvider"; 10 | import { JetTokensContextProvider } from "./providers/JetTokensContextProvider"; 11 | import { ExchangePairContextProvider } from "./providers/ExchangePairContextProvider"; 12 | 13 | import * as amplitude from '@amplitude/analytics-browser'; 14 | import { Analytics } from '@vercel/analytics/react'; 15 | import { SpeedInsights } from "@vercel/speed-insights/react"; 16 | 17 | import { 18 | apiInit, 19 | getAllCurrencies, 20 | getExchangesPair, 21 | getMyBalance, 22 | getMyServerData, 23 | initMainnet, 24 | mainnetInited, 25 | setApiConfig, 26 | } from "./api"; 27 | 28 | import { userActions } from "./store/reducers"; 29 | import { ROUTE_NAMES } from "./router/constants"; 30 | 31 | export function App() { 32 | const intervalIdRef = useRef(undefined); 33 | const dispatch = useDispatch(); 34 | const { i18n } = useTranslation(); 35 | 36 | useEffect(() => { 37 | if ( 38 | window.location.pathname.includes("/receive?tonAddress=") || 39 | window.location.pathname.includes("/history?apiKey=") || 40 | (window.location.pathname.includes("/nft/") && window.location.pathname.includes("?tonAddress=")) 41 | ) { 42 | return; 43 | } 44 | 45 | if ( 46 | window.location.pathname.includes("/receive") || 47 | window.location.pathname.includes("/nft") || 48 | window.location.pathname.includes("/swap") || 49 | window.location.pathname.includes("/market") || 50 | window.location.pathname.includes("/history") || 51 | window.location.pathname.includes("/send") 52 | ) { 53 | const requestTokenData = async () => { 54 | const response = await apiInit({ 55 | payload: { 56 | init_data: (window as any).Telegram.WebApp.initData, 57 | }, 58 | }); 59 | 60 | if (response instanceof Error && response.message === "busy") { 61 | return; 62 | } 63 | 64 | if (response && response.data) { 65 | await setApiConfig({ 66 | newConfigValue: response.data, 67 | }); 68 | } 69 | }; 70 | 71 | const requestAllCurrencies = async () => { 72 | const response = await getAllCurrencies(); 73 | 74 | if (response instanceof Error && response.message === "busy") { 75 | return; 76 | } 77 | 78 | if (response && response.data) { 79 | dispatch(userActions.setAllCurrencies(response.data?.currencies)); 80 | } 81 | }; 82 | 83 | const requestMyServerData = async () => { 84 | try { 85 | const response = await getMyServerData(); 86 | 87 | const langCode = response.data.lang_code; 88 | i18n.changeLanguage(langCode); 89 | 90 | dispatch(userActions.setServerData(response.data)); 91 | } catch (error: any) { 92 | if (error.response.data.error === "Unauthorized") { 93 | console.log("[xJetWallet] You are not authorized!"); 94 | } 95 | } 96 | }; 97 | 98 | const requestMyBalance = async () => { 99 | try { 100 | const response = await getMyBalance(); 101 | 102 | dispatch(userActions.setBalances(response.data?.balances)); 103 | } catch (e) { 104 | console.log("[xJetWallet | Balance] You are not authorized!"); 105 | } 106 | }; 107 | 108 | const requestExhangesPair = async () => { 109 | const response = await getExchangesPair(); 110 | 111 | dispatch(userActions.setExchangesPair(response.pairs)); 112 | }; 113 | 114 | if (mainnetInited) { 115 | return; 116 | } 117 | 118 | initMainnet().then(() => { 119 | requestAllCurrencies(); 120 | requestTokenData().then(() => { 121 | Promise.all([ 122 | requestMyServerData(), 123 | requestExhangesPair(), 124 | requestMyBalance(), 125 | ]); 126 | }); 127 | }); 128 | 129 | return; 130 | } 131 | 132 | router.navigate("/", { replace: true }); 133 | }, [dispatch, i18n]); 134 | 135 | useEffect(() => { 136 | if (intervalIdRef.current) { 137 | clearInterval(intervalIdRef.current); 138 | } 139 | 140 | intervalIdRef.current = setInterval(async () => { 141 | await balanceCheckWatcher(); 142 | }, 10000); 143 | 144 | return () => { 145 | clearInterval(intervalIdRef.current); 146 | intervalIdRef.current = undefined; 147 | }; 148 | }, []); 149 | 150 | useEffect(() => { 151 | try { 152 | amplitude.init(process.env.REACT_APP_AMPLITUDE_API_KEY as string); 153 | amplitude.track('App Opened'); 154 | } catch (e) { 155 | console.error('Amplitude initialization error:', e); 156 | } 157 | try { 158 | if ((window as any).Telegram) { 159 | const identifyEvent = new amplitude.Identify(); 160 | identifyEvent.set('telegram_id', (window as any).Telegram.WebApp.initDataUnsafe.user.id); 161 | amplitude.identify(identifyEvent); 162 | } 163 | } catch (e) { 164 | console.error('Telegram user identification error:', e); 165 | } 166 | }, []); 167 | 168 | useEffect(() => { 169 | (window as any) 170 | .Telegram 171 | .WebApp 172 | .SettingsButton 173 | .show() 174 | .onClick(openSettings); 175 | }, []); 176 | 177 | function openSettings() { 178 | router.navigate(ROUTE_NAMES.SETTINGS); 179 | } 180 | 181 | return ( 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | ); 192 | } 193 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./methods"; 2 | export * from "./utils"; 3 | -------------------------------------------------------------------------------- /src/api/utils/index.ts: -------------------------------------------------------------------------------- 1 | import nacl from "tweetnacl"; 2 | 3 | function toHexString(byteArray: any) { 4 | return Array.prototype.map 5 | .call(byteArray, function (byte) { 6 | return ("0" + (byte & 0xff).toString(16)).slice(-2); 7 | }) 8 | .join(""); 9 | } 10 | 11 | function fromHexString(hexString: string) { 12 | return Uint8Array.from( 13 | // @ts-ignore 14 | hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)) 15 | ); 16 | } 17 | 18 | export const sign_message = async (message: any, private_key: any) => { 19 | if (!private_key) { 20 | return; 21 | } 22 | 23 | if (!message.query_id) { 24 | message.query_id = Math.trunc(Date.now() / 1000 + 60) * 65536; 25 | } 26 | 27 | let keyPair = nacl.sign.keyPair.fromSeed(fromHexString(private_key)); 28 | private_key = keyPair.secretKey; 29 | 30 | let cleanMessage = JSON.stringify(message).replace(/\\n/g, ""); 31 | 32 | message.signature = nacl.sign.detached( 33 | new TextEncoder().encode(cleanMessage), 34 | private_key 35 | ); 36 | 37 | message.signature = toHexString(message.signature); 38 | return message; 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/ActionText/ActionText.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import cx from "classnames"; 3 | 4 | import { ActionTextProps } from "./ActionText.types"; 5 | 6 | import styles from "./ActionText.module.css"; 7 | import { Text } from "../Text"; 8 | 9 | export const ActionText: FC = ({ 10 | top, 11 | middle, 12 | bottom, 13 | withoutPadding, 14 | className = "", 15 | }) => { 16 | return ( 17 |
23 | {top ? ( 24 | 30 | {top} 31 | 32 | ) : null} 33 | {middle ? ( 34 | 35 | {middle} 36 | 37 | ) : null} 38 | {bottom ? ( 39 | 45 | {bottom} 46 | 47 | ) : null} 48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/ActionText/ActionText.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 10px; 5 | align-items: center; 6 | } 7 | 8 | .__with_padding { 9 | padding: 24px 0; 10 | } 11 | 12 | .__wrapper > span { 13 | text-align: center; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/ActionText/ActionText.types.ts: -------------------------------------------------------------------------------- 1 | export interface ActionTextProps { 2 | top?: string; 3 | middle?: string; 4 | bottom?: string; 5 | withoutPadding?: boolean; 6 | className?: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/ActionText/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ActionText.component"; 2 | -------------------------------------------------------------------------------- /src/components/AppTitle/AppTitle.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import { Text } from "../Text"; 4 | 5 | import { AppTitleProps } from "./AppTitle.types"; 6 | 7 | import { ReactComponent as Arrow15OutlineIcon } from "../../icons/Arrow15Outline.svg"; 8 | 9 | import styles from "./AppTitle.module.css"; 10 | 11 | export const AppTitle: FC = ({ screenName }) => { 12 | return ( 13 |
14 | 15 | xJet 16 | 17 | 18 | 19 | {screenName} 20 | 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/AppTitle/AppTitle.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | gap: 10px; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/AppTitle/AppTitle.types.ts: -------------------------------------------------------------------------------- 1 | export interface AppTitleProps { 2 | screenName: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/AppTitle/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AppTitle.component"; 2 | -------------------------------------------------------------------------------- /src/components/Avatar/Avatar.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useMemo, useState } from "react"; 2 | import cx from "classnames"; 3 | 4 | import { AvatarProps } from "./Avatar.types"; 5 | 6 | import styles from "./Avatar.module.css"; 7 | import { Text } from "../Text"; 8 | 9 | export const Avatar: FC = ({ 10 | src, 11 | alt, 12 | size = 24, 13 | type = "circle", 14 | fallbackName = "", 15 | className = "", 16 | }) => { 17 | const [isAvatarLoadError, setIsAvatarLoadError] = useState(!src); 18 | 19 | const onAvatarLoadFailed = () => { 20 | setIsAvatarLoadError(true); 21 | }; 22 | 23 | useEffect(() => { 24 | setIsAvatarLoadError(!src); 25 | }, [src]); 26 | 27 | const FallBackElement = useMemo(() => { 28 | if (typeof fallbackName === "object") { 29 | return fallbackName; 30 | } else { 31 | return ( 32 | 39 | {fallbackName} 40 | 41 | ); 42 | } 43 | }, [fallbackName]); 44 | 45 | return ( 46 |
57 | {!isAvatarLoadError ? ( 58 | {alt} 64 | ) : null} 65 | 66 | {isAvatarLoadError ? FallBackElement : null} 67 |
68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/components/Avatar/Avatar.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | overflow: hidden; 3 | max-width: 100%; 4 | aspect-ratio: 1 / 1; 5 | } 6 | 7 | .__wrapper:not(.__load_error) { 8 | display: inline-block; 9 | } 10 | 11 | .__load_error { 12 | display: inline-flex; 13 | align-items: center; 14 | justify-content: center; 15 | 16 | background-color: var(--background_avatar_fallback); 17 | } 18 | 19 | .__load_error > svg { 20 | color: var(--accent); 21 | } 22 | 23 | .__content { 24 | width: 100%; 25 | height: 100%; 26 | 27 | aspect-ratio: 1 / 1; 28 | object-fit: cover; 29 | } 30 | 31 | .__fallback_content { 32 | text-transform: uppercase; 33 | } 34 | 35 | .__type_circle { 36 | border-radius: var(--avatar_circle_border_radius); 37 | } 38 | 39 | .__type_square { 40 | border-radius: var(--avatar_square_border_radius); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Avatar/Avatar.types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export interface AvatarProps { 4 | size?: number | string; 5 | fallbackName?: ReactNode; 6 | src?: string; 7 | className?: string; 8 | alt?: string; 9 | type?: "square" | "circle"; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Avatar/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Avatar.component"; 2 | -------------------------------------------------------------------------------- /src/components/Block/Block.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import cx from "classnames"; 3 | 4 | import { BlockProps } from "./Block.types"; 5 | 6 | import styles from "./Block.module.css"; 7 | 8 | export const Block: FC = ({ 9 | children, 10 | style = {}, 11 | padding = 24, 12 | className = "", 13 | noBackground = false, 14 | onClick = () => {}, 15 | }) => { 16 | return ( 17 |
25 | {children} 26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/Block/Block.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | overflow: hidden; 3 | display: flex; 4 | } 5 | 6 | .__with_background { 7 | background-color: var(--background_block); 8 | border-radius: var(--block_border_radius); 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Block/Block.types.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ReactNode } from "react"; 2 | 3 | export interface BlockProps { 4 | className?: string; 5 | style?: CSSProperties; 6 | padding?: number | string; 7 | children?: ReactNode; 8 | noBackground?: boolean; 9 | onClick?: () => void; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Block/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Block.component"; 2 | -------------------------------------------------------------------------------- /src/components/BlockHeader/BlockHeader.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import cx from "classnames"; 3 | 4 | import { Text } from "../Text"; 5 | 6 | import { BlockHeaderProps } from "./BlockHeader.types"; 7 | 8 | import styles from "./BlockHeader.module.css"; 9 | 10 | export const BlockHeader: React.FC = ({ 11 | children, 12 | after, 13 | className = "", 14 | }) => { 15 | return ( 16 |
21 | {children ? ( 22 | 28 | {children} 29 | 30 | ) : null} 31 | {after ? ( 32 | 38 | {after} 39 | 40 | ) : null} 41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/components/BlockHeader/BlockHeader.module.css: -------------------------------------------------------------------------------- 1 | ._wrapper { 2 | display: inline-flex; 3 | align-items: center; 4 | 5 | gap: 10px; 6 | } 7 | 8 | ._content, 9 | ._after { 10 | flex: 1 1; 11 | display: flex; 12 | 13 | letter-spacing: 0.06em; 14 | } 15 | 16 | ._content { 17 | justify-content: flex-start; 18 | color: var(--accent); 19 | } 20 | 21 | ._after { 22 | justify-content: flex-end; 23 | color: var(--color_primary_color); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/BlockHeader/BlockHeader.types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export interface BlockHeaderProps { 4 | className?: string; 5 | children?: ReactNode; 6 | after?: ReactNode; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/BlockHeader/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./BlockHeader.component"; 2 | export * from "./BlockHeader.types"; 3 | -------------------------------------------------------------------------------- /src/components/Button/Button.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import cx from "classnames"; 3 | 4 | import { ButtonProps } from "./Button.types"; 5 | 6 | import styles from "./Button.module.css"; 7 | import { Text } from "../Text"; 8 | 9 | export const Button: FC = ({ 10 | before, 11 | children, 12 | stretched, 13 | disabled, 14 | color, 15 | style, 16 | hasHover = true, 17 | size = "s", 18 | mode = "primary", 19 | className = "", 20 | onClick = () => {}, 21 | }) => { 22 | return ( 23 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/Button/Button.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | display: inline-block; 3 | outline: none; 4 | border: none; 5 | padding: 12px; 6 | 7 | position: relative; 8 | 9 | border-radius: var(--buttons_border_radius); 10 | 11 | overflow: hidden; 12 | 13 | height: fit-content; 14 | margin: 0; 15 | } 16 | 17 | .__wrapper:not(.__disabled).__with_hover { 18 | cursor: pointer; 19 | } 20 | 21 | .__wrapper_in { 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | gap: 6px; 26 | } 27 | 28 | .__before { 29 | display: flex; 30 | } 31 | 32 | .__content { 33 | display: flex; 34 | } 35 | 36 | .__content_in { 37 | } 38 | 39 | /* ? mouse events ui handler */ 40 | 41 | .__wrapper:not(.__disabled):not(.__mode_primary).__with_hover:hover .__overlay { 42 | background-color: var(--button_hover_background); 43 | } 44 | 45 | .__wrapper:not(.__disabled):not(.__mode_primary).__with_hover:active 46 | .__overlay { 47 | background-color: var(--button_active_background); 48 | } 49 | 50 | .__wrapper:not(.__disabled).__mode_primary.__with_hover:hover .__overlay { 51 | background-color: var(--button_primary_hover_background); 52 | } 53 | 54 | .__wrapper:not(.__disabled).__mode_primary.__with_hover:active .__overlay { 55 | background-color: var(--button_primary_active_background); 56 | } 57 | 58 | .__overlay { 59 | position: absolute; 60 | top: 0; 61 | left: 0; 62 | width: 100%; 63 | height: 100%; 64 | 65 | transition: all 0.1s linear; 66 | } 67 | 68 | /* ? button state */ 69 | 70 | .__disabled { 71 | pointer-events: none; 72 | opacity: 0.5; 73 | } 74 | 75 | .__stretched { 76 | flex: 1; 77 | } 78 | 79 | /* ? sizes */ 80 | 81 | .__size_content { 82 | height: fit-content; 83 | width: fit-content; 84 | } 85 | 86 | .__size_s { 87 | min-height: 48px; 88 | } 89 | 90 | .__size_s .__content_in { 91 | font-weight: 600; 92 | font-size: 12px; 93 | line-height: 15px; 94 | } 95 | 96 | .__size_m { 97 | min-height: 53px; 98 | } 99 | 100 | .__size_m .__content_in { 101 | font-weight: 600; 102 | font-size: 14px; 103 | line-height: 17px; 104 | } 105 | 106 | .__size_filter { 107 | min-height: fit-content; 108 | width: fit-content; 109 | min-width: 50px; 110 | } 111 | 112 | .__size_filter .__content_in { 113 | font-size: 14px; 114 | font-weight: 600; 115 | } 116 | 117 | .__size_l { 118 | } 119 | 120 | /* ? modes */ 121 | 122 | .__mode_primary { 123 | background-color: var(--background_primary_button); 124 | } 125 | 126 | .__mode_primary .__wrapper_in, 127 | .__mode_primary .__content_in { 128 | color: var(--color_button_primary); 129 | } 130 | 131 | .__mode_secondary { 132 | background-color: var(--background_block); 133 | } 134 | 135 | .__mode_secondary_disabled { 136 | color: var(--background_primary_button); 137 | background-color: color-mix(in srgb, var(--background_primary_button), transparent 50%); 138 | } 139 | 140 | .__mode_secondary .__wrapper_in { 141 | color: var(--color_primary_color); 142 | } 143 | 144 | .__mode_secondary_with_accent_text { 145 | background-color: var(--background_block); 146 | } 147 | 148 | .__mode_transparent_with_accent_text .__wrapper_in, 149 | .__mode_transparent_with_accent_text .__content_in, 150 | .__mode_secondary_with_accent_text .__wrapper_in, 151 | .__mode_secondary_with_accent_text .__content_in { 152 | color: var(--accent); 153 | } 154 | 155 | .__mode_transparent { 156 | background-color: transparent; 157 | } 158 | 159 | .__mode_transparent_with_accent_text { 160 | background-color: transparent; 161 | } 162 | -------------------------------------------------------------------------------- /src/components/Button/Button.types.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ReactNode } from "react"; 2 | 3 | export interface ButtonProps { 4 | size?: "s" | "m" | "l" | "content" | "filter"; 5 | mode?: 6 | | "primary" 7 | | "secondary" 8 | | "secondary_disabled" 9 | | "secondary_with_accent_text" 10 | | "transparent" 11 | | "transparent_with_accent_text"; 12 | before?: ReactNode; 13 | stretched?: boolean; 14 | children?: ReactNode; 15 | className?: string; 16 | disabled?: boolean; 17 | hasHover?: boolean; 18 | color?: string; 19 | style?: CSSProperties; 20 | onClick?: () => void; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Button/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Button.component"; 2 | -------------------------------------------------------------------------------- /src/components/Cell/Cell.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import cx from "classnames"; 3 | 4 | import { CellProps } from "./Cell.types"; 5 | 6 | import { Text } from "../Text"; 7 | 8 | import styles from "./Cell.module.css"; 9 | 10 | export const Cell: FC = ({ 11 | before, 12 | after, 13 | children, 14 | description, 15 | afterStyles = {}, 16 | className = "", 17 | withCursor = false, 18 | onClick = () => {}, 19 | }) => { 20 | return ( 21 |
29 |
30 | {before ?
{before}
: null} 31 |
32 |
33 | {children} 34 |
35 | {description ? ( 36 |
37 | {description} 38 |
39 | ) : null} 40 |
41 | {after ? ( 42 |
43 | {after} 44 |
45 | ) : null} 46 |
47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/Cell/Cell.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | width: 100%; 3 | height: fit-content; 4 | } 5 | 6 | .__wrapper > a { 7 | text-decoration: none; 8 | } 9 | 10 | .__wrapper_in { 11 | display: flex; 12 | align-items: center; 13 | gap: 10px; 14 | } 15 | 16 | .__before { 17 | display: flex; 18 | } 19 | 20 | .__content { 21 | flex-grow: 1; 22 | overflow: hidden; 23 | 24 | justify-content: flex-start; 25 | display: flex; 26 | flex-direction: column; 27 | } 28 | 29 | .__content_in { 30 | } 31 | 32 | .__content_description { 33 | } 34 | 35 | .__content_description .__content_text { 36 | color: var(--accent); 37 | } 38 | 39 | .__after { 40 | display: flex; 41 | text-align: end; 42 | gap: 6px; 43 | align-items: center; 44 | overflow: hidden; 45 | } 46 | 47 | .__content_text { 48 | font-weight: 600; 49 | font-size: 14px; 50 | line-height: 17px; 51 | 52 | display: block; 53 | text-overflow: ellipsis; 54 | overflow: hidden; 55 | } 56 | 57 | /* ? Mouse cases */ 58 | 59 | .__with_cursor { 60 | cursor: pointer; 61 | } 62 | 63 | /* ? Cell render cases */ 64 | 65 | .__wrapper:not(.__with_description) .__content_text { 66 | } 67 | -------------------------------------------------------------------------------- /src/components/Cell/Cell.types.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ReactNode } from "react"; 2 | 3 | export interface CellProps { 4 | before?: ReactNode; 5 | children?: ReactNode; 6 | description?: ReactNode; 7 | after?: ReactNode; 8 | className?: string; 9 | href?: string; 10 | target?: string; 11 | withCursor?: boolean; 12 | afterStyles?: CSSProperties; 13 | onClick?: () => void; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Cell/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Cell.component"; 2 | -------------------------------------------------------------------------------- /src/components/ErrorBlock/ErrorBlock.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import { ErrorBlockProps } from "./ErrorBlock.types"; 4 | 5 | import { ReactComponent as Security24OutlineIcon } from "../../icons/Security24Outline.svg"; 6 | 7 | import styles from "./ErrorBlock.module.css"; 8 | import { Block } from "../Block"; 9 | import { Cell } from "../Cell"; 10 | 11 | export const ErrorBlock: FC = ({ 12 | color, 13 | iconColor, 14 | backgroundColor, 15 | text = "", 16 | }) => { 17 | return ( 18 | 26 | 31 | } 32 | > 33 | {text} 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/ErrorBlock/ErrorBlock.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | border: 2px solid var(--color_error); 3 | } 4 | -------------------------------------------------------------------------------- /src/components/ErrorBlock/ErrorBlock.types.ts: -------------------------------------------------------------------------------- 1 | export interface ErrorBlockProps { 2 | text?: string; 3 | color?: string; 4 | backgroundColor?: string; 5 | iconColor?: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/ErrorBlock/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ErrorBlock.component"; 2 | -------------------------------------------------------------------------------- /src/components/Filters/Filters.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { FiltersProps } from "./Filters.types"; 3 | import cx from "classnames"; 4 | import styles from "./Filters.module.css"; 5 | import { Button } from "../Button"; 6 | import { useTranslation } from "react-i18next"; 7 | 8 | export const Filters: FC = ({ 9 | setItem, 10 | selectedItem, 11 | menuItems, 12 | className = "", 13 | }) => { 14 | const { t } = useTranslation(); 15 | 16 | return ( 17 | <> 18 |
23 | {menuItems.map((value, id) => { 24 | return ( 25 | 40 | ); 41 | })} 42 |
43 | 44 | ); 45 | }; 46 | 47 | export default Filters; 48 | -------------------------------------------------------------------------------- /src/components/Filters/Filters.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | display: flex; 3 | justify-content: left; 4 | gap: 10px; 5 | } 6 | 7 | .__button { 8 | background-color: var(--tg-theme-button-color); 9 | opacity: 0.2; 10 | border-radius: 18px; 11 | } -------------------------------------------------------------------------------- /src/components/Filters/Filters.types.ts: -------------------------------------------------------------------------------- 1 | export interface FiltersProps { 2 | setItem: (item: string) => void; 3 | selectedItem: string; 4 | menuItems: string[]; 5 | className?: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Filters.component"; 2 | -------------------------------------------------------------------------------- /src/components/Group/Group.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import cx from "classnames"; 3 | 4 | import { GroupProps } from "./Group.types"; 5 | 6 | import styles from "./Group.module.css"; 7 | 8 | export const Group: FC = ({ 9 | space = 0, 10 | children, 11 | className = "", 12 | onClick, 13 | }) => { 14 | return ( 15 |
22 | {children} 23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/Group/Group.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/Group/Group.types.ts: -------------------------------------------------------------------------------- 1 | import { MouseEvent, ReactNode } from "react"; 2 | 3 | export interface GroupProps { 4 | space?: number; 5 | className?: string; 6 | children: ReactNode; 7 | onClick?(event: MouseEvent): any; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Group/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Group.component"; 2 | -------------------------------------------------------------------------------- /src/components/Input/Input.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useRef } from "react"; 2 | import cx from "classnames"; 3 | 4 | import { InputProps } from "./Input.types"; 5 | 6 | import styles from "./Input.module.css"; 7 | 8 | export const Input: FC = ({ 9 | disabled, 10 | defaultValue, 11 | value, 12 | after, 13 | inputMode, 14 | indicator, 15 | readonly, 16 | placeholder, 17 | style, 18 | selectAll, 19 | type = "text", 20 | className = "", 21 | onChange = () => {}, 22 | onFocus = () => {}, 23 | onBlur = () => {}, 24 | }) => { 25 | const inputRef = useRef(null); 26 | 27 | const onInputValueClick = () => { 28 | if (!selectAll) { 29 | return; 30 | } 31 | 32 | return inputRef.current && inputRef.current.select(); 33 | }; 34 | 35 | return ( 36 |
43 |
44 | 60 | {indicator ? ( 61 |
{indicator}
62 | ) : null} 63 | {after ?
{after}
: null} 64 |
65 |
66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /src/components/Input/Input.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | padding: 0 var(--input_padding); 3 | border-radius: var(--input_border_radius); 4 | background-color: var(--background_block); 5 | } 6 | 7 | .__wrapper_in { 8 | display: flex; 9 | align-items: center; 10 | gap: 18px; 11 | } 12 | 13 | .__content { 14 | flex-grow: 1; 15 | 16 | font-size: 14px; 17 | font-weight: 600px; 18 | line-height: 17px; 19 | 20 | color: var(--color_input_value); 21 | 22 | outline: none; 23 | border: none; 24 | background-color: transparent; 25 | 26 | overflow: hidden; 27 | 28 | padding: var(--input_padding) 0; 29 | margin: 0; 30 | } 31 | 32 | .__after { 33 | display: flex; 34 | } 35 | 36 | .__indicator { 37 | display: flex; 38 | } 39 | 40 | .__disabled { 41 | opacity: 0.5; 42 | pointer-events: none; 43 | } 44 | -------------------------------------------------------------------------------- /src/components/Input/Input.types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CSSProperties, 3 | ChangeEvent, 4 | HTMLInputTypeAttribute, 5 | ReactNode, 6 | } from "react"; 7 | 8 | export interface InputProps { 9 | value?: string; 10 | defaultValue?: string; 11 | after?: ReactNode; 12 | indicator?: ReactNode; 13 | className?: string; 14 | disabled?: boolean; 15 | readonly?: boolean; 16 | placeholder?: string; 17 | type?: HTMLInputTypeAttribute; 18 | selectAll?: boolean; 19 | onChange?: (e: ChangeEvent) => void; 20 | onFocus?: () => void; 21 | onBlur?: () => void; 22 | style?: CSSProperties; 23 | inputMode?: 24 | | "email" 25 | | "search" 26 | | "tel" 27 | | "text" 28 | | "url" 29 | | "none" 30 | | "numeric" 31 | | "decimal" 32 | | undefined; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Input/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Input.component"; 2 | -------------------------------------------------------------------------------- /src/components/Link/Link.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import cx from "classnames"; 3 | 4 | import { LinkProps } from "./Link.types"; 5 | 6 | import styles from "./Link.module.css"; 7 | 8 | export const Link: FC = ({ 9 | href, 10 | children, 11 | withCursor = false, 12 | target = "_blank", 13 | className = "", 14 | onClick = () => {}, 15 | }) => { 16 | if (!href) { 17 | return <>{children}; 18 | } 19 | 20 | return ( 21 | 30 | {children} 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/Link/Link.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | text-decoration: none; 3 | color: initial; 4 | } 5 | 6 | .__with_cursor { 7 | cursor: pointer; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Link/Link.types.ts: -------------------------------------------------------------------------------- 1 | import { HTMLAttributeAnchorTarget, ReactNode } from "react"; 2 | 3 | export interface LinkProps { 4 | className?: string; 5 | target?: HTMLAttributeAnchorTarget; 6 | children?: ReactNode; 7 | href?: string; 8 | withCursor?: boolean; 9 | onClick?: () => void; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Link/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Link.component"; 2 | -------------------------------------------------------------------------------- /src/components/Panel/Panel.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import cx from "classnames"; 3 | 4 | import { PanelProps } from "./Panel.types"; 5 | 6 | import styles from "./Panel.module.css"; 7 | 8 | export const Panel: FC = ({ 9 | children, 10 | header, 11 | className = "", 12 | centerVertical = false, 13 | centerHorizontal = false, 14 | }) => { 15 | return ( 16 |
25 | {header ? header : null} 26 |
{children}
27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/Panel/Panel.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | height: 100%; 3 | min-height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | 8 | .__wrapper_in { 9 | flex-grow: 1; 10 | padding: 24px; 11 | } 12 | 13 | .__with_header .__wrapper_in { 14 | margin-top: var(--panel_header_height); 15 | } 16 | 17 | .__center .__wrapper_in { 18 | display: flex; 19 | flex-direction: column; 20 | } 21 | 22 | .__center.__center_vertical .__wrapper_in { 23 | justify-content: center; 24 | } 25 | 26 | .__center.__center_horizontal .__wrapper_in { 27 | align-items: center; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Panel/Panel.types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export interface PanelProps { 4 | children: ReactNode; 5 | header?: any; 6 | className?: string; 7 | centerVertical?: boolean; 8 | centerHorizontal?: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Panel/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Panel.component"; 2 | -------------------------------------------------------------------------------- /src/components/PanelHeader/PanelHeader.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import cx from "classnames"; 3 | 4 | import { PanelHeaderProps } from "./PanelHeader.types"; 5 | 6 | import styles from "./PanelHeader.module.css"; 7 | 8 | export const PanelHeader: FC = ({ 9 | children, 10 | after, 11 | before, 12 | className = "", 13 | }) => { 14 | return ( 15 |
20 |
21 | {before ?
{before}
: null} 22 |
{children}
23 | {after ?
{after}
: null} 24 |
25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/PanelHeader/PanelHeader.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | height: var(--panel_header_height); 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | 8 | background-color: var(--panel_header_background); 9 | } 10 | 11 | .__wrapper_in { 12 | width: 100%; 13 | height: 100%; 14 | display: flex; 15 | align-items: center; 16 | } 17 | 18 | .__content { 19 | flex-grow: 1; 20 | text-align: center; 21 | } 22 | 23 | .__before, 24 | .__after { 25 | width: var(--panel_header_button_width); 26 | display: flex; 27 | align-items: center; 28 | justify-content: center; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/PanelHeader/PanelHeader.types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export interface PanelHeaderProps { 4 | children: ReactNode; 5 | after?: ReactNode; 6 | before?: ReactNode; 7 | className?: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/PanelHeader/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./PanelHeader.component"; 2 | -------------------------------------------------------------------------------- /src/components/RichCell/RichCell.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import cx from "classnames"; 3 | 4 | import { RichCellProps } from "./RichCell.types"; 5 | 6 | import { Text } from "../Text"; 7 | 8 | import styles from "./RichCell.module.css"; 9 | 10 | export const RichCell: FC = ({ 11 | before, 12 | after, 13 | children, 14 | description, 15 | afterStyles = {}, 16 | className = "", 17 | withCursor = false, 18 | }) => { 19 | return ( 20 |
27 |
28 | {before ?
{before}
: null} 29 |
30 |
31 | {children} 32 |
33 | {description ? ( 34 |
35 | {description} 36 |
37 | ) : null} 38 |
39 | {after ? ( 40 |
41 | {after} 42 |
43 | ) : null} 44 |
45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/RichCell/RichCell.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | width: 100%; 3 | height: fit-content; 4 | background: var(--background_block); 5 | 6 | border-radius: 18px; 7 | overflow: hidden; 8 | } 9 | 10 | .__wrapper > a { 11 | text-decoration: none; 12 | } 13 | 14 | .__wrapper_in { 15 | display: flex; 16 | align-items: flex-start; 17 | gap: 12px; 18 | padding: 12px; 19 | } 20 | 21 | .__before { 22 | display: flex; 23 | } 24 | 25 | .__content { 26 | flex-grow: 1; 27 | overflow: hidden; 28 | 29 | justify-content: flex-start; 30 | display: flex; 31 | flex-direction: column; 32 | 33 | gap: 6px; 34 | } 35 | 36 | .__content_in { 37 | } 38 | 39 | .__content_description { 40 | } 41 | 42 | .__content_description .__content_text { 43 | color: var(--color_gray_color); 44 | 45 | font-weight: 400; 46 | font-size: 12px; 47 | line-height: 15px; 48 | } 49 | 50 | .__content_in .__content_text { 51 | color: var(--accent); 52 | 53 | font-weight: 600; 54 | font-size: 12px; 55 | line-height: 15px; 56 | } 57 | 58 | .__after { 59 | display: flex; 60 | text-align: end; 61 | gap: 6px; 62 | align-items: center; 63 | overflow: hidden; 64 | } 65 | 66 | .__content_text { 67 | display: block; 68 | text-overflow: ellipsis; 69 | overflow: hidden; 70 | } 71 | 72 | /* ? Mouse cases */ 73 | 74 | .__with_cursor { 75 | cursor: pointer; 76 | transition: all 0.1s linear; 77 | } 78 | 79 | .__with_cursor:hover { 80 | background-color: var(--button_primary_active_background); 81 | } 82 | 83 | .__with_cursor:active { 84 | background-color: var(--button_primary_hover_background); 85 | } 86 | 87 | /* ? Cell render cases */ 88 | 89 | .__wrapper:not(.__with_description) .__content_text { 90 | } 91 | -------------------------------------------------------------------------------- /src/components/RichCell/RichCell.types.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ReactNode } from "react"; 2 | 3 | export interface RichCellProps { 4 | before?: ReactNode; 5 | children?: ReactNode; 6 | description?: ReactNode; 7 | after?: ReactNode; 8 | className?: string; 9 | href?: string; 10 | target?: string; 11 | withCursor?: boolean; 12 | afterStyles?: CSSProperties; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/RichCell/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RichCell.component"; 2 | -------------------------------------------------------------------------------- /src/components/Select/Select.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import cx from "classnames"; 3 | 4 | import { Text } from "../Text"; 5 | 6 | import { SelectProps } from "./Select.types"; 7 | 8 | import { ReactComponent as ArrowDown15OutlineIcon } from "../../icons/ArrowDown15Outline.svg"; 9 | 10 | import styles from "./Select.module.css"; 11 | 12 | export const Select: FC = ({ 13 | disabled, 14 | style, 15 | value = "", 16 | className = "", 17 | onClick = () => {}, 18 | }) => { 19 | return ( 20 |
26 |
27 |
28 | 35 | {value} 36 | 37 | {!disabled ? ( 38 |
39 | 40 |
41 | ) : null} 42 |
43 |
44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/Select/Select.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | } 3 | 4 | .__wrapepr_in { 5 | position: relative; 6 | } 7 | 8 | .__content { 9 | display: flex; 10 | align-items: center; 11 | gap: 10px; 12 | cursor: pointer; 13 | } 14 | 15 | .__content_in { 16 | user-select: none; 17 | } 18 | 19 | .__content_icon { 20 | display: flex; 21 | } 22 | 23 | .__list_popout { 24 | position: absolute; 25 | max-height: 165px; 26 | width: fit-content; 27 | overflow: auto; 28 | z-index: 10; 29 | right: -18px; 30 | 31 | background-color: var(--background_content); 32 | 33 | border-radius: var(--select_option_list_border_radius); 34 | min-width: max(100%, 100px); 35 | max-width: 116px; 36 | } 37 | 38 | .__list_popout_space { 39 | height: 18px; 40 | background-color: var(--background_block); 41 | } 42 | 43 | .__list_popout_in { 44 | background-color: var(--background_block); 45 | overflow: auto; 46 | max-height: calc(165px - 18px); 47 | scrollbar-color: var(--accent); 48 | } 49 | 50 | .__list_popout_in::-webkit-scrollbar-track { 51 | background-color: var(--accent); 52 | } 53 | 54 | .__list_option { 55 | padding: 10px 15px; 56 | 57 | background-color: var(--background_block); 58 | 59 | cursor: pointer; 60 | 61 | user-select: none; 62 | 63 | text-align: center; 64 | overflow: hidden; 65 | text-overflow: ellipsis; 66 | } 67 | -------------------------------------------------------------------------------- /src/components/Select/Select.types.ts: -------------------------------------------------------------------------------- 1 | export interface SelectProps { 2 | value?: string | null; 3 | className?: string; 4 | disabled?: boolean; 5 | onClick?: () => void; 6 | style?: any; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Select/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Select.component"; 2 | -------------------------------------------------------------------------------- /src/components/Separator/Separator.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import cx from "classnames"; 3 | 4 | import { SeparatorProps } from "./Separator.types"; 5 | 6 | import styles from "./Separator.module.css"; 7 | 8 | export const Separator: React.FC = ({ className = "" }) => { 9 | return ( 10 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/Separator/Separator.module.css: -------------------------------------------------------------------------------- 1 | ._wrapper { 2 | background-color: var(--space_border_color); 3 | width: 100%; 4 | height: 1px; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Separator/Separator.types.ts: -------------------------------------------------------------------------------- 1 | export interface SeparatorProps { 2 | className?: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/Separator/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Separator.component"; 2 | export * from "./Separator.types"; 3 | -------------------------------------------------------------------------------- /src/components/Switch/Switch.component.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, FC, useEffect, useState } from "react"; 2 | import cx from "classnames"; 3 | 4 | import { SwitchProps } from "./Switch.types"; 5 | 6 | import styles from "./Switch.module.css"; 7 | 8 | export const Switch: FC = ({ 9 | disabled, 10 | checked = false, 11 | className = "", 12 | onChange = () => {}, 13 | }) => { 14 | const [isChecked, setIsChecked] = useState(checked); 15 | 16 | useEffect(() => { 17 | setIsChecked(checked); 18 | }, [checked]); 19 | 20 | return ( 21 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/Switch/Switch.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | position: relative; 3 | display: inline-block; 4 | width: 35px; 5 | height: 18px; 6 | } 7 | 8 | .__content { 9 | opacity: 0; 10 | width: 0; 11 | height: 0; 12 | } 13 | 14 | .__slider { 15 | position: absolute; 16 | cursor: pointer; 17 | top: 0; 18 | left: 0; 19 | right: 0; 20 | bottom: 0; 21 | background-color: var(--background_switch); 22 | -webkit-transition: 0.4s; 23 | transition: 0.2s; 24 | 25 | border-radius: 37px; 26 | } 27 | 28 | .__slider::before { 29 | position: absolute; 30 | content: ""; 31 | height: 14px; 32 | width: 14px; 33 | left: 2px; 34 | bottom: 2px; 35 | background-color: var(--accent); 36 | -webkit-transition: 0.4s; 37 | transition: 0.2s; 38 | 39 | border-radius: 37px; 40 | } 41 | 42 | .__content:checked + .__slider:before { 43 | transform: translateX(17px); 44 | } 45 | 46 | /* ? Switch render cases */ 47 | 48 | .__disabled { 49 | } 50 | -------------------------------------------------------------------------------- /src/components/Switch/Switch.types.ts: -------------------------------------------------------------------------------- 1 | export interface SwitchProps { 2 | checked?: boolean; 3 | className?: string; 4 | disabled?: boolean; 5 | onChange?: (newValue: boolean) => void; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Switch/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Switch.component"; 2 | -------------------------------------------------------------------------------- /src/components/Text/Text.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | import cx from "classnames"; 4 | 5 | import { TextProps } from "./Text.types"; 6 | 7 | export const Text: FC = ({ 8 | weight = "", 9 | size, 10 | lineHeight, 11 | children, 12 | style = {}, 13 | className = "", 14 | color = "", 15 | }) => { 16 | return ( 17 | 27 | {children} 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/Text/Text.types.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ReactNode } from "react"; 2 | 3 | export interface TextProps { 4 | weight?: "100" | "200" | "300" | "400" | "500" | "600" | "700" | "800"; 5 | size?: number; 6 | lineHeight?: number | string; 7 | color?: string; 8 | className?: string; 9 | style?: CSSProperties; 10 | children: ReactNode; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Text/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Text.component"; 2 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./PanelHeader"; 2 | export * from "./Panel"; 3 | export * from "./Text"; 4 | export * from "./AppTitle"; 5 | export * from "./ActionText"; 6 | export * from "./Button"; 7 | export * from "./Group"; 8 | export * from "./Cell"; 9 | export * from "./Avatar"; 10 | export * from "./Link"; 11 | export * from "./Block"; 12 | export * from "./Input"; 13 | export * from "./Switch"; 14 | export * from "./Select"; 15 | export * from "./ErrorBlock"; 16 | export * from "./BlockHeader"; 17 | export * from "./RichCell"; 18 | export * from "./Separator"; 19 | export * from "./Filters"; 20 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const isMobile: boolean = /iPhone|iPad|iPod|Android/i.test( 2 | navigator.userAgent 3 | ); 4 | -------------------------------------------------------------------------------- /src/constants/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": [ 3 | { 4 | "id": "xApps", 5 | "title": "xApps", 6 | "cells": [ 7 | { 8 | "title": "Tonbet", 9 | "subtitle": "The betting platform on the TON blockchain", 10 | "imageUrl": "https://github.com/xJetLabs/assets/blob/main/tonbet-logo.jpeg?raw=true", 11 | "action": "https://t.me/tonbetapp_bot?start=referral_code_R-BET-XJET", 12 | "type": "text" 13 | }, 14 | { 15 | "title": "INVADERS RPG", 16 | "subtitle": "The RPG game in Telegram", 17 | "imageUrl": "https://github.com/xJetLabs/assets/blob/main/inc-logo.jpeg?raw=true", 18 | "action": "https://t.me/ton_invaders_bot?start=263737173", 19 | "type": "text" 20 | }, 21 | { 22 | "title": "EXTON", 23 | "subtitle": "Exchange on xJetSwap API", 24 | "imageUrl": "https://github.com/xJetLabs/assets/blob/main/exton-logo.jpeg?raw=true", 25 | "action": "https://t.me/EXTON_SWAP_BOT", 26 | "type": "text" 27 | } 28 | ], 29 | "showedOnMainScreen": 3 30 | }, 31 | { 32 | "id": "xJetNews", 33 | "title": "xJetNews", 34 | "cells": [ 35 | { 36 | "title": "Swaps available in xJetSwap!", 37 | "subtitle": "You can swap TON, IVS, EXC and more on dedust!", 38 | "imageUrl": "https://raw.githubusercontent.com/xJetLabs/assets/main/xjet-logo.png", 39 | "action": "https://t.me/xJetSwapBot/swap", 40 | "type": "text" 41 | }, 42 | { 43 | "title": "NFTs in xJet!", 44 | "subtitle": "Now you can use NFT in our bot, besides the possibility of convenient sale contracts will appear soon.", 45 | "imageUrl": "https://raw.githubusercontent.com/xJetLabs/assets/main/64523743ad0e4caacf782d0f.png", 46 | "action": "https://t.me/xJetNews/45", 47 | "type": "text" 48 | }, 49 | { 50 | "title": "Buying TON with a bank card is already available!", 51 | "subtitle": "TLDR; first post", 52 | "imageUrl": "https://github.com/xJetLabs/assets/blob/main/ton_symbol.png?raw=true", 53 | "action": "https://t.me/xJetNews/44", 54 | "type": "text" 55 | } 56 | ], 57 | "showedOnMainScreen": 2 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /src/hooks/useQuery.ts: -------------------------------------------------------------------------------- 1 | import { useLocation } from "react-router-dom"; 2 | import { useMemo } from "react"; 3 | 4 | function useQuery() { 5 | const { search } = useLocation(); 6 | 7 | return useMemo(() => new URLSearchParams(search), [search]); 8 | } 9 | 10 | export { useQuery }; 11 | -------------------------------------------------------------------------------- /src/icons/Arrow15Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/ArrowDown15Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/AstralyxLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /src/icons/Back24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/BankCard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/Burn24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/Cart18Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/icons/Chains20Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/Cheque24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/Copy20Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/CopySuccess24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /src/icons/Date24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/File24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/GoArrow24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/History24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/Info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/Invoice24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 11 | 13 | 15 | 16 | 18 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/icons/LogoOutline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 10 | 12 | 14 | 15 | 17 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/icons/LogoQR40Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 10 | 12 | 14 | 15 | 17 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/icons/Menu24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /src/icons/MinusSmall.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/Picture24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/PlusSmall.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/Program24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/QRCopy17Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/Receipt18Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/icons/Receive24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/Search17Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/Security24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/Send24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/Settings24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/Star24.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/Star24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/Swap24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/Switch15Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/Switcher.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/Switching24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/icons/Text24Outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/USAFlag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/icons/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/russianFlag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/ton.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xJetLabs/wallet-webapp/9c1428a5a9dda2b5756d5613ec92d026879fa9d7/src/images/ton.jpeg -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { Provider } from "react-redux"; 4 | 5 | import { store } from "./store"; 6 | 7 | import { App } from "./App"; 8 | 9 | import "./styles/global.css"; 10 | 11 | const root = ReactDOM.createRoot( 12 | document.getElementById("root") as HTMLElement 13 | ); 14 | root.render( 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /src/panels/ErrorDeposit/ErrorDeposit.module.css: -------------------------------------------------------------------------------- 1 | .error { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | .img { 8 | width: 100px; 9 | height: 100px; 10 | } -------------------------------------------------------------------------------- /src/panels/ErrorDeposit/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import { Button, Group, Input, Panel, Text } from "../../components"; 5 | import { ReactComponent as Error } from "../../icons/cross.svg"; 6 | import style from "./ErrorDeposit.module.css"; 7 | 8 | export const ErrorDeposit: FC = () => { 9 | const { t } = useTranslation(); 10 | 11 | return ( 12 | 13 | 14 | 15 | {t("Deposits are no longer available")} 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/panels/History/History.module.css: -------------------------------------------------------------------------------- 1 | .logo_animation { 2 | overflow: visible; 3 | 4 | animation: logoLoading ease-in-out infinite 1s; 5 | } 6 | 7 | @keyframes logoLoading { 8 | 0% { 9 | transform: scale(1); 10 | } 11 | 50% { 12 | transform: scale(1.1); 13 | } 14 | 100% { 15 | transform: scale(1); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/panels/History/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./History.panel"; 2 | -------------------------------------------------------------------------------- /src/panels/Home/Home.module.css: -------------------------------------------------------------------------------- 1 | .__buttonGroup { 2 | display: flex; 3 | } 4 | 5 | .__buttonGroup > button { 6 | min-height: 53px; 7 | } 8 | 9 | .__action_text { 10 | padding: 48px 24px; 11 | } 12 | 13 | .__buttonGroup > button:not(:first-child):not(:last-child) { 14 | border-radius: 0; 15 | border-width: 0 1px; 16 | border-color: var(--space_border_color); 17 | border-style: solid; 18 | } 19 | 20 | .__buttonGroup > button:first-child { 21 | border-top-right-radius: 0; 22 | border-bottom-right-radius: 0; 23 | } 24 | 25 | .__buttonGroup > button:last-child { 26 | border-top-left-radius: 0; 27 | border-bottom-left-radius: 0; 28 | } 29 | 30 | .__buttonGroup_main { 31 | display: flex; 32 | gap: 10px; 33 | } 34 | 35 | .__buttonGroup_main > button > div { 36 | flex-direction: column; 37 | } 38 | 39 | .__group { 40 | margin-top: 24px; 41 | } 42 | 43 | .__logo_icon { 44 | width: 24px; 45 | overflow: visible; 46 | position: relative; 47 | left: -6px; 48 | } 49 | 50 | .__jetton_with_url_title { 51 | display: flex; 52 | align-items: center; 53 | gap: 10px; 54 | } 55 | 56 | .__panel_mobile .__balance_group_focused .__buttonGroup, 57 | .__panel_mobile .__balance_group_focused .__action_text { 58 | display: none; 59 | } 60 | 61 | .__panel_mobile .__balance_group_focused, 62 | .__panel_mobile .__balance_group_focused .__button_group { 63 | row-gap: 0 !important; 64 | } 65 | 66 | .__centered_text { 67 | display: flex; 68 | width: 100%; 69 | text-align: center; 70 | Display: flex; 71 | justify-content: center; 72 | } -------------------------------------------------------------------------------- /src/panels/Home/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Home.panel"; 2 | -------------------------------------------------------------------------------- /src/panels/Load/Load.module.css: -------------------------------------------------------------------------------- 1 | .logo_animation { 2 | overflow: visible; 3 | 4 | animation: logoLoading ease-in-out infinite 1s; 5 | } 6 | 7 | @keyframes logoLoading { 8 | 0% { 9 | transform: scale(1); 10 | } 11 | 50% { 12 | transform: scale(1.1); 13 | } 14 | 100% { 15 | transform: scale(1); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/panels/Load/Load.panel.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { useDispatch } from "react-redux"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | import { 7 | apiInit, 8 | getAllCurrencies, 9 | getExchangesPair, 10 | getMyBalance, 11 | getMyServerData, 12 | initMainnet, 13 | mainnetInited, 14 | setApiConfig, 15 | } from "../../api"; 16 | 17 | import { userActions } from "../../store/reducers"; 18 | 19 | import { ROUTE_NAMES } from "../../router/constants"; 20 | 21 | import { Panel } from "../../components"; 22 | 23 | import { ReactComponent as LogoIcon } from "../../icons/Logo.svg"; 24 | 25 | import styles from "./Load.module.css"; 26 | 27 | export const LoadPanel: FC = () => { 28 | const navigate = useNavigate(); 29 | const dispatch = useDispatch(); 30 | const { i18n } = useTranslation(); 31 | 32 | useEffect(() => { 33 | const requestTokenData = async () => { 34 | const response = await apiInit({ 35 | payload: { 36 | init_data: (window as any).Telegram.WebApp.initData, 37 | }, 38 | }); 39 | 40 | if (response instanceof Error && response.message === "busy") { 41 | return; 42 | } 43 | 44 | if (response && response.data) { 45 | await setApiConfig({ 46 | newConfigValue: response.data, 47 | }); 48 | } 49 | }; 50 | 51 | const requestAllCurrencies = async () => { 52 | const response = await getAllCurrencies(); 53 | 54 | if (response instanceof Error && response.message === "busy") { 55 | return; 56 | } 57 | 58 | if (response && response.data) { 59 | dispatch(userActions.setAllCurrencies(response.data?.currencies)); 60 | } 61 | }; 62 | 63 | const requestMyServerData = async () => { 64 | try { 65 | const response = await getMyServerData(); 66 | 67 | const langCode = response.data.lang_code; 68 | i18n.changeLanguage(langCode); 69 | 70 | dispatch(userActions.setServerData(response.data)); 71 | } catch (error: any) { 72 | if (error.response.data.error === "Unauthorized") { 73 | console.log("[xJetWallet] You are not authorized!"); 74 | } 75 | } 76 | }; 77 | 78 | const requestMyBalance = async () => { 79 | try { 80 | const response = await getMyBalance(); 81 | 82 | dispatch(userActions.setBalances(response.data?.balances)); 83 | 84 | navigate(ROUTE_NAMES.HOME, { 85 | replace: true, 86 | }); 87 | } catch (e) { 88 | console.log("[xJetWallet | Balance] You are not authorized!"); 89 | } 90 | }; 91 | 92 | const requestExhangesPair = async () => { 93 | const response = await getExchangesPair(); 94 | 95 | dispatch(userActions.setExchangesPair(response.pairs)); 96 | }; 97 | 98 | // if (mainnetInited) { 99 | if (mainnetInited) { 100 | return; 101 | } 102 | 103 | initMainnet().then(() => { 104 | requestAllCurrencies(); 105 | requestTokenData().then(() => { 106 | Promise.all([ 107 | requestMyServerData(), 108 | requestExhangesPair(), 109 | requestMyBalance(), 110 | ]); 111 | }); 112 | }); 113 | }, [navigate, dispatch, i18n]); 114 | 115 | return ( 116 | 117 | 118 | 119 | ); 120 | }; 121 | -------------------------------------------------------------------------------- /src/panels/Load/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Load.panel"; 2 | -------------------------------------------------------------------------------- /src/panels/Menu/Menu.module.css: -------------------------------------------------------------------------------- 1 | .button_group { 2 | display: flex; 3 | align-items: center; 4 | gap: 12px; 5 | } 6 | 7 | .block_content { 8 | } 9 | 10 | .button_group > button > div { 11 | flex-direction: column; 12 | } 13 | 14 | .block_header_button { 15 | cursor: pointer; 16 | } 17 | 18 | .block_cell:first-of-type:not(:last-of-type), 19 | a.block_cell:first-of-type:not(:last-of-type) > div { 20 | border-bottom-left-radius: 0; 21 | border-bottom-right-radius: 0; 22 | } 23 | 24 | .block_cell:not(:first-of-type):not(:last-of-type), 25 | a.block_cell:not(:first-of-type):not(:last-of-type) > div { 26 | border-radius: 0; 27 | } 28 | 29 | .block_cell:last-of-type:not(:first-of-type), 30 | a.block_cell:last-of-type:not(:first-of-type) > div { 31 | border-top-left-radius: 0; 32 | border-top-right-radius: 0; 33 | } 34 | 35 | .__block_header { 36 | padding: 0 12px; 37 | } 38 | 39 | .block_cell_title { 40 | text-overflow: ellipsis; 41 | display: -webkit-box; 42 | -webkit-line-clamp: 1; /* number of lines to show */ 43 | line-clamp: 1; 44 | -webkit-box-orient: vertical; 45 | color: inherit; 46 | } 47 | 48 | .block_cell_description { 49 | overflow: hidden; 50 | text-overflow: ellipsis; 51 | display: -webkit-box; 52 | -webkit-line-clamp: 2; /* number of lines to show */ 53 | line-clamp: 2; 54 | -webkit-box-orient: vertical; 55 | } 56 | -------------------------------------------------------------------------------- /src/panels/Menu/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Menu.panel"; 2 | -------------------------------------------------------------------------------- /src/panels/MenuExpanded/MenuExpanded.module.css: -------------------------------------------------------------------------------- 1 | .block_content { 2 | } 3 | 4 | .block_cell:first-of-type:not(:last-of-type), 5 | a.block_cell:first-of-type:not(:last-of-type) > div { 6 | border-bottom-left-radius: 0; 7 | border-bottom-right-radius: 0; 8 | } 9 | 10 | .block_cell:not(:first-of-type):not(:last-of-type), 11 | a.block_cell:not(:first-of-type):not(:last-of-type) > div { 12 | border-radius: 0; 13 | } 14 | 15 | .block_cell:last-of-type:not(:first-of-type), 16 | a.block_cell:last-of-type:not(:first-of-type) > div { 17 | border-top-left-radius: 0; 18 | border-top-right-radius: 0; 19 | } 20 | 21 | .block_cell_title { 22 | text-overflow: ellipsis; 23 | display: -webkit-box; 24 | -webkit-line-clamp: 1; /* number of lines to show */ 25 | line-clamp: 1; 26 | -webkit-box-orient: vertical; 27 | color: inherit; 28 | } 29 | 30 | .block_cell_description { 31 | overflow: hidden; 32 | text-overflow: ellipsis; 33 | display: -webkit-box; 34 | -webkit-line-clamp: 2; /* number of lines to show */ 35 | line-clamp: 2; 36 | -webkit-box-orient: vertical; 37 | } 38 | -------------------------------------------------------------------------------- /src/panels/MenuExpanded/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MenuExpanded.panel"; 2 | -------------------------------------------------------------------------------- /src/panels/Nft/Nft.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | display: flex; 3 | flex-wrap: wrap; 4 | gap: 1em; 5 | } 6 | 7 | .__wrapper .__block { 8 | flex: 1 0 calc(50% - 18px); 9 | flex-basis: calc(50% - 18px); 10 | max-width: calc(50% - 23px); 11 | border: 1px solid var(--tg-theme-secondary-bg-color); 12 | border-radius: 15px; 13 | padding: 5px; 14 | background-color: var(--tg-theme-secondary-bg-color); 15 | color: var(--tg-theme-text-color); 16 | cursor: pointer; 17 | } 18 | 19 | .__wrapper .__block > div:first-child { 20 | border-radius: 10px; 21 | } 22 | 23 | .__content { 24 | display: flex; 25 | flex-direction: column; 26 | justify-content: center; 27 | align-items: center; 28 | height: 80vh; 29 | } 30 | 31 | .__button { 32 | display: "flex"; 33 | flex-Direction: row; 34 | align-Items: center; 35 | justify-Content: center; 36 | width: 100%; 37 | max-height: 10px; 38 | margin-top: 20px; 39 | } 40 | -------------------------------------------------------------------------------- /src/panels/Nft/NftDetail.panel.module.css: -------------------------------------------------------------------------------- 1 | .__wrapper { 2 | display: flex; 3 | flex-wrap: wrap; 4 | gap: 12px; 5 | padding: 1.5rem; 6 | } 7 | 8 | .__button_group { 9 | display: flex; 10 | flex-flow: column; 11 | gap: 12px; 12 | width: 100%; 13 | } 14 | 15 | .__sell_button { 16 | background: var(--background_block); 17 | } 18 | -------------------------------------------------------------------------------- /src/panels/Nft/NftDetail.panel.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | import styles from "./NftDetail.panel.module.css"; 4 | import { Avatar, Button, Text } from "../../components"; 5 | import { useNavigate, useParams } from "react-router-dom"; 6 | import { getUserNFT } from "../../api"; 7 | import { NFT } from "../../types"; 8 | import { useSelector } from "react-redux"; 9 | import { myTonAddressSelector } from "../../store/reducers/user/user.selectors"; 10 | import { useTranslation } from "react-i18next"; 11 | import { useQuery } from "../../hooks/useQuery"; 12 | 13 | import * as amplitude from '@amplitude/analytics-browser'; 14 | 15 | export function NftDetailPanel() { 16 | const params = useParams(); 17 | const navigate = useNavigate(); 18 | const { t } = useTranslation(); 19 | const query: any = useQuery(); 20 | 21 | const [isLoaded, setIsLoaded] = useState(false); 22 | const [currentNft, setCurrentNtf] = useState(); 23 | const myTonAddress = useSelector(myTonAddressSelector); 24 | 25 | const navigateToSendNtf = () => { 26 | amplitude.track("NFTDetailedPage.SendButton.Pushed"); 27 | try { 28 | window.navigator.vibrate(70); 29 | } catch (error) { 30 | (window as any).Telegram.WebApp.HapticFeedback.impactOccurred("light"); 31 | } 32 | 33 | navigate(`/nft/${params.address}/send`); 34 | }; 35 | 36 | const navigateToSellNtf = () => { 37 | amplitude.track("NFTDetailedPage.SellButton.Pushed"); 38 | try { 39 | window.navigator.vibrate(70); 40 | } catch (error) { 41 | (window as any).Telegram.WebApp.HapticFeedback.impactOccurred("light"); 42 | } 43 | 44 | navigate(`/nft/${params.address}/sell`); 45 | }; 46 | 47 | useEffect(() => { 48 | amplitude.track("NFTDetailedPage.Launched"); 49 | // Если нет tonAddress в параметрах url, то применяются эти стили как дефолтные 50 | if (query.get("tonAddress") !== null) { 51 | document.body.style.setProperty("--tg-color-scheme", "dark"); 52 | document.body.style.setProperty("--tg-theme-bg-color", "#212121"); 53 | document.body.style.setProperty("--tg-theme-button-color", "#8774e1"); 54 | document.body.style.setProperty( 55 | "--tg-theme-button-text-color", 56 | "#ffffff" 57 | ); 58 | document.body.style.setProperty("--tg-theme-hint-color", "#aaaaaa"); 59 | document.body.style.setProperty("--tg-theme-link-color", "#8774e1"); 60 | document.body.style.setProperty( 61 | "--tg-theme-secondary-bg-color", 62 | "#181818" 63 | ); 64 | document.body.style.setProperty("--tg-theme-text-color", "#fff"); 65 | document.body.style.setProperty("--tg-viewport-height", "100vh"); 66 | document.body.style.setProperty("--tg-viewport-stable-height", "100vh"); 67 | } 68 | }, [query]); 69 | 70 | useEffect(() => { 71 | getUserNFT(myTonAddress || query.get("tonAddress")).then((data) => { 72 | const current = data.filter( 73 | (item: any) => item.address === params.address 74 | ); 75 | setCurrentNtf(current[0]); 76 | setIsLoaded(true); 77 | }); 78 | }, [params.address, myTonAddress, query]); 79 | 80 | if (isLoaded) return ( 81 |
82 | 83 | 84 | 85 | {currentNft?.metadata.name} 86 | 97 | NFT 98 | 99 | 100 | 101 | 108 | {currentNft?.metadata.description} 109 | 110 | 111 | {currentNft?.collection && ( 112 | 118 | {t("Collection")}{" "} 119 | 125 | {currentNft.collection.name} 126 | 127 | 128 | )} 129 | 130 | {query.get("tonAddress") === null && ( 131 |
132 | 140 | 141 | 150 |
151 | )} 152 |
153 | ); 154 | return
; 155 | } 156 | -------------------------------------------------------------------------------- /src/panels/Nft/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Nft.panel"; 2 | export * from "./NftDetail.panel"; 3 | -------------------------------------------------------------------------------- /src/panels/PurchaseTonPage/PurchaseFiatSelect.panel.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useContext } from "react"; 2 | 3 | import { SwapDataContext } from "../../providers/SwapDataContextProvider"; 4 | 5 | import { 6 | Block, 7 | Cell, 8 | Group, 9 | Input, 10 | Panel, 11 | } from "../../components"; 12 | 13 | import { ReactComponent as Search17OutlineIcon } from "../../icons/Search17Outline.svg"; 14 | 15 | import { useLocation, useNavigate } from "react-router-dom"; 16 | import { ReactComponent as BankCard } from "../../icons/BankCard.svg"; 17 | 18 | export const PurchaseFiatSelect: FC = () => { 19 | const { setData, allTokens }: any = 20 | useContext(SwapDataContext); 21 | const { state } = useLocation(); 22 | const navigate = useNavigate(); 23 | 24 | const { position } = state; 25 | 26 | const tokensToRender = allTokens.reduce( 27 | (result: object[], current: any) => { 28 | result.push({ 29 | symbol: current.base_symbol?.toLowerCase(), 30 | image: current.image, 31 | }); 32 | return result; 33 | }, 34 | [] 35 | ); 36 | 37 | const tokenSelected = (currency: string) => { 38 | setData((prev: any) => ({ 39 | ...prev, 40 | selectedTokens: { 41 | ...prev.selectedTokens, 42 | [position]: currency, 43 | priceInTon: 0, 44 | }, 45 | })); 46 | 47 | navigate(-1); 48 | }; 49 | 50 | return ( 51 | 52 | 53 | } /> 54 | {tokensToRender.map((v: any, index: number) => { 55 | return ( 56 | 57 | 61 | } 62 | onClick={() => 63 | tokenSelected(v.symbol?.toUpperCase()) 64 | } 65 | > 66 | {v?.symbol?.toUpperCase()} 67 | 68 | 69 | ); 70 | })} 71 | 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /src/panels/PurchaseTonPage/PurchaseTonFirstStep.panel.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState, useContext } from "react"; 2 | import { SwapDataContext } from "../../providers/SwapDataContextProvider"; 3 | import { initFiatPayment } from "../../api/methods"; 4 | import { useLocation } from "react-router-dom"; 5 | import { 6 | Button, 7 | Group, 8 | Panel, 9 | Text, 10 | } from "../../components"; 11 | import { Timeline } from "antd"; 12 | import { useTranslation } from "react-i18next"; 13 | 14 | export const PurchaseTonFirstStep: FC = () => { 15 | const { setData, ...data }: any = useContext(SwapDataContext); 16 | const { t } = useTranslation(); 17 | const { state } = useLocation(); 18 | 19 | const renderStep = (step: string) => { 20 | return {step}; 21 | }; 22 | const [paymentState, setPaymentState] = useState({ 23 | renderSteps: [renderStep(t("Creating a payment") as string)], 24 | pendingStep: Waiting for details, 25 | paymentId: "", 26 | paymentUrl: "", 27 | }); 28 | 29 | const wasPaid = () => { 30 | window.open(paymentState.paymentUrl, "_blank"); 31 | }; 32 | 33 | useEffect(() => { 34 | const initPayment = async () => { 35 | const payment = (await initFiatPayment(state.inAmount)).data; 36 | 37 | if (!("id" in payment)) throw new Error("No payment id"); 38 | if (typeof payment.id != "string") 39 | throw new Error("Payment id is not a string"); 40 | 41 | setPaymentState({ 42 | renderSteps: [ 43 | ...paymentState.renderSteps, 44 | renderStep( 45 | t("Waiting for details. Created payment with id ") + payment.id 46 | ), 47 | ], 48 | pendingStep: ( 49 | 50 | 51 | Please pay {state.inAmount} {state.currency} using button below: 52 | 53 |
54 |
55 | ), 56 | paymentId: payment.id, 57 | paymentUrl: payment.fiat_gateway, 58 | }); 59 | }; 60 | 61 | initPayment(); 62 | }, [setPaymentState]); 63 | 64 | return ( 65 | 70 |
71 | { 81 | return { 82 | children: step, 83 | }; 84 | })} 85 | /> 86 | {document.body.innerText.indexOf(t("Waiting for details")) != -1 ? ( 87 | 88 | 91 | 92 | ) : null} 93 |
94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /src/panels/PurchaseTonPage/PurchaseTonPage.module.css: -------------------------------------------------------------------------------- 1 | .__button_group { 2 | display: flex; 3 | align-items: center; 4 | gap: 12px; 5 | } 6 | 7 | .__switch_button { 8 | flex: 0 0 48px; 9 | } 10 | 11 | .__dex_info { 12 | background: none; 13 | border-radius: 0; 14 | } 15 | 16 | .__dex_info > div { 17 | padding: 0; 18 | } 19 | 20 | .__dex_info > div > div:last-child { 21 | flex: 1 0 35px; 22 | } 23 | 24 | .__dex_info > div > div:first-child > div:first-child span { 25 | font-weight: 600 !important; 26 | } 27 | 28 | .__dex_info > div > div:first-child > div:last-child span { 29 | font-weight: 400 !important; 30 | } 31 | 32 | .__dex_info span { 33 | font-size: 14px !important; 34 | line-height: 17px !important; 35 | } 36 | 37 | .__price_block_header > span:last-of-type { 38 | flex: 2; 39 | } 40 | -------------------------------------------------------------------------------- /src/panels/PurchaseTonPage/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./PurchaseTonPage.panel"; 2 | export * from "./PurchaseFiatSelect.panel"; 3 | export * from "./PurchaseTonFirstStep.panel"; -------------------------------------------------------------------------------- /src/panels/Receive/Receive.module.css: -------------------------------------------------------------------------------- 1 | .__qr_wrapper { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | padding: 12px 0 36px; 6 | } 7 | 8 | .__qr_wrapper > span { 9 | max-width: 191px; 10 | text-align: center; 11 | } 12 | -------------------------------------------------------------------------------- /src/panels/Receive/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Receive.panel"; 2 | -------------------------------------------------------------------------------- /src/panels/SelectTransfer/SelectTransfer.panel.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, FC, useEffect, useMemo, useState } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { useSelector } from "react-redux"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | import { formatNumber } from "../../utils"; 7 | 8 | import { 9 | Avatar, 10 | Block, 11 | Cell, 12 | Group, 13 | Input, 14 | Panel, 15 | Text, 16 | } from "../../components"; 17 | 18 | import { aviableTransferBalancesSelector } from "../../store/reducers/user/user.selectors"; 19 | 20 | import { ReactComponent as Search17Outline } from "../../icons/Search17Outline.svg"; 21 | 22 | import ton from "../../images/ton.jpeg"; 23 | 24 | import * as amplitude from '@amplitude/analytics-browser'; 25 | 26 | export const SelectTransferPanel: FC = () => { 27 | const { t } = useTranslation(); 28 | 29 | const [filterValue, setFilterValue] = useState(""); 30 | 31 | const navigate = useNavigate(); 32 | 33 | const allBalances = useSelector(aviableTransferBalancesSelector); 34 | 35 | const filtredAllBalances = useMemo(() => { 36 | return allBalances.filter((v: any) => 37 | v?.currency.toLowerCase().startsWith(filterValue.toLowerCase()) 38 | ); 39 | }, [filterValue, allBalances]); 40 | 41 | const onInputChange = (e: ChangeEvent) => { 42 | const newValue = e.currentTarget.value || ""; 43 | 44 | setFilterValue(newValue); 45 | }; 46 | 47 | useEffect(() => { 48 | amplitude.track('SendPage.ChooseToken.Launched'); 49 | }); 50 | 51 | useEffect(() => { 52 | if ((window as any).Telegram.WebApp.MainButton.isVisible) { 53 | (window as any).Telegram.WebApp.MainButton.hide(); 54 | } 55 | }, []); 56 | 57 | return ( 58 | 59 | 60 | } 63 | onChange={onInputChange} 64 | /> 65 | {filtredAllBalances.map((v: any, i: any) => { 66 | if (!v) { 67 | return null; 68 | } 69 | 70 | const imageURL = v.currency === "ton" ? ton : v.image; 71 | 72 | return ( 73 | { 77 | try { 78 | window.navigator.vibrate(70); 79 | } catch (e) { 80 | (window as any).Telegram.WebApp.HapticFeedback.impactOccurred( 81 | "light" 82 | ); 83 | } 84 | 85 | amplitude.track("SendPage.ChooseToken.Selected", { 86 | token: v.currency, 87 | }); 88 | navigate("/send", { 89 | state: { 90 | currency: v.currency, 91 | }, 92 | }); 93 | }} 94 | > 95 | 102 | } 103 | after={ 104 | 110 | {formatNumber(v.amount)} {v.currency.toUpperCase()} 111 | 112 | } 113 | > 114 | {v.name} 115 | 116 | 117 | ); 118 | })} 119 | 120 | 121 | ); 122 | }; 123 | -------------------------------------------------------------------------------- /src/panels/SelectTransfer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SelectTransfer.panel"; 2 | -------------------------------------------------------------------------------- /src/panels/SellNft/SellNft.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, useEffect, useState, useContext } from "react"; 2 | import { useNavigate, useParams } from "react-router-dom"; 3 | import { useTranslation } from "react-i18next"; 4 | import { useSelector } from "react-redux"; 5 | 6 | import { errorMapping } from "../../utils"; 7 | import { 8 | Avatar, 9 | Block, 10 | Button, 11 | Cell, 12 | ErrorBlock, 13 | Group, 14 | Input, 15 | Panel, 16 | Select, 17 | Text, 18 | } from "../../components"; 19 | 20 | import { ReactComponent as Date24OutlineIcon } from "../../icons/Date24Outline.svg"; 21 | 22 | import { myTonAddressSelector } from "../../store/reducers/user/user.selectors"; 23 | import { ROUTE_NAMES } from "../../router/constants"; 24 | import { getUserNFT, sellNft } from "../../api"; 25 | import { NFT } from "../../types"; 26 | import { JetTokensContext } from "../../providers/JetTokensContextProvider"; 27 | 28 | export function SellNftPanel() { 29 | const navigate = useNavigate(); 30 | const params = useParams(); 31 | const { t } = useTranslation(); 32 | 33 | const [formData, setFormData] = useState<{ 34 | nft_address: string; 35 | currency: string; 36 | price: number; 37 | }>({ 38 | nft_address: params.address || "", 39 | currency: "", 40 | price: 0, 41 | }); 42 | const [isAwaitResponse, setIsAwaitResponse] = useState(false); 43 | const [error, setError] = useState(null); 44 | const [currentNft, setCurrentNtf] = useState(); 45 | const myTonAddress = useSelector(myTonAddressSelector); 46 | const { jetToken }: any = useContext(JetTokensContext); 47 | 48 | const isButtonDisabled = 49 | !formData.nft_address || !formData.currency || !formData.price; 50 | 51 | function navigateToCurrencySelect() { 52 | try { 53 | window.navigator.vibrate(70); // Вибрация 54 | } catch (e) { 55 | (window as any).Telegram.WebApp.HapticFeedback.impactOccurred("light"); 56 | } 57 | 58 | navigate(ROUTE_NAMES.SELL_NFT_SELECTCURRENCY); 59 | } 60 | 61 | useEffect(() => { 62 | setFormData((prev) => ({ ...prev, currency: jetToken.symbol })); 63 | }, [jetToken.symbol]); 64 | 65 | useEffect(() => { 66 | getUserNFT(myTonAddress).then((data) => { 67 | const current = data.filter( 68 | (item: any) => item.address === params.address 69 | ); 70 | setCurrentNtf(current[0]); 71 | }); 72 | }, [params.address, myTonAddress]); 73 | 74 | async function nftSell() { 75 | // Включаем состояние загрузки 76 | setIsAwaitResponse(true); 77 | 78 | // Отпарвляем запрос в API с данными 79 | const response: any = await sellNft({ payload: formData }).finally(() => { 80 | // При успешной отправке завершаем загрузку 81 | setIsAwaitResponse(false); 82 | }); 83 | 84 | if (response.data?.success) { 85 | // Редиректим юзера в старницу "Success" с данными текущего NFT 86 | navigate(ROUTE_NAMES.SELL_NFT_SUCCESS, { 87 | state: { ...formData, nftName: currentNft?.metadata.name }, 88 | }); 89 | } else if (response?.response?.data?.error || response?.data?.error) { 90 | try { 91 | window.navigator.vibrate(200); // Вибрация 92 | } catch (e) { 93 | (window as any).Telegram.WebApp.HapticFeedback.impactOccurred("heavy"); 94 | } 95 | 96 | // Присваиваем ошибку в состояние error 97 | setError(response?.response?.data?.error || response?.data?.error); 98 | } 99 | 100 | // Отключаем загрузку 101 | setIsAwaitResponse(false); 102 | } 103 | 104 | return ( 105 | 106 | 107 | 108 | }> 109 |
110 | 117 | {currentNft?.metadata.name} 118 | 119 |
120 |
121 |
122 | 123 | 124 | 133 | } 134 | disabled={isAwaitResponse} 135 | onChange={(e: ChangeEvent) => { 136 | setFormData((prev) => ({ 137 | ...prev, 138 | price: Number(e.target.value), 139 | })); 140 | }} 141 | /> 142 | 143 | 144 | 150 | 159 | {error ? : null} 160 |
161 |
162 | ); 163 | } 164 | -------------------------------------------------------------------------------- /src/panels/SellNft/SellNftCurrencies.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo, useState, ChangeEvent } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import { useNavigate } from "react-router-dom"; 4 | import { useSelector } from "react-redux"; 5 | 6 | import { Block, Cell, Group, Input, Panel, Text } from "../../components"; 7 | import { allCurrenciesSelector } from "../../store/reducers/user/user.selectors"; 8 | import { JetTokensContext } from "../../providers/JetTokensContextProvider"; 9 | 10 | import { ReactComponent as Search17Outline } from "../../icons/Search17Outline.svg"; 11 | 12 | export function SellNftCurrencies() { 13 | const navigate = useNavigate(); 14 | const { t } = useTranslation(); 15 | const allJetTokens = useSelector(allCurrenciesSelector); 16 | const { setJetToken }: any = useContext(JetTokensContext); 17 | 18 | const [filterValue, setFilterValue] = useState(""); 19 | 20 | const filtredJetTokens = useMemo(() => { 21 | return allJetTokens.filter((v: any) => 22 | v?.name.toLowerCase().startsWith(filterValue.toLowerCase()) 23 | ); 24 | }, [filterValue, allJetTokens]); 25 | 26 | function onInputChange(e: ChangeEvent) { 27 | const newValue = e.currentTarget.value || ""; 28 | 29 | setFilterValue(newValue); 30 | } 31 | 32 | function changeCurrency(currency: string) { 33 | // Замыкание 34 | return () => { 35 | try { 36 | window.navigator.vibrate(70); // Вибрация 37 | } catch (e) { 38 | (window as any).Telegram.WebApp.HapticFeedback.impactOccurred("light"); 39 | } 40 | 41 | // Логика изменения токена 42 | setJetToken(currency); 43 | navigate(-1); // Возвращаемся назад 44 | }; 45 | } 46 | 47 | return ( 48 | 49 | 50 | } 53 | onChange={onInputChange} 54 | /> 55 | 56 | {filtredJetTokens.map((currency: any, index: any) => ( 57 | 63 | 71 | {currency.emoji} 72 | 73 | } 74 | after={ 75 | 82 | {t(currency.name)} 83 | 84 | } 85 | /> 86 | 87 | ))} 88 | 89 | 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/panels/SellNft/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SellNft"; 2 | -------------------------------------------------------------------------------- /src/panels/SellNftSuccess/SellNftSuccess.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useLocation, useNavigate } from "react-router-dom"; 3 | 4 | import { ActionText, Button, Group, Panel } from "../../components"; 5 | 6 | import { useTranslation } from "react-i18next"; 7 | 8 | export function SellNftSuccessPanel() { 9 | const navigate = useNavigate(); 10 | const { t } = useTranslation(); 11 | 12 | const { state } = useLocation(); 13 | 14 | useEffect(() => { 15 | try { 16 | window.navigator.vibrate(200); 17 | } catch (e) { 18 | (window as any).Telegram.WebApp.HapticFeedback.impactOccurred("heavy"); 19 | } 20 | }, []); 21 | 22 | return ( 23 | 24 | 25 | 31 | 48 | 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/panels/SellNftSuccess/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SellNftSuccess"; 2 | -------------------------------------------------------------------------------- /src/panels/Send/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Send.panel"; 2 | -------------------------------------------------------------------------------- /src/panels/SendNft/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SendNft"; 2 | -------------------------------------------------------------------------------- /src/panels/SendNftSuccessPanel/SendNftSuccessPanel.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect } from "react"; 2 | import { useLocation, useNavigate } from "react-router-dom"; 3 | import { ActionText, Button, Group, Panel } from "../../components"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | export const SendNftSuccessPanel: FC = () => { 7 | const navigate = useNavigate(); 8 | const { t } = useTranslation(); 9 | 10 | const { state } = useLocation(); 11 | 12 | useEffect(() => { 13 | try { 14 | window.navigator.vibrate(200); 15 | } catch (e) { 16 | (window as any).Telegram.WebApp.HapticFeedback.impactOccurred("heavy"); 17 | } 18 | }, []); 19 | 20 | return ( 21 | 22 | 23 | 28 | 45 | 46 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/panels/SendNftSuccessPanel/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SendNftSuccessPanel"; 2 | -------------------------------------------------------------------------------- /src/panels/SendSuccess/SendSuccess.panel.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect } from "react"; 2 | import { useLocation, useNavigate } from "react-router-dom"; 3 | 4 | import { ActionText, Button, Group, Panel } from "../../components"; 5 | import { useTranslation } from "react-i18next"; 6 | 7 | import { formatNumber, formatToken } from "../../utils"; 8 | 9 | export const SendSuccessPanel: FC = () => { 10 | const navigate = useNavigate(); 11 | 12 | const { t } = useTranslation(); 13 | const { state } = useLocation(); 14 | 15 | useEffect(() => { 16 | try { 17 | window.navigator.vibrate(200); 18 | } catch (e) { 19 | (window as any).Telegram.WebApp.HapticFeedback.impactOccurred("heavy"); 20 | } 21 | }, []); 22 | 23 | useEffect(() => { 24 | if (!(window as any).Telegram.WebApp.MainButton.isVisible) { 25 | (window as any).Telegram.WebApp.MainButton.show(); 26 | } 27 | (window as any) 28 | .Telegram 29 | .WebApp 30 | .MainButton 31 | .setText(t("Back")) 32 | .onClick(buttonAction) 33 | .color = (window as any).Telegram.WebApp.themeParams.button_color; 34 | 35 | return () => { 36 | (window as any) 37 | .Telegram 38 | .WebApp 39 | .MainButton 40 | .offClick(buttonAction); 41 | } 42 | }); 43 | 44 | function buttonAction() { 45 | try { 46 | window.navigator.vibrate(70); 47 | } catch (e) { 48 | (window as any).Telegram.WebApp.HapticFeedback.impactOccurred( 49 | "light" 50 | ); 51 | } 52 | 53 | navigate(-3); 54 | } 55 | 56 | return ( 57 | 58 | 59 | 66 | 67 | 68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/panels/SendSuccess/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SendSuccess.panel"; 2 | -------------------------------------------------------------------------------- /src/panels/Settings/SelectCurrency.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | 5 | import { Block, Cell, Group, Panel, Text } from "../../components"; 6 | import { userActions } from "../../store/reducers"; 7 | import { updateSettings } from "../../api"; 8 | import { myServerData } from "../../store/reducers/user/user.selectors"; 9 | 10 | export function SelectCurrency() { 11 | const navigate = useNavigate(); 12 | const dispatch = useDispatch(); 13 | const { t } = useTranslation(); 14 | const myData = useSelector(myServerData); 15 | 16 | const allCurrencies = [ 17 | { 18 | name: "usd", 19 | symbol: "$", 20 | }, 21 | { 22 | name: "rub", 23 | symbol: "₽", 24 | }, 25 | { 26 | name: "cny", 27 | symbol: "¥", 28 | }, 29 | { 30 | name: "uah", 31 | symbol: "₴", 32 | }, 33 | { 34 | name: "kzt", 35 | symbol: "₸", 36 | }, 37 | { 38 | name: "eur", 39 | symbol: "€", 40 | }, 41 | { 42 | name: "gbp", 43 | symbol: "£", 44 | }, 45 | ]; 46 | 47 | function changeCurrency(currency: string) { 48 | // Замыкание 49 | return () => { 50 | try { 51 | window.navigator.vibrate(70); // Вибрация 52 | } catch (e) { 53 | (window as any).Telegram.WebApp.HapticFeedback.impactOccurred("light"); 54 | } 55 | 56 | // Логика изменения валюты глобально 57 | updateSettings({ currency }).then(() => { 58 | dispatch( 59 | userActions.setServerData({ 60 | ...myData, 61 | localCurrency: currency, 62 | }) 63 | ); 64 | }); 65 | navigate(-1); // Возвращаемся назад 66 | }; 67 | } 68 | 69 | return ( 70 | 71 | 72 | {allCurrencies.map((currency, index) => ( 73 | 79 | 87 | {currency.symbol} 88 | 89 | } 90 | after={ 91 | 98 | {t(currency.name)} 99 | 100 | } 101 | /> 102 | 103 | ))} 104 | 105 | 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /src/panels/Settings/SelectLanguage.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | import { useNavigate } from "react-router-dom"; 3 | 4 | import { Avatar, Block, Cell, Group, Panel, Text } from "../../components"; 5 | import { updateSettings } from "../../api"; 6 | 7 | export function SelectLanguage() { 8 | const { t, i18n } = useTranslation(); 9 | const navigate = useNavigate(); 10 | 11 | function changeLanguage(lang: "ru" | "en") { 12 | // Замыкание 13 | return () => { 14 | try { 15 | window.navigator.vibrate(70); // Вибрация 16 | } catch (e) { 17 | (window as any).Telegram.WebApp.HapticFeedback.impactOccurred("light"); 18 | } 19 | 20 | // Отправляем запрос в API 21 | // При успешной отправке меняем язык в самом сайте и переходим на главную страницу 22 | updateSettings({ langCode: lang }).then(() => { 23 | i18n.changeLanguage(lang); // Меняем язык глобально 24 | navigate(-2); // Возвращаем в главное меню 25 | }); 26 | }; 27 | } 28 | 29 | return ( 30 | 31 | 32 | {/* } 35 | onChange={onInputChange} 36 | /> */} 37 | 38 | 46 | } 47 | after={ 48 | 54 | {t("Russian")} 55 | 56 | } 57 | /> 58 | 59 | 60 | 61 | 69 | } 70 | after={ 71 | 77 | {t("English")} 78 | 79 | } 80 | /> 81 | 82 | 83 | 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/panels/Settings/Settings.module.css: -------------------------------------------------------------------------------- 1 | .__astalyx_info { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | gap: 10px; 6 | } 7 | 8 | .__redoubt_info { 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | gap: 10px; 13 | } -------------------------------------------------------------------------------- /src/panels/Settings/Settings.panel.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "react-router-dom"; 2 | 3 | import { 4 | Block, 5 | Cell, 6 | Group, 7 | Input, 8 | Link, 9 | Panel, 10 | Select, 11 | Text, 12 | } from "../../components"; 13 | 14 | import { ReactComponent as GoArrow24OutlineIcon } from "../../icons/GoArrow24Outline.svg"; 15 | import { ReactComponent as AstralyxLogoIcon } from "../../icons/AstralyxLogo.svg"; 16 | 17 | import styles from "./Settings.module.css"; 18 | import { ROUTE_NAMES } from "../../router/constants"; 19 | import { useTranslation } from "react-i18next"; 20 | import { useSelector } from "react-redux"; 21 | import { myServerData } from "../../store/reducers/user/user.selectors"; 22 | 23 | export function SettingsPanel() { 24 | const navigate = useNavigate(); 25 | const { t, i18n } = useTranslation(); 26 | 27 | const navigateToLanguageSelect = () => { 28 | try { 29 | window.navigator.vibrate(70); // Вибрация 30 | } catch (e) { 31 | (window as any).Telegram.WebApp.HapticFeedback.impactOccurred("light"); 32 | } 33 | 34 | navigate(ROUTE_NAMES.SETTINGS_LANGUAGE); 35 | }; 36 | 37 | const navigateToCurrencySelect = () => { 38 | try { 39 | window.navigator.vibrate(70); // Вибрация 40 | } catch (e) { 41 | (window as any).Telegram.WebApp.HapticFeedback.impactOccurred("light"); 42 | } 43 | 44 | navigate(ROUTE_NAMES.SETTINGS_CURRENCY); 45 | }; 46 | 47 | const myData = useSelector(myServerData); 48 | 49 | return ( 50 | 51 | 52 | 53 | 59 | {t("Wallet")} 60 | 61 | 62 | 76 | } 77 | /> 78 | 79 | 88 | } 89 | /> 90 | 91 | 92 | 93 | 99 | {t("OTHER")} 100 | 101 | 102 | } 104 | withCursor 105 | > 106 | Github 107 | 108 | 109 | 110 | } 112 | withCursor 113 | > 114 | {t("Channel")} 115 | 116 | 117 | 118 | } 120 | withCursor 121 | > 122 | F.A.Q. 123 | 124 | 125 | 126 | } 128 | withCursor 129 | > 130 | {t("Support")} 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | Data from 140 | 141 | 142 | re:doubt 143 | 144 | 145 | 146 | 147 | 148 | 149 | ); 150 | } 151 | -------------------------------------------------------------------------------- /src/panels/Settings/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Settings.panel"; 2 | -------------------------------------------------------------------------------- /src/panels/Swap/Swap.module.css: -------------------------------------------------------------------------------- 1 | .__button_group { 2 | display: flex; 3 | align-items: center; 4 | gap: 12px; 5 | } 6 | 7 | .__switch_button { 8 | flex: 0 0 48px; 9 | } 10 | 11 | .__dex_info { 12 | background: none; 13 | border-radius: 0; 14 | } 15 | 16 | .__dex_info > div { 17 | padding: 0; 18 | } 19 | 20 | .__dex_info > div > div:last-child { 21 | flex: 1 0 35px; 22 | } 23 | 24 | .__dex_info > div > div:first-child > div:first-child span { 25 | font-weight: 600 !important; 26 | } 27 | 28 | .__dex_info > div > div:first-child > div:last-child span { 29 | font-weight: 400 !important; 30 | } 31 | 32 | .__dex_info span { 33 | font-size: 14px !important; 34 | line-height: 17px !important; 35 | } 36 | 37 | .__price_block_header > span:last-of-type { 38 | flex: 2; 39 | } 40 | -------------------------------------------------------------------------------- /src/panels/Swap/SwapSelect.panel.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useContext } from "react"; 2 | 3 | import { SwapDataContext } from "../../providers/SwapDataContextProvider"; 4 | 5 | import { 6 | Avatar, 7 | Block, 8 | Cell, 9 | Group, 10 | Input, 11 | Panel, 12 | Text, 13 | } from "../../components"; 14 | 15 | import { ReactComponent as Search17OutlineIcon } from "../../icons/Search17Outline.svg"; 16 | import ton from "../../images/ton.jpeg"; 17 | 18 | import { useSelector } from "react-redux"; 19 | import { allCurrenciesSelector } from "../../store/reducers/user/user.selectors"; 20 | import { formatNumber } from "../../utils"; 21 | import { useLocation, useNavigate } from "react-router-dom"; 22 | 23 | export const SwapSelect: FC = () => { 24 | const { setData, allTokens, selectedTokens }: any = 25 | useContext(SwapDataContext); 26 | const { state } = useLocation(); 27 | const navigate = useNavigate(); 28 | 29 | const { position } = state; 30 | 31 | const invertPositions: any = { 32 | first: "second", 33 | second: "first", 34 | }; 35 | 36 | const allJetTokens = useSelector(allCurrenciesSelector); 37 | 38 | const tokensToRender = allTokens.reduce( 39 | (result: object[], current: any, index: number) => { 40 | const jetToken = allJetTokens.find( 41 | (v: any) => 42 | v.symbol?.toLowerCase() === current.base_symbol?.toLowerCase() 43 | ); 44 | 45 | if ( 46 | index === 0 && 47 | selectedTokens[invertPositions[position]] !== "TON" && 48 | selectedTokens[position] !== "TON" 49 | ) { 50 | const tonData = allJetTokens.find( 51 | (v: any) => v.symbol?.toLowerCase() === "ton" 52 | ); 53 | 54 | result.push(tonData); 55 | } 56 | 57 | if ( 58 | typeof jetToken !== "undefined" && 59 | selectedTokens[invertPositions[position]] !== 60 | jetToken.symbol?.toUpperCase() && 61 | selectedTokens[position] !== jetToken.symbol?.toUpperCase() && 62 | current?.quote_symbol === "JTON" 63 | ) { 64 | result.push({ ...jetToken, ...current }); 65 | } 66 | 67 | return result; 68 | }, 69 | [] 70 | ); 71 | 72 | const tokenSelected = (currency: string, lastPrice: number) => { 73 | setData((prev: any) => ({ 74 | ...prev, 75 | selectedTokens: { 76 | ...prev.selectedTokens, 77 | [position]: currency, 78 | priceInTon: lastPrice, 79 | }, 80 | })); 81 | 82 | navigate(-1); 83 | }; 84 | 85 | return ( 86 | 87 | 88 | } /> 89 | {tokensToRender.map((v: any, index: number) => { 90 | const src = v.symbol === "ton" ? ton : v?.image; 91 | 92 | return ( 93 | 94 | 102 | } 103 | after={ 104 | v.symbol !== "ton" ? ( 105 | 111 | {formatNumber(Number(v.quote_volume))}{" "} 112 | {v.symbol?.toUpperCase()} 113 | 114 | ) : null 115 | } 116 | onClick={() => 117 | tokenSelected(v.symbol?.toUpperCase(), Number(v.last_price)) 118 | } 119 | > 120 | {v?.symbol?.toUpperCase()} 121 | 122 | 123 | ); 124 | })} 125 | 126 | 127 | ); 128 | }; 129 | -------------------------------------------------------------------------------- /src/panels/Swap/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Swap.panel"; 2 | export * from "./SwapSelect.panel"; 3 | -------------------------------------------------------------------------------- /src/panels/Trading/Trading.module.css: -------------------------------------------------------------------------------- 1 | .__header { 2 | display: flex; 3 | align-items: center; 4 | gap: 8px; 5 | border-bottom: 1px solid var(--tg-theme-hint-color); 6 | padding-bottom: 10px; 7 | } 8 | 9 | .__switcher { 10 | display: flex; 11 | background: var(--tg-theme-secondary-bg-color); 12 | margin-top: 12px; 13 | border-radius: 14px; 14 | position: relative; 15 | } 16 | 17 | .__switcher_button { 18 | width: 100%; 19 | font-family: var(--text_font); 20 | font-size: 14px; 21 | cursor: pointer; 22 | padding-top: 6px; 23 | padding-bottom: 6px; 24 | text-align: center; 25 | color: var(--color_primary_color); 26 | margin: 6px; 27 | border-radius: 8px; 28 | background: none; 29 | outline: none; 30 | border: none; 31 | z-index: 2; 32 | } 33 | 34 | .__active_switch { 35 | position: absolute; 36 | height: calc(100% - 12px); 37 | width: calc(50% - 6px); 38 | /* transition: 0.4s ease; */ 39 | border-radius: 8px; 40 | z-index: 1; 41 | margin: 6px; 42 | } 43 | 44 | .__active_switch.buy { 45 | background: #29b77f; 46 | transform: translateX(0%); 47 | } 48 | 49 | .__active_switch.sell { 50 | background: #de2c2c; 51 | transform: translateX(100%); 52 | } 53 | 54 | .sell{ 55 | background: #de2c2c; 56 | } 57 | .__select { 58 | /* display: none; */ 59 | } 60 | 61 | .__select::before { 62 | content: "Arrow"; 63 | } 64 | 65 | .__separator { 66 | background-color: var(--tg-theme-secondary-bg-color); 67 | } -------------------------------------------------------------------------------- /src/panels/Trading/TradingSuccess.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | 4 | import { ActionText, Group, Panel } from "../../components"; 5 | 6 | import { useTranslation } from "react-i18next"; 7 | 8 | export const TradingSuccessPanel: FC = () => { 9 | const navigate = useNavigate(); 10 | const { t } = useTranslation(); 11 | 12 | useEffect(() => { 13 | try { 14 | window.navigator.vibrate(200); 15 | } catch (e) { 16 | (window as any).Telegram.WebApp.HapticFeedback.impactOccurred("heavy"); 17 | } 18 | }, []); 19 | 20 | useEffect(() => { 21 | if (!(window as any).Telegram.WebApp.MainButton.isVisible) { 22 | (window as any).Telegram.WebApp.MainButton.show(); 23 | } 24 | (window as any) 25 | .Telegram 26 | .WebApp 27 | .MainButton 28 | .setText(t("Back")) 29 | .onClick(buttonAction) 30 | .color = (window as any).Telegram.WebApp.themeParams.button_color; 31 | 32 | return () => { 33 | (window as any) 34 | .Telegram 35 | .WebApp 36 | .MainButton 37 | .offClick(buttonAction); 38 | } 39 | }); 40 | 41 | function buttonAction() { 42 | try { 43 | window.navigator.vibrate(70); 44 | } catch (e) { 45 | (window as any).Telegram.WebApp.HapticFeedback.impactOccurred( 46 | "light" 47 | ); 48 | } 49 | 50 | navigate(-2); 51 | } 52 | 53 | return ( 54 | 55 | 56 | 61 | 62 | 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /src/panels/Trading/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./TradingPanel"; 2 | -------------------------------------------------------------------------------- /src/panels/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Home"; 2 | export * from "./Load"; 3 | export * from "./Settings"; 4 | export * from "./SendSuccess"; 5 | export * from "./History"; 6 | export * from "./Receive"; 7 | export * from "./Send"; 8 | export * from "./SelectTransfer"; 9 | export * from "./Menu"; 10 | export * from "./MenuExpanded"; 11 | export * from "./Nft"; 12 | export * from "./Swap"; 13 | export * from "./PurchaseTonPage"; 14 | -------------------------------------------------------------------------------- /src/providers/ExchangePairContextProvider.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode, createContext, useState, useContext } from "react"; 2 | 3 | export const ExchangePair = createContext({}); 4 | 5 | export function useExchangePairContext(): any { 6 | return useContext(ExchangePair); 7 | } 8 | 9 | const initialState = { 10 | id: "647f771d6103be25ab2befb5", 11 | assets: ["exc", "ton"], 12 | providers: { 13 | dedust: { 14 | pool: "", 15 | fee: 0, 16 | reserves: ["", ""], 17 | cache_expire: 0, 18 | }, 19 | }, 20 | active: true, 21 | trading_data: { 22 | change_24h: 0, 23 | avg_price: 0, 24 | }, 25 | }; 26 | 27 | export const ExchangePairContextProvider: FC<{ children: ReactNode }> = ({ 28 | children, 29 | }) => { 30 | const [selectedExchangePair, setSelectedExchangePair] = 31 | useState(initialState); 32 | 33 | return ( 34 | 40 | {children} 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/providers/JetTokensContextProvider.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode, createContext, useState } from "react"; 2 | 3 | export const JetTokensContext = createContext({}); 4 | 5 | const initialState = { 6 | selectedToken: { 7 | id: "6370a5a11f8246a8dde20adf", 8 | name: "TON", 9 | emoji: "💎", 10 | url: "https://ton.org/", 11 | symbol: "ton", 12 | master_contract: null, 13 | rates: null, 14 | verified: true, 15 | image: null, 16 | }, 17 | }; 18 | 19 | export const JetTokensContextProvider: FC<{ children: ReactNode }> = ({ 20 | children, 21 | }) => { 22 | const [jetToken, setJetToken] = useState(initialState.selectedToken); 23 | 24 | return ( 25 | 31 | {children} 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/providers/PurchaseTonContextProvider.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode, createContext, useState } from "react"; 2 | 3 | export const PurchaseTonContext = createContext({}); 4 | 5 | export const PURCHASE_TON_DEFAULT_STATE = { 6 | allTokens: [], 7 | selectedTokens: { 8 | first: "RUB", 9 | second: null, 10 | priceInTon: 0, 11 | }, 12 | selectionToken: { 13 | type: null, 14 | position: null, 15 | }, 16 | }; 17 | 18 | export const PurchaseTonContextProvider: FC<{ children: ReactNode }> = ({ 19 | children, 20 | }) => { 21 | const [data, setData] = useState(PURCHASE_TON_DEFAULT_STATE); 22 | 23 | return ( 24 | 30 | {children} 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/providers/SwapDataContextProvider.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode, createContext, useState } from "react"; 2 | 3 | export const SwapDataContext = createContext({}); 4 | 5 | export const SWAP_DATA_DEFAULT_STATE = { 6 | allTokens: [], 7 | selectedTokens: { 8 | first: "TON", 9 | second: null, 10 | priceInTon: 0, 11 | }, 12 | selectionToken: { 13 | type: null, 14 | position: null, 15 | }, 16 | }; 17 | 18 | export const SwapDataContextProvider: FC<{ children: ReactNode }> = ({ 19 | children, 20 | }) => { 21 | const [data, setData] = useState(SWAP_DATA_DEFAULT_STATE); 22 | 23 | return ( 24 | 30 | {children} 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/router/constants.ts: -------------------------------------------------------------------------------- 1 | export const ROUTE_NAMES = { 2 | LOAD: "/", 3 | HOME: "/home", 4 | SETTINGS: "/settings", 5 | SETTINGS_LANGUAGE: "/settings/language", 6 | SETTINGS_CURRENCY: "/settings/currency", 7 | HISTORY: "/history", 8 | RECEIVE: "/receive", 9 | SEND: "/send", 10 | SEND_SELECT: "/send/select", 11 | SEND_SUCCESS: "/send/success", 12 | SEND_NFT: "/nft/:address/send", 13 | SEND_NFT_SUCCESS: "/nft/send/success", 14 | SELL_NFT: "/nft/:address/sell", 15 | SELL_NFT_SELECTCURRENCY: "/nft/:address/sell/selectCurrency", 16 | SELL_NFT_SUCCESS: "/nft/sell/success", 17 | MENU: "/market", 18 | MENU_EXPANDED: "/menuexpanded", 19 | BUY_TON: "/purchaseTon", 20 | FIAT_SELECT: "/pTonSelectFiat", 21 | BUY_TON_STEP1: "/pTonStep1", 22 | NFT: "/nft", 23 | NFT_DETAIL: "/nft/:address", 24 | SWAP: "/swap", 25 | SWAP_SELECT: "/swap/select", 26 | SWAP_SUCCESS: "/swap/success", 27 | }; 28 | -------------------------------------------------------------------------------- /src/router/index.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter } from "react-router-dom"; 2 | 3 | import { ROUTE_NAMES } from "./constants"; 4 | 5 | import { 6 | HistoryPanel, 7 | HomePanel, 8 | LoadPanel, 9 | MenuExpandedPanel, 10 | MenuPanel, 11 | NftPanel, 12 | ReceivePanel, 13 | SelectTransferPanel, 14 | SendPanel, 15 | SendSuccessPanel, 16 | SettingsPanel, 17 | PurchaseTonPage, 18 | PurchaseFiatSelect, 19 | PurchaseTonFirstStep, 20 | NftDetailPanel, 21 | } from "../panels"; 22 | import { SendNftPanel } from "../panels/SendNft"; 23 | import { SendNftSuccessPanel } from "../panels/SendNftSuccessPanel"; 24 | import { SelectLanguage } from "../panels/Settings/SelectLanguage"; 25 | import { SelectCurrency } from "../panels/Settings/SelectCurrency"; 26 | import { SellNftPanel } from "../panels/SellNft"; 27 | import { SellNftSuccessPanel } from "../panels/SellNftSuccess"; 28 | import { SellNftCurrencies } from "../panels/SellNft/SellNftCurrencies"; 29 | import { TradingPanel } from "../panels/Trading"; 30 | import { TradingSelectPanel } from "../panels/Trading/TradingSelect.panel"; 31 | import { TradingSuccessPanel } from "../panels/Trading/TradingSuccess"; 32 | import { ErrorDeposit } from "../panels/ErrorDeposit"; 33 | 34 | export const router = createBrowserRouter([ 35 | { 36 | path: ROUTE_NAMES.LOAD, 37 | element: , 38 | }, 39 | { 40 | path: ROUTE_NAMES.HOME, 41 | element: , 42 | }, 43 | { 44 | path: ROUTE_NAMES.SETTINGS, 45 | element: , 46 | }, 47 | { 48 | path: ROUTE_NAMES.SETTINGS_LANGUAGE, 49 | element: , 50 | }, 51 | { 52 | path: ROUTE_NAMES.SETTINGS_CURRENCY, 53 | element: , 54 | }, 55 | { 56 | path: ROUTE_NAMES.HISTORY, 57 | element: , 58 | }, 59 | { 60 | path: ROUTE_NAMES.RECEIVE, 61 | element: , 62 | }, 63 | { 64 | path: ROUTE_NAMES.SEND, 65 | element: , 66 | }, 67 | { 68 | path: ROUTE_NAMES.SEND_SELECT, 69 | element: , 70 | }, 71 | { 72 | path: ROUTE_NAMES.SEND_SUCCESS, 73 | element: , 74 | }, 75 | { 76 | path: ROUTE_NAMES.SEND_NFT, 77 | element: , 78 | }, 79 | { 80 | path: ROUTE_NAMES.SEND_NFT_SUCCESS, 81 | element: , 82 | }, 83 | { 84 | path: ROUTE_NAMES.SELL_NFT, 85 | element: , 86 | }, 87 | { 88 | path: ROUTE_NAMES.SELL_NFT_SELECTCURRENCY, 89 | element: , 90 | }, 91 | { 92 | path: ROUTE_NAMES.SELL_NFT_SUCCESS, 93 | element: , 94 | }, 95 | { 96 | path: ROUTE_NAMES.MENU, 97 | element: , 98 | }, 99 | { 100 | path: ROUTE_NAMES.MENU_EXPANDED, 101 | element: , 102 | }, 103 | { 104 | path: ROUTE_NAMES.BUY_TON, 105 | element: , 106 | }, 107 | { 108 | path: ROUTE_NAMES.FIAT_SELECT, 109 | element: , 110 | }, 111 | { 112 | path: ROUTE_NAMES.BUY_TON_STEP1, 113 | element: , 114 | }, 115 | { 116 | path: ROUTE_NAMES.NFT, 117 | element: , 118 | }, 119 | { 120 | path: ROUTE_NAMES.NFT_DETAIL, 121 | element: , 122 | }, 123 | { 124 | path: ROUTE_NAMES.SWAP, 125 | element: , 126 | }, 127 | { 128 | path: ROUTE_NAMES.SWAP_SELECT, 129 | element: , 130 | }, 131 | { 132 | path: ROUTE_NAMES.SWAP_SUCCESS, 133 | element: , 134 | }, 135 | ]); 136 | 137 | router.subscribe((v) => { 138 | try { 139 | if (window.history.state.idx === 0 && v.historyAction !== "PUSH") { 140 | (window as any).Telegram.WebApp.BackButton.hide(); 141 | } else { 142 | (window as any).Telegram.WebApp.BackButton.show(); 143 | } 144 | } catch (e) { 145 | console.log("[xJetWallet] Please login via Telegram!"); 146 | } 147 | }); 148 | -------------------------------------------------------------------------------- /src/store/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const SLICE_NAMES = { 2 | USER: "user", 3 | }; 4 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | 3 | import { SLICE_NAMES } from "./constants"; 4 | import { userReducer } from "./reducers"; 5 | 6 | export const store = configureStore({ 7 | reducer: { 8 | [SLICE_NAMES.USER]: userReducer, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /src/store/reducers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./user"; 2 | -------------------------------------------------------------------------------- /src/store/reducers/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./user.slice"; 2 | -------------------------------------------------------------------------------- /src/store/reducers/user/user.selectors.ts: -------------------------------------------------------------------------------- 1 | import { SLICE_NAMES } from "../../constants"; 2 | 3 | export const myAllBalancesSelector = (state: any) => { 4 | return [ 5 | ...(state[SLICE_NAMES.USER].verifiedBalances || []), 6 | ...(state[SLICE_NAMES.USER].unverifiedBalances || []), 7 | ]; 8 | }; 9 | 10 | export const aviableTransferBalancesSelector = (state: any) => { 11 | return [ 12 | ...(state[SLICE_NAMES.USER].verifiedBalances || []), 13 | ...(state[SLICE_NAMES.USER].unverifiedBalances || []), 14 | ] 15 | .filter((v: any) => { 16 | return v?.currency !== "ton" ? v?.amount && v?.amount > 0 : true; 17 | }) 18 | .sort((a: any, b: any) => { 19 | if (b?.currency === "ton") { 20 | return 1; 21 | } 22 | 23 | if (a?.currency === "ton") { 24 | return -1; 25 | } 26 | 27 | return Number(b?.amount || 0) - Number(a?.amount || 0); 28 | }); 29 | }; 30 | 31 | export const myVerifiedBalancesSelector = (state: any) => { 32 | return (state[SLICE_NAMES.USER].verifiedBalances || []).filter((v: any) => { 33 | return v.currency !== "ton" && v.amount > 0; 34 | }); 35 | }; 36 | 37 | export const myUnverifiedBalancesSelector = (state: any) => { 38 | return (state[SLICE_NAMES.USER].unverifiedBalances || []).filter((v: any) => { 39 | return v.currency !== "ton" && v.amount > 0; 40 | }); 41 | }; 42 | 43 | export const myBalancesSelector = (state: any) => { 44 | return (state[SLICE_NAMES.USER].balances || []) 45 | }; 46 | 47 | export const myTonBalanceSelector = (state: any) => { 48 | return (state[SLICE_NAMES.USER].balances || []).find( 49 | (v: any) => v?.currency === "ton" 50 | ); 51 | }; 52 | 53 | export const totalUSDValueSelector = (state: any) => { 54 | return (state[SLICE_NAMES.USER].balances || []).reduce((a: any, b: any) => { 55 | return a + Number(b?.values?.usd || 0); 56 | }, 0); 57 | }; 58 | 59 | export const totalTONValueSelector = (state: any) => { 60 | return (state[SLICE_NAMES.USER].balances || []).reduce((a: any, b: any) => { 61 | if (b.currency === "ton") { 62 | return a + Number(b.amount || 0); 63 | } 64 | 65 | return a + Number(b?.values?.ton || 0); 66 | }, 0); 67 | }; 68 | 69 | export const totalEXCValueSelector = (state: any) => { 70 | return (state[SLICE_NAMES.USER].balances || []).reduce((a: any, b: any) => { 71 | if (b.currency === "exc") { 72 | return a + Number(b.amount || 0); 73 | } 74 | 75 | return a + Number(b?.values?.ton || 0); 76 | }, 0); 77 | }; 78 | 79 | export const totalBOLTValueSelector = (state: any) => { 80 | return (state[SLICE_NAMES.USER].balances || []).reduce((a: any, b: any) => { 81 | if (b.currency === "bolt") { 82 | return a + Number(b.amount || 0); 83 | } 84 | 85 | return a + Number(b?.values?.ton || 0); 86 | }, 0); 87 | }; 88 | 89 | export const totalLAVEValueSelector = (state: any) => { 90 | return (state[SLICE_NAMES.USER].balances || []).reduce((a: any, b: any) => { 91 | if (b.currency === "lave") { 92 | return a + Number(b.amount || 0); 93 | } 94 | 95 | return a + Number(b?.values?.ton || 0); 96 | }, 0); 97 | }; 98 | 99 | export const totalTAKEValueSelector = (state: any) => { 100 | return (state[SLICE_NAMES.USER].balances || []).reduce((a: any, b: any) => { 101 | if (b.currency === "take") { 102 | return a + Number(b.amount || 0); 103 | } 104 | 105 | return a + Number(b?.values?.ton || 0); 106 | }, 0); 107 | }; 108 | 109 | export const totalJUSDTValueSelector = (state: any) => { 110 | return (state[SLICE_NAMES.USER].balances || []).reduce((a: any, b: any) => { 111 | if (b.currency === "jusdt") { 112 | return a + Number(b.amount || 0); 113 | } 114 | 115 | return a + Number(b?.values?.ton || 0); 116 | }, 0); 117 | }; 118 | 119 | export const totalJUSDCValueSelector = (state: any) => { 120 | return (state[SLICE_NAMES.USER].balances || []).reduce((a: any, b: any) => { 121 | if (b.currency === "jusdc") { 122 | return a + Number(b.amount || 0); 123 | } 124 | 125 | return a + Number(b?.values?.ton || 0); 126 | }, 0); 127 | }; 128 | 129 | export const totalKISSValueSelector = (state: any) => { 130 | return (state[SLICE_NAMES.USER].balances || []).reduce((a: any, b: any) => { 131 | if (b.currency === "kiss") { 132 | return a + Number(b.amount || 0); 133 | } 134 | 135 | return a + Number(b?.values?.ton || 0); 136 | }, 0); 137 | }; 138 | 139 | export const totalAmountsSelector = (state: any) => { 140 | return (state[SLICE_NAMES.USER].balances || []).reduce((a: any, b: any) => { 141 | return a + Number(isNaN(b.amount) ? 0 : b.amount); 142 | }, 0); 143 | }; 144 | 145 | export const myTonAddressSelector = (state: any) => { 146 | return state[SLICE_NAMES.USER].serverData?.service_wallet || ""; 147 | }; 148 | 149 | export const currencyDataSelector = (state: any, currency: string) => { 150 | return (state[SLICE_NAMES.USER].balances || []).find( 151 | (v: any) => v?.currency === currency 152 | ); 153 | }; 154 | 155 | export const allCurrenciesSelector = (state: any) => { 156 | return state[SLICE_NAMES.USER].allCurrencies || []; 157 | }; 158 | 159 | export const availableFiatsSelector = (state: any) => { 160 | return state[SLICE_NAMES.USER].availableFiats; 161 | }; 162 | 163 | export const myServerData = (state: any) => { 164 | return state[SLICE_NAMES.USER].serverData; 165 | }; 166 | 167 | export const exhangesPair = (state: any) => { 168 | return state[SLICE_NAMES.USER].exchangesPair; 169 | }; 170 | -------------------------------------------------------------------------------- /src/store/reducers/user/user.slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, current } from "@reduxjs/toolkit"; 2 | 3 | import { SLICE_NAMES } from "../../constants"; 4 | 5 | const initialState: any = { 6 | serverData: null, 7 | balances: null, 8 | verifiedBalances: null, 9 | unverifiedBalances: null, 10 | allCurrencies: null, 11 | history: [], 12 | availableFiats: [ 13 | { base_symbol: "RUB", last_price: 10000, price: 10000, minAmount: 5 }, 14 | ], 15 | exchangesPair: [], 16 | }; 17 | 18 | const userSlice = createSlice({ 19 | name: SLICE_NAMES.USER, 20 | initialState, 21 | reducers: { 22 | setServerData(draft, action) { 23 | draft.serverData = action.payload; 24 | }, 25 | setBalances(draft, action) { 26 | if (!action.payload) { 27 | return; 28 | } 29 | 30 | draft.balances = action.payload.reduce( 31 | (res: Array, value: any) => { 32 | const currencyData = (draft.allCurrencies || []).find((v: any) => { 33 | return v.symbol === value.currency; 34 | }); 35 | 36 | if (currencyData) { 37 | res.push({ ...value, ...currencyData, values: value?.values }); 38 | } 39 | 40 | return res; 41 | }, 42 | [] 43 | ); 44 | draft.verifiedBalances = action.payload 45 | .reduce((res: Array, value: any) => { 46 | const currencyData = (current(draft.allCurrencies) || []).find( 47 | (v: any) => { 48 | return v.symbol === value.currency; 49 | } 50 | ); 51 | 52 | if (currencyData && currencyData.verified === true) { 53 | res.push({ ...value, ...currencyData, values: value?.values }); 54 | } 55 | 56 | return res; 57 | }, []) 58 | .sort((a: any, b: any) => { 59 | return Number(!a?.values?.usd || 0) - Number(!b?.values?.usd || 0); 60 | }); 61 | draft.unverifiedBalances = action.payload 62 | .reduce((res: Array, value: any) => { 63 | const currencyData = (current(draft.allCurrencies) || []).find( 64 | (v: any) => { 65 | return v.symbol === value.currency; 66 | } 67 | ); 68 | 69 | if (currencyData && currencyData.verified === false) { 70 | res.push({ ...value, ...currencyData, values: value?.values }); 71 | } 72 | 73 | return res; 74 | }, []) 75 | .sort((a: any, b: any) => { 76 | return Number(!a?.values?.ton || 0) - Number(!b?.values?.ton || 0); 77 | }); 78 | }, 79 | setAllCurrencies(draft, action) { 80 | if (!action.payload) { 81 | return; 82 | } 83 | 84 | draft.allCurrencies = action.payload; 85 | }, 86 | setAvailableFiats(draft, action) { 87 | if (!action.payload) { 88 | return; 89 | } 90 | 91 | draft.availableFiats = action.payload; 92 | }, 93 | 94 | setExchangesPair(draft, action) { 95 | if (!action.payload) { 96 | return; 97 | } 98 | 99 | draft.exchangesPair = action.payload; 100 | }, 101 | }, 102 | }); 103 | 104 | export const userReducer = userSlice.reducer; 105 | export const userActions = userSlice.actions; 106 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"); 2 | 3 | :root { 4 | /* ? sizes variables */ 5 | 6 | --panel_header_height: 72px; 7 | --panel_header_button_width: 72px; 8 | 9 | --buttons_border_radius: 18px; 10 | 11 | --block_border_radius: 18px; 12 | 13 | --avatar_circle_border_radius: 50%; 14 | --avatar_square_border_radius: 9px; 15 | 16 | --input_padding: 18px; 17 | --input_border_radius: 18px; 18 | 19 | --select_option_list_border_radius: 0 0 12px 12px; 20 | 21 | /* ? font info */ 22 | 23 | --text_font: "Inter"; 24 | } 25 | 26 | body[theme="tg"] { 27 | /* ? color variables */ 28 | 29 | --background_content: var(--tg-theme-bg-color); 30 | --background_block: var(--tg-theme-secondary-bg-color); 31 | --background_switch: var(--tg-theme-secondary-bg-color); 32 | --background_primary_button: var(--tg-theme-button-color); 33 | --background_avatar_fallback: rgba(255, 255, 255, 0.025); 34 | 35 | --panel_header_background: var(--tg-theme-bg-color); 36 | 37 | --color_primary_color: var(--tg-theme-text-color); 38 | --color_black_color: var(--tg-theme-bg-color); 39 | --color_gray_color: var(--tg-theme-hint-color); 40 | --color_avatar_fallback: var(--tg-theme-text-color); 41 | --color_button_primary: var(--tg-theme-button-text-color); 42 | --color_button_accent: var(--tg-theme-text-color); 43 | --color_input_value: var(--tg-theme-text-color); 44 | --color_qr: var(--tg-theme-button-color); 45 | --color_positive: rgba(111, 237, 109, 1); 46 | --color_negative: rgba(237, 109, 109, 1); 47 | --color_error: rgba(237, 109, 109, 1); 48 | 49 | --accent: var(--tg-theme-link-color); 50 | 51 | --error_popout_border_color: #ed6d6d; 52 | --error_popout_background_color: rgba(237, 109, 109, 0.15); 53 | 54 | --space_border_color: rgba(255, 255, 255, 0.025); 55 | 56 | /* ? animation color variables */ 57 | 58 | --button_hover_background: rgba(255, 255, 255, 0.01); 59 | --button_active_background: rgba(255, 255, 255, 0.03); 60 | 61 | --button_primary_hover_background: rgba(0, 0, 0, 0.1); 62 | --button_primary_active_background: rgba(0, 0, 0, 0.2); 63 | } 64 | 65 | body[theme="static"] { 66 | /* ? color variables */ 67 | 68 | --background_content: #161518; 69 | --background_block: rgba(255, 255, 255, 0.05); 70 | --background_switch: rgba(255, 255, 255, 0.05); 71 | --background_primary_button: #d3f7a6; 72 | --background_avatar_fallback: #d3f7a6; 73 | 74 | --panel_header_background: #161518; 75 | 76 | --color_primary_color: #fff; 77 | --color_black_color: #161518; 78 | --color_gray_color: rgba(217, 217, 217, 0.25); 79 | --color_avatar_fallback: #161518; 80 | --color_button_primary: #161518; 81 | --color_button_accent: #d3f7a6; 82 | --color_input_value: #d3f7a6; 83 | --color_qr: #d3f7a6; 84 | --color_positive: rgba(111, 237, 109, 1); 85 | --color_negative: rgba(237, 109, 109, 1); 86 | --color_error: rgba(237, 109, 109, 1); 87 | 88 | --accent: #d3f7a6; 89 | 90 | --error_popout_border_color: #ed6d6d; 91 | --error_popout_background_color: rgba(237, 109, 109, 0.15); 92 | 93 | --space_border_color: rgba(255, 255, 255, 0.025); 94 | 95 | /* ? animation color variables */ 96 | 97 | --button_hover_background: rgba(255, 255, 255, 0.04); 98 | --button_active_background: rgba(255, 255, 255, 0.1); 99 | 100 | --button_primary_hover_background: rgba(0, 0, 0, 0.1); 101 | --button_primary_active_background: rgba(0, 0, 0, 0.2); 102 | } 103 | 104 | * { 105 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 106 | -webkit-tap-highlight-color: transparent; 107 | } 108 | 109 | html, 110 | body, 111 | #root { 112 | min-height: 100%; 113 | height: 100%; 114 | } 115 | 116 | html { 117 | overflow: hidden; 118 | } 119 | 120 | body { 121 | background-color: var(--background_content); 122 | margin: 0; 123 | overflow: auto; 124 | } 125 | 126 | span { 127 | color: var(--color_primary_color); 128 | font-family: var(--text_font); 129 | } 130 | 131 | .darktheme { 132 | color: white; 133 | } 134 | 135 | .darktheme .ant-timeline-item-tail { 136 | border-inline-start: 2px solid rgba(255, 255, 255, 0.06); 137 | } 138 | 139 | .ant-timeline-pending .ant-timeline-item-content { 140 | font-size: 16px !important; 141 | } 142 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface NFT { 2 | address: string; 3 | index: number; 4 | owner: { 5 | address: string; 6 | name: string; 7 | is_scam: boolean; 8 | icon: string; 9 | }; 10 | collection?: { 11 | address: string; 12 | name: string; 13 | }; 14 | verified: boolean; 15 | metadata: { 16 | name: string; 17 | image: string; 18 | description: string; 19 | }; 20 | previews: { 21 | resolution: string; 22 | url: string; 23 | }[]; 24 | dns: string; 25 | approved_by: string[]; 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const formatNumber = (number: number, config: object = {}) => { 2 | if (!number) { 3 | number = 0; 4 | } 5 | 6 | const enFormat = new Intl.NumberFormat("en-US", { 7 | ...{ maximumFractionDigits: 3, minimumFractionDigits: 0 }, 8 | ...config, 9 | }); 10 | 11 | return enFormat.format(number); 12 | }; 13 | 14 | export const countCharts = function (string: string, c: string) { 15 | var result = 0, 16 | i = 0; 17 | for (i; i < string.length; i++) if (string[i] === c) result++; 18 | return result; 19 | }; 20 | 21 | export const errorMapping = (serverError: string) => { 22 | switch (serverError) { 23 | case "error_insufficientFunds": 24 | return "You don't have enough TON"; 25 | case "Invalid ton_address": 26 | return "Receiver address is invalid"; 27 | default: 28 | return serverError.replace("error_", ""); // "An error occured"; 29 | } 30 | }; 31 | 32 | export const formatToken = (token: string) => { 33 | if (!token) { 34 | return ""; 35 | } 36 | 37 | if (token.length < 16) { 38 | return token; 39 | } 40 | 41 | const [beforeCopy, afterCopy] = [token.slice(), token.slice()]; 42 | 43 | const before = beforeCopy.slice(0, 6); 44 | const after = afterCopy.slice(-7); 45 | 46 | return `${before}...${after}`; 47 | }; 48 | 49 | export const formatDate = (date: Date) => { 50 | const dateStr = 51 | ("00" + date.getDate()).slice(-2) + 52 | "." + 53 | ("00" + (date.getMonth() + 1)).slice(-2) + 54 | "." + 55 | date.getFullYear() + 56 | " " + 57 | ("00" + date.getHours()).slice(-2) + 58 | ":" + 59 | ("00" + date.getMinutes()).slice(-2) + 60 | ":" + 61 | ("00" + date.getSeconds()).slice(-2); 62 | 63 | return dateStr; 64 | }; 65 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "typeRoots": ["./custom.d.ts"] 19 | }, 20 | "include": ["src"] 21 | } 22 | --------------------------------------------------------------------------------