Compare commits

..

No commits in common. "e24832fa1a28fdf62440eb4538c45a7ec21e4cd2" and "2414cad2d2586894f88c86b7d94117b07245ac7a" have entirely different histories.

52 changed files with 722 additions and 4037 deletions

View File

@ -5,7 +5,7 @@
<link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Neptune Web Wallet</title>
<title>Web Wallet</title>
</head>
<body>
<div id="app"></div>

View File

@ -1,5 +1,5 @@
{
"name": "neptune-web-wallet",
"name": "webtoon-admin",
"version": "0.0.0",
"private": true,
"type": "module",

View File

@ -3,8 +3,8 @@ import { LayoutVue } from '@/components'
const config = {
token: {
colorPrimary: '#007FCF',
borderRadius: 4,
colorPrimary: '#ff7789',
borderRadius: 0,
},
}
</script>

View File

@ -1,6 +1,6 @@
import axios from 'axios'
import router from '@/router'
import { STATUS_CODE_SUCCESS, ACCESS_TOKEN, STATUS_CODE_UNAUTHORIZED } from '@/utils'
import { STATUS_CODE_SUCCESS, ACCESS_TOKEN, STATUS_CODE_UNAUTHORIZED } from '@/helpers'
axios.defaults.withCredentials = false

View File

@ -1,28 +0,0 @@
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse-dot {
0%,
100% {
opacity: 1;
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
}
50% {
opacity: 0.8;
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}

View File

@ -1,16 +1,12 @@
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100;200;300;400;500;600;700;800;900&display=swap');
html {
font-family: 'Noto Sans JP';
font-size: 15px;
}
*,
*::before,
*::after {
margin: 0;
padding: 0;
position: unset;
font-family: 'Noto Sans JP';
}
img {
@ -18,6 +14,303 @@ img {
height: auto;
}
body {
min-height: 100vh;
color: var(--vt-c-gray-2);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family: 'Noto Sans JP';
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-thumb {
background-color: var(--vt-c-main-gray-v1);
}
&::-webkit-scrollbar-thumb {
background-color: var(--vt-c-main-gray-v2);
border-radius: 10px;
transition: all 0.2s ease-in-out;
}
&::-webkit-scrollbar-track {
border-radius: 10px;
}
}
*,
*::before,
*::after {
position: unset;
}
.box {
// padding: 20px;
&.full {
min-height: calc(100% - 20px);
}
&.no-border {
border: none;
}
.box-head {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 66px;
padding: 0 20px;
background-color: var(--vt-c-white-mute);
.box-search {
display: flex;
gap: 35px;
align-items: center;
.ant-input-search {
width: 322px;
.ant-input {
border: none;
background: unset;
outline: unset;
box-shadow: unset;
}
.ant-input-group-addon {
background: unset;
.ant-input-search-button {
background: unset;
border: unset;
box-shadow: unset;
.anticon {
svg {
fill: var(--vt-c-gray-2);
}
}
}
}
}
}
.title {
font-weight: 900;
font-size: 18px;
line-height: 26px;
color: var(--vt-c-gray-2);
.title-page {
color: var(--vt-c-gray-v9);
}
&.manga {
color: var(--vt-c-gray-v9);
}
}
.box-btn {
@include center_flex;
width: 140px;
height: 40px;
border-radius: 6px;
color: var(--vt-c-main);
background-color: var(--vt-c-white);
box-shadow: var(--vt-box-shadow);
border: 1px solid var(--vt-c-main);
font-weight: 700;
font-size: 14px;
}
}
.box-body {
padding: 0 20px;
overflow-y: auto;
height: calc(100vh - 135px);
position: relative;
&.no-pagination {
height: calc(100vh - 66px);
}
&:has(.form-create-chap) {
.ant-tabs {
overflow-y: auto;
height: calc(100vh - 235px);
}
}
&.no_padding {
padding: 0;
}
.btn-save-setting {
&:disabled {
border-color: var(--vt-c-gray-1);
background-color: var(--vt-c-gray-1);
}
}
}
}
.btn-base {
margin-right: 12px;
&.btn-delete {
background-color: var(--vt-c-red-v3);
color: var(--vt-c-white);
&:hover {
border-color: unset !important;
color: var(--vt-c-white);
}
}
}
.ant-tag {
&.custom {
width: 72px;
height: 28px;
border: none;
@include center_flex;
}
}
.ant-picker {
width: 100%;
&.has-value {
border-color: var(--vt-c-main);
}
}
.rounded-btn {
border-radius: 100px !important;
}
.bold-label {
label {
font-weight: 700;
}
}
.ant-input-number {
width: 100%;
}
.full-width {
width: 100%;
}
.ant-input[disabled] {
background-color: var(--vt-c-background-dark-1);
color: var(--vt-c-text-dark-3);
}
.ant-tabs {
&.custom {
.ant-tabs-nav {
background-color: var(--vt-c-white-mute);
padding: 0 20px;
}
.ant-tabs-content-holder {
padding: 0 20px;
}
.ant-tabs-nav-list {
.ant-tabs-tab {
min-width: 68px;
justify-content: center;
padding: 0 16px 6px;
.ant-tabs-tab-btn {
font-size: 16px;
font-weight: 700;
line-height: 23.17px;
}
&:not(.ant-tabs-tab-active) {
color: var(--vt-c-gray-v9);
}
}
}
.ant-tabs-ink-bar {
height: 3px;
}
.ant-tabs-content {
&:has(.tab-report) {
height: calc(100vh - 180px);
overflow-y: scroll;
}
&:has(.tab-ranking) {
height: calc(100vh - 180px);
overflow-y: auto;
}
}
}
&.manga-tabs {
.ant-tabs-content-holder {
margin-bottom: 150px;
}
}
&.master-tabs {
.ant-tabs-content-holder {
margin-bottom: 70px;
}
}
}
.ant-form-item {
.ant-form-item-explain-error {
font-size: 12px;
}
}
.ant-spin-spinning {
width: 100%;
}
.flex-center {
@include center_flex;
}
.flex-col-center {
display: flex;
flex-direction: column;
align-items: center;
}
.btn-common {
@include baseBtn(
$bg: var(--vt-c-main-color),
$width: unset,
$height: unset,
$borderRadius: unset
);
margin: 0 auto 5px;
padding: var(--vt-btn-padding);
color: var(--vt-c-white);
font-weight: 500;
outline: none;
cursor: pointer;
border-radius: 8px;
border: 2px solid transparent;
transition: all 0.3s ease;
&:hover {
border: 2px solid var(--vt-c-black-bold);
color: var(--vt-c-black-bold);
}
}
p {
font-size: 1rem;
margin: 1em;
@ -31,3 +324,97 @@ h2 {
margin-bottom: 1rem;
text-align: center;
}
.highlight {
color: var(--vt-c-main-color);
}
.spacer {
height: 1px;
width: 90%;
background: linear-gradient(to right, transparent, rgb(224, 224, 224), transparent);
margin: 2rem auto;
}
.note {
margin-top: 1rem;
font-size: 0.9rem;
color: --vt-c-gray-note;
}
.auth-container {
@include center_flex;
min-height: 100vh;
background-color: var(--vt-c-white);
padding: 20px;
}
.auth-card {
background-color: var(--vt-c-white);
border: 2px solid var(--vt-c-main-color);
width: 100%;
max-width: 600px;
padding: 0;
&-header {
padding: 5px 10px;
min-height: 30px;
display: flex;
align-items: center;
border-bottom: 2px solid var(--vt-c-main-color);
h2 {
font-size: 1rem;
margin: 0;
color: var(--vt-c-black-bold);
}
}
&-content {
padding: 20px;
text-align: center;
}
}
.wallet-icon {
margin: 20px 0 30px 0;
.icon-circle {
@include center_flex;
width: 60px;
height: 60px;
background-color: var(--vt-c-main-color);
border-radius: 50%;
margin: 0 auto;
}
}
.password-section {
margin-bottom: 20px;
}
.auth-button-group {
display: flex;
gap: 10px;
margin-top: 20px;
.auth-btn {
flex: 1;
border-radius: 4px;
padding: 12px 16px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
&.secondary {
background-color: var(--vt-c-white);
color: var(--vt-c-black-bold);
border: 1px solid var(--vt-c-black-bold);
&:hover {
background-color: var(--vt-c-white-soft);
}
}
}
}

View File

@ -1,30 +0,0 @@
.flex-center {
@include center_flex;
}
.flex-col-center {
display: flex;
flex-direction: column;
align-items: center;
}
.card-base {
@include card-base;
}
// ==================== TEXT UTILITIES ====================
.text-primary {
color: var(--text-primary);
}
.text-secondary {
color: var(--text-secondary);
}
.text-muted {
color: var(--text-muted);
}
.highlight {
color: var(--primary-color);
}

View File

@ -118,40 +118,53 @@ $fw: 100;
justify-content: center;
}
@mixin btn-primary {
background: var(--primary-color);
border-color: var(--primary-color);
font-weight: var(--font-semibold);
height: auto;
padding: var(--btn-padding-y) var(--btn-padding-x);
transition: var(--transition-all);
&:hover {
background: var(--primary-hover);
border-color: var(--primary-hover);
}
&:active,
&:focus {
background: var(--primary-hover);
border-color: var(--primary-hover);
}
@mixin center_pos {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
@mixin card-base {
background: var(--bg-white);
border-radius: var(--card-radius);
padding: var(--card-padding);
box-shadow: var(--card-shadow);
transition: var(--transition-all);
animation: fadeIn 0.6s ease-out;
@media (max-width: 768px) {
padding: var(--card-padding-mobile);
}
&:hover {
transform: translateY(-4px);
box-shadow: var(--card-shadow-hover);
}
@mixin line_clamp($line) {
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: $line;
-webkit-box-orient: vertical;
}
@mixin position($pos: absolute, $left: 0, $right: 0, $top: 0, $bottom: 0) {
position: $pos;
top: $top;
left: $left;
right: $right;
bottom: $bottom;
}
// function
@function sum($numbers...) {
$sum: 0;
@each $number in $numbers {
$sum: $sum + $number;
}
@return $sum;
}
@function calc_v2($size) {
@return calc(100% - $size);
}
@mixin baseBtn(
$bg: var(--vt-c-main),
$width: 92px,
$height: 36px,
$borderRadius: var(--vt-br-btn)
) {
display: flex;
justify-content: center;
align-items: center;
width: $width;
height: $height;
background-color: $bg;
border-radius: $borderRadius;
box-shadow: unset;
}

View File

@ -1,154 +1,113 @@
:root {
// ==================== COLORS ====================
--vt-btn-padding: 11px 8px;
--vt-c-main-color: #009688;
--vt-c-badge-caption-shadow: rgba(0, 151, 115, 1);
--vt-input-shadow: 0 0 0 2px rgba(0, 150, 136, 0.1);
--vt-input-shadow-focus: 0 0 0 2px rgba(0, 150, 136, 0.1);
--vt-c-gray-note: rgb(85, 85, 85);
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-white-v2: #d9d9d9;
--vt-c-white-v5: #fafafa;
--vt-c-white-v7: #f0f5f8;
--vt-c-white-v8: #fbfbfb;
// Primary Colors
--primary-color: #007fcf;
--primary-hover: #0066a6;
--primary-light: #e8f4fc;
--primary-bg: #f5fbff;
--vt-c-black: #181818;
--vt-c-black-v1: #5d6679;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-black-bold: #000000;
--vt-c-black-bold-v2: #131523;
--vt-c-black-bold-v3: #252c32;
--vt-c-black-bold-v3: #00000026;
--vt-c-black-bold-v4: #00000080;
--vt-c-black-bold-v5: #00000008;
// Secondary Colors
--secondary-color: #ff9500;
--secondary-hover: #e68600;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
// Text Colors
--text-primary: #2c3e50;
--text-secondary: #5a6c7d;
--text-muted: #8b95a5;
--text-light: #ffffff;
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
--vt-c-text-dark-3: rgba(0, 0, 0, 0.88);
// Background Colors
--bg-gradient-start: #f0f8ff;
--bg-gradient-end: #e6f2ff;
--bg-white: #ffffff;
--bg-light: #f8fcff;
--bg-hover: #e8f4fc;
--vt-c-background-dark-1: rgba(0, 0, 0, 0.04);
// Border Colors
--border-light: #e6f2ff;
--border-color: #ebf5ff;
--border-primary: #007fcf;
--vt-box-shadow-1: 0px 0px 8px rgba(78, 37, 0, 0.78);
--vt-box-shadow-2: 0px 4px 4px rgba(0, 0, 0, 0.25);
--vt-box-shadow-3: 0px 0px 5px 0px var(--vt-c-black-bold-v5) inset;
// Status Colors
--success-color: #10b981;
--warning-color: #f59e0b;
--error-color: #ef4444;
--info-color: #007fcf;
--vt-c-main: #ff7789;
--vt-c-main-title: #519fb0;
--vt-c-main-light: #6659ff;
--vt-c-main-bg: #eff1f7;
--vt-c-main-bg-v1: #f4f7ff;
--vt-c-main-black: #051121;
--vt-c-main-clear: #e6e8ef;
--vt-c-main-link: #4335ef;
--vt-c-main-red: red;
--vt-c-main-gray: #7e84a3;
--vt-c-main-gray-v1: #fff3;
--vt-c-main-gray-v2: #0003;
--vt-c-gray-v4: #a9a5a3;
--vt-c-gray-v5: #666666;
--vt-c-gray-v6: #999999;
--vt-c-gray-v9: #888888;
--vt-c-gray-v10: #444444;
--vt-c-gray-v11: #6d6d6d;
--vt-c-gray-v12: #b9bdc7;
--vt-c-gray-v17: #e6e6e6;
--vt-c-blue: #3375f3;
--vt-c-blue-v3: #162dff;
--vt-c-blue-v4: #67abba;
--vt-c-blue-v5: #20aee5;
--vt-c-red-v3: #ff0f0f;
// ==================== SPACING ====================
--vt-c-violet: #5671fb;
--vt-c-pink: #ffe6ea;
--vt-c-pink-2: #ff6d6d;
--vt-c-pink-3: #e05266;
--vt-c-green: #1e8e3e;
--vt-c-green-2: #93cb9c;
--vt-c-green-3: #429d7c;
--spacing-xs: 0.25rem; // 4px
--spacing-sm: 0.5rem; // 8px
--spacing-md: 0.75rem; // 12px
--spacing-lg: 1rem; // 16px
--spacing-xl: 1.5rem; // 24px
--spacing-2xl: 2rem; // 32px
--spacing-3xl: 2.5rem; // 40px
--spacing-4xl: 3rem; // 48px
--vt-c-turquoise: #3fb3ce;
// ==================== BORDER RADIUS ====================
--color-background: #f5f7fb;
--color-background-event: #3fb3ce;
--color-background-sidebar: #e6e6e6;
--color-background-soft: var(--vt-c-white-soft);
--radius-sm: 8px;
--radius-md: 10px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-full: 9999px;
--color-border: #b8b7b7;
--color-border-v1: #cdd4e7;
--color-border-shadow: #dfdfdf40;
--color-border-hover: var(--vt-c-divider-light-1);
// ==================== BOX SHADOWS ====================
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--shadow-xs: 0 1px 3px rgba(0, 0, 0, 0.05);
--shadow-sm: 0 2px 10px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 20px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.12);
--shadow-xl: 0 12px 40px rgba(0, 0, 0, 0.15);
--shadow-primary: 0 4px 12px rgba(0, 127, 207, 0.25);
--shadow-secondary: 0 4px 12px rgba(255, 149, 0, 0.25);
--font-size: 14px;
--vt-font-btn: 92px;
--vt-br-btn: 3px;
--section-gap: 160px;
// ==================== TRANSITIONS ====================
--vt-c-gray-1: #e9e9e9;
--vt-c-gray-2: #444444;
--vt-c-gray-3: #adadad66;
--vt-c-gray-4: #f9f9f9;
--vt-c-gray-5: #b4b4b4;
--vt-c-gray-6: #d9d9d9;
--vt-c-gray-7: #ececec;
--vt-c-gray-8: #636363;
--vt-c-gray-9: #aaaaaa;
--vt-c-green-dark: #435855;
--transition-fast: 0.2s ease;
--transition-normal: 0.3s ease;
--transition-slow: 0.5s ease;
--transition-all: all 0.3s ease;
--vt-c-orange: #f3a964;
// ==================== TYPOGRAPHY ====================
// Font Families
--font-primary: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
--font-mono: 'Courier New', monospace;
--font-noto: 'Noto Sans JP';
// Font Sizes
--font-xs: 0.75rem; // 12px
--font-sm: 0.85rem; // 13.6px
--font-base: 0.9rem; // 14.4px
--font-md: 0.95rem; // 15.2px
--font-lg: 1rem; // 16px
--font-xl: 1.1rem; // 17.6px
--font-2xl: 1.2rem; // 19.2px
--font-3xl: 1.5rem; // 24px
--font-4xl: 3rem; // 48px
// Font Weights
--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
// Line Heights
--leading-tight: 1.2;
--leading-normal: 1.5;
--leading-relaxed: 1.75;
// Letter Spacing
--tracking-tight: -1px;
--tracking-normal: 0;
--tracking-wide: 0.5px;
--tracking-wider: 1px;
// ==================== Z-INDEX ====================
--z-base: 1;
--z-dropdown: 10;
--z-sticky: 20;
--z-fixed: 30;
--z-modal-backdrop: 40;
--z-modal: 50;
--z-popover: 60;
--z-tooltip: 70;
// ==================== BREAKPOINTS ====================
--breakpoint-xs: 480px;
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
--breakpoint-2xl: 1536px;
// ==================== COMPONENTS SPECIFIC ====================
// Card
--card-padding: var(--spacing-2xl);
--card-padding-mobile: var(--spacing-xl);
--card-radius: var(--radius-xl);
--card-shadow: var(--shadow-md);
--card-shadow-hover: var(--shadow-lg);
// Button
--btn-padding-y: 0.75rem;
--btn-padding-x: 1rem;
--btn-radius: var(--radius-md);
--btn-transition: var(--transition-all);
// QR Code
--qr-size: 200px;
--qr-border: 3px solid var(--border-light);
--qr-radius: var(--radius-lg);
--qr-shadow: var(--shadow-sm);
// Tabs
--tabs-height: 3px;
--tabs-padding: 12px 20px;
--tabs-padding-mobile: 10px 16px;
--vt-box-shadow: 0px 0px 4px 0px var(--vt-c-gray-3);
--vt-box-shadow-active: 0px 0px 4px 0px var(--vt-c-main);
}

View File

@ -1,5 +1,3 @@
@import '__base';
@import '__variables';
@import '__mixin';
@import '__animations';
@import '__common';
@import '__base';

View File

@ -1,235 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import ButtonCommon from '@/components/common/ButtonCommon.vue'
import { formatNumberToLocaleString } from '@/utils'
const availableBalance = ref(0)
const pendingBalance = ref(0)
const receiveAddress = ref('kaspa:qpn80v050r3jxv6mzt8tzss6dhvllc3rvcuuy86z6djgmvzx0napvhuj7ugh9')
const walletStatus = ref('Online')
const currentDaaScore = ref(255953336)
const copyAddress = () => {
navigator.clipboard.writeText(receiveAddress.value)
}
const handleSend = () => {
console.log('Send clicked')
}
const handleScanQR = () => {
console.log('Scan QR clicked')
}
</script>
<template>
<div class="wallet-info-container">
<!-- Balance Section -->
<div class="balance-section">
<div class="balance-label">Available</div>
<div class="balance-amount">{{ availableBalance }} KAS</div>
<div class="pending-section">
<span class="pending-label">Pending</span>
<span class="pending-amount">{{ pendingBalance }} KAS</span>
</div>
</div>
<!-- Receive Address Section -->
<div class="receive-section">
<div class="address-label">Receive Address:</div>
<div class="address-value" @click="copyAddress">
{{ receiveAddress }}
<svg
class="copy-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</div>
</div>
<!-- QR Code Section -->
<div class="qr-section">
<div class="qr-placeholder">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect x="0" y="0" width="100" height="100" fill="white" />
<rect x="5" y="5" width="35" height="35" fill="black" />
<rect x="10" y="10" width="25" height="25" fill="white" />
<rect x="15" y="15" width="15" height="15" fill="black" />
<rect x="60" y="5" width="35" height="35" fill="black" />
<rect x="65" y="10" width="25" height="25" fill="white" />
<rect x="70" y="15" width="15" height="15" fill="black" />
<rect x="5" y="60" width="35" height="35" fill="black" />
<rect x="10" y="65" width="25" height="25" fill="white" />
<rect x="15" y="70" width="15" height="15" fill="black" />
<rect x="45" y="45" width="10" height="10" fill="black" />
<rect x="60" y="60" width="10" height="10" fill="black" />
<rect x="70" y="70" width="10" height="10" fill="black" />
</svg>
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<ButtonCommon type="primary" size="large" block @click="handleSend" class="btn-send">
SEND
</ButtonCommon>
<ButtonCommon type="primary" size="large" block @click="handleScanQR" class="btn-scan">
Scan QR code
</ButtonCommon>
</div>
<!-- Wallet Status -->
<div class="wallet-status">
<span
>Wallet Status: <strong>{{ walletStatus }}</strong></span
>
<span
>DAA score: <strong>{{ formatNumberToLocaleString(currentDaaScore) }}</strong></span
>
</div>
</div>
</template>
<style lang="scss" scoped>
.wallet-info-container {
@include card-base;
}
.balance-section {
text-align: center;
margin-bottom: var(--spacing-3xl);
padding-bottom: var(--spacing-2xl);
border-bottom: 2px solid var(--border-color);
.balance-label {
color: var(--text-muted);
font-size: var(--font-base);
margin-bottom: var(--spacing-sm);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
}
.balance-amount {
font-size: var(--font-4xl);
font-weight: var(--font-bold);
color: var(--text-primary);
margin-bottom: var(--spacing-lg);
letter-spacing: var(--tracking-tight);
}
.pending-section {
display: flex;
justify-content: center;
align-items: center;
gap: var(--spacing-sm);
color: var(--text-secondary);
font-size: var(--font-md);
.pending-label {
font-weight: var(--font-medium);
}
.pending-amount {
font-weight: var(--font-semibold);
}
}
}
.receive-section {
margin-bottom: var(--spacing-2xl);
.address-label {
font-size: var(--font-base);
color: var(--text-secondary);
margin-bottom: var(--spacing-md);
font-weight: var(--font-semibold);
}
.address-value {
background: var(--bg-light);
padding: var(--spacing-lg);
border-radius: var(--radius-md);
word-break: break-all;
font-family: var(--font-mono);
font-size: var(--font-sm);
color: var(--primary-color);
cursor: pointer;
transition: var(--transition-all);
display: flex;
align-items: center;
gap: var(--spacing-sm);
border: 2px solid transparent;
&:hover {
background: var(--bg-hover);
border-color: var(--border-primary);
}
.copy-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
color: var(--primary-color);
}
}
}
.qr-section {
display: flex;
justify-content: center;
margin-bottom: var(--spacing-2xl);
.qr-placeholder {
width: var(--qr-size);
height: var(--qr-size);
background: var(--bg-white);
border: var(--qr-border);
border-radius: var(--qr-radius);
padding: var(--spacing-lg);
box-shadow: var(--qr-shadow);
transition: var(--transition-normal);
&:hover {
transform: scale(1.05);
}
svg {
width: 100%;
height: 100%;
}
}
}
.action-buttons {
display: flex;
gap: var(--spacing-lg);
margin-bottom: var(--spacing-2xl);
:deep(.btn-send),
:deep(.btn-scan) {
letter-spacing: var(--tracking-wide);
}
}
.wallet-status {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
padding: 1.25rem;
background: var(--bg-light);
border-radius: var(--radius-md);
font-size: var(--font-base);
color: var(--text-secondary);
strong {
color: var(--text-primary);
font-weight: var(--font-semibold);
}
}
</style>

View File

@ -1,436 +0,0 @@
<script setup lang="ts">
import { ref, defineEmits, onMounted } from 'vue'
import { ButtonCommon } from '@/components'
import { useSeedStore } from '@/stores'
const emit = defineEmits<{
next: []
back: []
}>()
const seedStore = useSeedStore()
const seedWords = ref<string[]>([])
const currentQuestionIndex = ref(0)
const selectedAnswer = ref('')
const isCorrect = ref(false)
const showResult = ref(false)
const generateQuiz = (): {
position: number
correctWord: string
options: string[]
} | null => {
if (seedWords.value.length === 0) return null
const randomPosition = Math.floor(Math.random() * 12) + 1
currentQuestionIndex.value = randomPosition - 1
const correctWord = seedWords.value[randomPosition - 1]
const options = [correctWord]
const BIP39_WORDS = [
'abandon',
'ability',
'able',
'about',
'above',
'absent',
'absorb',
'abstract',
'absurd',
'abuse',
'access',
'accident',
'account',
'accuse',
'achieve',
'acid',
'acoustic',
'acquire',
'across',
'act',
'action',
'actor',
'actress',
'actual',
'adapt',
'add',
'addict',
'address',
'adjust',
'admit',
'adult',
'advance',
'advice',
'aerobic',
'affair',
'afford',
'afraid',
'again',
'age',
'agent',
'agree',
'ahead',
'aim',
'air',
'airport',
'aisle',
'alarm',
'album',
'alcohol',
'alert',
'alien',
'all',
'alley',
'allow',
'almost',
'alone',
'alpha',
'already',
'also',
'alter',
'always',
'amateur',
'amazing',
'among',
'amount',
'amused',
'analyst',
'anchor',
'ancient',
'anger',
'angle',
'angry',
'animal',
'ankle',
'announce',
'annual',
'another',
'answer',
'antenna',
'antique',
'anxiety',
'any',
'apart',
'apology',
'appear',
'apple',
'approve',
'april',
'arch',
'arctic',
'area',
'arena',
'argue',
'arm',
'armed',
'armor',
'army',
'around',
'arrange',
'arrest',
]
while (options.length < 4) {
const randomWord = BIP39_WORDS[Math.floor(Math.random() * BIP39_WORDS.length)]
if (!options.includes(randomWord)) {
options.push(randomWord)
}
}
options.sort(() => Math.random() - 0.5)
return {
position: randomPosition,
correctWord,
options,
}
}
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) {
emit('next')
} else {
showResult.value = false
selectedAnswer.value = ''
const newQuiz = generateQuiz()
if (newQuiz) {
quizData.value = newQuiz
}
}
}
onMounted(() => {
const words = seedStore.getSeedWords()
if (words.length > 0) {
seedWords.value = words
const newQuiz = generateQuiz()
if (newQuiz) {
quizData.value = newQuiz
}
} else {
const sampleWords = [
'abandon',
'ability',
'able',
'about',
'above',
'absent',
'absorb',
'abstract',
'absurd',
'abuse',
'access',
'accident',
]
seedWords.value = sampleWords
const newQuiz = generateQuiz()
if (newQuiz) {
quizData.value = newQuiz
}
}
})
</script>
<template>
<div class="confirm-container">
<div class="confirm-card">
<div class="confirm-header">
<h1 class="confirm-title">Recovery Seed</h1>
</div>
<div class="confirm-content">
<div class="progress-indicators">
<div class="progress-circle"></div>
<div class="progress-circle active"></div>
<div class="progress-circle"></div>
</div>
<div class="instruction-text">
<p>
Make sure you wrote the phrase down correctly by answering this quick
checkup.
</p>
</div>
<div class="quiz-section">
<h2 class="quiz-question">What is the {{ quizData?.position }}th word?</h2>
<div class="answer-options">
<button
v-for="(option, index) in quizData?.options"
:key="index"
class="answer-button"
:class="{
selected: selectedAnswer === option,
correct: showResult && option === quizData?.correctWord,
incorrect:
showResult &&
selectedAnswer === option &&
option !== quizData?.correctWord,
}"
@click="handleAnswerSelect(option)"
:disabled="showResult"
>
{{ option }}
</button>
</div>
<div v-if="showResult" class="result-message">
<p v-if="isCorrect" class="success-message"> Correct! You can proceed.</p>
<p v-else class="error-message"> Incorrect. Please try again.</p>
</div>
</div>
<div class="confirm-actions">
<ButtonCommon
v-if="showResult && isCorrect"
type="primary"
size="large"
@click="handleNext"
>
CONTINUE
</ButtonCommon>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.confirm-container {
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-xl);
background: var(--bg-light);
min-height: 100vh;
}
.confirm-card {
@include card-base;
max-width: 500px;
width: 100%;
border: 2px solid var(--primary-color);
}
.confirm-header {
text-align: center;
margin-bottom: var(--spacing-2xl);
padding-bottom: var(--spacing-xl);
border-bottom: 1px solid var(--border-color);
.confirm-title {
font-size: var(--font-2xl);
font-weight: var(--font-bold);
color: var(--text-primary);
margin: 0;
}
}
.confirm-content {
.progress-indicators {
display: flex;
justify-content: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-2xl);
.progress-circle {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--border-light);
transition: background-color 0.3s ease;
&.active {
background: var(--primary-color);
}
}
}
.instruction-text {
text-align: center;
margin-bottom: var(--spacing-2xl);
p {
font-size: var(--font-sm);
color: var(--text-secondary);
line-height: var(--leading-normal);
margin: 0;
}
}
.quiz-section {
margin-bottom: var(--spacing-2xl);
.quiz-question {
font-size: var(--font-lg);
font-weight: var(--font-bold);
color: var(--text-primary);
text-align: center;
margin-bottom: var(--spacing-xl);
}
.answer-options {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
.answer-button {
padding: var(--spacing-md);
border: 2px solid var(--border-light);
background: var(--bg-white);
border-radius: var(--radius-md);
font-size: var(--font-sm);
font-weight: var(--font-medium);
color: var(--text-primary);
cursor: pointer;
transition: all 0.3s ease;
&:hover:not(:disabled) {
border-color: var(--primary-color);
background: var(--primary-light);
}
&.selected {
border-color: var(--primary-color);
background: var(--primary-light);
}
&.correct {
border-color: var(--success-color);
background: var(--success-light);
color: var(--success-color);
}
&.incorrect {
border-color: var(--error-color);
background: var(--error-light);
color: var(--error-color);
}
&:disabled {
cursor: not-allowed;
opacity: 0.7;
}
}
}
.result-message {
text-align: center;
.success-message {
color: var(--success-color);
font-weight: var(--font-medium);
margin: 0;
}
.error-message {
color: var(--error-color);
font-weight: var(--font-medium);
margin: 0;
}
}
}
.confirm-actions {
display: flex;
justify-content: space-between;
gap: var(--spacing-md);
}
}
@media (max-width: 640px) {
.confirm-container {
padding: var(--spacing-md);
}
.confirm-card {
max-width: 100%;
}
.quiz-section {
.answer-options {
grid-template-columns: 1fr;
}
}
.confirm-actions {
flex-direction: column;
}
}
</style>

View File

@ -1,561 +1,61 @@
<script setup lang="ts">
import { ref, computed, defineEmits } from 'vue'
import { ButtonCommon, FormCommon, KeystoreDownloadComponent } from '@/components'
const emit = defineEmits<{
navigateToOpenWallet: [event: Event]
navigateToRecoverySeed: []
}>()
const step = ref(1)
import { ref } from 'vue'
import { ButtonCommon, FormCommon } from '@/components'
const password = ref('')
const confirmPassword = ref('')
const passwordError = ref('')
const confirmPasswordError = ref('')
const passwordStrength = computed(() => {
if (!password.value) return { level: 0, text: '', color: '' }
const handleCancel = () => {}
let strength = 0
const checks = {
length: password.value.length >= 8,
uppercase: /[A-Z]/.test(password.value),
lowercase: /[a-z]/.test(password.value),
number: /[0-9]/.test(password.value),
special: /[!@#$%^&*(),.?":{}|<>]/.test(password.value),
}
strength = Object.values(checks).filter(Boolean).length
if (strength <= 2) return { level: 1, text: 'Weak', color: 'var(--error-color)' }
if (strength <= 3) return { level: 2, text: 'Medium', color: 'var(--warning-color)' }
if (strength <= 4) return { level: 3, text: 'Good', color: 'var(--info-color)' }
return { level: 4, text: 'Strong', color: 'var(--success-color)' }
})
const handleIHaveWallet = () => {}
const isPasswordMatch = computed(() => {
if (!confirmPassword.value) return true
return password.value === confirmPassword.value
})
const canProceed = computed(() => {
return (
password.value.length >= 8 &&
confirmPassword.value.length >= 8 &&
isPasswordMatch.value &&
passwordStrength.value.level >= 2
)
})
const handleIHaveWallet = (e: Event) => {
e.preventDefault()
emit('navigateToOpenWallet', e)
}
const handleNextPassword = () => {
if (!canProceed.value) {
if (password.value.length < 8) {
passwordError.value = 'Password must be at least 8 characters'
}
if (!isPasswordMatch.value) {
confirmPasswordError.value = 'Passwords do not match'
}
return
}
step.value = 2
}
function downloadKeystoreFile() {
// Gi lp ni dung keystore (có th tu chnh)
const data = {
account: 'kaspa-wallet',
version: 1,
enc: 'mock-data',
created: new Date().toISOString(),
note: 'Exported from web-wallet',
hint: 'Replace bằng file thực tế trong tích hợp thật.',
}
const file = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const link = document.createElement('a')
link.href = URL.createObjectURL(file)
link.download = 'kaspa-wallet-keystore.json'
link.click()
setTimeout(() => URL.revokeObjectURL(link.href), 2300)
step.value = 3
}
function handleBack() {
if (step.value === 2) step.value = 1
else if (step.value === 3) step.value = 2
}
function resetAll() {
password.value = ''
confirmPassword.value = ''
passwordError.value = ''
confirmPasswordError.value = ''
step.value = 1
}
const handleNext = () => {}
</script>
<template>
<div class="auth-container">
<div class="auth-card">
<template v-if="step === 1">
<div class="auth-card-header">
<div class="logo-container">
<div class="logo-circle">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
class="neptune-logo"
>
<defs>
<linearGradient
id="neptuneGradient"
x1="0%"
y1="0%"
x2="100%"
y2="100%"
>
<stop
offset="0%"
style="stop-color: #007fcf; stop-opacity: 1"
/>
<stop
offset="100%"
style="stop-color: #0066a6; stop-opacity: 1"
/>
</linearGradient>
<linearGradient
id="ringGradient"
x1="0%"
y1="0%"
x2="100%"
y2="0%"
>
<stop
offset="0%"
style="stop-color: #007fcf; stop-opacity: 0.3"
/>
<stop
offset="50%"
style="stop-color: #007fcf; stop-opacity: 0.6"
/>
<stop
offset="100%"
style="stop-color: #007fcf; stop-opacity: 0.3"
/>
</linearGradient>
</defs>
<div class="auth-card-header">
<h2>Create Wallet</h2>
</div>
<circle cx="50" cy="50" r="28" fill="url(#neptuneGradient)" />
<ellipse
cx="50"
cy="45"
rx="22"
ry="6"
fill="rgba(255, 255, 255, 0.1)"
/>
<ellipse cx="50" cy="55" rx="20" ry="5" fill="rgba(0, 0, 0, 0.1)" />
<ellipse
cx="50"
cy="50"
rx="42"
ry="12"
fill="none"
stroke="url(#ringGradient)"
stroke-width="4"
opacity="0.8"
/>
<circle cx="42" cy="42" r="6" fill="rgba(255, 255, 255, 0.4)" />
</svg>
</div>
<div class="logo-text">
<span class="coin-name">Neptune</span>
<span class="coin-symbol">NPTUN</span>
</div>
</div>
<h1 class="auth-title">Create New Wallet</h1>
<p class="auth-subtitle">Secure your wallet with a strong password</p>
<div class="auth-card-content">
<div class="password-section">
<FormCommon
v-model="password"
type="password"
label="Create a password for your new wallet"
placeholder="Password"
show-password-toggle
required
/>
</div>
<div class="auth-card-content">
<div class="form-group">
<FormCommon
v-model="password"
type="password"
label="Create Password"
placeholder="Enter your password"
show-password-toggle
required
:error="passwordError"
@input="passwordError = ''"
/>
<div v-if="password" class="password-strength">
<div class="strength-bar">
<div
class="strength-fill"
:style="{
width: `${(passwordStrength.level / 4) * 100}%`,
backgroundColor: passwordStrength.color,
}"
></div>
</div>
<span
class="strength-text"
:style="{ color: passwordStrength.color }"
>{{ passwordStrength.text }}</span
>
</div>
</div>
<div class="form-group">
<FormCommon
v-model="confirmPassword"
type="password"
label="Confirm Password"
placeholder="Re-enter your password"
show-password-toggle
required
:error="confirmPasswordError"
@input="confirmPasswordError = ''"
/>
<div
v-if="confirmPassword"
class="password-match"
:class="{ match: isPasswordMatch }"
>
<span v-if="isPasswordMatch" class="match-text">
Passwords match
</span>
<span v-else class="match-text error"> Passwords do not match </span>
</div>
</div>
<p class="helper-text">
Password must be at least 8 characters with uppercase, lowercase, and
numbers.
</p>
<div class="auth-button-group">
<ButtonCommon
type="primary"
size="large"
class="auth-button"
block
:disabled="!canProceed"
@click="handleNextPassword"
>Create Wallet</ButtonCommon
>
<div class="secondary-actions">
<button class="link-button" @click="handleIHaveWallet">
Already have a wallet?
</button>
</div>
</div>
<div class="password-section">
<FormCommon
v-model="confirmPassword"
type="password"
label="Confirm password"
placeholder="Confirm Password"
show-password-toggle
required
/>
</div>
</template>
<template v-else-if="step === 2">
<KeystoreDownloadComponent @download="downloadKeystoreFile" @back="handleBack" />
</template>
<template v-else-if="step === 3">
<div class="well-done-step">
<h2 class="done-main">You are done!</h2>
<p class="done-desc">
You are now ready to take advantage of all that your wallet has to offer!
Access with keystore file should only be used in an offline setting.
</p>
<div class="center-svg" style="margin: 14px auto 12px auto">
<svg width="180" height="95" viewBox="0 0 175 92" fill="none">
<rect x="111" y="37" width="64" height="33" rx="7" fill="#23B1EC" />
<rect
x="30.5"
y="37.5"
width="80"
height="46"
rx="7.5"
fill="#D6F9FE"
stroke="#AEEBF8"
stroke-width="5"
/>
<rect x="56" y="67" width="32" height="10" rx="3" fill="#B0F3A6" />
<rect x="46" y="49" width="52" height="12" rx="3" fill="#a2d2f5" />
<circle cx="155" cy="52" r="8" fill="#fff" />
<rect x="121" y="43" width="27" height="7" rx="1.5" fill="#5AE9D2" />
<rect x="128" y="59" width="17" height="4" rx="1.5" fill="#FCEBBA" />
<circle cx="40" cy="27" r="7" fill="#A2D2F5" />
<g>
<circle cx="128" cy="21" r="3" fill="#FF8585" />
<circle cx="57.5" cy="20.5" r="1.5" fill="#67DEFF" />
<rect x="95" y="18" width="7" height="5" rx="2" fill="#A2D2F5" />
</g>
</svg>
</div>
<div class="btn-row">
<ButtonCommon
class="done-btn"
type="primary"
size="large"
block
style="margin-bottom: 0.3em"
@click="$router.push('/')"
>Access Wallet</ButtonCommon
>
<button class="done-link" type="button" @click="resetAll">
Create Another Wallet
</button>
</div>
<div class="auth-button-group">
<ButtonCommon class="auth-btn secondary" @click="handleCancel">
Cancel
</ButtonCommon>
<ButtonCommon class="auth-btn secondary" @click="handleIHaveWallet">
I have a wallet
</ButtonCommon>
<ButtonCommon class="auth-btn primary" @click="handleNext"> Next </ButtonCommon>
</div>
</template>
<template v-else>
<slot> </slot>
</template>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.auth-container {
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-xl);
background: var(--bg-light);
}
.auth-card {
@include card-base;
max-width: 720px;
width: 100%;
@media (max-width: 640px) {
max-width: 100%;
}
}
.auth-card-header {
text-align: center;
margin-bottom: var(--spacing-2xl);
padding-bottom: var(--spacing-xl);
border-bottom: 1px solid var(--border-color);
.logo-container {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
.logo-circle {
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary-light), var(--bg-white));
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-sm);
box-shadow: 0 2px 8px rgba(0, 127, 207, 0.15);
.neptune-logo {
width: 100%;
height: 100%;
}
}
.logo-text {
display: flex;
flex-direction: column;
align-items: flex-start;
.coin-name {
font-size: var(--font-lg);
font-weight: var(--font-bold);
color: var(--text-primary);
line-height: 1;
margin-bottom: 2px;
}
.coin-symbol {
font-size: var(--font-xs);
font-weight: var(--font-medium);
color: var(--primary-color);
background: var(--primary-light);
padding: 2px 8px;
border-radius: var(--radius-sm);
}
}
}
.auth-title {
font-size: var(--font-2xl);
font-weight: var(--font-bold);
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.auth-subtitle {
font-size: var(--font-sm);
color: var(--text-secondary);
margin: 0;
}
}
.auth-card-content {
.form-group {
margin-bottom: var(--spacing-xl);
}
}
.password-strength {
margin-top: var(--spacing-sm);
display: flex;
align-items: center;
gap: var(--spacing-md);
.strength-bar {
flex: 1;
height: 4px;
background: var(--border-light);
border-radius: var(--radius-full);
overflow: hidden;
.strength-fill {
height: 100%;
transition: all 0.3s ease;
}
}
.strength-text {
font-size: var(--font-xs);
font-weight: var(--font-medium);
min-width: 50px;
text-align: right;
}
}
.password-match {
margin-top: var(--spacing-sm);
font-size: var(--font-xs);
&.match .match-text {
color: var(--success-color);
}
.match-text.error {
color: var(--error-color);
}
}
.helper-text {
font-size: var(--font-xs);
color: var(--text-muted);
margin: 0 0 var(--spacing-xl);
line-height: var(--leading-normal);
}
.auth-button {
width: fit-content;
margin: 0 auto;
}
.auth-button-group {
margin-top: var(--spacing-2xl);
display: flex;
flex-direction: column;
.secondary-actions {
display: flex;
justify-content: center;
align-items: center;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
}
.link-button {
background: none;
border: none;
color: var(--primary-color);
font-size: var(--font-sm);
cursor: pointer;
transition: color 0.2s ease;
padding: 0;
&:hover {
color: var(--primary-hover);
text-decoration: underline;
}
}
.separator {
color: var(--text-muted);
font-size: var(--font-sm);
}
}
@media (max-width: 640px) {
.auth-container {
padding: var(--spacing-md);
}
.auth-card-header {
.logo-container {
.logo-circle {
width: 40px;
height: 40px;
}
.logo-text {
.coin-name {
font-size: var(--font-md);
}
}
}
.auth-title {
font-size: var(--font-xl);
}
}
}
.well-done-step {
text-align: center;
padding: 20px 8px;
.done-title {
color: var(--primary-color);
font-weight: 700;
letter-spacing: 0.07em;
margin-bottom: 1px;
}
.done-main {
font-size: 1.36rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 2px;
}
.done-desc {
color: var(--text-secondary);
font-size: 1.11em;
max-width: 410px;
margin: 2px auto 15px auto;
}
.center-svg {
display: flex;
justify-content: center;
}
.btn-row {
display: flex;
flex-direction: column;
gap: 11px;
align-items: center;
width: 100%;
max-width: 400px;
margin: 0 auto 5px auto;
}
.done-btn {
margin-bottom: 0.3em;
}
.done-link {
background: none;
border: none;
color: var(--primary-color);
font-size: 1em;
text-decoration: underline;
cursor: pointer;
margin: 0 auto;
font-weight: 600;
}
}
</style>
<style lang="scss" scoped></style>

View File

@ -1,270 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ButtonCommon, FormCommon } from '@/components'
import { validateSeedPhrase18 } from '@/utils/helpers/seedPhrase'
const emit = defineEmits<{
(
e: 'import-success',
data: { type: 'seed' | 'privatekey'; value: string | string[]; passphrase?: string }
): void
}>()
const tab = ref<'seedphrase' | 'privatekey'>('seedphrase')
const seedWords = ref<string[]>(Array(18).fill(''))
const seedError = ref('')
const passphrase = ref('')
const privateKey = ref('')
const privateKeyError = ref('')
const inputBoxFocus = (idx: number) => {
document.getElementById('input-' + idx)?.focus()
}
const validateSeed = () => {
if (seedWords.value.some((w) => !w.trim())) {
seedError.value = 'Please enter all 18 words.'
return false
}
if (!validateSeedPhrase18(seedWords.value)) {
seedError.value = 'One or more words are invalid.'
return false
}
seedError.value = ''
return true
}
const validateKey = () => {
if (!privateKey.value.trim()) {
privateKeyError.value = 'Please enter your private key.'
return false
}
privateKeyError.value = ''
return true
}
const handleContinue = () => {
if (tab.value === 'seedphrase') {
if (validateSeed()) {
emit('import-success', {
type: 'seed',
value: seedWords.value,
passphrase: passphrase.value,
})
}
} else {
if (validateKey()) {
emit('import-success', { type: 'privatekey', value: privateKey.value })
}
}
}
</script>
<template>
<div class="import-wallet dark-card">
<h2 class="title">Import Wallet</h2>
<div class="desc">Pick your import method</div>
<div class="tabs">
<button
:class="['tab-btn', tab === 'seedphrase' && 'active']"
@click="tab = 'seedphrase'"
>
Import by seed phrase
</button>
<button
:class="['tab-btn', tab === 'privatekey' && 'active']"
@click="tab = 'privatekey'"
>
Import by private key
</button>
</div>
<div v-if="tab === 'seedphrase'" class="tab-pane">
<div class="seed-row-radio">
<div class="radio active">18 words</div>
</div>
<div class="seed-inputs">
<div class="seed-input-grid">
<div v-for="(word, i) in seedWords" :key="i" class="seed-box">
<input
:id="'input-' + i"
type="text"
inputmode="text"
autocapitalize="off"
autocomplete="off"
spellcheck="false"
v-model="seedWords[i]"
:placeholder="i + 1 + '.'"
maxlength="24"
@keydown.enter="inputBoxFocus(i + 1)"
:class="{ error: seedError && !word.trim() }"
@focus="seedError = ''"
/>
</div>
</div>
</div>
<div class="form-row mt-sm">
<FormCommon
v-model="passphrase"
:label="'Seed passphrase (optional)'"
placeholder="Enter seed passphrase"
/>
</div>
<div v-if="seedError" class="error-text">{{ seedError }}</div>
</div>
<div v-else class="tab-pane">
<div class="form-row mb-md">
<FormCommon
v-model="privateKey"
type="text"
label="Private key"
placeholder="Enter private key"
:error="privateKeyError"
@focus="privateKeyError = ''"
/>
</div>
</div>
<ButtonCommon
class="mt-lg"
type="primary"
block
size="large"
:disabled="
tab === 'seedphrase'
? !seedWords.every((w) => w) || !!seedError
: !privateKey || !!privateKeyError
"
@click="handleContinue"
>Continue</ButtonCommon
>
</div>
</template>
<style lang="scss" scoped>
.import-wallet {
max-width: 420px;
width: 100%;
margin: 24px auto;
background: var(--bg-light);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-primary);
padding: 32px 28px 24px 28px;
color: var(--text-primary);
.title {
font-weight: 700;
font-size: 1.45rem;
text-align: center;
margin-bottom: 5px;
}
.desc {
text-align: center;
color: var(--text-secondary);
font-size: 1rem;
margin-bottom: 24px;
}
.tabs {
display: flex;
background: var(--text-primary);
border-radius: 13px;
overflow: hidden;
margin-bottom: 18px;
.tab-btn {
flex: 1;
padding: 13px;
border: none;
background: none;
color: var(--text-secondary);
font-size: 1rem;
cursor: pointer;
transition: 0.18s;
&.active {
background: var(--primary-color);
color: var(--text-light);
}
&:hover:not(.active) {
background: var(--bg-secondary);
}
}
}
.tab-pane {
margin-top: 0.5rem;
}
.seed-row-radio {
display: flex;
gap: 24px;
align-items: center;
margin-bottom: 16px;
.radio {
background: var(--bg-secondary);
color: var(--text-secondary);
border-radius: 14px;
font-size: 1.08rem;
padding: 7px 24px 7px 18px;
font-weight: 500;
display: flex;
align-items: center;
gap: 5px;
&.active {
background: var(--primary-color);
color: var(--text-light);
}
opacity: 1;
}
}
.seed-inputs {
width: 100%;
}
.seed-input-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.seed-box input {
width: 100%;
padding: 8px 12px;
border-radius: 9px;
border: 2px solid var(--border-color);
background: var(--bg-secondary);
color: var(--text-secondary);
outline: none;
font-size: 15px;
transition:
border 0.16s,
box-shadow 0.16s;
&:focus {
border-color: var(--primary-color);
box-shadow: 0 2px 8px var(--shadow-primary);
}
&.error {
border-color: var(--error-color);
background: var(--bg-secondary);
color: var(--text-light);
}
}
.form-row {
margin-top: 19px;
margin-bottom: 4px;
}
.mt-sm {
margin-top: 9px;
}
.mb-md {
margin-bottom: 14px;
}
.mt-lg {
margin-top: 32px;
}
.error-text {
color: var(--error-color);
font-size: 0.97em;
margin-top: 3px;
}
}
@media (max-width: 600px) {
.import-wallet {
padding: 16px 5px;
}
.seed-input-grid {
gap: 8px;
grid-template-columns: repeat(3, 1fr);
}
}
</style>

View File

@ -1,235 +0,0 @@
<script setup lang="ts">
const emit = defineEmits<{
(e: 'download'): void
(e: 'back'): void
}>()
function handleDownload() {
emit('download')
}
function handleBack() {
emit('back')
}
</script>
<template>
<div class="keystore-step">
<div class="step-content">
<h2 class="title">Download keystore file</h2>
<div class="desc">Important things to know before downloading your keystore file.</div>
<div class="box-list">
<div class="box">
<div class="icn">
<svg width="44" height="44" viewBox="0 0 36 36">
<g>
<path
d="M15,29 L29,29 C30.1045695,29 31,28.1045695 31,27 L31,9 C31,7.8954305 30.1045695,7 29,7 L7,7 C5.8954305,7 5,7.8954305 5,9 L5,27 C5,28.1045695 5.8954305,29 7,29 L11,29"
fill="#d8f7fa"
/>
<path
d="M12.5,20.5 L17.5,25.5 L27.5,15.5"
stroke="#51c7ce"
stroke-width="2.2"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
/>
</g>
</svg>
</div>
<div class="box-title">Don't lose it</div>
<div class="box-desc">Be careful, it can not be recovered if you lose it.</div>
</div>
<div class="box">
<div class="icn">
<svg width="46" height="46" viewBox="0 0 28 28">
<g>
<circle cx="16" cy="16" r="12" fill="#e3fae5" />
<text
x="11"
y="21"
font-size="15"
font-family="Arial"
fill="#48b783"
font-weight="bold"
>
$
</text>
</g>
</svg>
</div>
<div class="box-title">Don't share it</div>
<div class="box-desc">
Your funds will be stolen if you use this file on a malicious phishing site.
</div>
</div>
<div class="box">
<div class="icn">
<svg width="46" height="46" viewBox="0 0 28 28">
<g>
<rect x="5" y="7" width="18" height="16" rx="3" fill="#c6f1fc" />
<rect x="7" y="10" width="14" height="10" rx="2" fill="#96e2fc" />
<text
x="10"
y="19"
font-size="9"
font-family="monospace"
fill="#418aaf"
>
{ }
</text>
</g>
</svg>
</div>
<div class="box-title">Make a backup</div>
<div class="box-desc">
Secure it like the millions of dollars it may one day be worth.
</div>
</div>
</div>
<div class="btn-row">
<button class="back-btn" @click="handleBack">Back</button>
<button class="main-btn" @click="handleDownload">Acknowledge & Download</button>
</div>
<div class="not-recommended">
<span class="warn-icn">&#9888;</span>
<div>
<span class="strong">NOT RECOMMENDED</span><br />
This information is sensitive, and these options should only be used in offline
or secure environments.
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.keystore-step {
max-width: 650px;
margin: 0 auto;
border-radius: 14px;
box-shadow: var(--shadow-md);
padding: 34px 16px 28px 16px;
}
.step-content {
padding: 6px 5px 0 5px;
}
.step-title {
color: var(--primary-color);
font-weight: 700;
letter-spacing: 0.07em;
font-size: 1.07rem;
margin-bottom: 2px;
}
.title {
color: var(--text-primary);
font-size: 1.36rem;
font-weight: 700;
margin-bottom: 4px;
}
.desc {
color: var(--text-secondary);
font-size: 1.09rem;
margin-bottom: 23px;
}
.box-list {
display: flex;
gap: 21px;
margin-bottom: 32px;
justify-content: center;
flex-wrap: wrap;
}
.box {
border: 2px solid var(--border-color);
border-radius: 12px;
background: var(--bg-light);
padding: 21px 19px 17px 19px;
flex: 1 1 140px;
min-width: 196px;
max-width: 250px;
display: flex;
flex-direction: column;
align-items: center;
.icn {
margin-bottom: 12px;
}
.box-title {
font-weight: 700;
margin-bottom: 5px;
font-size: 1.07em;
color: var(--text-primary);
}
.box-desc {
color: var(--text-secondary);
font-size: 0.99em;
}
}
.btn-row {
display: flex;
gap: 14px;
margin: 23px 0 0 0;
justify-content: center;
align-items: center;
}
.back-btn {
padding: 10px 32px;
background: none;
border-radius: 8px;
border: 2px solid var(--border-color);
color: var(--text-primary);
font-weight: 700;
font-size: 1em;
cursor: pointer;
transition: 0.13s;
&:hover {
background: var(--bg-light);
}
}
.main-btn {
padding: 10px 32px;
background: var(--primary-color);
border-radius: 8px;
border: none;
color: var(--text-light);
font-weight: 700;
font-size: 1em;
cursor: pointer;
transition: 0.12s;
box-shadow: 0 4px 18px var(--shadow-primary);
&:hover {
background: var(--primary-hover);
}
}
.not-recommended {
background: var(--bg-secondary);
border-radius: 10px;
color: var(--secondary-color);
padding: 13px 17px;
margin-top: 28px;
font-size: 1.09em;
display: flex;
align-items: flex-start;
gap: 11px;
.warn-icn {
font-size: 1.43em;
color: var(--error-color);
margin-top: 3px;
}
.strong {
font-weight: 700;
margin-right: 8px;
}
}
@media (max-width: 900px) {
.steps-bar .step {
min-width: 90px;
font-size: 14px;
}
}
@media (max-width: 650px) {
.keystore-step {
padding: 15px 3px 13px 3px;
}
.box {
padding: 12px 5px 10px 5px;
}
}
</style>

View File

@ -1,17 +1,12 @@
<script setup lang="ts">
import { ButtonCommon } from '@/components'
const emit = defineEmits<{
goToCreate: []
goToLogin: []
}>()
const handleGoToCreate = () => {
emit('goToCreate')
const goToNewWallet = () => {
window.open('https://kaspa-ng.org', '_blank')
}
const handleGoToLogin = () => {
emit('goToLogin')
const goToLegacyWallet = () => {
window.open('https://wallet.kaspanet.io', '_blank')
}
</script>
@ -21,19 +16,21 @@ const handleGoToLogin = () => {
<div class="welcome-box">
<div class="header-section">
<h2>Welcome to the New Wallet Experience</h2>
<p>Choose the next action:</p>
</div>
<div
class="button-group"
style="display: flex; flex-direction: column; gap: 1rem; margin: 2rem 0"
>
<ButtonCommon type="primary" size="large" @click="handleGoToCreate">
Create new wallet
</ButtonCommon>
<ButtonCommon type="default" size="large" @click="handleGoToLogin">
Open existing wallet
<p>
We've launched a new version of the Kaspa Wallet at
<br />
<span class="highlight"> https://kaspa-ng.org </span>
</p>
<ButtonCommon @click="goToNewWallet">
Go to the new Kaspa NG Wallet
</ButtonCommon>
</div>
<div class="spacer"></div>
<p>
Already have funds on the old wallet?<br />
You can still use <span class="highlight">https://wallet.kaspanet.io</span>
</p>
<ButtonCommon @click="goToLegacyWallet"> Continue on Legacy Wallet </ButtonCommon>
<div class="note">Thank you for being a part of the Kaspa community!</div>
</div>
</div>
@ -43,15 +40,15 @@ const handleGoToLogin = () => {
<style lang="scss" scoped>
.welcome-page {
min-height: 100vh;
background-color: var(--bg-light);
background-color: var(--vt-c-white);
position: relative;
.welcome-card {
background-color: var(--bg-white);
box-shadow: var(--shadow-md);
border-radius: var(--radius-md);
background-color: var(--vt-c-white);
box-shadow: 0 0 15px var(--vt-c-badge-caption-shadow);
border-radius: 10px;
width: 100%;
max-width: 500px;
max-width: 800px;
flex-direction: column;
text-align: center;
padding: 2rem;
@ -62,15 +59,4 @@ const handleGoToLogin = () => {
}
}
}
p {
margin: 1rem;
font-size: var(--font-sm);
}
.note {
margin-top: 1rem;
font-size: 0.9rem;
color: var(--text-secondary);
}
</style>

View File

@ -1,172 +1,44 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ButtonCommon, FormCommon } from '@/components'
const router = useRouter()
const password = ref('')
const passwordError = ref('')
const isLoading = ref(false)
const handleOpenWallet = async () => {
if (!password.value) {
passwordError.value = 'Please enter your password'
return
}
const handleOpenWallet = () => {}
isLoading.value = true
passwordError.value = ''
try {
await new Promise((resolve) => setTimeout(resolve, 1500))
router.push('/')
} catch (error) {
passwordError.value = 'Invalid password. Please try again.'
} finally {
isLoading.value = false
}
}
const emit = defineEmits<{
navigateToCreate: []
}>()
const navigateToNewWallet = () => {
emit('navigateToCreate')
}
const handleNewWallet = () => {}
</script>
<template>
<div class="auth-container">
<div class="auth-card">
<div class="auth-card-header">
<h2>Open Wallet</h2>
</div>
<div class="auth-card-content">
<div class="wallet-icon">
<div class="logo-container">
<div class="logo-circle">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
class="neptune-logo"
>
<defs>
<linearGradient
id="neptuneGradient"
x1="0%"
y1="0%"
x2="100%"
y2="100%"
>
<stop
offset="0%"
style="stop-color: #007fcf; stop-opacity: 1"
/>
<stop
offset="100%"
style="stop-color: #0066a6; stop-opacity: 1"
/>
</linearGradient>
<linearGradient
id="ringGradient"
x1="0%"
y1="0%"
x2="100%"
y2="0%"
>
<stop
offset="0%"
style="stop-color: #007fcf; stop-opacity: 0.3"
/>
<stop
offset="50%"
style="stop-color: #007fcf; stop-opacity: 0.6"
/>
<stop
offset="100%"
style="stop-color: #007fcf; stop-opacity: 0.3"
/>
</linearGradient>
</defs>
<circle cx="50" cy="50" r="28" fill="url(#neptuneGradient)" />
<ellipse
cx="50"
cy="45"
rx="22"
ry="6"
fill="rgba(255, 255, 255, 0.1)"
/>
<ellipse cx="50" cy="55" rx="20" ry="5" fill="rgba(0, 0, 0, 0.1)" />
<ellipse
cx="50"
cy="50"
rx="42"
ry="12"
fill="none"
stroke="url(#ringGradient)"
stroke-width="4"
opacity="0.8"
/>
<circle cx="42" cy="42" r="6" fill="rgba(255, 255, 255, 0.4)" />
</svg>
</div>
<div class="logo-text">
<span class="coin-name">Neptune</span>
<span class="coin-symbol">NPTUN</span>
</div>
</div>
<div class="icon-circle"></div>
</div>
<div class="form-group">
<div class="password-section">
<FormCommon
v-model="password"
type="password"
label="Enter your password"
placeholder="Password"
label="Unlock the wallet with your password:"
placeholder="Enter your password"
show-password-toggle
required
:error="passwordError"
:disabled="isLoading"
@input="passwordError = ''"
@keyup.enter="handleOpenWallet"
/>
</div>
<div class="security-notice">
<div class="notice-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path
d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"
stroke="currentColor"
stroke-width="2"
/>
<path
d="M9 12l2 2 4-4"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
<span>Your password is encrypted and stored locally</span>
</div>
<div class="auth-button-group">
<ButtonCommon
type="primary"
size="large"
block
:disabled="!password || isLoading"
:loading="isLoading"
@click="handleOpenWallet"
>
{{ isLoading ? 'Opening...' : 'Open Wallet' }}
<ButtonCommon class="auth-btn secondary" @click="handleNewWallet">
NEW WALLET
</ButtonCommon>
<ButtonCommon type="default" size="large" block @click="navigateToNewWallet">
New Wallet
<ButtonCommon class="auth-btn primary" @click="handleOpenWallet">
OPEN WALLET
</ButtonCommon>
</div>
</div>
@ -174,173 +46,4 @@ const navigateToNewWallet = () => {
</div>
</template>
<style lang="scss" scoped>
.auth-container {
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-xl);
background: var(--bg-light);
}
.auth-card {
@include card-base;
max-width: 420px;
width: 100%;
@media (max-width: 640px) {
max-width: 100%;
}
}
.auth-card-content {
.form-group {
margin-bottom: var(--spacing-xl);
}
}
.logo-container {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
.logo-circle {
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary-light), var(--bg-white));
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-sm);
box-shadow: 0 2px 8px rgba(0, 127, 207, 0.15);
.neptune-logo {
width: 100%;
height: 100%;
}
}
.logo-text {
display: flex;
flex-direction: column;
align-items: flex-start;
.coin-name {
font-size: var(--font-lg);
font-weight: var(--font-bold);
color: var(--text-primary);
line-height: 1;
margin-bottom: 2px;
}
.coin-symbol {
font-size: var(--font-xs);
font-weight: var(--font-medium);
color: var(--primary-color);
background: var(--primary-light);
padding: 2px 8px;
border-radius: var(--radius-sm);
}
}
}
.security-notice {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-md);
background: var(--bg-light);
border-radius: var(--radius-md);
border: 1px solid var(--border-light);
margin-bottom: var(--spacing-xl);
.notice-icon {
color: var(--success-color);
flex-shrink: 0;
}
span {
font-size: var(--font-xs);
color: var(--text-secondary);
line-height: var(--leading-normal);
}
}
.auth-button-group {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
margin-bottom: var(--spacing-xl);
}
// Help Links
.help-links {
display: flex;
justify-content: center;
align-items: center;
gap: var(--spacing-sm);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--border-color);
.link-button {
background: none;
border: none;
color: var(--primary-color);
font-size: var(--font-sm);
cursor: pointer;
transition: color 0.2s ease;
padding: 0;
&:hover:not(:disabled) {
color: var(--primary-hover);
text-decoration: underline;
}
&:disabled {
color: var(--text-muted);
cursor: not-allowed;
}
}
.separator {
color: var(--text-muted);
font-size: var(--font-sm);
}
}
// Responsive Design
@media (max-width: 640px) {
.auth-container {
padding: var(--spacing-md);
}
.auth-card-header {
.logo-container {
.logo-circle {
width: 40px;
height: 40px;
}
.logo-text {
.coin-name {
font-size: var(--font-md);
}
}
}
.auth-title {
font-size: var(--font-xl);
}
}
.wallet-icon {
.icon-circle {
width: 64px;
height: 64px;
}
}
}
</style>
<style lang="scss" scoped></style>

View File

@ -1,207 +0,0 @@
<script setup lang="ts">
import { ref, defineEmits, onMounted } from 'vue'
import { ButtonCommon } from '@/components'
import { generateSeedPhrase } from '@/utils'
import { useSeedStore } from '@/stores'
const emit = defineEmits<{
next: []
back: []
}>()
const seedStore = useSeedStore()
const seedWords = ref<string[]>([])
onMounted(() => {
const words = generateSeedPhrase()
seedWords.value = words
seedStore.setSeedWords(words)
})
const handleNext = () => {
emit('next')
}
const handleBack = () => {
emit('back')
}
</script>
<template>
<div class="recovery-container">
<div class="recovery-card">
<div class="recovery-header">
<h1 class="recovery-title">Recovery Seed</h1>
</div>
<div class="recovery-content">
<div class="instruction-text">
<p>
Your wallet is accessible by a seed phrase. The seed phrase is an ordered
12-word secret phrase.
</p>
<p>
Make sure no one is looking, as anyone with your seed phrase can access your
wallet your funds. Write it down and keep it safe.
</p>
</div>
<div class="seed-words-container">
<div class="seed-words-grid">
<div v-for="(word, index) in seedWords" :key="index" class="seed-word-item">
<span class="word-number">{{ index + 1 }}</span>
<span class="word-text">{{ word }}</span>
</div>
</div>
</div>
<div class="cool-fact">
<p>
Cool fact: there are more 12-word phrase combinations than nanoseconds since
the big bang!
</p>
</div>
<div class="recovery-actions">
<ButtonCommon type="default" size="large" @click="handleBack">
BACK
</ButtonCommon>
<ButtonCommon type="primary" size="large" @click="handleNext">
NEXT
</ButtonCommon>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.recovery-container {
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-xl);
background: var(--bg-light);
min-height: 100vh;
}
.recovery-card {
@include card-base;
max-width: 500px;
width: 100%;
border: 2px solid var(--primary-color);
}
.recovery-header {
text-align: center;
margin-bottom: var(--spacing-2xl);
padding-bottom: var(--spacing-xl);
border-bottom: 1px solid var(--border-color);
.recovery-title {
font-size: var(--font-2xl);
font-weight: var(--font-bold);
color: var(--text-primary);
margin: 0;
}
}
.recovery-content {
.instruction-text {
margin-bottom: var(--spacing-2xl);
p {
font-size: var(--font-sm);
color: var(--text-secondary);
line-height: var(--leading-normal);
margin-bottom: var(--spacing-md);
&:last-child {
margin-bottom: 0;
}
}
}
.seed-words-container {
margin-bottom: var(--spacing-2xl);
padding: var(--spacing-lg);
background: var(--bg-hover);
border-radius: var(--radius-md);
border: 1px solid var(--border-light);
.seed-words-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--spacing-md);
.seed-word-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
background: var(--bg-white);
border-radius: var(--radius-sm);
border: 1px solid var(--border-light);
.word-number {
font-size: var(--font-xs);
font-weight: var(--font-bold);
color: var(--text-muted);
min-width: 20px;
}
.word-text {
font-size: var(--font-sm);
font-weight: var(--font-medium);
color: var(--text-primary);
}
}
}
}
.cool-fact {
margin-bottom: var(--spacing-2xl);
text-align: center;
p {
font-size: var(--font-xs);
color: var(--text-muted);
font-style: italic;
margin: 0;
}
}
.recovery-actions {
display: flex;
justify-content: space-between;
gap: var(--spacing-md);
}
}
// Responsive Design
@media (max-width: 640px) {
.recovery-container {
padding: var(--spacing-md);
}
.recovery-card {
max-width: 100%;
}
.seed-words-container {
.seed-words-grid {
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-sm);
.seed-word-item {
padding: var(--spacing-xs);
.word-number {
min-width: 16px;
}
}
}
}
}
</style>

View File

@ -1,67 +1,5 @@
<script setup lang="ts">
import { Button } from 'ant-design-vue'
import type { ButtonProps } from '@/interface'
const props = withDefaults(defineProps<ButtonProps>(), {
type: 'default',
size: 'large',
block: false,
disabled: false,
loading: false,
htmlType: 'button',
})
const emit = defineEmits(['click'])
const handleClick = () => {
if (!props.disabled && !props.loading) {
emit('click')
}
}
</script>
<template>
<Button
:type="props.type"
:size="props.size"
:block="props.block"
:disabled="props.disabled"
:loading="props.loading"
:html-type="props.htmlType"
@click="handleClick"
class="btn-common"
>
<button class="btn-common">
<slot />
</Button>
</button>
</template>
<style lang="scss" scoped>
.btn-common {
:deep(.ant-btn) {
background: var(--primary-color);
border-color: var(--primary-color);
font-weight: var(--font-semibold);
height: auto;
padding: var(--btn-padding-y) var(--btn-padding-x);
transition: var(--transition-all);
border-radius: var(--btn-radius);
letter-spacing: var(--tracking-wide);
transition: all 2s ease-in-out;
&:hover {
background: var(--primary-hover);
border-color: var(--primary-hover);
}
&:active,
&:focus {
background: var(--primary-hover);
border-color: var(--primary-hover);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
}
</style>

View File

@ -89,19 +89,19 @@ const handleBlur = (e: FocusEvent) => {
.input-container {
position: relative;
border: 1px solid var(--border-color);
border: 1px solid var(--vt-c-gray-6);
border-radius: 6px;
transition: all 0.2s ease;
&.focused {
border-color: var(--border-primary);
box-shadow: var(--shadow-primary);
border-color: var(--vt-c-main-color);
box-shadow: var(--vt-input-shadow-focus);
}
&.error {
border-color: var(--error-color);
background-color: var();
border-color: var(--vt-c-red-v3);
background-color: var()
}
&.disabled {
background-color: var(--bg-hover);
background-color: var(--vt-c-gray-4);
cursor: not-allowed;
}
}
@ -109,18 +109,17 @@ const handleBlur = (e: FocusEvent) => {
.form-input {
width: 100%;
padding: 12px 40px 12px 12px;
border: var(--border-color);
background: var(--text-light);
font-size: var(--font-base);
color: var(--text-primary);
border: none;
background: transparent;
font-size: 14px;
color: var(--vt-c-black-bold);
outline: none;
border-radius: var(--radius-md);
&::placeholder {
color: var(--text-muted);
color: var(--vt-c-gray-8);
}
&:disabled {
cursor: not-allowed;
color: var(--text-muted);
color: var(--vt-c-gray-8);
}
}
@ -132,10 +131,10 @@ const handleBlur = (e: FocusEvent) => {
background: none;
border: none;
cursor: pointer;
color: var(--text-muted);
color: var(--vt-c-gray-8);
padding: 4px;
&:hover {
color: var(--text-primary);
color: var(--vt-c-black-bold);
}
&:disabled {
cursor: not-allowed;
@ -146,7 +145,7 @@ const handleBlur = (e: FocusEvent) => {
.error-message {
margin-top: 4px;
font-size: 12px;
color: var(--error-color);
color: var(--vt-c-red-v3);
display: flex;
align-items: center;
}

View File

@ -1,200 +1,31 @@
<script setup lang="ts">
import type { IconProps } from '@/interface'
import { computed } from 'vue'
import {
EyeOutlined,
EyeInvisibleOutlined,
SearchOutlined,
CloseOutlined,
RightOutlined,
LeftOutlined,
} from '@ant-design/icons-vue'
import type { IconProps } from '@/interface'
const props = withDefaults(defineProps<IconProps>(), {
size: 16,
color: 'currentColor',
icon: '',
})
const iconSize = computed(() => {
if (typeof props.size === 'number') {
return `${props.size}px`
}
return props.size
})
const iconMap: Record<string, any> = {
eye: EyeOutlined,
'eye-off': EyeInvisibleOutlined,
search: SearchOutlined,
close: CloseOutlined,
'arrow-right': RightOutlined,
'arrow-left': LeftOutlined,
}
const IconComponent = computed(() => iconMap[props.icon])
</script>
<template>
<svg
:width="iconSize"
:height="iconSize"
:class="props.class"
:style="{ color: props.color }"
viewBox="0 0 24 24"
fill="none"
>
<!-- Eye Icon -->
<path
v-if="icon === 'eye'"
d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"
stroke="currentColor"
stroke-width="2"
/>
<circle
v-if="icon === 'eye'"
cx="12"
cy="12"
r="3"
stroke="currentColor"
stroke-width="2"
/>
<!-- Eye Off Icon -->
<path
v-if="icon === 'eye-off'"
d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"
stroke="currentColor"
stroke-width="2"
/>
<line
v-if="icon === 'eye-off'"
x1="1"
y1="1"
x2="23"
y2="23"
stroke="currentColor"
stroke-width="2"
/>
<!-- Wallet Icon -->
<path
v-if="icon === 'wallet'"
d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
stroke="currentColor"
stroke-width="2"
/>
<path
v-if="icon === 'wallet'"
d="M9 12L11 14L15 10"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<!-- Shield Icon -->
<path
v-if="icon === 'shield'"
d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"
stroke="currentColor"
stroke-width="2"
/>
<path
v-if="icon === 'shield'"
d="M9 12l2 2 4-4"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<!-- Lock Icon -->
<rect
v-if="icon === 'lock'"
x="3"
y="11"
width="18"
height="11"
rx="2"
ry="2"
stroke="currentColor"
stroke-width="2"
/>
<path
v-if="icon === 'lock'"
d="M7 11V7a5 5 0 0 1 10 0v4"
stroke="currentColor"
stroke-width="2"
/>
<!-- Key Icon -->
<path
v-if="icon === 'key'"
d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<!-- Plus Icon -->
<line
v-if="icon === 'plus'"
x1="12"
y1="5"
x2="12"
y2="19"
stroke="currentColor"
stroke-width="2"
/>
<line
v-if="icon === 'plus'"
x1="5"
y1="12"
x2="19"
y2="12"
stroke="currentColor"
stroke-width="2"
/>
<!-- Check Icon -->
<path
v-if="icon === 'check'"
d="M20 6L9 17l-5-5"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<!-- X Icon -->
<line
v-if="icon === 'x'"
x1="18"
y1="6"
x2="6"
y2="18"
stroke="currentColor"
stroke-width="2"
/>
<line
v-if="icon === 'x'"
x1="6"
y1="6"
x2="18"
y2="18"
stroke="currentColor"
stroke-width="2"
/>
<!-- Arrow Right Icon -->
<path
v-if="icon === 'arrow-right'"
d="M5 12h14m-7-7l7 7-7 7"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<!-- Copy Icon -->
<rect
v-if="icon === 'copy'"
x="9"
y="9"
width="13"
height="13"
rx="2"
ry="2"
stroke="currentColor"
stroke-width="2"
/>
<path
v-if="icon === 'copy'"
d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
stroke="currentColor"
stroke-width="2"
/>
</svg>
<component :is="IconComponent" :style="{ fontSize: size, color }" :class="props.class" />
</template>

View File

@ -4,10 +4,6 @@ import FormCommon from './common/FormCommon.vue'
import OnboardingComponent from './auth/OnboardingComponent.vue'
import OpenWalletComponent from './auth/OpenWalletComponent.vue'
import CreateWalletComponent from './auth/CreateWalletComponent.vue'
import RecoverySeedComponent from './auth/RecoverySeedComponent.vue'
import ConfirmSeedComponent from './auth/ConfirmSeedComponent.vue'
import ImportWalletComponent from './auth/ImportWalletComponent.vue'
import KeystoreDownloadComponent from './auth/KeystoreDownloadComponent.vue'
import { IconCommon } from './icon'
export {
@ -17,9 +13,5 @@ export {
OnboardingComponent,
OpenWalletComponent,
CreateWalletComponent,
RecoverySeedComponent,
ConfirmSeedComponent,
IconCommon,
ImportWalletComponent,
KeystoreDownloadComponent,
}

View File

@ -10,3 +10,7 @@ export const CURRENT_YEAR = dayjs(new Date()).format('YYYY')
export const MONTHS = Array.from({ length: 12 }, (item, i) => {
return dayjs(new Date(0, i)).format('MM')
})
export const FORMAT_DAY = (day: any, format = 'YYYY-MM-DD') => {
return dayjs(new Date(day)).format(format)
}

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

@ -0,0 +1,3 @@
export * from './constants/code'
export * from './constants/constants'
export * from './constants/localStorage'

View File

@ -1,20 +0,0 @@
import type { ButtonType, ButtonSize } from 'ant-design-vue/es/button'
// Button Component Props
export interface ButtonProps {
type?: ButtonType
size?: ButtonSize
block?: boolean
disabled?: boolean
loading?: boolean
htmlType?: 'button' | 'submit' | 'reset'
}
// Icon Component Props
export interface IconProps {
name?: string
size?: number | string
color?: string
class?: string
icon: string
}

View File

@ -1,19 +0,0 @@
// Network Status Interface
export interface NetworkStatus {
network: string
daaScore: number
dagHeader: number
dagBlocks: number
difficulty: number
medianOffset: string
medianTimeUTC: string
}
// Wallet Tab Props
export interface WalletTabProps {
network: string
}
export interface DebugTabProps {}
export interface TransactionsTabProps {}

View File

@ -1,10 +0,0 @@
export interface IconProps {
icon: string
size?: number | string
color?: string
class?: string
}
export interface HighlightProps {
class?: string
}

View File

@ -1,2 +1,9 @@
export * from './common'
export * from './home'
interface Props {
name?: string
size?: number | string
color?: string
class?: string
}
export interface IconProps extends Props {
icon: string
}

View File

@ -1,5 +1,5 @@
import * as Page from '@/views'
import { getToken } from '@/utils'
import { getToken } from '@/helpers'
const ifAuthenticated = (to: any, from: any, next: any) => {
if (getToken()) {
@ -20,30 +20,24 @@ const ifNotAuthenticated = (to: any, from: any, next: any) => {
export const routes: any = [
{
path: '/',
name: 'index',
component: Page.Home,
redirect: '/home',
children: [
{
path: 'home',
name: 'home',
component: Page.Home,
},
{
path: ':pathMatch(.*)*',
component: Page.NotFound,
name: 'page-not-found',
},
],
},
{
path: '/login',
name: 'login',
component: Page.Auth,
path: '/onboarding',
name: 'onboarding',
component: Page.Login,
beforeEnter: ifNotAuthenticated,
},
{
path: '/recovery-seed',
name: 'recovery-seed',
component: Page.Auth,
beforeEnter: ifNotAuthenticated,
},
{
path: '/confirm-seed',
name: 'confirm-seed',
component: Page.Auth,
beforeEnter: ifNotAuthenticated,
},
{
path: '/:pathMatch(.*)*',
component: Page.NotFound,
name: 'page-not-found',
},
]

View File

@ -1,85 +0,0 @@
import { ref } from 'vue'
// Auth flow states
export type AuthState = 'onboarding' | 'login' | 'create' | 'recovery' | 'confirm' | 'complete'
// Auth store to manage the flow
const currentState = ref<AuthState>('onboarding')
export const useAuthStore = () => {
const getCurrentState = () => currentState.value
const setState = (state: AuthState) => {
currentState.value = state
}
const nextStep = () => {
switch (currentState.value) {
case 'onboarding':
setState('login')
break
case 'login':
// Stay in login, user chooses create or open
break
case 'create':
setState('recovery')
break
case 'recovery':
setState('confirm')
break
case 'confirm':
setState('complete')
break
case 'complete':
// Flow complete
break
}
}
const previousStep = () => {
switch (currentState.value) {
case 'onboarding':
// Can't go back from onboarding
break
case 'login':
setState('onboarding')
break
case 'create':
setState('login')
break
case 'recovery':
setState('create')
break
case 'confirm':
setState('recovery')
break
case 'complete':
setState('confirm')
break
}
}
const goToCreate = () => {
setState('create')
}
const goToLogin = () => {
setState('login')
}
const resetFlow = () => {
setState('onboarding')
localStorage.removeItem('onboarding-completed')
}
return {
currentState: currentState.value,
getCurrentState,
setState,
nextStep,
previousStep,
goToCreate,
goToLogin,
resetFlow,
}
}

View File

@ -1,2 +1 @@
export * from './seedStore'
export * from './authStore'
export {}

View File

@ -1,33 +0,0 @@
import { ref } from 'vue'
const seedWords = ref<string[]>([])
const isSeedGenerated = ref(false)
export const useSeedStore = () => {
const setSeedWords = (words: string[]) => {
seedWords.value = words
isSeedGenerated.value = true
}
const getSeedWords = () => {
return seedWords.value
}
const clearSeedWords = () => {
seedWords.value = []
isSeedGenerated.value = false
}
const hasSeedWords = () => {
return isSeedGenerated.value && seedWords.value.length > 0
}
return {
seedWords: seedWords.value,
isSeedGenerated: isSeedGenerated.value,
setSeedWords,
getSeedWords,
clearSeedWords,
hasSeedWords,
}
}

View File

@ -1,9 +0,0 @@
import dayjs from 'dayjs'
export const formatNumberToLocaleString = (num: number): string => {
return num.toLocaleString('en-US')
}
export const formatDate = (day: any, format = 'YYYY-MM-DD') => {
return dayjs(new Date(day)).format(format)
}

View File

@ -1,133 +0,0 @@
// BIP39 English wordlist (first 100 words for demo)
const BIP39_WORDS = [
'abandon',
'ability',
'able',
'about',
'above',
'absent',
'absorb',
'abstract',
'absurd',
'abuse',
'access',
'accident',
'account',
'accuse',
'achieve',
'acid',
'acoustic',
'acquire',
'across',
'act',
'action',
'actor',
'actress',
'actual',
'adapt',
'add',
'addict',
'address',
'adjust',
'admit',
'adult',
'advance',
'advice',
'aerobic',
'affair',
'afford',
'afraid',
'again',
'age',
'agent',
'agree',
'ahead',
'aim',
'air',
'airport',
'aisle',
'alarm',
'album',
'alcohol',
'alert',
'alien',
'all',
'alley',
'allow',
'almost',
'alone',
'alpha',
'already',
'also',
'alter',
'always',
'amateur',
'amazing',
'among',
'amount',
'amused',
'analyst',
'anchor',
'ancient',
'anger',
'angle',
'angry',
'animal',
'ankle',
'announce',
'annual',
'another',
'answer',
'antenna',
'antique',
'anxiety',
'any',
'apart',
'apology',
'appear',
'apple',
'approve',
'april',
'arch',
'arctic',
'area',
'arena',
'argue',
'arm',
'armed',
'armor',
'army',
'around',
'arrange',
'arrest',
]
/**
* Generate a random seed phrase with 12 words
* In a real application, you would use a proper BIP39 library
*/
export const generateSeedPhrase = (): string[] => {
const words: string[] = []
for (let i = 0; i < 12; i++) {
const randomIndex = Math.floor(Math.random() * BIP39_WORDS.length)
words.push(BIP39_WORDS[randomIndex])
}
return words
}
/**
* Validate if a seed phrase is valid (basic validation)
*/
export const validateSeedPhrase = (words: string[]): boolean => {
if (words.length !== 12) return false
// Check if all words are in the BIP39 wordlist
return words.every((word) => BIP39_WORDS.includes(word.toLowerCase()))
}
export const validateSeedPhrase18 = (words: string[]): boolean => {
if (words.length !== 18) return false
return words.every((word) => BIP39_WORDS.includes(word.toLowerCase()))
}

View File

@ -1,5 +0,0 @@
export * from './constants/code'
export * from './constants/constants'
export * from './helpers/format'
export * from './helpers/localStorage'
export * from './helpers/seedPhrase'

View File

@ -1,89 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
import { OnboardingComponent } from '@/components'
import { useAuthStore } from '@/stores'
import { LoginTab, CreateTab, RecoveryTab, ConfirmTab } from './components'
const authStore = useAuthStore()
const currentState = computed(() => authStore.getCurrentState())
const handleOnboardingComplete = () => {
authStore.nextStep()
}
const handleGoToCreate = () => {
authStore.goToCreate()
}
const handleGoToLogin = () => {
authStore.goToLogin()
}
const handleNext = () => {
authStore.nextStep()
}
const handleBack = () => {
authStore.previousStep()
}
</script>
<template>
<div class="auth-container">
<OnboardingComponent
v-if="currentState === 'onboarding'"
@go-to-create="handleGoToCreate"
@go-to-login="handleGoToLogin"
/>
<LoginTab v-else-if="currentState === 'login'" @go-to-create="handleGoToCreate" />
<CreateTab
v-else-if="currentState === 'create'"
@go-to-login="handleGoToLogin"
@next="handleNext"
/>
<RecoveryTab
v-else-if="currentState === 'recovery'"
@back="handleBack"
@next="handleNext"
/>
<ConfirmTab v-else-if="currentState === 'confirm'" @back="handleBack" @next="handleNext" />
<div v-else-if="currentState === 'complete'" class="complete-state">
<h2>Wallet Setup Complete!</h2>
<p>Your wallet has been successfully created.</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.auth-container {
min-height: 100vh;
background: var(--bg-light);
font-family: var(--font-primary);
}
.complete-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
text-align: center;
padding: var(--spacing-xl);
h2 {
color: var(--success-color);
margin-bottom: var(--spacing-md);
}
p {
color: var(--text-secondary);
font-size: var(--font-lg);
}
}
</style>

View File

@ -0,0 +1,8 @@
<script setup lang="ts">
import { CreateWalletComponent } from '@/components'
</script>
<template>
<div></div>
<CreateWalletComponent />
</template>

View File

@ -1,28 +0,0 @@
<script setup lang="ts">
import { ConfirmSeedComponent } from '@/components'
const emit = defineEmits<{
next: []
back: []
}>()
const handleNext = () => {
emit('next')
}
const handleBack = () => {
emit('back')
}
</script>
<template>
<div class="confirm-tab">
<ConfirmSeedComponent @next="handleNext" @back="handleBack" />
</div>
</template>
<style lang="scss" scoped>
.confirm-tab {
padding: var(--spacing-lg);
}
</style>

View File

@ -1,31 +0,0 @@
<script setup lang="ts">
import { CreateWalletComponent } from '@/components'
const emit = defineEmits<{
goToLogin: []
next: []
}>()
const handleNavigateToOpenWallet = () => {
emit('goToLogin')
}
const handleNavigateToRecoverySeed = () => {
emit('next')
}
</script>
<template>
<div class="create-tab">
<CreateWalletComponent
@navigateToOpenWallet="handleNavigateToOpenWallet"
@navigateToRecoverySeed="handleNavigateToRecoverySeed"
/>
</div>
</template>
<style lang="scss" scoped>
.create-tab {
padding: var(--spacing-lg);
}
</style>

View File

@ -1,33 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ImportWalletComponent, OpenWalletComponent } from '@/components'
const emit = defineEmits<{ goToCreate: [] }>()
const stage = ref<'import' | 'password'>('import')
const importData = ref<any>(null)
const handleImported = (payload: {
type: 'seed' | 'privatekey'
value: string | string[]
passphrase?: string
}) => {
importData.value = payload
stage.value = 'password'
}
const handleNavigateToCreate = () => {
emit('goToCreate')
}
</script>
<template>
<div class="login-tab">
<ImportWalletComponent v-if="stage === 'import'" @import-success="handleImported" />
<OpenWalletComponent v-else @navigateToCreate="handleNavigateToCreate" />
</div>
</template>
<style lang="scss" scoped>
.login-tab {
padding: var(--spacing-lg);
}
</style>

View File

@ -1,28 +0,0 @@
<script setup lang="ts">
import { RecoverySeedComponent } from '@/components'
const emit = defineEmits<{
next: []
back: []
}>()
const handleNext = () => {
emit('next')
}
const handleBack = () => {
emit('back')
}
</script>
<template>
<div class="recovery-tab">
<RecoverySeedComponent @next="handleNext" @back="handleBack" />
</div>
</template>
<style lang="scss" scoped>
.recovery-tab {
padding: var(--spacing-lg);
}
</style>

View File

@ -1,4 +0,0 @@
export { default as LoginTab } from './LoginTab.vue'
export { default as CreateTab } from './CreateTab.vue'
export { default as RecoveryTab } from './RecoveryTab.vue'
export { default as ConfirmTab } from './ConfirmTab.vue'

View File

@ -1,89 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Tabs, Row, Col } from 'ant-design-vue'
import WalletInfo from '@/components/WalletInfo.vue'
import { TransactionsTab, WalletTab, NetworkTab, DebugTab } from './components'
const activeTab = ref('WALLET')
const network = ref('kaspa-mainnet')
</script>
<script setup lang="ts"></script>
<template>
<div class="home-container">
<Row :gutter="[24, 24]">
<!-- Left Column --->
<Col :xs="24" :lg="10">
<WalletInfo />
</Col>
<!-- Right Column - Tabs Content -->
<Col :xs="24" :lg="12">
<Tabs v-model:activeKey="activeTab" size="large" class="main-tabs">
<!-- TRANSACTIONS TAB -->
<Tabs.TabPane key="TRANSACTIONS" tab="TRANSACTIONS">
<TransactionsTab />
</Tabs.TabPane>
<!-- WALLET TAB -->
<Tabs.TabPane key="WALLET" tab="WALLET">
<WalletTab :network="network" />
</Tabs.TabPane>
<!-- NETWORK TAB -->
<Tabs.TabPane key="NETWORK" tab="NETWORK">
<NetworkTab />
</Tabs.TabPane>
<!-- DEBUG TAB -->
<Tabs.TabPane key="DEBUG" tab="DEBUG">
<DebugTab />
</Tabs.TabPane>
</Tabs>
</Col>
</Row>
</div>
<div class="box">Home page</div>
</template>
<style lang="scss" scoped>
.home-container {
min-height: 100vh;
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
padding: var(--spacing-lg);
font-family: var(--font-primary);
@media (min-width: 768px) {
padding: var(--spacing-2xl);
}
}
:deep(.main-tabs) {
.ant-tabs-nav {
margin-bottom: var(--spacing-lg);
}
.ant-tabs-tab {
font-size: 14px;
font-weight: var(--font-semibold);
letter-spacing: var(--tracking-wide);
padding: 10px 16px;
@media (max-width: 768px) {
font-size: 12px;
padding: 8px 12px;
}
}
.ant-tabs-ink-bar {
background: var(--primary-color);
height: var(--tabs-height);
}
.ant-tabs-tab-active .ant-tabs-tab-btn {
color: var(--primary-color);
}
.ant-tabs-content {
padding-top: var(--spacing-lg);
}
}
</style>
<style lang="scss" scoped></style>

View File

@ -1,96 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { EditOutlined } from '@ant-design/icons-vue'
import ButtonCommon from '@/components/common/ButtonCommon.vue'
const inUseUtxosCount = ref(0)
const inUseUtxosAmount = ref(0)
const handleShowUTXOs = () => {
console.log('Show UTXOs')
}
const handleForceTransactionUpdate = () => {
console.log('Force transaction times update')
}
const handleScanMoreAddresses = () => {
console.log('Scan More Addresses')
}
</script>
<template>
<div class="content-card debug-card">
<div class="debug-header">
<h3 class="debug-title">
IN USE UTXOS
<EditOutlined style="margin-left: 8px; font-size: 16px" />
</h3>
<div class="debug-info">
<p><strong>COUNT</strong> {{ inUseUtxosCount }}</p>
<p><strong>AMOUNT</strong> {{ inUseUtxosAmount }} KAS</p>
</div>
</div>
<div class="debug-actions">
<ButtonCommon type="primary" size="large" block @click="handleShowUTXOs">
Show UTXOs
</ButtonCommon>
<ButtonCommon type="primary" size="large" block @click="handleForceTransactionUpdate">
Force transaction times update
</ButtonCommon>
<ButtonCommon type="primary" size="large" block @click="handleScanMoreAddresses">
Scan More Addresses
</ButtonCommon>
</div>
</div>
</template>
<style lang="scss" scoped>
.content-card {
@include card-base;
}
.debug-card {
.debug-header {
text-align: center;
margin-bottom: var(--spacing-2xl);
padding-bottom: var(--spacing-xl);
border-bottom: 2px solid var(--border-color);
.debug-title {
font-size: var(--font-2xl);
font-weight: var(--font-bold);
color: var(--text-primary);
margin-bottom: var(--spacing-lg);
letter-spacing: var(--tracking-wide);
display: flex;
align-items: center;
justify-content: center;
}
.debug-info {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
p {
margin: 0;
font-size: var(--font-lg);
color: var(--text-secondary);
strong {
font-weight: var(--font-semibold);
margin-right: var(--spacing-sm);
}
}
}
}
.debug-actions {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
}
</style>

View File

@ -1,325 +0,0 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { formatNumberToLocaleString } from '@/utils'
import type { NetworkStatus } from '@/interface'
const networkStatus = ref<NetworkStatus>({
network: 'kaspa-mainnet',
daaScore: 0,
dagHeader: 0,
dagBlocks: 0,
difficulty: 0,
medianOffset: '00:00:00',
medianTimeUTC: '',
})
const loading = ref(true)
const error = ref('')
const isConnected = ref(false)
// Kaspa RPC Client instance (placeholder)
// You'll need to implement this using kaspa-wasm or kaspa RPC library
let rpcClient: any = null
let unsubscribe: (() => void) | null = null
// Initialize Kaspa RPC connection
const initializeKaspaRPC = async () => {
try {
loading.value = true
error.value = ''
await simulateRPCConnection()
isConnected.value = true
loading.value = false
} catch (err) {
error.value = 'Failed to connect to Kaspa network'
loading.value = false
isConnected.value = false
useMockData()
}
}
// Simulate RPC connection (mock for development)
const simulateRPCConnection = async (): Promise<void> => {
return new Promise((resolve) => {
setTimeout(() => {
// Initial data
networkStatus.value = {
network: 'kaspa-mainnet',
daaScore: 256315320,
dagHeader: 1437265,
dagBlocks: 1437265,
difficulty: 33048964118340300.0,
medianOffset: '00:00:00',
medianTimeUTC: new Date().toISOString().replace('T', ' ').substring(0, 19),
}
// Simulate DAA score increment (real Kaspa ~1 block/sec)
const mockSubscription = setInterval(() => {
networkStatus.value.daaScore += 1
networkStatus.value.dagHeader += 1
networkStatus.value.dagBlocks += 1
updateMedianTime()
}, 1000)
// Store cleanup function
unsubscribe = () => clearInterval(mockSubscription)
resolve()
}, 1000)
})
}
// Update median time
const updateMedianTime = () => {
networkStatus.value.medianTimeUTC = new Date().toISOString().replace('T', ' ').substring(0, 19)
}
// Use mock data fallback
const useMockData = () => {
networkStatus.value = {
network: 'kaspa-mainnet',
daaScore: 256315320,
dagHeader: 1437265,
dagBlocks: 1437265,
difficulty: 33048964118340300.0,
medianOffset: '00:00:00',
medianTimeUTC: new Date().toISOString().replace('T', ' ').substring(0, 19),
}
}
// Retry connection
const retryConnection = () => {
initializeKaspaRPC()
}
// Cleanup on unmount
const cleanup = () => {
if (unsubscribe) {
unsubscribe()
unsubscribe = null
}
if (rpcClient) {
// TODO: Disconnect RPC client
rpcClient.disconnect()
rpcClient = null
}
isConnected.value = false
}
onMounted(() => {
initializeKaspaRPC()
})
onUnmounted(() => {
cleanup()
})
</script>
<template>
<div class="content-card">
<div class="network-status-container">
<h2 class="section-title">NETWORK STATUS</h2>
<!-- Loading State -->
<div v-if="loading && networkStatus.daaScore === 0" class="loading-state">
<div class="spinner"></div>
<p>Loading network data...</p>
</div>
<!-- Error State -->
<div v-else-if="error" class="error-state">
<p>{{ error }}</p>
<button @click="retryConnection" class="retry-button">Retry Connection</button>
</div>
<!-- Data Display -->
<div v-else class="status-grid">
<div class="status-item">
<span class="status-label">Network</span>
<span class="status-value">{{ networkStatus.network }}</span>
</div>
<div class="status-item">
<span class="status-label">DAA Score</span>
<span class="status-value">{{
formatNumberToLocaleString(networkStatus.daaScore)
}}</span>
</div>
<div class="status-item">
<span class="status-label">DAG Header</span>
<span class="status-value">{{
formatNumberToLocaleString(networkStatus.dagHeader)
}}</span>
</div>
<div class="status-item">
<span class="status-label">DAG Blocks</span>
<span class="status-value">{{
formatNumberToLocaleString(networkStatus.dagBlocks)
}}</span>
</div>
<div class="status-item">
<span class="status-label">Difficulty</span>
<span class="status-value">{{
formatNumberToLocaleString(networkStatus.difficulty)
}}</span>
</div>
<div class="status-item">
<span class="status-label">Median Offset</span>
<span class="status-value">{{ networkStatus.medianOffset }}</span>
</div>
<div class="status-item">
<span class="status-label">Median Time UTC</span>
<span class="status-value">{{ networkStatus.medianTimeUTC }}</span>
</div>
<!-- Last Update Indicator -->
<div class="update-indicator">
<span class="update-dot" :class="{ connected: isConnected }"></span>
<span class="update-text">
{{ isConnected ? 'Connected - Live updates' : 'Connecting...' }}
</span>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.content-card {
@include card-base;
}
.network-status-container {
.section-title {
font-size: var(--font-xl);
font-weight: var(--font-bold);
color: var(--text-primary);
margin-bottom: var(--spacing-2xl);
letter-spacing: var(--tracking-wider);
text-align: center;
padding-bottom: var(--spacing-lg);
border-bottom: 3px solid var(--primary-color);
}
.status-grid {
display: flex;
flex-direction: column;
gap: var(--spacing-xl);
}
.status-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-lg) var(--spacing-xl);
background: var(--bg-light);
border-radius: var(--radius-md);
transition: var(--transition-all);
&:hover {
background: var(--bg-hover);
transform: translateX(5px);
}
.status-label {
font-weight: var(--font-semibold);
color: var(--text-secondary);
font-size: var(--font-md);
}
.status-value {
font-weight: var(--font-semibold);
color: var(--text-primary);
font-size: var(--font-lg);
text-align: right;
font-family: var(--font-mono);
}
}
// Loading State
.loading-state {
text-align: center;
padding: var(--spacing-4xl);
color: var(--text-secondary);
.spinner {
width: 40px;
height: 40px;
margin: 0 auto var(--spacing-lg);
border: 4px solid var(--border-light);
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
p {
margin: 0;
font-size: var(--font-md);
}
}
// Error State
.error-state {
text-align: center;
padding: var(--spacing-4xl);
color: var(--error-color);
p {
margin-bottom: var(--spacing-lg);
font-size: var(--font-md);
}
.retry-button {
padding: var(--spacing-sm) var(--spacing-lg);
background: var(--primary-color);
color: var(--text-light);
border: none;
border-radius: var(--radius-md);
cursor: pointer;
font-size: var(--font-sm);
transition: var(--transition-all);
&:hover {
background: var(--primary-hover);
}
}
}
// Update Indicator
.update-indicator {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
margin-top: var(--spacing-2xl);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--border-color);
.update-dot {
width: 8px;
height: 8px;
background: var(--text-muted);
border-radius: 50%;
transition: var(--transition-all);
&.connected {
background: var(--success-color);
animation: pulse-dot 2s infinite;
}
}
.update-text {
font-size: var(--font-xs);
color: var(--text-muted);
}
}
}
</style>

View File

@ -1,41 +0,0 @@
<script setup lang="ts">
// Component for Transactions Tab
</script>
<template>
<div class="content-card">
<div class="tab-content-header">
<h2>Transaction History</h2>
</div>
<div class="empty-state">
<p>No transactions yet</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.content-card {
@include card-base;
}
.tab-content-header {
text-align: center;
margin-bottom: var(--spacing-2xl);
padding-bottom: var(--spacing-lg);
border-bottom: 2px solid var(--border-color);
h2 {
font-size: 1.3rem;
font-weight: var(--font-bold);
color: var(--text-primary);
margin: 0;
}
}
.empty-state {
text-align: center;
padding: var(--spacing-4xl) var(--spacing-lg);
color: var(--text-muted);
font-size: var(--font-lg);
}
</style>

View File

@ -1,117 +0,0 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Divider } from 'ant-design-vue'
import ButtonCommon from '@/components/common/ButtonCommon.vue'
import type { WalletTabProps } from '@/interface'
const props = defineProps<WalletTabProps>()
const walletVersion = ref('1.1.38')
const walletStatus = ref('Online')
const networkName = computed(() => props.network.replace('-mainnet', ''))
const handleBackupFile = () => {
console.log('Backup File')
}
const handleBackupSeed = () => {
console.log('Backup Seed')
}
const handleRecoverFromSeed = () => {
console.log('Recover From Seed')
}
</script>
<template>
<div class="content-card wallet-info-card">
<div class="wallet-header">
<h2 class="wallet-title">KASPA WALLET</h2>
<p class="wallet-version">Version {{ walletVersion }}</p>
<p class="wallet-status-text">
Status: <strong>{{ walletStatus }}</strong>
</p>
<p class="wallet-network">
Network: <strong>{{ networkName }}</strong>
</p>
</div>
<div class="wallet-actions">
<ButtonCommon type="primary" size="large" block @click="handleBackupFile">
Backup File
</ButtonCommon>
<ButtonCommon type="primary" size="large" block @click="handleBackupSeed">
Backup Seed
</ButtonCommon>
<ButtonCommon type="primary" size="large" block @click="handleRecoverFromSeed">
Recover From Seed
</ButtonCommon>
</div>
<Divider />
</div>
</template>
<style lang="scss" scoped>
.content-card {
@include card-base;
}
.wallet-info-card {
.wallet-header {
text-align: center;
margin-bottom: var(--spacing-2xl);
padding-bottom: var(--spacing-xl);
border-bottom: 2px solid var(--border-color);
.wallet-title {
font-size: var(--font-3xl);
font-weight: var(--font-bold);
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
letter-spacing: var(--tracking-wider);
}
.wallet-version {
font-size: var(--font-lg);
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
}
.wallet-status-text,
.wallet-network {
font-size: var(--font-md);
color: var(--text-secondary);
margin-bottom: var(--spacing-xs);
strong {
color: var(--text-primary);
}
}
}
.wallet-actions {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.donations-section,
.developer-section {
.section-subtitle {
font-size: var(--font-lg);
font-weight: var(--font-semibold);
color: var(--text-primary);
margin: 0;
cursor: pointer;
transition: var(--transition-normal);
&:hover {
color: var(--primary-color);
}
}
}
}
</style>

View File

@ -1,4 +0,0 @@
export { default as TransactionsTab } from './TransactionsTab.vue'
export { default as WalletTab } from './WalletTab.vue'
export { default as NetworkTab } from './NetworkTab.vue'
export { default as DebugTab } from './DebugTab.vue'

View File

@ -1,3 +1,3 @@
export const Home = () => import('@/views/Home/HomeView.vue')
export const NotFound = () => import('@/views/NotFound/NotFoundView.vue')
export const Auth = () => import('@/views/Auth/AuthView.vue')
export const Login = () => import('@/views/Auth/LoginView.vue')

View File

@ -1,27 +1,24 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import VueDevTools from 'vite-plugin-vue-devtools'
import tailwindcss from '@tailwindcss/vite'
// https://vitejs.dev/config/
export default defineConfig({
server: {
port: 3008,
},
plugins: [vue(), vueJsx(), VueDevTools()],
css: {
preprocessorOptions: {
scss: {
additionalData: `
@import "@/assets/scss/__variables.scss";
@import "@/assets/scss/__mixin.scss";
`,
},
},
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 3008,
},
plugins: [
vue(),
vueJsx(),
VueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})