init_base

This commit is contained in:
NguyenAnhQuan 2025-11-24 16:44:48 +07:00
commit 570a5f5317
46 changed files with 4353 additions and 0 deletions

19
.editorconfig Normal file
View File

@ -0,0 +1,19 @@
# EditorConfig helps maintain consistent coding styles
# https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
VITE_APP_API=
VITE_NODE_NETWORK=

8
.eslintignore Normal file
View File

@ -0,0 +1,8 @@
node_modules
dist
.vscode
.git
*.config.js
*.config.ts
components.json

37
.eslintrc.cjs Normal file
View File

@ -0,0 +1,37 @@
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:vue/vue3-recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier',
],
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 'latest',
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
plugins: ['@typescript-eslint', 'vue'],
rules: {
'vue/multi-word-component-names': 'off',
'vue/no-v-html': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
},
}

52
.gitignore vendored Normal file
View File

@ -0,0 +1,52 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Dependencies
node_modules
# Build outputs
dist
dist-ssr
out
build
*.local
# Environment files
.env
.env.local
.env.*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# OS files
Thumbs.db
.DS_Store
# TypeScript
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history

10
.prettierignore Normal file
View File

@ -0,0 +1,10 @@
node_modules
dist
.vscode
.git
pnpm-lock.yaml
package-lock.json
*.min.js
*.min.css
public

12
.prettierrc.json Normal file
View File

@ -0,0 +1,12 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"trailingComma": "es5",
"printWidth": 100,
"arrowParens": "avoid",
"endOfLine": "auto",
"vueIndentScriptAndStyle": false
}

8
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss"
]
}

26
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,26 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue"
],
"typescript.tsdk": "node_modules/typescript/lib",
"vetur.validation.template": false
}

59
README.md Normal file
View File

@ -0,0 +1,59 @@
# Neptune Wallet
A modern, secure wallet application for the Neptune blockchain network.
## ✨ Features
- 🎨 Modern UI with Shadcn-vue components and Tailwind CSS v4
- 🌓 Dark/Light theme support
- 🔒 Secure wallet management
- 💼 Transaction handling
- 📊 UTXO tracking
- 🌐 Network monitoring
## 🚀 Quick Start
```bash
# Install dependencies
pnpm install
# Setup environment
cp .env.example .env
# Start development server
pnpm dev
# Build for production
pnpm build
```
## 📋 Available Scripts
- `pnpm dev` - Start development server
- `pnpm build` - Build for production
- `pnpm preview` - Preview production build
- `pnpm type-check` - Run TypeScript type checking
- `pnpm lint` - Lint and fix code with ESLint
- `pnpm format` - Format code with Prettier
## 🛠️ Tech Stack
- **Framework**: Vue 3.5 + TypeScript
- **Build Tool**: Vite 7
- **State Management**: Pinia 2.3
- **Routing**: Vue Router 4.5
- **HTTP Client**: Axios 1.7
- **i18n**: Vue I18n 10.0
- **Styling**: Tailwind CSS v4 (CSS-first configuration)
- **UI Components**: Shadcn-vue (New York style)
- **Icons**: Lucide Vue Next
- **Utilities**: VueUse, class-variance-authority, clsx
## 📖 Documentation
For detailed installation and setup instructions, see [INSTALLATION.md](./INSTALLATION.md)
## 📝 License
Apache-2.0

21
components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "new-york",
"typescript": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/style.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"composables": "@/composables"
},
"registries": {}
}

17
index.html Normal file
View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Neptune Wallet - Secure cryptocurrency wallet for Neptune network" />
<title>Neptune Wallet</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

46
package.json Normal file
View File

@ -0,0 +1,46 @@
{
"name": "neptune-wallet",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"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}\""
},
"dependencies": {
"@tailwindcss/vite": "^4.1.17",
"@vueuse/core": "^14.0.0",
"axios": "^1.7.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.554.0",
"pinia": "^2.3.1",
"reka-ui": "^2.6.0",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17",
"tailwindcss-animate": "^1.0.7",
"vue": "^3.5.24",
"vue-i18n": "^10.0.8",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.15.0",
"@types/node": "^24.10.1",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/eslint-config-prettier": "^10.0.0",
"@vue/eslint-config-typescript": "^14.0.0",
"@vue/tsconfig": "^0.8.1",
"eslint": "^9.0.0",
"eslint-plugin-vue": "^9.30.0",
"prettier": "^3.4.0",
"typescript": "~5.9.3",
"vite": "^7.2.4",
"vue-tsc": "^3.1.4"
}
}

3036
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

30
src/App.vue Normal file
View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { ThemeToggle } from '@/components/ui/theme-toggle'
import neptuneLogo from '@/assets/imgs/neptune_logo.jpg'
</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>

80
src/api/neptuneApi.ts Normal file
View File

@ -0,0 +1,80 @@
import request from './request'
const DEFAULT_MIN_BLOCK_HEIGHT = 0
/**
* JSON-RPC 2.0 call helper
*/
export const callJsonRpc = (method: string, params: any = []) => {
const requestData = {
jsonrpc: '2.0',
method,
params,
id: 1,
}
return request({
method: 'POST',
data: requestData,
})
}
/**
* Get UTXOs from view key
*/
export const getUtxosFromViewKey = async (
viewKey: string,
startBlock: number = DEFAULT_MIN_BLOCK_HEIGHT,
endBlock: number | null = null,
maxSearchDepth: number = 1000
): Promise<any> => {
const params = {
viewKey,
startBlock,
endBlock,
maxSearchDepth,
}
return await callJsonRpc('wallet_getUtxoInfo', params)
}
/**
* Get balance from view key
*/
export const getBalance = async (
viewKey: string,
startBlock: number = DEFAULT_MIN_BLOCK_HEIGHT,
endBlock: number | null = null,
maxSearchDepth: number = 1000
): Promise<any> => {
const params = {
viewKey,
startBlock,
endBlock,
maxSearchDepth,
}
return await callJsonRpc('wallet_getBalanceFromViewKey', params)
}
/**
* Get current block height
*/
export const getBlockHeight = async (): Promise<any> => {
return await callJsonRpc('chain_height')
}
/**
* Get network info
*/
export const getNetworkInfo = async (): Promise<any> => {
return await callJsonRpc('node_network')
}
/**
* Broadcast signed transaction
*/
export const broadcastSignedTransaction = async (transactionHex: string): Promise<any> => {
const params = {
transactionHex,
}
return await callJsonRpc('mempool_submitTransaction', params)
}

50
src/api/request.ts Normal file
View File

@ -0,0 +1,50 @@
import axios, { type AxiosInstance, type AxiosResponse, type AxiosError } from 'axios'
const STATUS_CODE_SUCCESS = 200
export const API_URL = import.meta.env.VITE_APP_API || ''
// Create axios instance
const instance: AxiosInstance = axios.create({
baseURL: API_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
withCredentials: false,
})
// Request interceptor
instance.interceptors.request.use(
config => {
// Add request interceptors here (e.g., auth tokens)
return config
},
(error: AxiosError) => {
return Promise.reject(error)
}
)
// Response interceptor
instance.interceptors.response.use(
(response: AxiosResponse) => {
if (response?.status !== STATUS_CODE_SUCCESS) {
return Promise.reject(response?.data)
}
return response.data
},
(error: AxiosError) => {
if (error?.response?.data) {
return Promise.reject(error?.response?.data)
}
return Promise.reject(error)
}
)
// Set locale for API requests
export const setLocaleApi = (locale: string) => {
instance.defaults.headers.common['lang'] = locale
}
export default instance

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import type { PrimitiveProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import type { ButtonVariants } from "."
import { Primitive } from "reka-ui"
import { cn } from "@/lib/utils"
import { buttonVariants } from "."
interface Props extends PrimitiveProps {
variant?: ButtonVariants["variant"]
size?: ButtonVariants["size"]
class?: HTMLAttributes["class"]
}
const props = withDefaults(defineProps<Props>(), {
as: "button",
})
</script>
<template>
<Primitive
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>

View File

@ -0,0 +1,38 @@
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export { default as Button } from "./Button.vue"
export const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
"default": "h-9 px-4 py-2",
"xs": "h-7 rounded px-2",
"sm": "h-8 rounded-md px-3 text-xs",
"lg": "h-10 rounded-md px-8",
"icon": "h-9 w-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
)
export type ButtonVariants = VariantProps<typeof buttonVariants>

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import type { SwitchRootEmits, SwitchRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
SwitchRoot,
SwitchThumb,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<SwitchRootProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<SwitchRootEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<SwitchRoot
v-bind="forwarded"
:class="cn(
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
props.class,
)"
>
<SwitchThumb
:class="cn('pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0')"
>
<slot name="thumb" />
</SwitchThumb>
</SwitchRoot>
</template>

View File

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

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
import { Switch } from '@/components/ui/switch'
import { useTheme } from '@/composables/useTheme'
import { Moon, Sun } from 'lucide-vue-next'
import { watchEffect } from 'vue';
const { isDark } = useTheme()
watchEffect(() => console.log('isDark :>> ', isDark.value))
</script>
<template>
<div class="flex items-center gap-2">
<Sun class="h-4 w-4 text-muted-foreground transition-colors" :class="{ 'text-foreground': !isDark }" />
<Switch v-model="isDark" />
<Moon class="h-4 w-4 text-muted-foreground transition-colors" :class="{ 'text-foreground': isDark }" />
</div>
</template>

View File

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

View File

@ -0,0 +1,53 @@
import { ref, watch } from 'vue'
type Theme = 'light' | 'dark'
const theme = ref<Theme>('light')
const isDark = ref(false)
// Initialize theme immediately
function initTheme() {
if (window != null) return
// Load saved theme or default to light
const savedTheme = localStorage.getItem('theme') as Theme | null
if (savedTheme && ['light', 'dark'].includes(savedTheme)) {
theme.value = savedTheme
}
applyTheme(theme.value)
}
function applyTheme(newTheme: Theme) {
const root = document.documentElement
root.classList.toggle('dark', newTheme === 'dark')
isDark.value = newTheme === 'dark'
// Save to localStorage
localStorage.setItem('theme', newTheme)
}
// Initialize on module load
if (window != null) initTheme()
export function useTheme() {
const setTheme = (newTheme: Theme) => {
theme.value = newTheme
applyTheme(newTheme)
}
const toggleTheme = () => {
setTheme(isDark.value ? 'light' : 'dark')
}
watch(isDark, () => setTheme(isDark.value ? 'dark' : 'light'))
return {
theme,
isDark,
setTheme,
toggleTheme,
}
}

19
src/i18n/index.ts Normal file
View File

@ -0,0 +1,19 @@
import { createI18n } from 'vue-i18n'
import en from './locales/en'
import jp from './locales/jp'
export type MessageSchema = typeof en
const i18n = createI18n<[MessageSchema], 'en' | 'jp'>({
legacy: false,
locale: 'en',
fallbackLocale: 'en',
messages: {
en,
jp,
},
globalInjection: true,
})
export default i18n

41
src/i18n/locales/en.ts Normal file
View File

@ -0,0 +1,41 @@
export default {
common: {
app_name: 'Neptune Wallet',
welcome: 'Welcome to Neptune Wallet',
cancel: 'Cancel',
confirm: 'Confirm',
save: 'Save',
back: 'Back',
next: 'Next',
submit: 'Submit',
close: 'Close',
},
wallet: {
title: 'Wallet',
create: 'Create New Wallet',
recover: 'Recover Wallet',
balance: 'Balance',
address: 'Address',
send: 'Send',
receive: 'Receive',
},
auth: {
login: 'Login',
password: 'Password',
unlock: 'Unlock Wallet',
create_password: 'Create Password',
confirm_password: 'Confirm Password',
},
validation: {
required: 'This field is required',
invalid_password: 'Invalid password',
password_mismatch: 'Passwords do not match',
invalid_address: 'Invalid address',
},
errors: {
generic: 'An error occurred',
network: 'Network error',
failed_to_load: 'Failed to load data',
},
}

41
src/i18n/locales/jp.ts Normal file
View File

@ -0,0 +1,41 @@
export default {
common: {
app_name: 'Neptune ウォレット',
welcome: 'Neptune ウォレットへようこそ',
cancel: 'キャンセル',
confirm: '確認',
save: '保存',
back: '戻る',
next: '次へ',
submit: '送信',
close: '閉じる',
},
wallet: {
title: 'ウォレット',
create: '新しいウォレットを作成',
recover: 'ウォレットを復元',
balance: '残高',
address: 'アドレス',
send: '送信',
receive: '受信',
},
auth: {
login: 'ログイン',
password: 'パスワード',
unlock: 'ウォレットをアンロック',
create_password: 'パスワードを作成',
confirm_password: 'パスワードを確認',
},
validation: {
required: 'この項目は必須です',
invalid_password: '無効なパスワード',
password_mismatch: 'パスワードが一致しません',
invalid_address: '無効なアドレス',
},
errors: {
generic: 'エラーが発生しました',
network: 'ネットワークエラー',
failed_to_load: 'データの読み込みに失敗しました',
},
}

8
src/lib/utils.ts Normal file
View File

@ -0,0 +1,8 @@
import type { ClassValue } from 'clsx'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

20
src/main.ts Normal file
View File

@ -0,0 +1,20 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import i18n from './i18n'
import App from './App.vue'
import { useTheme } from './composables/useTheme'
import './style.css'
// Initialize theme before mounting app
useTheme()
const app = createApp(App)
// Install plugins
app.use(createPinia())
app.use(router)
app.use(i18n)
app.mount('#app')

46
src/router/index.ts Normal file
View File

@ -0,0 +1,46 @@
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'),
},
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
})
// Navigation guards
router.beforeEach((to, _from, next) => {
// Add authentication logic here
// const hasWallet = useNeptuneStore().hasWallet
if (to.meta.requiresAuth) {
// Check if user has wallet
// if (!hasWallet) {
// next({ name: 'auth' })
// return
// }
}
next()
})
export default router

52
src/stores/authStore.ts Normal file
View File

@ -0,0 +1,52 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export type AuthState = 'onboarding' | 'login' | 'create' | 'recovery' | 'confirm' | 'complete'
export const useAuthStore = defineStore('auth', () => {
// State
const currentState = ref<AuthState>('onboarding')
const previousState = ref<AuthState | null>(null)
// Getters
const getCurrentState = computed(() => currentState.value)
const getPreviousState = computed(() => previousState.value)
// Actions
const setState = (state: AuthState) => {
previousState.value = currentState.value
currentState.value = state
}
const goToCreate = () => {
setState('create')
}
const goToLogin = () => {
setState('login')
}
const goToRecover = () => {
setState('recovery')
}
const goBack = () => {
if (previousState.value) {
setState(previousState.value)
} else {
setState('onboarding')
}
}
return {
currentState,
getCurrentState,
getPreviousState,
setState,
goToCreate,
goToLogin,
goToRecover,
goBack,
}
})

3
src/stores/index.ts Normal file
View File

@ -0,0 +1,3 @@
export { useAuthStore } from './authStore'
export { useNeptuneStore } from './neptuneStore'

140
src/stores/neptuneStore.ts Normal file
View File

@ -0,0 +1,140 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export interface WalletState {
receiverId: string | null
viewKey: string | null
spendingKey: string | null
address: string | null
network: 'mainnet' | 'testnet'
balance: string | null
pendingBalance: string | null
utxos: any[]
minBlockHeight: number | null
}
export const useNeptuneStore = defineStore('neptune', () => {
const defaultNetwork = (import.meta.env.VITE_NODE_NETWORK || 'mainnet') as 'mainnet' | 'testnet'
// State
const wallet = ref<WalletState>({
receiverId: null,
viewKey: null,
spendingKey: null,
address: null,
network: defaultNetwork,
balance: null,
pendingBalance: null,
utxos: [],
minBlockHeight: null,
})
const keystoreFileName = ref<string | null>(null)
// Getters
const getWallet = computed(() => wallet.value)
const getReceiverId = computed(() => wallet.value.receiverId)
const getViewKey = computed(() => wallet.value.viewKey)
const getSpendingKey = computed(() => wallet.value.spendingKey)
const getAddress = computed(() => wallet.value.address)
const getNetwork = computed(() => wallet.value.network)
const getBalance = computed(() => wallet.value.balance)
const getPendingBalance = computed(() => wallet.value.pendingBalance)
const getUtxos = computed(() => wallet.value.utxos)
const getMinBlockHeight = computed(() => wallet.value.minBlockHeight ?? null)
const hasWallet = computed(() => wallet.value.address !== null)
const getKeystoreFileName = computed(() => keystoreFileName.value ?? null)
// Actions
const setReceiverId = (receiverId: string | null) => {
wallet.value.receiverId = receiverId
}
const setViewKey = (viewKey: string | null) => {
wallet.value.viewKey = viewKey
}
const setSpendingKey = (spendingKey: string | null) => {
wallet.value.spendingKey = spendingKey
}
const setAddress = (address: string | null) => {
wallet.value.address = address
}
const setNetwork = (network: 'mainnet' | 'testnet') => {
wallet.value.network = network
}
const setBalance = (balance: string | null) => {
wallet.value.balance = balance
}
const setPendingBalance = (pendingBalance: string | null) => {
wallet.value.pendingBalance = pendingBalance
}
const setUtxos = (utxos: any[]) => {
wallet.value.utxos = utxos
}
const setMinBlockHeight = (minBlockHeight: number | null) => {
wallet.value.minBlockHeight = minBlockHeight
}
const setWallet = (walletData: Partial<WalletState>) => {
wallet.value = { ...wallet.value, ...walletData }
}
const setKeystoreFileName = (fileName: string | null) => {
keystoreFileName.value = fileName
}
const clearWallet = () => {
wallet.value = {
receiverId: null,
viewKey: null,
spendingKey: null,
address: null,
network: defaultNetwork,
balance: null,
pendingBalance: null,
utxos: [],
minBlockHeight: null,
}
keystoreFileName.value = null
}
return {
// State
wallet,
keystoreFileName,
// Getters
getWallet,
getReceiverId,
getViewKey,
getSpendingKey,
getAddress,
getNetwork,
getBalance,
getPendingBalance,
getUtxos,
getMinBlockHeight,
hasWallet,
getKeystoreFileName,
// Actions
setReceiverId,
setViewKey,
setSpendingKey,
setAddress,
setNetwork,
setBalance,
setPendingBalance,
setUtxos,
setMinBlockHeight,
setWallet,
setKeystoreFileName,
clearWallet,
}
})

121
src/style.css Normal file
View File

@ -0,0 +1,121 @@
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

6
src/utils/constants.ts Normal file
View File

@ -0,0 +1,6 @@
export const PAGE_FIRST = 1
export const PER_PAGE = 20
export const POLLING_INTERVAL = 1000 * 60 // 1 minute
export const DEFAULT_MIN_BLOCK_HEIGHT = 0
export const STATUS_CODE_SUCCESS = 200

13
src/views/AuthView.vue Normal file
View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<div class="container mx-auto px-4 py-8">
<h1 class="text-4xl font-bold text-foreground">{{ t('auth.login') }}</h1>
<p class="mt-4 text-muted-foreground">Auth View - Coming soon</p>
</div>
</template>

13
src/views/HomeView.vue Normal file
View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<div class="container mx-auto px-4 py-8">
<h1 class="text-4xl font-bold text-foreground">{{ t('wallet.title') }}</h1>
<p class="mt-4 text-muted-foreground">Home View - Coming soon</p>
</div>
</template>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { Button } from '@/components/ui/button'
const router = useRouter()
const goHome = () => {
router.push({ name: 'home' })
}
</script>
<template>
<div class="flex min-h-screen items-center justify-center">
<div class="text-center">
<h1 class="text-6xl font-bold text-foreground">404</h1>
<p class="mt-4 text-xl text-muted-foreground">Page not found</p>
<Button class="mt-8" @click="goHome">Go Home</Button>
</div>
</div>
</template>

4
tailwind.config.ts Normal file
View File

@ -0,0 +1,4 @@
// Tailwind CSS v4 uses CSS-first configuration
// This file is kept for shadcn-vue compatibility but configuration is done in CSS
export default {}

24
tsconfig.app.json Normal file
View File

@ -0,0 +1,24 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"types": [
"vite/client"
],
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
},
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue"
]
}

13
tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

23
tsconfig.node.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

14
vite.config.ts Normal file
View File

@ -0,0 +1,14 @@
import path from 'node:path'
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})