├── .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 |

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 |
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 |
6 |
--------------------------------------------------------------------------------
/src/icons/ArrowDown15Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/AstralyxLogo.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/src/icons/Back24Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/BankCard.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/icons/Burn24Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/Cart18Outline.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/icons/Chains20Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/Cheque24Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/Copy20Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/CopySuccess24Outline.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/src/icons/Date24Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/File24Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/GoArrow24Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/History24Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/Info.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/icons/Invoice24Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/Logo.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/src/icons/LogoOutline.svg:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/src/icons/LogoQR40Outline.svg:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/src/icons/Menu24Outline.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/icons/MinusSmall.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/icons/Picture24Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/PlusSmall.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/icons/Program24Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/QRCopy17Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/Receipt18Outline.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/icons/Receive24Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/Search17Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/Security24Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/Send24Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/Settings24Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/Star24.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/icons/Star24Outline.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/icons/Swap24Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/Switch15Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/Switcher.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/icons/Switching24Outline.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/Text24Outline.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/icons/USAFlag.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/cross.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 | : null}
156 | >
157 | {isAwaitResponse ? `${t("Selling")}...` : t("Sell")}
158 |
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