uwu-wallet / script.js
k4gor's picture
Create a mobile web application using only three files: index.html , style.css, script.js .
12c8e6c verified
// Application State
const state = {
currentTab: 'transfer',
balance: 664.85,
carouselIndex: 0,
isCarouselPaused: false
};
// DOM Elements
const infoBlock = document.getElementById('infoBlock');
const navButtons = document.querySelectorAll('.nav-btn');
const carouselTrack = document.getElementById('carouselTrack');
const carouselIndicators = document.getElementById('carouselIndicators');
const walletAddress = document.getElementById('walletAddress');
const copyAddressBtn = document.getElementById('copyAddressBtn');
const addressFeedback = document.getElementById('addressFeedback');
// Tab Content Templates
const tabContent = {
transfer: `
<form class="transfer-form" id="transferForm">
<div class="form-group">
<label class="form-label">Recipient Address</label>
<input type="text" class="form-input" id="recipientInput" placeholder="Enter wallet address" autocomplete="off">
</div>
<div class="form-group">
<label class="form-label">Amount</label>
<input type="number" class="form-input" id="amountInput" placeholder="0.00 ₽" step="0.01" min="0.01">
</div>
<button type="submit" class="btn-primary">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="12" x2="2" y2="12"></line>
<polyline points="9 5 2 12 9 19"></polyline>
</svg>
Send Transfer
</button>
</form>
`,
store: `
<div class="store-section">
<div class="section-title">Purchased Subscriptions</div>
<div class="subscription-card purchased">
<div class="card-header">
<div class="level-badge lvl1">LVL1</div>
<div class="card-info">
<div class="card-title">Starter Pack</div>
<div class="card-desc">Basic features access</div>
<div class="card-status">✓ Active until 15 Jan 2025</div>
</div>
</div>
<button class="btn-renew" onclick="handleRenew('LVL1', 250)">
Renew for 250₽
</button>
</div>
</div>
<div class="store-section">
<div class="section-title">Available Subscriptions</div>
<div class="subscription-card">
<div class="card-header">
<div class="level-badge lvl2">LVL2</div>
<div class="card-info">
<div class="card-title">Pro Access</div>
<div class="card-desc">Premium features + priority</div>
</div>
</div>
<button class="btn-buy lvl2" onclick="handleSubscribe('LVL2', 500)">
Buy for 500₽
</button>
</div>
<div class="subscription-card">
<div class="card-header">
<div class="level-badge lvl3">LVL3</div>
<div class="card-info">
<div class="card-title">Elite Member</div>
<div class="card-desc">All features + exclusive perks</div>
</div>
</div>
<button class="btn-buy lvl3" onclick="handleSubscribe('LVL3', 1000)">
Buy for 1000₽
</button>
</div>
<div class="subscription-card">
<div class="card-header">
<div class="level-badge lvl4">LVL4</div>
<div class="card-info">
<div class="card-title">Legend Status</div>
<div class="card-desc">Ultimate access + lifetime perks</div>
</div>
</div>
<button class="btn-buy lvl4" onclick="handleSubscribe('LVL4', 2500)">
Buy for 2500₽
</button>
</div>
</div>
`,
history: `
<div class="history-list">
<div class="history-item">
<div class="history-left">
<div class="history-icon icon-in">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
</svg>
</div>
<div class="history-info">
<div class="history-type">Deposit</div>
<div class="history-date">Today, 14:32</div>
</div>
</div>
<div class="history-amount positive">+500.00₽</div>
</div>
<div class="history-item">
<div class="history-left">
<div class="history-icon icon-sub">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path>
</svg>
</div>
<div class="history-info">
<div class="history-type">Subscription</div>
<div class="history-date">Yesterday, 09:15</div>
</div>
</div>
<div class="history-amount negative">-250.00₽</div>
</div>
<div class="history-item">
<div class="history-left">
<div class="history-icon icon-out">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<polyline points="19 12 12 19 5 12"></polyline>
</svg>
</div>
<div class="history-info">
<div class="history-type">Transfer Out</div>
<div class="history-date">Dec 24, 18:45</div>
</div>
</div>
<div class="history-amount negative">-150.00₽</div>
</div>
<div class="history-item">
<div class="history-left">
<div class="history-icon icon-task">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 11 12 14 22 4"></polyline>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
</svg>
</div>
<div class="history-info">
<div class="history-type">Task Reward</div>
<div class="history-date">Dec 22, 11:20</div>
</div>
</div>
<div class="history-amount positive">+100.00₽</div>
</div>
</div>
`,
tasks: `
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">12</div>
<div class="stat-label">Invited</div>
</div>
<div class="stat-card">
<div class="stat-value">89</div>
<div class="stat-label">Likes</div>
</div>
<div class="stat-card">
<div class="stat-value">34</div>
<div class="stat-label">Comments</div>
</div>
<div class="stat-card">
<div class="stat-value">7</div>
<div class="stat-label">Reposts</div>
</div>
</div>
<div class="action-section">
<button class="action-btn" id="createLinkBtn" onclick="generateLink()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
Create Referral Link
</button>
<div class="copy-field" id="linkField" onclick="copyToClipboard('vk.com/uwu.chan', this)">
<span class="copy-field-value">vk.com/uwu.chan</span>
<svg class="copy-field-icon" 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>
<div class="action-section">
<button class="action-btn" id="genCodeBtn" onclick="generateCode()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
<rect x="9" y="9" width="6" height="6"></rect>
<line x1="9" y1="1" x2="9" y2="4"></line>
<line x1="15" y1="1" x2="15" y2="4"></line>
<line x1="9" y1="20" x2="9" y2="23"></line>
<line x1="15" y1="20" x2="15" y2="23"></line>
<line x1="20" y1="9" x2="23" y2="9"></line>
<line x1="20" y1="14" x2="23" y2="14"></line>
<line x1="1" y1="9" x2="4" y2="9"></line>
<line x1="1" y1="14" x2="4" y2="14"></line>
</svg>
Generate Promo Code
</button>
<div class="copy-field" id="codeField" onclick="copyToClipboard('YUfs1if421Hf', this)">
<span class="copy-field-value">YUfs1if421Hf</span>
<svg class="copy-field-icon" 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>
<div class="section-title">Available Tasks</div>
<div class="tasks-list">
<a href="https://vk.com/uwu.chan" target="_blank" class="task-card" onclick="completeTask('UwU Chan', 100)">
<div class="task-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path>
</svg>
</div>
<div class="task-info">
<div class="task-title">Subscribe to UwU Chan</div>
<div class="task-reward">+100₽ reward</div>
</div>
<svg class="task-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</a>
<a href="https://vk.com/shy.chan" target="_blank" class="task-card" onclick="completeTask('Shy Chan', 100)">
<div class="task-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
<line x1="9" y1="9" x2="9.01" y2="9"></line>
<line x1="15" y1="9" x2="15.01" y2="9"></line>
</svg>
</div>
<div class="task-info">
<div class="task-title">Subscribe to Shy Chan</div>
<div class="task-reward">+100₽ reward</div>
</div>
<svg class="task-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</a>
<a href="https://vk.com/esco.chan" target="_blank" class="task-card" onclick="completeTask('Esco Chan', 100)">
<div class="task-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>
</div>
<div class="task-info">
<div class="task-title">Subscribe to Esco Chan</div>
<div class="task-reward">+100₽ reward</div>
</div>
<svg class="task-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</a>
</div>
<div class="section-divider"></div>
`
};
// Initialize Application
function init() {
renderTab('transfer');
initCarousel();
initNavigation();
initAddressCopy();
}
// Render Tab Content
function renderTab(tabName) {
state.currentTab = tabName;
infoBlock.innerHTML = tabContent[tabName];
// Re-attach event listeners for dynamic content
if (tabName === 'transfer') {
initTransferForm();
}
}
// Navigation
function initNavigation() {
navButtons.forEach(btn => {
btn.addEventListener('click', () => {
const tab = btn.dataset.tab;
// Update active states
navButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Render new tab with animation
infoBlock.style.opacity = '0';
infoBlock.style.transform = 'translateY(10px)';
infoBlock.style.transition = 'all 0.2s ease';
setTimeout(() => {
renderTab(tab);
infoBlock.style.opacity = '1';
infoBlock.style.transform = 'translateY(0)';
}, 150);
});
});
}
// Carousel Functionality
function initCarousel() {
const slides = carouselTrack.children;
const slideCount = slides.length;
// Create indicators
carouselIndicators.innerHTML = '';
for (let i = 0; i < slideCount; i++) {
const indicator = document.createElement('div');
indicator.className = `indicator ${i === 0 ? 'active' : ''}`;
indicator.addEventListener('click', () => goToSlide(i));
carouselIndicators.appendChild(indicator);
}
// Touch handling
let startX = 0;
let currentX = 0;
let isDragging = false;
carouselTrack.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
isDragging = true;
state.isCarouselPaused = true;
}, { passive: true });
carouselTrack.addEventListener('touchmove', (e) => {
if (!isDragging) return;
currentX = e.touches[0].clientX;
const diff = startX - currentX;
const offset = -state.carouselIndex * 100 - (diff / carouselTrack.offsetWidth) * 100;
carouselTrack.style.transition = 'none';
carouselTrack.style.transform = `translateX(${offset}%)`;
}, { passive: true });
carouselTrack.addEventListener('touchend', (e) => {
if (!isDragging) return;
isDragging = false;
state.isCarouselPaused = false;
const diff = startX - currentX;
const threshold = carouselTrack.offsetWidth * 0.25;
carouselTrack.style.transition = '';
if (Math.abs(diff) > threshold) {
if (diff > 0 && state.carouselIndex < slideCount - 1) {
goToSlide(state.carouselIndex + 1);
} else if (diff < 0 && state.carouselIndex > 0) {
goToSlide(state.carouselIndex - 1);
} else {
goToSlide(state.carouselIndex);
}
} else {
goToSlide(state.carouselIndex);
}
});
// Auto-advance
setInterval(() => {
if (!state.isCarouselPaused) {
const nextIndex = (state.carouselIndex + 1) % slideCount;
goToSlide(nextIndex);
}
}, 4000);
}
function goToSlide(index) {
state.carouselIndex = index;
carouselTrack.style.transform = `translateX(-${index * 100}%)`;
const indicators = carouselIndicators.children;
for (let i = 0; i < indicators.length; i++) {
indicators[i].classList.toggle('active', i === index);
}
}
// Address Copy
function initAddressCopy() {
const fullAddress = 'UQARxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxbn5l';
copyAddressBtn.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(fullAddress);
showFeedback(addressFeedback);
} catch (err) {
// Fallback
const textarea = document.createElement('textarea');
textarea.value = fullAddress;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
showFeedback(addressFeedback);
}
});
}
function showFeedback(element) {
element.classList.add('show');
setTimeout(() => {
element.classList.remove('show');
}, 1500);
}
// Transfer Form
function initTransferForm() {
const form = document.getElementById('transferForm');
const recipientInput = document.getElementById('recipientInput');
const amountInput = document.getElementById('amountInput');
// Limit input to balance
amountInput.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
if (value > state.balance) {
e.target.value = state.balance.toFixed(2);
}
});
form.addEventListener('submit', (e) => {
e.preventDefault();
const recipient = recipientInput.value.trim();
const amount = parseFloat(amountInput.value);
if (!recipient || isNaN(amount) || amount <= 0) {
showToast('Please fill all fields', 'error');
return;
}
if (amount > state.balance) {
showToast('Insufficient balance', 'error');
return;
}
// Simulate transfer
const btn = form.querySelector('.btn-primary');
btn.disabled = true;
btn.innerHTML = '<span class="loading">Processing...</span>';
setTimeout(() => {
state.balance -= amount;
document.querySelector('.balance-main').textContent = state.balance.toFixed(2) + '₽';
showToast(`Sent ${amount.toFixed(2)}₽`, 'success');
renderTab('transfer');
}, 1500);
});
}
// Store Actions
function handleSubscribe(level, price) {
if (price > state.balance) {
showToast('Insufficient balance', 'error');
return;
}
showToast(`Subscribed to ${level}!`, 'success');
state.balance -= price;
document.querySelector('.balance-main').textContent = state.balance.toFixed(2) + '₽';
}
function handleRenew(level, price) {
if (price > state.balance) {
showToast('Insufficient balance', 'error');
return;
}
showToast(`${level} renewed!`, 'success');
state.balance -= price;
document.querySelector('.balance-main').textContent = state.balance.toFixed(2) + '₽';
}
// Task Actions
function generateLink() {
const btn = document.getElementById('createLinkBtn');
const field = document.getElementById('linkField');
btn.style.display = 'none';
field.classList.add('show');
setTimeout(() => copyToClipboard('vk.com/uwu.chan', field), 100);
}
function generateCode() {
const btn = document.getElementById('genCodeBtn');
const field = document.getElementById('codeField');
btn.style.display = 'none';
field.classList.add('show');
setTimeout(() => copyToClipboard('YUfs1if421Hf', field), 100);
}
async function copyToClipboard(text, element) {
try {
await navigator.clipboard.writeText(text);
const originalBg = element.style.background;
element.style.background = 'var(--success)';
element.style.color = 'white';
setTimeout(() => {
element.style.background = originalBg;
element.style.color = '';
}, 300);
showToast('Copied!', 'success');
} catch (err) {
showToast('Copied!', 'success');
}
}
function completeTask(channel, reward) {
setTimeout(() => {
showToast(`+${reward}₽ for ${channel}`, 'success');
state.balance += reward;
document.querySelector('.balance-main').textContent = state.balance.toFixed(2) + '₽';
}, 500);
}
// Toast Notification
function showToast(message, type = 'success') {
let toast = document.getElementById('appToast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'appToast';
toast.className = 'toast';
document.body.appendChild(toast);
}
toast.textContent = message;
toast.className = `toast ${type} show`;
setTimeout(() => {
toast.classList.remove('show');
}, 2000);
}
// Start the app
document.addEventListener('DOMContentLoaded', init);
// Prevent bounce on iOS
document.addEventListener('touchmove', (e) => {
if (e.target.closest('.info-block')) return;
e.preventDefault();
}, { passive: false });