feat: Complete wallet features - Multi-token support, Receive modal, Transaction history

This commit is contained in:
2025-10-28 08:04:14 +03:00
parent 2d1a2cf3ba
commit 9bf1c79e44
16 changed files with 1384 additions and 25 deletions
+307 -1
View File
@@ -56,6 +56,7 @@
"lucide-react": "^0.462.0",
"marked": "^12.0.1",
"next-themes": "^0.3.0",
"qrcode": "^1.5.4",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
@@ -75,6 +76,7 @@
"@eslint/js": "^9.9.0",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^22.5.5",
"@types/qrcode": "^1.5.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.5.0",
@@ -3708,6 +3710,16 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/qrcode": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
"integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/react": {
"version": "18.3.26",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
@@ -4266,6 +4278,15 @@
"node": ">=6"
}
},
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
@@ -4361,6 +4382,72 @@
"url": "https://polar.sh/cva"
}
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/cliui/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/cliui/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -4609,6 +4696,15 @@
}
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
@@ -4634,6 +4730,12 @@
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"license": "Apache-2.0"
},
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
@@ -5160,6 +5262,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
@@ -5876,6 +5987,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -5899,7 +6019,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -5972,6 +6091,15 @@
"node": ">= 6"
}
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -6188,6 +6316,23 @@
"node": ">=6"
}
},
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -6486,6 +6631,21 @@
"decimal.js-light": "^2.4.1"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -6629,6 +6789,12 @@
"node": ">=10"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -7299,6 +7465,12 @@
"node": ">= 8"
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -7418,6 +7590,140 @@
}
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/yargs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/yargs/node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yargs/node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+2
View File
@@ -59,6 +59,7 @@
"lucide-react": "^0.462.0",
"marked": "^12.0.1",
"next-themes": "^0.3.0",
"qrcode": "^1.5.4",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
@@ -78,6 +79,7 @@
"@eslint/js": "^9.9.0",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^22.5.5",
"@types/qrcode": "^1.5.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.5.0",
+1 -1
View File
@@ -50,4 +50,4 @@
.read-the-docs {
color: #5f7676;
}
}
+71 -7
View File
@@ -15,6 +15,7 @@ export const AccountBalance: React.FC = () => {
reserved: '0',
total: '0',
});
const [pezBalance, setPezBalance] = useState<string>('0');
const [isLoading, setIsLoading] = useState(false);
const fetchBalance = async () => {
@@ -22,12 +23,13 @@ export const AccountBalance: React.FC = () => {
setIsLoading(true);
try {
// Fetch HEZ balance
const { data: balanceData } = await api.query.system.account(selectedAccount.address);
const free = balanceData.free.toString();
const reserved = balanceData.reserved.toString();
// Convert from plancks to tokens (assuming 12 decimals like DOT)
// Convert from plancks to tokens (12 decimals)
const decimals = 12;
const divisor = Math.pow(10, decimals);
@@ -40,6 +42,23 @@ export const AccountBalance: React.FC = () => {
reserved: reservedTokens,
total: totalTokens,
});
// Fetch PEZ balance (Asset ID: 1)
try {
const pezAssetBalance = await api.query.assets.account(1, selectedAccount.address);
if (pezAssetBalance.isSome) {
const assetData = pezAssetBalance.unwrap();
const pezAmount = assetData.balance.toString();
const pezTokens = (parseInt(pezAmount) / divisor).toFixed(4);
setPezBalance(pezTokens);
} else {
setPezBalance('0');
}
} catch (error) {
console.error('Failed to fetch PEZ balance:', error);
setPezBalance('0');
}
} catch (error) {
console.error('Failed to fetch balance:', error);
} finally {
@@ -50,13 +69,15 @@ export const AccountBalance: React.FC = () => {
useEffect(() => {
fetchBalance();
// Subscribe to balance updates
let unsubscribe: () => void;
// Subscribe to HEZ balance updates
let unsubscribeHez: () => void;
let unsubscribePez: () => void;
const subscribeBalance = async () => {
if (!api || !isApiReady || !selectedAccount) return;
unsubscribe = await api.query.system.account(
// Subscribe to HEZ balance
unsubscribeHez = await api.query.system.account(
selectedAccount.address,
({ data: balanceData }) => {
const free = balanceData.free.toString();
@@ -76,12 +97,35 @@ export const AccountBalance: React.FC = () => {
});
}
);
// Subscribe to PEZ balance (Asset ID: 1)
try {
unsubscribePez = await api.query.assets.account(
1,
selectedAccount.address,
(assetBalance) => {
if (assetBalance.isSome) {
const assetData = assetBalance.unwrap();
const pezAmount = assetData.balance.toString();
const decimals = 12;
const divisor = Math.pow(10, decimals);
const pezTokens = (parseInt(pezAmount) / divisor).toFixed(4);
setPezBalance(pezTokens);
} else {
setPezBalance('0');
}
}
);
} catch (error) {
console.error('Failed to subscribe to PEZ balance:', error);
}
};
subscribeBalance();
return () => {
if (unsubscribe) unsubscribe();
if (unsubscribeHez) unsubscribeHez();
if (unsubscribePez) unsubscribePez();
};
}, [api, isApiReady, selectedAccount]);
@@ -100,12 +144,12 @@ export const AccountBalance: React.FC = () => {
return (
<div className="space-y-4">
{/* Total Balance Card */}
{/* HEZ Balance Card */}
<Card className="bg-gradient-to-br from-green-900/30 to-yellow-900/30 border-green-500/30">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-lg font-medium text-gray-300">
Total Balance
HEZ Balance
</CardTitle>
<Button
variant="ghost"
@@ -155,6 +199,26 @@ export const AccountBalance: React.FC = () => {
</CardContent>
</Card>
{/* PEZ Balance Card */}
<Card className="bg-gradient-to-br from-blue-900/30 to-purple-900/30 border-blue-500/30">
<CardHeader className="pb-3">
<CardTitle className="text-lg font-medium text-gray-300">
PEZ Token Balance
</CardTitle>
</CardHeader>
<CardContent>
<div>
<div className="text-4xl font-bold text-white mb-1">
{isLoading ? '...' : pezBalance}
<span className="text-2xl text-gray-400 ml-2">PEZ</span>
</div>
<div className="text-sm text-gray-400">
Governance & Rewards Token
</div>
</div>
</CardContent>
</Card>
{/* Account Info */}
<Card className="bg-gray-900 border-gray-800">
<CardContent className="pt-6">
+132
View File
@@ -0,0 +1,132 @@
import React, { useState } from 'react';
import { usePolkadot } from '@/contexts/PolkadotContext';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Copy, CheckCircle, QrCode } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import QRCode from 'qrcode';
interface ReceiveModalProps {
isOpen: boolean;
onClose: () => void;
}
export const ReceiveModal: React.FC<ReceiveModalProps> = ({ isOpen, onClose }) => {
const { selectedAccount } = usePolkadot();
const { toast } = useToast();
const [copied, setCopied] = useState(false);
const [qrCodeDataUrl, setQrCodeDataUrl] = useState<string>('');
React.useEffect(() => {
if (selectedAccount && isOpen) {
// Generate QR code
QRCode.toDataURL(selectedAccount.address, {
width: 300,
margin: 2,
color: {
dark: '#ffffff',
light: '#0f172a'
}
}).then(setQrCodeDataUrl).catch(console.error);
}
}, [selectedAccount, isOpen]);
const handleCopyAddress = async () => {
if (!selectedAccount) return;
try {
await navigator.clipboard.writeText(selectedAccount.address);
setCopied(true);
toast({
title: "Address Copied!",
description: "Your wallet address has been copied to clipboard",
});
setTimeout(() => setCopied(false), 2000);
} catch (error) {
toast({
title: "Copy Failed",
description: "Failed to copy address to clipboard",
variant: "destructive",
});
}
};
if (!selectedAccount) {
return null;
}
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="bg-gray-900 border-gray-800 max-w-md">
<DialogHeader>
<DialogTitle className="text-white">Receive Tokens</DialogTitle>
<DialogDescription className="text-gray-400">
Share this address to receive HEZ, PEZ, and other tokens
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* QR Code */}
<div className="bg-white rounded-lg p-4 mx-auto w-fit">
{qrCodeDataUrl ? (
<img src={qrCodeDataUrl} alt="QR Code" className="w-64 h-64" />
) : (
<div className="w-64 h-64 flex items-center justify-center bg-gray-200">
<QrCode className="w-16 h-16 text-gray-400 animate-pulse" />
</div>
)}
</div>
{/* Account Name */}
<div className="text-center">
<div className="text-sm text-gray-400 mb-1">Account Name</div>
<div className="text-xl font-semibold text-white">
{selectedAccount.meta.name || 'Unnamed Account'}
</div>
</div>
{/* Address */}
<div className="bg-gray-800/50 rounded-lg p-4">
<div className="text-sm text-gray-400 mb-2">Wallet Address</div>
<div className="bg-gray-900 rounded p-3 mb-3">
<div className="text-white font-mono text-sm break-all">
{selectedAccount.address}
</div>
</div>
<Button
onClick={handleCopyAddress}
className="w-full bg-gradient-to-r from-green-600 to-yellow-400 hover:from-green-700 hover:to-yellow-500"
>
{copied ? (
<>
<CheckCircle className="w-4 h-4 mr-2" />
Copied!
</>
) : (
<>
<Copy className="w-4 h-4 mr-2" />
Copy Address
</>
)}
</Button>
</div>
{/* Warning */}
<div className="bg-yellow-500/10 border border-yellow-500/30 rounded-lg p-3">
<p className="text-yellow-400 text-xs">
<strong>Important:</strong> Only send PezkuwiChain compatible tokens to this address. Sending other tokens may result in permanent loss.
</p>
</div>
</div>
</DialogContent>
</Dialog>
);
};
+289
View File
@@ -0,0 +1,289 @@
import React, { useEffect, useState } from 'react';
import { usePolkadot } from '@/contexts/PolkadotContext';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { History, ExternalLink, ArrowUpRight, ArrowDownRight, RefreshCw } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
interface TransactionHistoryProps {
isOpen: boolean;
onClose: () => void;
}
interface Transaction {
blockNumber: number;
extrinsicIndex: number;
hash: string;
method: string;
section: string;
from: string;
to?: string;
amount?: string;
success: boolean;
timestamp?: number;
}
export const TransactionHistory: React.FC<TransactionHistoryProps> = ({ isOpen, onClose }) => {
const { api, isApiReady, selectedAccount } = usePolkadot();
const { toast } = useToast();
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [isLoading, setIsLoading] = useState(false);
const fetchTransactions = async () => {
if (!api || !isApiReady || !selectedAccount) return;
setIsLoading(true);
try {
console.log('Fetching transactions...');
const currentBlock = await api.rpc.chain.getBlock();
const currentBlockNumber = currentBlock.block.header.number.toNumber();
console.log('Current block number:', currentBlockNumber);
const txList: Transaction[] = [];
const blocksToCheck = Math.min(200, currentBlockNumber);
for (let i = 0; i < blocksToCheck && txList.length < 20; i++) {
const blockNumber = currentBlockNumber - i;
try {
const blockHash = await api.rpc.chain.getBlockHash(blockNumber);
const block = await api.rpc.chain.getBlock(blockHash);
// Try to get timestamp, but don't fail if state is pruned
let timestamp = 0;
try {
const ts = await api.query.timestamp.now.at(blockHash);
timestamp = ts.toNumber();
} catch (error) {
// State pruned, use current time as fallback
timestamp = Date.now();
}
console.log(`Block #${blockNumber}: ${block.block.extrinsics.length} extrinsics`);
// Check each extrinsic in the block
block.block.extrinsics.forEach((extrinsic, index) => {
// Skip unsigned extrinsics (system calls)
if (!extrinsic.isSigned) {
return;
}
const { method, signer } = extrinsic;
console.log(` Extrinsic #${index}: ${method.section}.${method.method}, signer: ${signer.toString()}`);
// Check if transaction involves our account
const fromAddress = signer.toString();
const isFromOurAccount = fromAddress === selectedAccount.address;
// Parse balances.transfer or balances.transferKeepAlive
if (method.section === 'balances' &&
(method.method === 'transfer' || method.method === 'transferKeepAlive')) {
const [dest, value] = method.args;
const toAddress = dest.toString();
const isToOurAccount = toAddress === selectedAccount.address;
if (isFromOurAccount || isToOurAccount) {
txList.push({
blockNumber,
extrinsicIndex: index,
hash: extrinsic.hash.toHex(),
method: method.method,
section: method.section,
from: fromAddress,
to: toAddress,
amount: value.toString(),
success: true,
timestamp: timestamp,
});
}
}
// Parse assets.transfer (PEZ, USDT, etc.)
if (method.section === 'assets' && method.method === 'transfer') {
const [assetId, dest, value] = method.args;
const toAddress = dest.toString();
const isToOurAccount = toAddress === selectedAccount.address;
if (isFromOurAccount || isToOurAccount) {
txList.push({
blockNumber,
extrinsicIndex: index,
hash: extrinsic.hash.toHex(),
method: `${method.method} (Asset ${assetId.toString()})`,
section: method.section,
from: fromAddress,
to: toAddress,
amount: value.toString(),
success: true,
timestamp: timestamp,
});
}
}
});
} catch (blockError) {
console.warn(`Error processing block #${blockNumber}:`, blockError);
// Continue to next block
}
}
console.log('Found transactions:', txList.length);
setTransactions(txList);
} catch (error) {
console.error('Failed to fetch transactions:', error);
toast({
title: "Error",
description: "Failed to fetch transaction history",
variant: "destructive",
});
} finally {
setIsLoading(false);
}
};
useEffect(() => {
if (isOpen) {
fetchTransactions();
}
}, [isOpen, api, isApiReady, selectedAccount]);
const formatAmount = (amount: string, decimals: number = 12) => {
const value = parseInt(amount) / Math.pow(10, decimals);
return value.toFixed(4);
};
const formatTimestamp = (timestamp?: number) => {
if (!timestamp) return 'Unknown';
const date = new Date(timestamp);
return date.toLocaleString();
};
const isIncoming = (tx: Transaction) => {
return tx.to === selectedAccount?.address;
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="bg-gray-900 border-gray-800 max-w-3xl max-h-[80vh]">
<DialogHeader>
<div className="flex items-center justify-between">
<div>
<DialogTitle className="text-white">Transaction History</DialogTitle>
<DialogDescription className="text-gray-400">
Recent transactions involving your account
</DialogDescription>
</div>
<Button
variant="ghost"
size="icon"
onClick={fetchTransactions}
disabled={isLoading}
className="text-gray-400 hover:text-white"
>
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
</Button>
</div>
</DialogHeader>
<div className="space-y-3 overflow-y-auto max-h-[500px]">
{isLoading ? (
<div className="text-center py-12">
<RefreshCw className="w-12 h-12 text-gray-600 mx-auto mb-3 animate-spin" />
<p className="text-gray-400">Loading transactions...</p>
</div>
) : transactions.length === 0 ? (
<div className="text-center py-12">
<History className="w-12 h-12 text-gray-600 mx-auto mb-3" />
<p className="text-gray-500">No transactions found</p>
<p className="text-gray-600 text-sm mt-1">
Your recent transactions will appear here
</p>
</div>
) : (
transactions.map((tx, index) => (
<div
key={`${tx.blockNumber}-${tx.extrinsicIndex}`}
className="bg-gray-800/50 border border-gray-700 rounded-lg p-4 hover:bg-gray-800 transition-colors"
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
{isIncoming(tx) ? (
<div className="bg-green-500/20 p-2 rounded-lg">
<ArrowDownRight className="w-4 h-4 text-green-400" />
</div>
) : (
<div className="bg-yellow-500/20 p-2 rounded-lg">
<ArrowUpRight className="w-4 h-4 text-yellow-400" />
</div>
)}
<div>
<div className="text-white font-semibold">
{isIncoming(tx) ? 'Received' : 'Sent'}
</div>
<div className="text-xs text-gray-400">
{tx.section}.{tx.method}
</div>
</div>
</div>
<div className="text-right">
<div className="text-white font-mono">
{isIncoming(tx) ? '+' : '-'}{formatAmount(tx.amount || '0')}
</div>
<div className="text-xs text-gray-400">
Block #{tx.blockNumber}
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<span className="text-gray-500">From:</span>
<div className="text-gray-300 font-mono">
{tx.from.slice(0, 8)}...{tx.from.slice(-6)}
</div>
</div>
{tx.to && (
<div>
<span className="text-gray-500">To:</span>
<div className="text-gray-300 font-mono">
{tx.to.slice(0, 8)}...{tx.to.slice(-6)}
</div>
</div>
)}
</div>
<div className="flex items-center justify-between mt-2 pt-2 border-t border-gray-700">
<div className="text-xs text-gray-500">
{formatTimestamp(tx.timestamp)}
</div>
<Button
variant="ghost"
size="sm"
className="text-xs text-blue-400 hover:text-blue-300"
onClick={() => {
toast({
title: "Transaction Details",
description: `Block #${tx.blockNumber}, Extrinsic #${tx.extrinsicIndex}`,
});
}}
>
View Details
<ExternalLink className="w-3 h-3 ml-1" />
</Button>
</div>
</div>
))
)}
</div>
</DialogContent>
</Dialog>
);
};
+80 -12
View File
@@ -10,6 +10,13 @@ import {
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { ArrowRight, Loader2, CheckCircle, XCircle } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
@@ -18,16 +25,38 @@ interface TransferModalProps {
onClose: () => void;
}
type TokenType = 'HEZ' | 'PEZ' | 'USDT' | 'BTC' | 'ETH' | 'DOT';
interface Token {
symbol: TokenType;
name: string;
assetId?: number;
decimals: number;
color: string;
}
const TOKENS: Token[] = [
{ symbol: 'HEZ', name: 'Hez Token', decimals: 12, color: 'from-green-600 to-yellow-400' },
{ symbol: 'PEZ', name: 'Pez Token', assetId: 1, decimals: 12, color: 'from-blue-600 to-purple-400' },
{ symbol: 'USDT', name: 'Tether USD', assetId: 2, decimals: 6, color: 'from-green-500 to-green-600' },
{ symbol: 'BTC', name: 'Bitcoin', assetId: 3, decimals: 8, color: 'from-orange-500 to-yellow-500' },
{ symbol: 'ETH', name: 'Ethereum', assetId: 4, decimals: 18, color: 'from-purple-500 to-blue-500' },
{ symbol: 'DOT', name: 'Polkadot', assetId: 5, decimals: 10, color: 'from-pink-500 to-red-500' },
];
export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose }) => {
const { api, isApiReady, selectedAccount } = usePolkadot();
const { toast } = useToast();
const [selectedToken, setSelectedToken] = useState<TokenType>('HEZ');
const [recipient, setRecipient] = useState('');
const [amount, setAmount] = useState('');
const [isTransferring, setIsTransferring] = useState(false);
const [txStatus, setTxStatus] = useState<'idle' | 'signing' | 'pending' | 'success' | 'error'>('idle');
const [txHash, setTxHash] = useState('');
const currentToken = TOKENS.find(t => t.symbol === selectedToken) || TOKENS[0];
const handleTransfer = async () => {
if (!api || !isApiReady || !selectedAccount) {
toast({
@@ -55,12 +84,22 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose })
const { web3FromAddress } = await import('@polkadot/extension-dapp');
const injector = await web3FromAddress(selectedAccount.address);
// Convert amount to plancks (12 decimals)
const decimals = 12;
const amountInPlancks = BigInt(parseFloat(amount) * Math.pow(10, decimals));
// Convert amount to smallest unit
const amountInSmallestUnit = BigInt(parseFloat(amount) * Math.pow(10, currentToken.decimals));
// Create transfer transaction
const transfer = api.tx.balances.transferKeepAlive(recipient, amountInPlancks.toString());
let transfer;
// Create appropriate transfer transaction based on token type
if (selectedToken === 'HEZ') {
// Native token transfer
transfer = api.tx.balances.transferKeepAlive(recipient, amountInSmallestUnit.toString());
} else {
// Asset token transfer (PEZ, USDT, BTC, ETH, DOT)
if (!currentToken.assetId) {
throw new Error('Asset ID not configured');
}
transfer = api.tx.assets.transfer(currentToken.assetId, recipient, amountInSmallestUnit.toString());
}
setTxStatus('pending');
@@ -96,7 +135,7 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose })
setTxStatus('success');
toast({
title: "Transfer Successful!",
description: `Sent ${amount} HEZ to ${recipient.slice(0, 8)}...${recipient.slice(-6)}`,
description: `Sent ${amount} ${selectedToken} to ${recipient.slice(0, 8)}...${recipient.slice(-6)}`,
});
// Reset form after 2 seconds
@@ -133,6 +172,7 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose })
setAmount('');
setTxStatus('idle');
setTxHash('');
setSelectedToken('HEZ');
onClose();
}
};
@@ -141,9 +181,9 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose })
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="bg-gray-900 border-gray-800">
<DialogHeader>
<DialogTitle className="text-white">Send HEZ</DialogTitle>
<DialogTitle className="text-white">Send Tokens</DialogTitle>
<DialogDescription className="text-gray-400">
Transfer HEZ tokens to another account
Transfer tokens to another account
</DialogDescription>
</DialogHeader>
@@ -175,6 +215,31 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose })
</div>
) : (
<div className="space-y-4">
{/* Token Selection */}
<div>
<Label htmlFor="token" className="text-white">Select Token</Label>
<Select value={selectedToken} onValueChange={(value) => setSelectedToken(value as TokenType)} disabled={isTransferring}>
<SelectTrigger className="bg-gray-800 border-gray-700 text-white mt-2">
<SelectValue placeholder="Select token" />
</SelectTrigger>
<SelectContent className="bg-gray-800 border-gray-700">
{TOKENS.map((token) => (
<SelectItem
key={token.symbol}
value={token.symbol}
className="text-white hover:bg-gray-700 focus:bg-gray-700"
>
<div className="flex items-center gap-2">
<div className={`w-3 h-3 rounded-full bg-gradient-to-r ${token.color}`}></div>
<span className="font-semibold">{token.symbol}</span>
<span className="text-gray-400 text-sm">- {token.name}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="recipient" className="text-white">Recipient Address</Label>
<Input
@@ -188,17 +253,20 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose })
</div>
<div>
<Label htmlFor="amount" className="text-white">Amount (HEZ)</Label>
<Label htmlFor="amount" className="text-white">Amount ({selectedToken})</Label>
<Input
id="amount"
type="number"
step="0.0001"
step={selectedToken === 'HEZ' || selectedToken === 'PEZ' ? '0.0001' : '0.000001'}
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.0000"
className="bg-gray-800 border-gray-700 text-white mt-2"
disabled={isTransferring}
/>
<div className="text-xs text-gray-500 mt-1">
Decimals: {currentToken.decimals}
</div>
</div>
{txStatus === 'signing' && (
@@ -221,7 +289,7 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose })
<Button
onClick={handleTransfer}
disabled={isTransferring || !recipient || !amount}
className="w-full bg-gradient-to-r from-green-600 to-yellow-400 hover:from-green-700 hover:to-yellow-500"
className={`w-full bg-gradient-to-r ${currentToken.color} hover:opacity-90`}
>
{isTransferring ? (
<>
@@ -230,7 +298,7 @@ export const TransferModal: React.FC<TransferModalProps> = ({ isOpen, onClose })
</>
) : (
<>
Send
Send {selectedToken}
<ArrowRight className="w-4 h-4 ml-2" />
</>
)}
+123
View File
@@ -0,0 +1,123 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=JetBrains+Mono:wght@100..800&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Kurdish color scheme - kesk u sor u zer */
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 148 100% 32%; /* Kurdish green */
--primary-foreground: 0 0% 98%;
--secondary: 358 84% 52%; /* Kurdish red */
--secondary-foreground: 0 0% 98%;
--muted: 52 100% 50%; /* Kurdish yellow muted */
--muted-foreground: 0 0% 20%;
--accent: 52 100% 50%; /* Kurdish yellow */
--accent-foreground: 0 0% 9%;
--destructive: 358 84% 52%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 148 100% 32%;
--radius: 0.5rem;
--chart-1: 148 100% 32%;
--chart-2: 358 84% 52%;
--chart-3: 52 100% 50%;
--chart-4: 148 100% 25%;
--chart-5: 358 84% 40%;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 0 0% 3.9%;
--sidebar-primary: 148 100% 32%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 52 100% 50%;
--sidebar-accent-foreground: 0 0% 9%;
--sidebar-border: 0 0% 89.8%;
--sidebar-ring: 148 100% 32%;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 148 100% 40%; /* Kurdish green dark */
--primary-foreground: 0 0% 9%;
--secondary: 358 84% 60%; /* Kurdish red dark */
--secondary-foreground: 0 0% 9%;
--muted: 52 100% 30%; /* Kurdish yellow dark muted */
--muted-foreground: 0 0% 98%;
--accent: 52 100% 45%; /* Kurdish yellow dark */
--accent-foreground: 0 0% 9%;
--destructive: 358 84% 52%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 148 100% 40%;
--chart-1: 148 100% 40%;
--chart-2: 358 84% 60%;
--chart-3: 52 100% 45%;
--chart-4: 148 100% 30%;
--chart-5: 358 84% 50%;
--sidebar-background: 0 0% 7%;
--sidebar-foreground: 0 0% 98%;
--sidebar-primary: 148 100% 40%;
--sidebar-primary-foreground: 0 0% 9%;
--sidebar-accent: 52 100% 45%;
--sidebar-accent-foreground: 0 0% 9%;
--sidebar-border: 0 0% 14.9%;
--sidebar-ring: 148 100% 40%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground font-sans dark:bg-background dark:text-foreground;
}
pre, code {
@apply font-mono;
}
}
.markdown-editor {
@apply font-mono text-base leading-relaxed;
}
.markdown-preview {
@apply prose max-w-none prose-blue dark:prose-invert;
}
.markdown-preview pre {
@apply bg-secondary p-4 rounded-md overflow-x-auto;
}
.markdown-preview code {
@apply text-sm font-mono text-primary;
}
.markdown-preview h1,
.markdown-preview h2,
.markdown-preview h3,
.markdown-preview h4,
.markdown-preview h5,
.markdown-preview h6 {
@apply font-sans font-semibold text-foreground;
}
.markdown-preview ul,
.markdown-preview ol {
@apply my-4 ml-6;
}
+59
View File
@@ -0,0 +1,59 @@
export default {
// Navigation
'nav.home': 'الرئيسية',
'nav.dashboard': 'لوحة التحكم',
'nav.governance': 'الحوكمة',
'nav.treasury': 'الخزينة',
'nav.staking': 'التخزين',
'nav.forum': 'المنتدى',
'nav.profile': 'الملف الشخصي',
'nav.admin': 'المدير',
// Hero Section
'hero.title': 'منصة حوكمة البلوكشين',
'hero.subtitle': 'حوكمة ديمقراطية وشفافة بتقنية البلوكشين',
'hero.cta.primary': 'ابدأ الآن',
'hero.cta.secondary': 'اعرف المزيد',
// Auth
'auth.login': 'تسجيل الدخول',
'auth.logout': 'تسجيل الخروج',
'auth.signup': 'إنشاء حساب',
'auth.email': 'البريد الإلكتروني',
'auth.password': 'كلمة المرور',
'auth.confirmPassword': 'تأكيد كلمة المرور',
'auth.rememberMe': 'تذكرني',
'auth.forgotPassword': 'نسيت كلمة المرور؟',
// Wallet
'wallet.connect': 'ربط المحفظة',
'wallet.disconnect': 'قطع الاتصال',
'wallet.balance': 'الرصيد',
'wallet.address': 'العنوان',
'wallet.network': 'الشبكة',
// Governance
'governance.proposals': 'المقترحات',
'governance.activeProposals': 'المقترحات النشطة',
'governance.vote': 'التصويت',
'governance.delegate': 'التفويض',
'governance.createProposal': 'إنشاء مقترح',
'governance.votingPower': 'قوة التصويت',
// Common
'common.loading': 'جاري التحميل...',
'common.save': 'حفظ',
'common.cancel': 'إلغاء',
'common.confirm': 'تأكيد',
'common.delete': 'حذف',
'common.edit': 'تعديل',
'common.search': 'بحث',
'common.filter': 'تصفية',
'common.sort': 'ترتيب',
'common.submit': 'إرسال',
'common.back': 'رجوع',
'common.next': 'التالي',
'common.previous': 'السابق',
'common.yes': 'نعم',
'common.no': 'لا'
}
+59
View File
@@ -0,0 +1,59 @@
export default {
// Navigation
'nav.home': 'سەرەتا',
'nav.dashboard': 'داشبۆرد',
'nav.governance': 'حوکمڕانی',
'nav.treasury': 'خەزێنە',
'nav.staking': 'ستەیکینگ',
'nav.forum': 'فۆرەم',
'nav.profile': 'پرۆفایل',
'nav.admin': 'بەڕێوەبەر',
// Hero Section
'hero.title': 'پلاتفۆرمی حوکمڕانی بلۆکچەین',
'hero.subtitle': 'حوکمڕانی دیموکراتی و شەفاف بە تەکنەلۆژیای بلۆکچەین',
'hero.cta.primary': 'دەست پێ بکە',
'hero.cta.secondary': 'زیاتر بزانە',
// Auth
'auth.login': 'چوونەژوورەوە',
'auth.logout': 'دەرچوون',
'auth.signup': 'تۆمارکردن',
'auth.email': 'ئیمەیڵ',
'auth.password': 'وشەی نهێنی',
'auth.confirmPassword': 'دووبارەکردنەوەی وشەی نهێنی',
'auth.rememberMe': 'بمهێنەوە یاد',
'auth.forgotPassword': 'وشەی نهێنیت لەبیر چووە؟',
// Wallet
'wallet.connect': 'جزدان بەستنەوە',
'wallet.disconnect': 'پچڕاندن',
'wallet.balance': 'باڵانس',
'wallet.address': 'ناونیشان',
'wallet.network': 'تۆڕ',
// Governance
'governance.proposals': 'پێشنیارەکان',
'governance.activeProposals': 'پێشنیارە چالاکەکان',
'governance.vote': 'دەنگدان',
'governance.delegate': 'نوێنەر',
'governance.createProposal': 'پێشنیار دروست بکە',
'governance.votingPower': 'هێزی دەنگدان',
// Common
'common.loading': 'چاوەڕوان بە...',
'common.save': 'پاشەکەوتکردن',
'common.cancel': 'هەڵوەشاندنەوە',
'common.confirm': 'دڵنیاکردنەوە',
'common.delete': 'سڕینەوە',
'common.edit': 'دەستکاریکردن',
'common.search': 'گەڕان',
'common.filter': 'فلتەر',
'common.sort': 'ڕیزکردن',
'common.submit': 'ناردن',
'common.back': 'گەڕانەوە',
'common.next': 'دواتر',
'common.previous': 'پێشوو',
'common.yes': 'بەڵێ',
'common.no': 'نەخێر'
}
+59
View File
@@ -0,0 +1,59 @@
export default {
// Navigation
'nav.home': 'Home',
'nav.dashboard': 'Dashboard',
'nav.governance': 'Governance',
'nav.treasury': 'Treasury',
'nav.staking': 'Staking',
'nav.forum': 'Forum',
'nav.profile': 'Profile',
'nav.admin': 'Admin',
// Hero Section
'hero.title': 'Blockchain Governance Platform',
'hero.subtitle': 'Democratic and transparent governance with blockchain technology',
'hero.cta.primary': 'Get Started',
'hero.cta.secondary': 'Learn More',
// Auth
'auth.login': 'Login',
'auth.logout': 'Logout',
'auth.signup': 'Sign Up',
'auth.email': 'Email',
'auth.password': 'Password',
'auth.confirmPassword': 'Confirm Password',
'auth.rememberMe': 'Remember me',
'auth.forgotPassword': 'Forgot password?',
// Wallet
'wallet.connect': 'Connect Wallet',
'wallet.disconnect': 'Disconnect',
'wallet.balance': 'Balance',
'wallet.address': 'Address',
'wallet.network': 'Network',
// Governance
'governance.proposals': 'Proposals',
'governance.activeProposals': 'Active Proposals',
'governance.vote': 'Vote',
'governance.delegate': 'Delegate',
'governance.createProposal': 'Create Proposal',
'governance.votingPower': 'Voting Power',
// Common
'common.loading': 'Loading...',
'common.save': 'Save',
'common.cancel': 'Cancel',
'common.confirm': 'Confirm',
'common.delete': 'Delete',
'common.edit': 'Edit',
'common.search': 'Search',
'common.filter': 'Filter',
'common.sort': 'Sort',
'common.submit': 'Submit',
'common.back': 'Back',
'common.next': 'Next',
'common.previous': 'Previous',
'common.yes': 'Yes',
'common.no': 'No'
}
+59
View File
@@ -0,0 +1,59 @@
export default {
// Navigation
'nav.home': 'خانه',
'nav.dashboard': 'داشبورد',
'nav.governance': 'حکمرانی',
'nav.treasury': 'خزانه',
'nav.staking': 'استیکینگ',
'nav.forum': 'انجمن',
'nav.profile': 'پروفایل',
'nav.admin': 'مدیر',
// Hero Section
'hero.title': 'پلتفرم حکمرانی بلاکچین',
'hero.subtitle': 'حکمرانی دموکراتیک و شفاف با فناوری بلاکچین',
'hero.cta.primary': 'شروع کنید',
'hero.cta.secondary': 'بیشتر بدانید',
// Auth
'auth.login': 'ورود',
'auth.logout': 'خروج',
'auth.signup': 'ثبت نام',
'auth.email': 'ایمیل',
'auth.password': 'رمز عبور',
'auth.confirmPassword': 'تایید رمز عبور',
'auth.rememberMe': 'مرا به خاطر بسپار',
'auth.forgotPassword': 'رمز عبور را فراموش کرده‌اید؟',
// Wallet
'wallet.connect': 'اتصال کیف پول',
'wallet.disconnect': 'قطع اتصال',
'wallet.balance': 'موجودی',
'wallet.address': 'آدرس',
'wallet.network': 'شبکه',
// Governance
'governance.proposals': 'پیشنهادات',
'governance.activeProposals': 'پیشنهادات فعال',
'governance.vote': 'رای دادن',
'governance.delegate': 'نماینده',
'governance.createProposal': 'ایجاد پیشنهاد',
'governance.votingPower': 'قدرت رای',
// Common
'common.loading': 'در حال بارگذاری...',
'common.save': 'ذخیره',
'common.cancel': 'لغو',
'common.confirm': 'تایید',
'common.delete': 'حذف',
'common.edit': 'ویرایش',
'common.search': 'جستجو',
'common.filter': 'فیلتر',
'common.sort': 'مرتب‌سازی',
'common.submit': 'ارسال',
'common.back': 'بازگشت',
'common.next': 'بعدی',
'common.previous': 'قبلی',
'common.yes': 'بله',
'common.no': 'خیر'
}
+59
View File
@@ -0,0 +1,59 @@
export default {
// Navigation
'nav.home': 'Destpêk',
'nav.dashboard': 'Panela Kontrolê',
'nav.governance': 'Rêveberî',
'nav.treasury': 'Xezîne',
'nav.staking': 'Staking',
'nav.forum': 'Forum',
'nav.profile': 'Profîl',
'nav.admin': 'Rêvebir',
// Hero Section
'hero.title': 'Platforma Rêveberiya Blockchain',
'hero.subtitle': 'Rêveberiya demokratîk û şeffaf a bi teknolojiya blockchain',
'hero.cta.primary': 'Dest Pê Bike',
'hero.cta.secondary': 'Zêdetir Bizane',
// Auth
'auth.login': 'Têkeve',
'auth.logout': 'Derkeve',
'auth.signup': 'Tomar Bibe',
'auth.email': 'E-peyam',
'auth.password': 'Şîfre',
'auth.confirmPassword': 'Şîfreyê Piştrast Bike',
'auth.rememberMe': 'Min bi bîr bîne',
'auth.forgotPassword': 'Şîfreya min ji bîr kir?',
// Wallet
'wallet.connect': 'Wallet Girê Bide',
'wallet.disconnect': 'Veqetîne',
'wallet.balance': 'Balans',
'wallet.address': 'Navnîşan',
'wallet.network': 'Tor',
// Governance
'governance.proposals': 'Pêşniyar',
'governance.activeProposals': 'Pêşniyarên Çalak',
'governance.vote': 'Deng Bide',
'governance.delegate': 'Temsîlkar',
'governance.createProposal': 'Pêşniyar Biafirîne',
'governance.votingPower': 'Hêza Dengdanê',
// Common
'common.loading': 'Tê barkirin...',
'common.save': 'Tomar Bike',
'common.cancel': 'Betal',
'common.confirm': 'Piştrast Bike',
'common.delete': 'Jê Bibe',
'common.edit': 'Biguherîne',
'common.search': 'Lêgerîn',
'common.filter': 'Parzûn',
'common.sort': 'Rêz Bike',
'common.submit': 'Bişîne',
'common.back': 'Paşve',
'common.next': 'Pêşve',
'common.previous': 'Berê',
'common.yes': 'Erê',
'common.no': 'Na'
}
+59
View File
@@ -0,0 +1,59 @@
export default {
// Navigation
'nav.home': 'Ana Sayfa',
'nav.dashboard': 'Kontrol Paneli',
'nav.governance': 'Yönetişim',
'nav.treasury': 'Hazine',
'nav.staking': 'Staking',
'nav.forum': 'Forum',
'nav.profile': 'Profil',
'nav.admin': 'Yönetici',
// Hero Section
'hero.title': 'Blockchain Yönetişim Platformu',
'hero.subtitle': 'Blockchain teknolojisi ile demokratik ve şeffaf yönetişim',
'hero.cta.primary': 'Başla',
'hero.cta.secondary': 'Daha Fazla Bilgi',
// Auth
'auth.login': 'Giriş Yap',
'auth.logout': 'Çıkış Yap',
'auth.signup': 'Kayıt Ol',
'auth.email': 'E-posta',
'auth.password': 'Şifre',
'auth.confirmPassword': 'Şifre Tekrar',
'auth.rememberMe': 'Beni hatırla',
'auth.forgotPassword': 'Şifremi unuttum?',
// Wallet
'wallet.connect': 'Cüzdan Bağla',
'wallet.disconnect': 'Bağlantıyı Kes',
'wallet.balance': 'Bakiye',
'wallet.address': 'Adres',
'wallet.network': 'Ağ',
// Governance
'governance.proposals': 'Öneriler',
'governance.activeProposals': 'Aktif Öneriler',
'governance.vote': 'Oy Ver',
'governance.delegate': 'Temsilci',
'governance.createProposal': 'Öneri Oluştur',
'governance.votingPower': 'Oy Gücü',
// Common
'common.loading': 'Yükleniyor...',
'common.save': 'Kaydet',
'common.cancel': 'İptal',
'common.confirm': 'Onayla',
'common.delete': 'Sil',
'common.edit': 'Düzenle',
'common.search': 'Ara',
'common.filter': 'Filtrele',
'common.sort': 'Sırala',
'common.submit': 'Gönder',
'common.back': 'Geri',
'common.next': 'İleri',
'common.previous': 'Önceki',
'common.yes': 'Evet',
'common.no': 'Hayır'
}
+25 -4
View File
@@ -2,12 +2,16 @@ import React, { useState } from 'react';
import { usePolkadot } from '@/contexts/PolkadotContext';
import { AccountBalance } from '@/components/AccountBalance';
import { TransferModal } from '@/components/TransferModal';
import { ReceiveModal } from '@/components/ReceiveModal';
import { TransactionHistory } from '@/components/TransactionHistory';
import { Button } from '@/components/ui/button';
import { ArrowUpRight, ArrowDownRight, History } from 'lucide-react';
const WalletDashboard: React.FC = () => {
const { selectedAccount } = usePolkadot();
const [isTransferModalOpen, setIsTransferModalOpen] = useState(false);
const [isReceiveModalOpen, setIsReceiveModalOpen] = useState(false);
const [isHistoryModalOpen, setIsHistoryModalOpen] = useState(false);
if (!selectedAccount) {
return (
@@ -45,20 +49,20 @@ const WalletDashboard: React.FC = () => {
<ArrowUpRight className="w-6 h-6 mb-2" />
<span>Send</span>
</Button>
<Button
onClick={() => setIsReceiveModalOpen(true)}
variant="outline"
className="border-gray-700 hover:bg-gray-800 h-24 flex flex-col items-center justify-center"
disabled
>
<ArrowDownRight className="w-6 h-6 mb-2" />
<span>Receive</span>
</Button>
<Button
onClick={() => setIsHistoryModalOpen(true)}
variant="outline"
className="border-gray-700 hover:bg-gray-800 h-24 flex flex-col items-center justify-center"
disabled
>
<History className="w-6 h-6 mb-2" />
<span>History</span>
@@ -74,6 +78,13 @@ const WalletDashboard: React.FC = () => {
<p className="text-gray-600 text-sm mt-1">
Your transaction history will appear here
</p>
<Button
onClick={() => setIsHistoryModalOpen(true)}
variant="outline"
className="mt-4 border-gray-700 hover:bg-gray-800"
>
View All Transactions
</Button>
</div>
</div>
</div>
@@ -84,8 +95,18 @@ const WalletDashboard: React.FC = () => {
isOpen={isTransferModalOpen}
onClose={() => setIsTransferModalOpen(false)}
/>
<ReceiveModal
isOpen={isReceiveModalOpen}
onClose={() => setIsReceiveModalOpen(false)}
/>
<TransactionHistory
isOpen={isHistoryModalOpen}
onClose={() => setIsHistoryModalOpen(false)}
/>
</div>
);
};
export default WalletDashboard;
export default WalletDashboard;