feat: 241125/auth_page_UI_and_set_up_tauri
5
.gitignore
vendored
@ -55,3 +55,8 @@ src/components.d.ts
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Tauri
|
||||
src-tauri/target
|
||||
src-tauri/Cargo.lock
|
||||
src-tauri/WixTools
|
||||
|
||||
|
||||
@ -14,12 +14,12 @@
|
||||
<meta name="theme-color" content="#3f51b5" />
|
||||
<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-title" content="Neptune Wallet" />
|
||||
<meta name="apple-mobile-web-app-title" content="Neptune Privacy" />
|
||||
|
||||
<!-- App Description -->
|
||||
<meta
|
||||
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) -->
|
||||
@ -30,7 +30,7 @@
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<title>Neptune Wallet</title>
|
||||
<title>Neptune Privacy</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
13
package.json
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "neptune-wallet",
|
||||
"name": "neptune-privacy",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
@ -9,10 +9,14 @@
|
||||
"preview": "vite preview",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"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": {
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tanstack/vue-form": "^1.26.0",
|
||||
"@vueuse/core": "^14.0.0",
|
||||
"axios": "^1.7.9",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@ -25,10 +29,13 @@
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vue": "^3.5.24",
|
||||
"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": {
|
||||
"@rushstack/eslint-patch": "^1.15.0",
|
||||
"@tauri-apps/cli": "^2.9.4",
|
||||
"@types/node": "^24.10.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
|
||||
208
pnpm-lock.yaml
generated
@ -11,6 +11,9 @@ importers:
|
||||
'@tailwindcss/vite':
|
||||
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))
|
||||
'@tanstack/vue-form':
|
||||
specifier: ^1.26.0
|
||||
version: 1.26.0(vue@3.5.24(typescript@5.9.3))
|
||||
'@vueuse/core':
|
||||
specifier: ^14.0.0
|
||||
version: 14.0.0(vue@3.5.24(typescript@5.9.3))
|
||||
@ -50,10 +53,19 @@ importers:
|
||||
vue-router:
|
||||
specifier: ^4.5.0
|
||||
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:
|
||||
'@rushstack/eslint-patch':
|
||||
specifier: ^1.15.0
|
||||
version: 1.15.0
|
||||
'@tauri-apps/cli':
|
||||
specifier: ^2.9.4
|
||||
version: 2.9.4
|
||||
'@types/node':
|
||||
specifier: ^24.10.1
|
||||
version: 24.10.1
|
||||
@ -600,14 +612,113 @@ packages:
|
||||
peerDependencies:
|
||||
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':
|
||||
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':
|
||||
resolution: {integrity: sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==}
|
||||
peerDependencies:
|
||||
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':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
@ -1684,6 +1795,20 @@ packages:
|
||||
peerDependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-L/G9IUjOWhBU0yun89rv8fKqmKC+T0HfhrFjlIml71WpfBv9eb4E9Bev8FMbyueBIU9vxQqbd+oOsVcDa5amGw==}
|
||||
hasBin: true
|
||||
@ -1718,6 +1843,9 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
zod@4.1.13:
|
||||
resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@babel/helper-string-parser@7.27.1': {}
|
||||
@ -2083,13 +2211,89 @@ snapshots:
|
||||
tailwindcss: 4.1.17
|
||||
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/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))':
|
||||
dependencies:
|
||||
'@tanstack/virtual-core': 3.13.12
|
||||
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/json-schema@7.0.15': {}
|
||||
@ -3223,6 +3427,8 @@ snapshots:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.24(typescript@5.9.3)
|
||||
|
||||
vue-sonner@2.0.9: {}
|
||||
|
||||
vue-tsc@3.1.5(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@volar/typescript': 2.4.23
|
||||
@ -3250,3 +3456,5 @@ snapshots:
|
||||
xml-name-validator@4.0.0: {}
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zod@4.1.13: {}
|
||||
|
||||
4
src-tauri/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
/gen/schemas
|
||||
31
src-tauri/Cargo.toml
Normal 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
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
11
src-tauri/capabilities/default.json
Normal 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
|
After Width: | Height: | Size: 5.6 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
@ -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>
|
||||
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 35 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
@ -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
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 695 B |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
25
src-tauri/src/lib.rs
Normal 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
@ -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
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/App.vue
@ -1,30 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ThemeToggle } from '@/components/ui/theme-toggle'
|
||||
import neptuneLogo from '@/assets/imgs/neptune_logo.jpg'
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-background">
|
||||
<!-- 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>
|
||||
|
||||
|
||||
|
||||
BIN
src/assets/fonts/Montserrat-VariableFont_wght.ttf
Normal file
94
src/components/commons/layout/Layout.vue
Normal 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>
|
||||
|
||||
1
src/components/commons/layout/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as Layout } from './Layout.vue'
|
||||
17
src/components/ui/alert/Alert.vue
Normal 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>
|
||||
14
src/components/ui/alert/AlertDescription.vue
Normal 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>
|
||||
14
src/components/ui/alert/AlertTitle.vue
Normal 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>
|
||||
24
src/components/ui/alert/index.ts
Normal 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>
|
||||
17
src/components/ui/badge/Badge.vue
Normal 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>
|
||||
26
src/components/ui/badge/index.ts
Normal 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>
|
||||
21
src/components/ui/card/Card.vue
Normal 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>
|
||||
14
src/components/ui/card/CardContent.vue
Normal 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>
|
||||
14
src/components/ui/card/CardDescription.vue
Normal 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>
|
||||
14
src/components/ui/card/CardFooter.vue
Normal 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>
|
||||
14
src/components/ui/card/CardHeader.vue
Normal 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>
|
||||
18
src/components/ui/card/CardTitle.vue
Normal 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>
|
||||
6
src/components/ui/card/index.ts
Normal 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"
|
||||
24
src/components/ui/input/Input.vue
Normal 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>
|
||||
1
src/components/ui/input/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as Input } from "./Input.vue"
|
||||
25
src/components/ui/label/Label.vue
Normal 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>
|
||||
1
src/components/ui/label/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as Label } from "./Label.vue"
|
||||
36
src/components/ui/progress/Progress.vue
Normal 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>
|
||||
1
src/components/ui/progress/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as Progress } from "./Progress.vue"
|
||||
29
src/components/ui/separator/Separator.vue
Normal 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>
|
||||
1
src/components/ui/separator/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as Separator } from "./Separator.vue"
|
||||
@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import { Moon, Sun } from 'lucide-vue-next'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
|
||||
|
||||
74
src/composables/useMobile.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
export default {
|
||||
common: {
|
||||
app_name: 'Neptune Wallet',
|
||||
welcome: 'Welcome to Neptune Wallet',
|
||||
app_name: 'Neptune Privacy',
|
||||
welcome: 'Welcome to Neptune Privacy',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
save: 'Save',
|
||||
@ -38,4 +38,3 @@ export default {
|
||||
failed_to_load: 'Failed to load data',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export default {
|
||||
common: {
|
||||
app_name: 'Neptune ウォレット',
|
||||
welcome: 'Neptune ウォレットへようこそ',
|
||||
app_name: 'Neptune Privacy',
|
||||
welcome: 'Neptune Privacyへようこそ',
|
||||
cancel: 'キャンセル',
|
||||
confirm: '確認',
|
||||
save: '保存',
|
||||
@ -38,4 +38,3 @@ export default {
|
||||
failed_to_load: 'データの読み込みに失敗しました',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,46 +1,23 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
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'),
|
||||
},
|
||||
]
|
||||
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
|
||||
import { routes } from './route'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
history: import.meta.env.NODE_ENV ? createWebHistory() : createWebHashHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
// Navigation guards
|
||||
router.beforeEach((to, _from, next) => {
|
||||
// Add authentication logic here
|
||||
// const hasWallet = useNeptuneStore().hasWallet
|
||||
const hasWallet = false // useNeptuneStore().hasWallet
|
||||
|
||||
if (to.meta.requiresAuth) {
|
||||
// Check if user has wallet
|
||||
// if (!hasWallet) {
|
||||
// next({ name: 'auth' })
|
||||
// return
|
||||
// }
|
||||
if (!hasWallet) {
|
||||
next({ name: 'auth' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
|
||||
28
src/router/route.ts
Normal 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'),
|
||||
},
|
||||
]
|
||||
@ -49,4 +49,3 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
goBack,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -4,6 +4,15 @@
|
||||
|
||||
@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 {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
@ -160,8 +169,17 @@
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family:
|
||||
'Montserrat',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
sans-serif;
|
||||
/* Mobile-first optimizations */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
80
src/views/Auth/AuthView.vue
Normal 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>
|
||||
279
src/views/Auth/components/create/ConfirmSeedStep.vue
Normal 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>
|
||||
259
src/views/Auth/components/create/CreatePasswordStep.vue
Normal 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>
|
||||
161
src/views/Auth/components/create/CreateWalletFlow.vue
Normal 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>
|
||||