feat: 241125/auth_page_UI_and_set_up_tauri

This commit is contained in:
NguyenAnhQuan 2025-11-26 00:14:32 +07:00
parent 4bba111113
commit 4da3a8ff7d
115 changed files with 2907 additions and 90 deletions

5
.gitignore vendored
View File

@ -55,3 +55,8 @@ src/components.d.ts
# Optional REPL history # Optional REPL history
.node_repl_history .node_repl_history
# Tauri
src-tauri/target
src-tauri/Cargo.lock
src-tauri/WixTools

View File

@ -14,12 +14,12 @@
<meta name="theme-color" content="#3f51b5" /> <meta name="theme-color" content="#3f51b5" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Neptune Wallet" /> <meta name="apple-mobile-web-app-title" content="Neptune Privacy" />
<!-- App Description --> <!-- App Description -->
<meta <meta
name="description" name="description"
content="Neptune Wallet - Secure cryptocurrency wallet for Neptune blockchain" content="Neptune Privacy - Secure cryptocurrency wallet for Neptune network"
/> />
<!-- Google Fonts - Inter (Modern, clean, mobile-optimized) --> <!-- Google Fonts - Inter (Modern, clean, mobile-optimized) -->
@ -30,7 +30,7 @@
rel="stylesheet" rel="stylesheet"
/> />
<title>Neptune Wallet</title> <title>Neptune Privacy</title>
</head> </head>
<body> <body>

View File

@ -1,5 +1,5 @@
{ {
"name": "neptune-wallet", "name": "neptune-privacy",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
@ -9,10 +9,14 @@
"preview": "vite preview", "preview": "vite preview",
"type-check": "vue-tsc --build --force", "type-check": "vue-tsc --build --force",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .eslintignore", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .eslintignore",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,vue,css,scss,json}\"" "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,vue,css,scss,json}\"",
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.17", "@tailwindcss/vite": "^4.1.17",
"@tanstack/vue-form": "^1.26.0",
"@vueuse/core": "^14.0.0", "@vueuse/core": "^14.0.0",
"axios": "^1.7.9", "axios": "^1.7.9",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@ -25,10 +29,13 @@
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vue": "^3.5.24", "vue": "^3.5.24",
"vue-i18n": "^10.0.8", "vue-i18n": "^10.0.8",
"vue-router": "^4.5.0" "vue-router": "^4.5.0",
"vue-sonner": "^2.0.9",
"zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.15.0", "@rushstack/eslint-patch": "^1.15.0",
"@tauri-apps/cli": "^2.9.4",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.0.0",

208
pnpm-lock.yaml generated
View File

@ -11,6 +11,9 @@ importers:
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.1.17 specifier: ^4.1.17
version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))
'@tanstack/vue-form':
specifier: ^1.26.0
version: 1.26.0(vue@3.5.24(typescript@5.9.3))
'@vueuse/core': '@vueuse/core':
specifier: ^14.0.0 specifier: ^14.0.0
version: 14.0.0(vue@3.5.24(typescript@5.9.3)) version: 14.0.0(vue@3.5.24(typescript@5.9.3))
@ -50,10 +53,19 @@ importers:
vue-router: vue-router:
specifier: ^4.5.0 specifier: ^4.5.0
version: 4.6.3(vue@3.5.24(typescript@5.9.3)) version: 4.6.3(vue@3.5.24(typescript@5.9.3))
vue-sonner:
specifier: ^2.0.9
version: 2.0.9
zod:
specifier: ^4.1.13
version: 4.1.13
devDependencies: devDependencies:
'@rushstack/eslint-patch': '@rushstack/eslint-patch':
specifier: ^1.15.0 specifier: ^1.15.0
version: 1.15.0 version: 1.15.0
'@tauri-apps/cli':
specifier: ^2.9.4
version: 2.9.4
'@types/node': '@types/node':
specifier: ^24.10.1 specifier: ^24.10.1
version: 24.10.1 version: 24.10.1
@ -600,14 +612,113 @@ packages:
peerDependencies: peerDependencies:
vite: ^5.2.0 || ^6 || ^7 vite: ^5.2.0 || ^6 || ^7
'@tanstack/devtools-event-client@0.3.5':
resolution: {integrity: sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw==}
engines: {node: '>=18'}
'@tanstack/form-core@1.26.0':
resolution: {integrity: sha512-CVSrNwnRt8V0vULOr82slIckaB7w7dOMKF+GMP9rmbaCBzXHJt+JQRj4NiH4PyPz31DAJoFE+BxcrhcVU2ZjTw==}
'@tanstack/pacer@0.15.4':
resolution: {integrity: sha512-vGY+CWsFZeac3dELgB6UZ4c7OacwsLb8hvL2gLS6hTgy8Fl0Bm/aLokHaeDIP+q9F9HUZTnp360z9uv78eg8pg==}
engines: {node: '>=18'}
'@tanstack/store@0.7.7':
resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==}
'@tanstack/virtual-core@3.13.12': '@tanstack/virtual-core@3.13.12':
resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
'@tanstack/vue-form@1.26.0':
resolution: {integrity: sha512-6dIxrA2ZpyrEq6QiSA47PUEvwedqYY+dBFfBvzhB2ugKBgwGvhomTaB5Bo7s2qaxeTcpBaEFifSny3IYxCULIQ==}
peerDependencies:
vue: ^3.4.0
'@tanstack/vue-store@0.7.7':
resolution: {integrity: sha512-6iv1Odmreff6TgEjQN11xoddsCnpn+/ul7MZ2DadHT3/RSY1YdoFafK8lCa889MEFi/5K0zAhf8psIkgTrRa9A==}
peerDependencies:
'@vue/composition-api': ^1.2.1
vue: ^2.5.0 || ^3.0.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
'@tanstack/vue-virtual@3.13.12': '@tanstack/vue-virtual@3.13.12':
resolution: {integrity: sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==} resolution: {integrity: sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==}
peerDependencies: peerDependencies:
vue: ^2.7.0 || ^3.0.0 vue: ^2.7.0 || ^3.0.0
'@tauri-apps/cli-darwin-arm64@2.9.4':
resolution: {integrity: sha512-9rHkMVtbMhe0AliVbrGpzMahOBg3rwV46JYRELxR9SN6iu1dvPOaMaiC4cP6M/aD1424ziXnnMdYU06RAH8oIw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tauri-apps/cli-darwin-x64@2.9.4':
resolution: {integrity: sha512-VT9ymNuT06f5TLjCZW2hfSxbVtZDhORk7CDUDYiq5TiSYQdxkl8MVBy0CCFFcOk4QAkUmqmVUA9r3YZ/N/vPRQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@2.9.4':
resolution: {integrity: sha512-tTWkEPig+2z3Rk0zqZYfjUYcgD+aSm72wdrIhdYobxbQZOBw0zfn50YtWv+av7bm0SHvv75f0l7JuwgZM1HFow==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@2.9.4':
resolution: {integrity: sha512-ql6vJ611qoqRYHxkKPnb2vHa27U+YRKRmIpLMMBeZnfFtZ938eao7402AQCH1mO2+/8ioUhbpy9R/ZcLTXVmkg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tauri-apps/cli-linux-arm64-musl@2.9.4':
resolution: {integrity: sha512-vg7yNn7ICTi6hRrcA/6ff2UpZQP7un3xe3SEld5QM0prgridbKAiXGaCKr3BnUBx/rGXegQlD/wiLcWdiiraSw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tauri-apps/cli-linux-riscv64-gnu@2.9.4':
resolution: {integrity: sha512-l8L+3VxNk6yv5T/Z/gv5ysngmIpsai40B9p6NQQyqYqxImqYX37pqREoEBl1YwG7szGnDibpWhidPrWKR59OJA==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
'@tauri-apps/cli-linux-x64-gnu@2.9.4':
resolution: {integrity: sha512-PepPhCXc/xVvE3foykNho46OmCyx47E/aG676vKTVp+mqin5d+IBqDL6wDKiGNT5OTTxKEyNlCQ81Xs2BQhhqA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tauri-apps/cli-linux-x64-musl@2.9.4':
resolution: {integrity: sha512-zcd1QVffh5tZs1u1SCKUV/V7RRynebgYUNWHuV0FsIF1MjnULUChEXhAhug7usCDq4GZReMJOoXa6rukEozWIw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tauri-apps/cli-win32-arm64-msvc@2.9.4':
resolution: {integrity: sha512-/7ZhnP6PY04bEob23q8MH/EoDISdmR1wuNm0k9d5HV7TDMd2GGCDa8dPXA4vJuglJKXIfXqxFmZ4L+J+MO42+w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@2.9.4':
resolution: {integrity: sha512-1LmAfaC4Cq+3O1Ir1ksdhczhdtFSTIV51tbAGtbV/mr348O+M52A/xwCCXQank0OcdBxy5BctqkMtuZnQvA8uQ==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@tauri-apps/cli-win32-x64-msvc@2.9.4':
resolution: {integrity: sha512-EdYd4c9wGvtPB95kqtEyY+bUR+k4kRw3IA30mAQ1jPH6z57AftT8q84qwv0RDp6kkEqOBKxeInKfqi4BESYuqg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tauri-apps/cli@2.9.4':
resolution: {integrity: sha512-pvylWC9QckrOS9ATWXIXcgu7g2hKK5xTL5ZQyZU/U0n9l88SEFGcWgLQNa8WZmd+wWIOWhkxOFcOl3i6ubDNNw==}
engines: {node: '>= 10'}
hasBin: true
'@types/estree@1.0.8': '@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@ -1684,6 +1795,20 @@ packages:
peerDependencies: peerDependencies:
vue: ^3.5.0 vue: ^3.5.0
vue-sonner@2.0.9:
resolution: {integrity: sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw==}
peerDependencies:
'@nuxt/kit': ^4.0.3
'@nuxt/schema': ^4.0.3
nuxt: ^4.0.3
peerDependenciesMeta:
'@nuxt/kit':
optional: true
'@nuxt/schema':
optional: true
nuxt:
optional: true
vue-tsc@3.1.5: vue-tsc@3.1.5:
resolution: {integrity: sha512-L/G9IUjOWhBU0yun89rv8fKqmKC+T0HfhrFjlIml71WpfBv9eb4E9Bev8FMbyueBIU9vxQqbd+oOsVcDa5amGw==} resolution: {integrity: sha512-L/G9IUjOWhBU0yun89rv8fKqmKC+T0HfhrFjlIml71WpfBv9eb4E9Bev8FMbyueBIU9vxQqbd+oOsVcDa5amGw==}
hasBin: true hasBin: true
@ -1718,6 +1843,9 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
zod@4.1.13:
resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==}
snapshots: snapshots:
'@babel/helper-string-parser@7.27.1': {} '@babel/helper-string-parser@7.27.1': {}
@ -2083,13 +2211,89 @@ snapshots:
tailwindcss: 4.1.17 tailwindcss: 4.1.17
vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)
'@tanstack/devtools-event-client@0.3.5': {}
'@tanstack/form-core@1.26.0':
dependencies:
'@tanstack/devtools-event-client': 0.3.5
'@tanstack/pacer': 0.15.4
'@tanstack/store': 0.7.7
'@tanstack/pacer@0.15.4':
dependencies:
'@tanstack/devtools-event-client': 0.3.5
'@tanstack/store': 0.7.7
'@tanstack/store@0.7.7': {}
'@tanstack/virtual-core@3.13.12': {} '@tanstack/virtual-core@3.13.12': {}
'@tanstack/vue-form@1.26.0(vue@3.5.24(typescript@5.9.3))':
dependencies:
'@tanstack/form-core': 1.26.0
'@tanstack/vue-store': 0.7.7(vue@3.5.24(typescript@5.9.3))
vue: 3.5.24(typescript@5.9.3)
transitivePeerDependencies:
- '@vue/composition-api'
'@tanstack/vue-store@0.7.7(vue@3.5.24(typescript@5.9.3))':
dependencies:
'@tanstack/store': 0.7.7
vue: 3.5.24(typescript@5.9.3)
vue-demi: 0.14.10(vue@3.5.24(typescript@5.9.3))
'@tanstack/vue-virtual@3.13.12(vue@3.5.24(typescript@5.9.3))': '@tanstack/vue-virtual@3.13.12(vue@3.5.24(typescript@5.9.3))':
dependencies: dependencies:
'@tanstack/virtual-core': 3.13.12 '@tanstack/virtual-core': 3.13.12
vue: 3.5.24(typescript@5.9.3) vue: 3.5.24(typescript@5.9.3)
'@tauri-apps/cli-darwin-arm64@2.9.4':
optional: true
'@tauri-apps/cli-darwin-x64@2.9.4':
optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@2.9.4':
optional: true
'@tauri-apps/cli-linux-arm64-gnu@2.9.4':
optional: true
'@tauri-apps/cli-linux-arm64-musl@2.9.4':
optional: true
'@tauri-apps/cli-linux-riscv64-gnu@2.9.4':
optional: true
'@tauri-apps/cli-linux-x64-gnu@2.9.4':
optional: true
'@tauri-apps/cli-linux-x64-musl@2.9.4':
optional: true
'@tauri-apps/cli-win32-arm64-msvc@2.9.4':
optional: true
'@tauri-apps/cli-win32-ia32-msvc@2.9.4':
optional: true
'@tauri-apps/cli-win32-x64-msvc@2.9.4':
optional: true
'@tauri-apps/cli@2.9.4':
optionalDependencies:
'@tauri-apps/cli-darwin-arm64': 2.9.4
'@tauri-apps/cli-darwin-x64': 2.9.4
'@tauri-apps/cli-linux-arm-gnueabihf': 2.9.4
'@tauri-apps/cli-linux-arm64-gnu': 2.9.4
'@tauri-apps/cli-linux-arm64-musl': 2.9.4
'@tauri-apps/cli-linux-riscv64-gnu': 2.9.4
'@tauri-apps/cli-linux-x64-gnu': 2.9.4
'@tauri-apps/cli-linux-x64-musl': 2.9.4
'@tauri-apps/cli-win32-arm64-msvc': 2.9.4
'@tauri-apps/cli-win32-ia32-msvc': 2.9.4
'@tauri-apps/cli-win32-x64-msvc': 2.9.4
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
'@types/json-schema@7.0.15': {} '@types/json-schema@7.0.15': {}
@ -3223,6 +3427,8 @@ snapshots:
'@vue/devtools-api': 6.6.4 '@vue/devtools-api': 6.6.4
vue: 3.5.24(typescript@5.9.3) vue: 3.5.24(typescript@5.9.3)
vue-sonner@2.0.9: {}
vue-tsc@3.1.5(typescript@5.9.3): vue-tsc@3.1.5(typescript@5.9.3):
dependencies: dependencies:
'@volar/typescript': 2.4.23 '@volar/typescript': 2.4.23
@ -3250,3 +3456,5 @@ snapshots:
xml-name-validator@4.0.0: {} xml-name-validator@4.0.0: {}
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}
zod@4.1.13: {}

4
src-tauri/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/gen/schemas

31
src-tauri/Cargo.toml Normal file
View File

@ -0,0 +1,31 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.77.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.5.1", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.9.2", features = [] }
tauri-plugin-log = "2"
[target.'cfg(target_os = "android")'.dependencies]
jni = "0.21"
[target.'cfg(target_os = "ios")'.dependencies]
objc = "0.2"

3
src-tauri/build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@ -0,0 +1,11 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": [
"main"
],
"permissions": [
"core:default"
]
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src-tauri/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

25
src-tauri/src/lib.rs Normal file
View File

@ -0,0 +1,25 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
// Setup logging with rotation and file targets
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.target(tauri_plugin_log::Target::new(
tauri_plugin_log::TargetKind::Stdout,
))
.target(tauri_plugin_log::Target::new(
tauri_plugin_log::TargetKind::LogDir {
file_name: Some("neptune-privacy".into())
}
))
.max_file_size(10_485_760) // 10MB
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepAll)
.build(),
)?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

6
src-tauri/src/main.rs Normal file
View File

@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run();
}

57
src-tauri/tauri.conf.json Normal file
View File

@ -0,0 +1,57 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Neptune Privacy",
"version": "0.1.0",
"identifier": "com.neptune.privacy",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:5173",
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build"
},
"app": {
"windows": [
{
"title": "Neptune Privacy",
"width": 800,
"height": 800,
"minWidth": 375,
"resizable": true,
"fullscreen": false,
"center": true
}
],
"security": {
"csp": {
"default-src": "'self' 'unsafe-inline'",
"connect-src": "'self' https: wss: http://localhost:* ws://localhost:*",
"script-src": "'self' 'unsafe-inline' 'unsafe-eval'",
"style-src": "'self' 'unsafe-inline'",
"img-src": "'self' data: https: http:",
"font-src": "'self' data:",
"worker-src": "'self' blob:"
},
"dangerousDisableAssetCspModification": false,
"freezePrototype": true
},
"withGlobalTauri": false,
"macOSPrivateApi": false
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"iOS": {
"minimumSystemVersion": "13.0"
},
"android": {
"minSdkVersion": 24
}
}
}

View File

@ -1,30 +1,7 @@
<script setup lang="ts"> <script setup lang="ts"></script>
import { ThemeToggle } from '@/components/ui/theme-toggle'
import neptuneLogo from '@/assets/imgs/neptune_logo.jpg'
</script>
<template> <template>
<div class="min-h-screen bg-background"> <router-view />
<!-- Header with Logo and Theme Toggle -->
<header class="border-b border-border">
<div class="container mx-auto flex items-center justify-between px-4 py-4">
<div class="flex items-center gap-3">
<img
:src="neptuneLogo"
alt="Neptune Wallet Logo"
class="h-12 w-12 rounded-lg object-cover"
/>
<span class="text-xl font-bold text-foreground">Neptune</span>
</div>
<ThemeToggle />
</div>
</header>
<!-- Main Content -->
<main>
<router-view />
</main>
</div>
</template> </template>

Binary file not shown.

View File

@ -0,0 +1,94 @@
<script setup lang="ts">
import { Home, Wallet, History, Settings } from 'lucide-vue-next'
const route = useRoute()
const router = useRouter()
// Navigation items for bottom tab bar
const navItems = [
{ name: 'Home', icon: Home, route: '/', label: 'Home' },
{ name: 'Wallet', icon: Wallet, route: '/wallet', label: 'Wallet' },
{ name: 'History', icon: History, route: '/history', label: 'History' },
{ name: 'Settings', icon: Settings, route: '/settings', label: 'Settings' },
]
const isActiveRoute = (routePath: string) => {
return route.path === routePath
}
</script>
<template>
<div class="flex h-screen flex-col bg-background">
<!-- Header -->
<header class="border-b border-border bg-card">
<div class="flex h-14 items-center justify-between px-4">
<div class="flex items-center gap-3">
<img
src="@/assets/imgs/neptune_logo.jpg"
alt="Neptune"
class="h-8 w-8 rounded-lg object-cover"
/>
<span class="text-lg font-semibold text-foreground">Neptune</span>
</div>
<ThemeToggle />
</div>
</header>
<!-- Main Content Area (Scrollable) -->
<main class="flex-1 overflow-y-auto">
<slot />
</main>
<!-- Bottom Navigation Bar -->
<nav
class="safe-area-bottom border-t border-border bg-card shadow-[0_-4px_6px_-1px_rgb(0_0_0/0.1),0_-2px_4px_-2px_rgb(0_0_0/0.1)]"
role="navigation"
aria-label="Main navigation"
>
<div class="grid grid-cols-4">
<button
v-for="item in navItems"
:key="item.name"
type="button"
class="flex flex-col items-center justify-center gap-1 py-2 transition-colors"
:class="[
isActiveRoute(item.route)
? 'text-primary'
: 'text-muted-foreground hover:text-foreground active:text-foreground',
]"
:aria-label="item.label"
:aria-current="isActiveRoute(item.route) ? 'page' : undefined"
@click="router.push(item.route)"
>
<component :is="item.icon" :class="['h-5 w-5', isActiveRoute(item.route) && 'stroke-[2.5]']" />
<span class="text-xs font-medium">{{ item.label }}</span>
</button>
</div>
</nav>
</div>
</template>
<style scoped>
/* Safe area for notched devices (iPhone X, etc.) */
.safe-area-bottom {
padding-bottom: env(safe-area-inset-bottom, 0);
}
/* Prevent overscroll on iOS */
main {
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
}
/* Active tab indicator animation */
button {
position: relative;
-webkit-tap-highlight-color: transparent;
}
/* Ripple effect for better touch feedback */
button:active {
transform: scale(0.95);
}
</style>

View File

@ -0,0 +1 @@
export { default as Layout } from './Layout.vue'

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import type { AlertVariants } from "."
import { cn } from "@/lib/utils"
import { alertVariants } from "."
const props = defineProps<{
class?: HTMLAttributes["class"]
variant?: AlertVariants["variant"]
}>()
</script>
<template>
<div :class="cn(alertVariants({ variant }), props.class)" role="alert">
<slot />
</div>
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div :class="cn('text-sm [&_p]:leading-relaxed', props.class)">
<slot />
</div>
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<h5 :class="cn('mb-1 font-medium leading-none tracking-tight', props.class)">
<slot />
</h5>
</template>

View File

@ -0,0 +1,24 @@
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export { default as Alert } from "./Alert.vue"
export { default as AlertDescription } from "./AlertDescription.vue"
export { default as AlertTitle } from "./AlertTitle.vue"
export const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
)
export type AlertVariants = VariantProps<typeof alertVariants>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import type { BadgeVariants } from "."
import { cn } from "@/lib/utils"
import { badgeVariants } from "."
const props = defineProps<{
variant?: BadgeVariants["variant"]
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div :class="cn(badgeVariants({ variant }), props.class)">
<slot />
</div>
</template>

View File

@ -0,0 +1,26 @@
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export { default as Badge } from "./Badge.vue"
export const badgeVariants = cva(
"inline-flex gap-1 items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
)
export type BadgeVariants = VariantProps<typeof badgeVariants>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
:class="
cn(
'rounded-xl border bg-card text-card-foreground shadow',
props.class,
)
"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div :class="cn('p-6 pt-0', props.class)">
<slot />
</div>
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<p :class="cn('text-sm text-muted-foreground', props.class)">
<slot />
</p>
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div :class="cn('flex items-center p-6 pt-0', props.class)">
<slot />
</div>
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
<slot />
</div>
</template>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<h3
:class="
cn('font-semibold leading-none tracking-tight', props.class)
"
>
<slot />
</h3>
</template>

View File

@ -0,0 +1,6 @@
export { default as Card } from "./Card.vue"
export { default as CardContent } from "./CardContent.vue"
export { default as CardDescription } from "./CardDescription.vue"
export { default as CardFooter } from "./CardFooter.vue"
export { default as CardHeader } from "./CardHeader.vue"
export { default as CardTitle } from "./CardTitle.vue"

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { useVModel } from "@vueuse/core"
import { cn } from "@/lib/utils"
const props = defineProps<{
defaultValue?: string | number
modelValue?: string | number
class?: HTMLAttributes["class"]
}>()
const emits = defineEmits<{
(e: "update:modelValue", payload: string | number): void
}>()
const modelValue = useVModel(props, "modelValue", emits, {
passive: true,
defaultValue: props.defaultValue,
})
</script>
<template>
<input v-model="modelValue" :class="cn('flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', props.class)">
</template>

View File

@ -0,0 +1 @@
export { default as Input } from "./Input.vue"

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
import type { LabelProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Label } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<LabelProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<Label
v-bind="delegatedProps"
:class="
cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
props.class,
)
"
>
<slot />
</Label>
</template>

View File

@ -0,0 +1 @@
export { default as Label } from "./Label.vue"

View File

@ -0,0 +1,36 @@
<script setup lang="ts">
import type { ProgressRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
ProgressIndicator,
ProgressRoot,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = withDefaults(
defineProps<ProgressRootProps & { class?: HTMLAttributes["class"] }>(),
{
modelValue: 0,
},
)
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<ProgressRoot
v-bind="delegatedProps"
:class="
cn(
'relative h-2 w-full overflow-hidden rounded-full bg-primary/20',
props.class,
)
"
>
<ProgressIndicator
class="h-full w-full flex-1 bg-primary transition-all"
:style="`transform: translateX(-${100 - (props.modelValue ?? 0)}%);`"
/>
</ProgressRoot>
</template>

View File

@ -0,0 +1 @@
export { default as Progress } from "./Progress.vue"

View File

@ -0,0 +1,29 @@
<script setup lang="ts">
import type { SeparatorProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { Separator } from "reka-ui"
import { cn } from "@/lib/utils"
const props = withDefaults(defineProps<
SeparatorProps & { class?: HTMLAttributes["class"] }
>(), {
orientation: "horizontal",
decorative: true,
})
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<Separator
v-bind="delegatedProps"
:class="
cn(
'shrink-0 bg-border',
props.orientation === 'horizontal' ? 'h-px w-full' : 'w-px h-full',
props.class,
)
"
/>
</template>

View File

@ -0,0 +1 @@
export { default as Separator } from "./Separator.vue"

View File

@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { useColorMode } from '@vueuse/core'
import { Moon, Sun } from 'lucide-vue-next' import { Moon, Sun } from 'lucide-vue-next'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'

View File

@ -0,0 +1,74 @@
import { computed } from 'vue'
import { useMediaQuery } from '@vueuse/core'
/**
* Detect if device is mobile
*/
export function useMobile() {
const isMobile = useMediaQuery('(max-width: 768px)')
return isMobile
}
/**
* Detect if device is iOS
*/
export function useIsIOS() {
return computed(() => {
if (typeof window === 'undefined') return false
return /iPhone|iPad|iPod/i.test(navigator.userAgent)
})
}
/**
* Get safe area insets for notched devices
*/
export function useSafeArea() {
return {
top: computed(() => {
if (typeof window === 'undefined') return 0
return parseInt(
getComputedStyle(document.documentElement).getPropertyValue('env(safe-area-inset-top)') ||
'0'
)
}),
bottom: computed(() => {
if (typeof window === 'undefined') return 0
return parseInt(
getComputedStyle(document.documentElement).getPropertyValue(
'env(safe-area-inset-bottom)'
) || '0'
)
}),
}
}
/**
* Prevent pull-to-refresh on mobile browsers
*/
export function usePreventPullToRefresh() {
if (typeof window === 'undefined') return
let touchStartY = 0
document.addEventListener(
'touchstart',
e => {
touchStartY = e.touches[0]?.clientY ?? 0
},
{ passive: false }
)
document.addEventListener(
'touchmove',
e => {
const touchY = e.touches[0]?.clientY ?? 0
const touchYDelta = touchY - touchStartY
// Prevent pull-to-refresh if scrolling up at the top
if (touchYDelta > 0 && window.scrollY === 0) {
e.preventDefault()
}
},
{ passive: false }
)
}

View File

@ -1,7 +1,7 @@
export default { export default {
common: { common: {
app_name: 'Neptune Wallet', app_name: 'Neptune Privacy',
welcome: 'Welcome to Neptune Wallet', welcome: 'Welcome to Neptune Privacy',
cancel: 'Cancel', cancel: 'Cancel',
confirm: 'Confirm', confirm: 'Confirm',
save: 'Save', save: 'Save',
@ -38,4 +38,3 @@ export default {
failed_to_load: 'Failed to load data', failed_to_load: 'Failed to load data',
}, },
} }

View File

@ -1,7 +1,7 @@
export default { export default {
common: { common: {
app_name: 'Neptune ウォレット', app_name: 'Neptune Privacy',
welcome: 'Neptune ウォレットへようこそ', welcome: 'Neptune Privacyへようこそ',
cancel: 'キャンセル', cancel: 'キャンセル',
confirm: '確認', confirm: '確認',
save: '保存', save: '保存',
@ -38,4 +38,3 @@ export default {
failed_to_load: 'データの読み込みに失敗しました', failed_to_load: 'データの読み込みに失敗しました',
}, },
} }

View File

@ -1,46 +1,23 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router' import { routes } from './route'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'home',
component: () => import('@/views/HomeView.vue'),
meta: { requiresAuth: true },
},
{
path: '/auth',
name: 'auth',
component: () => import('@/views/AuthView.vue'),
meta: { requiresAuth: false },
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('@/views/NotFoundView.vue'),
},
]
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: import.meta.env.NODE_ENV ? createWebHistory() : createWebHashHistory(),
routes, routes,
}) })
// Navigation guards // Navigation guards
router.beforeEach((to, _from, next) => { router.beforeEach((to, _from, next) => {
// Add authentication logic here const hasWallet = false // useNeptuneStore().hasWallet
// const hasWallet = useNeptuneStore().hasWallet
if (to.meta.requiresAuth) { if (to.meta.requiresAuth) {
// Check if user has wallet if (!hasWallet) {
// if (!hasWallet) { next({ name: 'auth' })
// next({ name: 'auth' }) return
// return }
// }
} }
next() next()
}) })
export default router export default router

28
src/router/route.ts Normal file
View File

@ -0,0 +1,28 @@
import type { RouteRecordRaw } from 'vue-router'
import { Layout } from '@/components/commons/layout'
import * as Pages from '@/views'
export const routes: RouteRecordRaw[] = [
{
path: '/auth',
name: 'auth',
component: Pages.AuthPage,
meta: { requiresAuth: false },
},
{
path: '/',
component: Layout,
meta: { requiresAuth: true },
children: [
// { path: '/home', name: 'home', component: Pages.HomePage },
{ path: '/wallet', name: 'wallet', component: Pages.WalletPage },
// { path: '/history', name: 'history', component: () => import('@/views/HistoryView.vue') },
// { path: '/settings', name: 'settings', component: () => import('@/views/SettingsView.vue') },
],
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('@/views/NotFoundView.vue'),
},
]

View File

@ -49,4 +49,3 @@ export const useAuthStore = defineStore('auth', () => {
goBack, goBack,
} }
}) })

View File

@ -4,6 +4,15 @@
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
/* Montserrat Variable Font */
@font-face {
font-family: 'Montserrat';
src: url('./assets/fonts/Montserrat-VariableFont_wght.ttf') format('truetype-variations');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@theme inline { @theme inline {
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 2px);
@ -160,8 +169,17 @@
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
font-family:
'Montserrat',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif;
/* Mobile-first optimizations */ /* Mobile-first optimizations */
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
} }
} }

View File

@ -0,0 +1,80 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useAuthStore } from '@/stores'
import { Toaster } from 'vue-sonner'
import { CreateWalletFlow, LoginTab, OnboardingTab, RecoverWalletFlow } from './components'
const authStore = useAuthStore()
const router = useRouter()
// const neptuneWallet = useNeptuneWallet()
const currentState = computed(() => authStore.getCurrentState)
const handleGoToCreate = () => {
authStore.goToCreate()
}
const handleGoToRecover = () => {
authStore.goToRecover()
}
const handlePasswordSubmit = async (password: string) => {
const loginRef = document.querySelector('login-tab') as any
try {
// TODO: Decrypt keystore with password
// await neptuneWallet.decryptKeystore(password)
// Mock success for now
router.push({ name: 'home' })
} catch (err) {
if (loginRef) {
loginRef.setError(true)
loginRef.setLoading(false)
}
console.error('Failed to unlock wallet:', err)
}
}
const handleAccessWallet = () => {
router.push({ name: 'wallet' })
}
const handleCancel = () => {
authStore.goBack()
}
</script>
<template>
<div class="min-h-screen">
<!-- Onboarding: Welcome screen -->
<OnboardingTab
v-if="currentState === 'onboarding'"
@go-to-create="handleGoToCreate"
@go-to-recover="handleGoToRecover"
/>
<!-- Login: Unlock existing wallet -->
<LoginTab
v-else-if="currentState === 'login'"
@go-to-create="handleGoToCreate"
@submit="handlePasswordSubmit"
/>
<!-- Create: New wallet creation flow -->
<CreateWalletFlow
v-else-if="currentState === 'create'"
@navigate-to-recover="handleGoToRecover"
@access-wallet="handleAccessWallet"
/>
<!-- Recovery: Recover wallet from seed phrase -->
<RecoverWalletFlow
v-else-if="currentState === 'recovery'"
@cancel="handleCancel"
@access-wallet="handleAccessWallet"
/>
<!-- Toast Notifications -->
<Toaster position="top-center" :duration="3000" />
</div>
</template>

View File

@ -0,0 +1,279 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ChevronLeft, Check, CheckCircle2, XCircle } from 'lucide-vue-next'
interface Props {
seedPhrase: string[]
}
const props = defineProps<Props>()
const emit = defineEmits<{
next: []
back: []
}>()
const seedWords = computed(() => props.seedPhrase || [])
const currentQuestionIndex = ref(0)
const selectedAnswer = ref('')
const isCorrect = ref(false)
const showResult = ref(false)
const correctCount = ref(0)
const totalQuestions = 3
const askedPositions = ref<Set<number>>(new Set())
const answeredQuestions = ref<number[]>([])
const generateQuiz = (): {
position: number
correctWord: string
options: string[]
} | null => {
if (!seedWords.value || seedWords.value.length === 0) return null
let randomPosition: number
let attempts = 0
const maxAttempts = 50
do {
randomPosition = Math.floor(Math.random() * seedWords.value.length) + 1
attempts++
if (attempts > maxAttempts) return null
} while (askedPositions.value.has(randomPosition))
currentQuestionIndex.value = randomPosition - 1
const correctWord = seedWords.value[randomPosition - 1]
const options = [correctWord]
const otherWords = seedWords.value.filter((_, index) => index !== randomPosition - 1)
while (options.length < 4 && otherWords.length > 0) {
const randomIndex = Math.floor(Math.random() * otherWords.length)
const randomWord = otherWords[randomIndex]
if (!options.includes(randomWord)) {
options.push(randomWord)
otherWords.splice(randomIndex, 1)
}
}
options.sort(() => Math.random() - 0.5)
return {
position: randomPosition,
correctWord: correctWord as string,
options: options as string[],
}
}
const quizData = ref<{
position: number
correctWord: string
options: string[]
} | null>(null)
const handleAnswerSelect = (answer: string) => {
selectedAnswer.value = answer
isCorrect.value = answer === quizData.value?.correctWord
showResult.value = true
}
const handleNext = () => {
if (isCorrect.value) {
correctCount.value++
askedPositions.value.add(quizData.value!.position)
answeredQuestions.value.push(quizData.value!.position)
if (correctCount.value >= totalQuestions) {
emit('next')
} else {
setTimeout(() => {
showResult.value = false
selectedAnswer.value = ''
const newQuiz = generateQuiz()
if (newQuiz) {
quizData.value = newQuiz
}
}, 800)
}
} else {
setTimeout(() => {
showResult.value = false
selectedAnswer.value = ''
const newQuiz = generateQuiz()
if (newQuiz) {
quizData.value = newQuiz
}
}, 1500)
}
}
const handleBack = () => {
emit('back')
}
onMounted(() => {
const newQuiz = generateQuiz()
if (newQuiz) {
quizData.value = newQuiz
}
})
</script>
<template>
<div class="space-y-6">
<!-- Header -->
<div class="space-y-2 text-center">
<h1 class="text-2xl font-bold text-foreground">Confirm Recovery Phrase</h1>
<p class="text-sm text-muted-foreground">
Select the correct word for each position
</p>
</div>
<Separator />
<!-- Progress -->
<div class="space-y-3">
<div class="flex items-center justify-center gap-2">
<div
v-for="i in totalQuestions"
:key="i"
class="flex h-8 w-8 items-center justify-center rounded-full transition-all"
:class="
answeredQuestions.includes(i)
? 'bg-green-500 text-white'
: i === correctCount + 1
? 'border-2 border-primary bg-primary/10 text-primary'
: 'bg-muted text-muted-foreground'
"
>
<CheckCircle2 v-if="answeredQuestions.includes(i)" :size="16" />
<span v-else class="text-xs font-bold">{{ i }}</span>
</div>
</div>
<p class="text-center text-sm font-semibold">
Question <span class="text-primary">{{ correctCount + 1 }}</span> of {{ totalQuestions }}
</p>
</div>
<!-- Quiz Section -->
<div v-if="quizData" class="space-y-5">
<!-- Question Card -->
<Card class="border-2 border-primary/30 bg-gradient-to-br from-primary/5 to-accent/5">
<CardContent class="py-8">
<h2 class="text-center text-xl font-bold text-foreground">
Select word
<span class="text-primary">#{{ quizData.position }}</span>
</h2>
<p class="mt-2 text-center text-sm text-muted-foreground">
What is the
{{
quizData.position === 1
? '1st'
: quizData.position === 2
? '2nd'
: quizData.position === 3
? '3rd'
: `${quizData.position}th`
}}
word in your recovery phrase?
</p>
</CardContent>
</Card>
<!-- Answer Options -->
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<button
v-for="(option, index) in quizData.options"
:key="index"
class="group relative overflow-hidden rounded-xl border-2 p-5 text-left transition-all disabled:cursor-not-allowed"
:class="{
'border-primary bg-primary/10 shadow-lg': selectedAnswer === option && !showResult,
'border-green-500 bg-green-500/10':
showResult && option === quizData.correctWord,
'border-destructive bg-destructive/10':
showResult && selectedAnswer === option && option !== quizData.correctWord,
'border-border hover:border-primary/50 hover:bg-accent': !selectedAnswer || (selectedAnswer !== option && !showResult),
}"
:disabled="showResult"
@click="handleAnswerSelect(option)"
>
<div class="flex items-center gap-3">
<!-- Option Number -->
<div
class="flex h-8 w-8 items-center justify-center rounded-full text-xs font-bold transition-colors"
:class="{
'bg-primary text-primary-foreground': selectedAnswer === option && !showResult,
'bg-green-500 text-white': showResult && option === quizData.correctWord,
'bg-destructive text-destructive-foreground': showResult && selectedAnswer === option && option !== quizData.correctWord,
'bg-muted text-muted-foreground': !selectedAnswer || (selectedAnswer !== option && !showResult),
}"
>
{{ String.fromCharCode(65 + index) }}
</div>
<!-- Word -->
<span class="flex-1 text-base font-semibold">{{ option }}</span>
<!-- Check/X Icon -->
<CheckCircle2
v-if="showResult && option === quizData.correctWord"
:size="24"
class="text-green-500"
/>
<XCircle
v-else-if="showResult && selectedAnswer === option && option !== quizData.correctWord"
:size="24"
class="text-destructive"
/>
</div>
</button>
</div>
<!-- Result Message -->
<div v-if="showResult" class="animate-in fade-in slide-in-from-top-4 duration-500">
<Alert
:variant="isCorrect ? 'default' : 'destructive'"
class="border-2"
>
<CheckCircle2 v-if="isCorrect" :size="20" class="text-green-500" />
<XCircle v-else :size="20" class="text-destructive" />
<AlertDescription class="text-base font-medium">
<span v-if="isCorrect && correctCount + 1 >= totalQuestions">
Perfect! You've verified your recovery phrase. 🎉
</span>
<span v-else-if="isCorrect">
Correct! Moving to next question...
</span>
<span v-else>
That's not correct. Please try again.
</span>
</AlertDescription>
</Alert>
</div>
</div>
<!-- Action Buttons -->
<div class="flex gap-3">
<Button
v-if="!showResult || !isCorrect || (isCorrect && correctCount + 1 < totalQuestions)"
variant="outline"
size="lg"
class="flex-1 gap-2"
@click="handleBack"
>
<ChevronLeft :size="18" />
Back
</Button>
<Button
v-if="showResult && isCorrect && correctCount + 1 >= totalQuestions"
size="lg"
class="flex-1 gap-2 text-base font-semibold"
@click="handleNext"
>
Continue
<Check :size="18" />
</Button>
</div>
</div>
</template>

View File

@ -0,0 +1,259 @@
<script setup lang="ts">
import { ref, computed, h } from 'vue'
import { useForm } from '@tanstack/vue-form'
import { z } from 'zod'
import { Eye, EyeOff, Lock, Check, X, ArrowLeft, Shield, KeyRound } from 'lucide-vue-next'
import { ThemeToggle } from '@/components/ui/theme-toggle'
const emit = defineEmits<{
next: [password: string]
navigateToRecover: []
}>()
const showPassword = ref(false)
const showConfirmPassword = ref(false)
const passwordSchema = z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain at least one uppercase letter')
.regex(/[a-z]/, 'Must contain at least one lowercase letter')
.regex(/[0-9]/, 'Must contain at least one number')
const form = useForm({
defaultValues: {
password: '',
confirmPassword: '',
},
validators: {
onChange: z.object({
password: passwordSchema,
confirmPassword: z.string(),
}),
},
onSubmit: async ({ value }) => {
if (value.password === value.confirmPassword) {
emit('next', value.password)
}
},
})
const passwordStrength = computed(() => {
const password = form.state.values.password
if (!password) return { level: 0, text: '', color: '', width: '0%' }
let strength = 0
const checks = {
length: password.length >= 8,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
number: /[0-9]/.test(password),
special: /[!@#$%^&*(),.?":{}|<>]/.test(password),
}
strength = Object.values(checks).filter(Boolean).length
if (strength <= 2) return { level: 1, text: 'Weak', color: 'bg-destructive', width: '25%' }
if (strength <= 3) return { level: 2, text: 'Fair', color: 'bg-yellow-500', width: '50%' }
if (strength <= 4) return { level: 3, text: 'Good', color: 'bg-blue-500', width: '75%' }
return { level: 4, text: 'Strong', color: 'bg-green-500', width: '100%' }
})
const isPasswordMatch = computed(() => {
const { password, confirmPassword } = form.state.values
if (!confirmPassword) return true
return password === confirmPassword
})
const canProceed = computed(() => {
const { password, confirmPassword } = form.state.values
return (
password.length >= 8 &&
confirmPassword.length >= 8 &&
isPasswordMatch.value &&
passwordStrength.value.level >= 2
)
})
const handleIHaveWallet = () => {
emit('navigateToRecover')
}
function isInvalid(field: any) {
return field.state.meta.isTouched && !field.state.meta.isValid
}
</script>
<template>
<div class="relative flex min-h-screen flex-col bg-gradient-to-br from-background via-background to-primary/5 p-4">
<!-- Theme Toggle -->
<div class="absolute right-4 top-4 z-10">
<ThemeToggle />
</div>
<!-- Back Button -->
<div class="absolute left-4 top-4 z-10">
<Button variant="ghost" size="icon" @click="handleIHaveWallet">
<ArrowLeft :size="20" />
</Button>
</div>
<!-- Content Container -->
<div class="flex flex-1 flex-col items-center justify-center">
<div class="w-full max-w-md space-y-8">
<!-- Header -->
<div class="flex flex-col items-center space-y-4 text-center">
<div class="flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-primary to-accent shadow-xl">
<Shield :size="32" class="text-primary-foreground" />
</div>
<div class="space-y-2">
<h1 class="text-3xl font-bold tracking-tight text-foreground">
Create Password
</h1>
<p class="text-base text-muted-foreground">
Secure your new wallet
</p>
</div>
</div>
<!-- Form -->
<Card class="border-2 border-border/50 shadow-xl">
<CardContent class="p-6">
<form id="create-password-form" @submit.prevent="form.handleSubmit">
<div class="space-y-6">
<!-- Password Field -->
<form.Field name="password">
<template #default="{ field }">
<div class="space-y-2">
<Label :for="field.name" class="text-base">Password</Label>
<div class="relative">
<Lock
:size="20"
class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
<Input
:id="field.name"
:name="field.name"
:model-value="field.state.value"
:type="showPassword ? 'text' : 'password'"
placeholder="Enter your password"
class="h-12 pl-11 pr-11 text-base"
:class="{ 'border-destructive': isInvalid(field) }"
autocomplete="new-password"
@blur="field.handleBlur"
@input="field.handleChange(($event.target as HTMLInputElement).value)"
/>
<button
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground"
@click="showPassword = !showPassword"
>
<Eye v-if="!showPassword" :size="20" />
<EyeOff v-else :size="20" />
</button>
</div>
<!-- Password Strength -->
<div v-if="field.state.value" class="space-y-2">
<div class="flex items-center gap-2">
<div class="h-2 flex-1 overflow-hidden rounded-full bg-muted">
<div
class="h-full transition-all duration-300"
:class="passwordStrength.color"
:style="{ width: passwordStrength.width }"
/>
</div>
<span class="text-xs font-semibold" :class="`text-${passwordStrength.color.replace('bg-', '')}`">
{{ passwordStrength.text }}
</span>
</div>
</div>
<!-- Error Message -->
<p v-if="isInvalid(field)" class="text-sm text-destructive">
{{ field.state.meta.errors[0] }}
</p>
</div>
</template>
</form.Field>
<!-- Confirm Password Field -->
<form.Field name="confirmPassword">
<template #default="{ field }">
<div class="space-y-2">
<Label :for="field.name" class="text-base">Confirm Password</Label>
<div class="relative">
<KeyRound
:size="20"
class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
<Input
:id="field.name"
:name="field.name"
:model-value="field.state.value"
:type="showConfirmPassword ? 'text' : 'password'"
placeholder="Re-enter your password"
class="h-12 pl-11 pr-11 text-base"
:class="{ 'border-destructive': field.state.value && !isPasswordMatch }"
autocomplete="new-password"
@blur="field.handleBlur"
@input="field.handleChange(($event.target as HTMLInputElement).value)"
/>
<button
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground"
@click="showConfirmPassword = !showConfirmPassword"
>
<Eye v-if="!showConfirmPassword" :size="20" />
<EyeOff v-else :size="20" />
</button>
</div>
<!-- Password Match Indicator -->
<div v-if="field.state.value" class="flex items-center gap-2">
<Check v-if="isPasswordMatch" :size="16" class="text-green-500" />
<X v-else :size="16" class="text-destructive" />
<span class="text-sm" :class="isPasswordMatch ? 'text-green-500' : 'text-destructive'">
{{ isPasswordMatch ? 'Passwords match' : 'Passwords do not match' }}
</span>
</div>
</div>
</template>
</form.Field>
<!-- Security Info -->
<Alert>
<Shield :size="16" />
<AlertDescription class="text-xs">
Use at least 8 characters with uppercase, lowercase, and numbers for a strong password.
</AlertDescription>
</Alert>
<!-- Submit Button -->
<Button
type="submit"
size="lg"
class="h-12 w-full text-base font-semibold"
:disabled="!canProceed"
>
Continue
</Button>
</div>
</form>
</CardContent>
</Card>
<!-- Footer Link -->
<div class="text-center">
<p class="text-sm text-muted-foreground">
Already have a wallet?
<Button variant="link" class="p-0 text-sm font-semibold text-primary" @click="handleIHaveWallet">
Import wallet
</Button>
</p>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,161 @@
<script setup lang="ts">
import { ref } from 'vue'
import CreatePasswordStep from './CreatePasswordStep.vue'
import SeedPhraseDisplayStep from './SeedPhraseDisplayStep.vue'
import ConfirmSeedStep from './ConfirmSeedStep.vue'
import WalletCreatedStep from './WalletCreatedStep.vue'
const emit = defineEmits<{
navigateToRecover: []
accessWallet: []
}>()
// TODO: Import useNeptuneWallet composable
// const { initWasm, generateWallet, createKeystore, clearWallet } = useNeptuneWallet()
const step = ref(1)
const seedPhrase = ref<string[]>([])
const password = ref('')
const isLoading = ref(false)
const handleNavigateToRecover = () => {
emit('navigateToRecover')
}
const handleNextFromPassword = async (pwd: string) => {
try {
isLoading.value = true
// TODO: Generate wallet
// const result = await generateWallet()
// seedPhrase.value = result.seed_phrase
// Mock seed phrase for now
seedPhrase.value = [
'abandon',
'ability',
'able',
'about',
'above',
'absent',
'absorb',
'abstract',
'absurd',
'abuse',
'access',
'accident',
'account',
'accuse',
'achieve',
'acid',
'acoustic',
'acquire',
]
password.value = pwd
step.value = 2
} catch (err) {
console.error('Failed to generate wallet:', err)
} finally {
isLoading.value = false
}
}
const handleBackToPassword = () => {
step.value = 1
}
const handleNextToConfirm = () => {
step.value = 3
}
const handleBackToSeedDisplay = () => {
step.value = 2
}
const handleNextToSuccess = () => {
step.value = 4
}
const handleAccessWallet = async () => {
try {
isLoading.value = true
// TODO: Create keystore
// const seedPhraseString = seedPhrase.value.join(' ')
// await createKeystore(seedPhraseString, password.value)
emit('accessWallet')
} catch (err) {
console.error('Failed to create keystore:', err)
} finally {
isLoading.value = false
}
}
const handleCreateAnother = () => {
step.value = 1
seedPhrase.value = []
password.value = ''
// TODO: Clear wallet
// clearWallet()
}
</script>
<template>
<div>
<!-- Step 1: Create Password -->
<CreatePasswordStep
v-if="step === 1"
@next="handleNextFromPassword"
@navigate-to-recover="handleNavigateToRecover"
/>
<!-- Step 2: Display Seed Phrase -->
<div
v-else-if="step === 2"
class="flex min-h-screen items-center justify-center bg-gradient-to-br from-background via-background to-primary/5 p-4"
>
<Card class="w-full max-w-2xl border-2 border-border/50 shadow-xl">
<CardContent class="p-6 md:p-8">
<SeedPhraseDisplayStep
:seed-phrase="seedPhrase"
@back="handleBackToPassword"
@next="handleNextToConfirm"
/>
</CardContent>
</Card>
</div>
<!-- Step 3: Confirm Seed Phrase -->
<div
v-else-if="step === 3"
class="flex min-h-screen items-center justify-center bg-gradient-to-br from-background via-background to-primary/5 p-4"
>
<Card class="w-full max-w-2xl border-2 border-border/50 shadow-xl">
<CardContent class="p-6 md:p-8">
<ConfirmSeedStep
:seed-phrase="seedPhrase"
@back="handleBackToSeedDisplay"
@next="handleNextToSuccess"
/>
</CardContent>
</Card>
</div>
<!-- Step 4: Wallet Created Success -->
<div
v-else-if="step === 4"
class="flex min-h-screen items-center justify-center bg-gradient-to-br from-background via-background to-primary/5 p-4"
>
<Card class="w-full max-w-2xl border-2 border-border/50 shadow-xl">
<CardContent class="p-6 md:p-8">
<WalletCreatedStep
:seed-phrase="seedPhrase"
:password="password"
@access-wallet="handleAccessWallet"
@create-another="handleCreateAnother"
/>
</CardContent>
</Card>
</div>
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More