Spaces:
Running
Running
| // 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 }); |