Spaces:
Sleeping
Sleeping
Update index.html
Browse files- index.html +335 -429
index.html
CHANGED
|
@@ -345,13 +345,13 @@
|
|
| 345 |
<div class="price-ticker glass" id="priceTicker">
|
| 346 |
<div class="ticker-item">
|
| 347 |
<span class="ticker-symbol">BTC</span>
|
| 348 |
-
<span class="ticker-price" id="btcPrice">$
|
| 349 |
-
<span class="ticker-change up" id="btcChange">+
|
| 350 |
</div>
|
| 351 |
<div class="ticker-item">
|
| 352 |
<span class="ticker-symbol">ETH</span>
|
| 353 |
-
<span class="ticker-price" id="ethPrice">$
|
| 354 |
-
<span class="ticker-change up" id="ethChange">+
|
| 355 |
</div>
|
| 356 |
</div>
|
| 357 |
|
|
@@ -525,10 +525,146 @@
|
|
| 525 |
<div class="toast-container" id="toastContainer"></div>
|
| 526 |
|
| 527 |
<script>
|
| 528 |
-
// ====================
|
| 529 |
-
const
|
| 530 |
-
|
| 531 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
|
| 533 |
// ==================== i18n ====================
|
| 534 |
const i18n = {
|
|
@@ -576,13 +712,11 @@
|
|
| 576 |
}
|
| 577 |
};
|
| 578 |
|
| 579 |
-
let currentLang = '
|
| 580 |
let currentUser = null;
|
| 581 |
-
let sessionToken = null;
|
| 582 |
let currentSort = 'hot';
|
| 583 |
let currentCategory = '';
|
| 584 |
let selectedMarket = null;
|
| 585 |
-
let categories = {};
|
| 586 |
|
| 587 |
function t(key) { return i18n[currentLang][key] || i18n.en[key] || key; }
|
| 588 |
|
|
@@ -599,178 +733,30 @@
|
|
| 599 |
});
|
| 600 |
}
|
| 601 |
|
| 602 |
-
// ====================
|
| 603 |
-
function
|
| 604 |
-
|
| 605 |
-
currentUser = user;
|
| 606 |
-
localStorage.setItem(SESSION_KEY, token);
|
| 607 |
-
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
| 608 |
-
}
|
| 609 |
-
|
| 610 |
-
function loadSession() {
|
| 611 |
-
sessionToken = localStorage.getItem(SESSION_KEY);
|
| 612 |
-
const userStr = localStorage.getItem(USER_KEY);
|
| 613 |
-
if (userStr) {
|
| 614 |
-
try {
|
| 615 |
-
currentUser = JSON.parse(userStr);
|
| 616 |
-
} catch (e) {
|
| 617 |
-
currentUser = null;
|
| 618 |
-
}
|
| 619 |
-
}
|
| 620 |
-
return sessionToken && currentUser;
|
| 621 |
-
}
|
| 622 |
-
|
| 623 |
-
function clearSession() {
|
| 624 |
-
sessionToken = null;
|
| 625 |
-
currentUser = null;
|
| 626 |
-
localStorage.removeItem(SESSION_KEY);
|
| 627 |
-
localStorage.removeItem(USER_KEY);
|
| 628 |
-
}
|
| 629 |
-
|
| 630 |
-
// ==================== API ====================
|
| 631 |
-
async function fetchAPI(endpoint, options = {}) {
|
| 632 |
-
const headers = {
|
| 633 |
-
'Content-Type': 'application/json',
|
| 634 |
-
...options.headers
|
| 635 |
-
};
|
| 636 |
-
|
| 637 |
-
// Add authentication headers
|
| 638 |
-
if (sessionToken) {
|
| 639 |
-
headers['Authorization'] = `Bearer ${sessionToken}`;
|
| 640 |
-
}
|
| 641 |
-
if (currentUser?.id) {
|
| 642 |
-
headers['X-User-ID'] = currentUser.id.toString();
|
| 643 |
-
}
|
| 644 |
-
|
| 645 |
-
try {
|
| 646 |
-
const response = await fetch(`${API_BASE}${endpoint}`, {
|
| 647 |
-
...options,
|
| 648 |
-
headers
|
| 649 |
-
});
|
| 650 |
-
|
| 651 |
-
if (response.status === 401) {
|
| 652 |
-
// Session expired or invalid
|
| 653 |
-
clearSession();
|
| 654 |
-
updateUserCard();
|
| 655 |
-
showToast(t('loginRequired'), 'error');
|
| 656 |
-
return null;
|
| 657 |
-
}
|
| 658 |
-
|
| 659 |
-
if (!response.ok) {
|
| 660 |
-
const error = await response.json();
|
| 661 |
-
throw new Error(error.detail || 'API Error');
|
| 662 |
-
}
|
| 663 |
-
|
| 664 |
-
return await response.json();
|
| 665 |
-
} catch (error) {
|
| 666 |
-
console.error('API Error:', error);
|
| 667 |
-
throw error;
|
| 668 |
-
}
|
| 669 |
-
}
|
| 670 |
-
|
| 671 |
-
// ==================== Authentication ====================
|
| 672 |
-
function handleLoginClick() {
|
| 673 |
-
if (currentUser) {
|
| 674 |
-
// Toggle dropdown
|
| 675 |
-
const menu = document.getElementById('userDropdownMenu');
|
| 676 |
-
menu.classList.toggle('active');
|
| 677 |
-
} else {
|
| 678 |
-
// Show login modal
|
| 679 |
-
document.getElementById('loginModal').classList.add('active');
|
| 680 |
-
}
|
| 681 |
-
}
|
| 682 |
-
|
| 683 |
-
function closeLoginModal() {
|
| 684 |
-
document.getElementById('loginModal').classList.remove('active');
|
| 685 |
-
}
|
| 686 |
-
|
| 687 |
-
async function loginWithHuggingFace() {
|
| 688 |
-
// HuggingFace OAuth flow
|
| 689 |
-
// In a real HF Space, this would use the built-in OAuth
|
| 690 |
-
// For now, we'll show a message
|
| 691 |
-
showToast('HuggingFace OAuth requires deployment on HF Spaces', 'error');
|
| 692 |
-
|
| 693 |
-
// If running on HF Spaces, uncomment this:
|
| 694 |
-
// window.location.href = '/login/huggingface';
|
| 695 |
-
}
|
| 696 |
-
|
| 697 |
-
async function demoLogin(userId) {
|
| 698 |
-
showToast(t('loggingIn'), 'success');
|
| 699 |
-
|
| 700 |
-
try {
|
| 701 |
-
const data = await fetchAPI(`/auth/demo-login?user_id=${userId}`, {
|
| 702 |
-
method: 'POST'
|
| 703 |
-
});
|
| 704 |
-
|
| 705 |
-
if (data?.success) {
|
| 706 |
-
saveSession(data.session_token, data.user);
|
| 707 |
-
updateUserCard();
|
| 708 |
-
closeLoginModal();
|
| 709 |
-
showToast(`${t('welcome')}, ${data.user.username}!`, 'success');
|
| 710 |
-
loadNotificationCount();
|
| 711 |
-
}
|
| 712 |
-
} catch (error) {
|
| 713 |
-
showToast('Login failed: ' + error.message, 'error');
|
| 714 |
-
}
|
| 715 |
-
}
|
| 716 |
-
|
| 717 |
-
async function handleLogout() {
|
| 718 |
-
try {
|
| 719 |
-
await fetchAPI('/auth/logout', { method: 'POST' });
|
| 720 |
-
} catch (e) {
|
| 721 |
-
// Ignore errors on logout
|
| 722 |
-
}
|
| 723 |
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
showToast(t('loggedOut'), 'success');
|
| 728 |
-
}
|
| 729 |
-
|
| 730 |
-
async function checkAuthStatus() {
|
| 731 |
-
if (!sessionToken) return false;
|
| 732 |
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
}
|
| 740 |
-
} catch (e) {
|
| 741 |
-
// Session invalid
|
| 742 |
}
|
|
|
|
| 743 |
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
async function loadCategories() {
|
| 750 |
-
try {
|
| 751 |
-
const data = await fetchAPI('/categories');
|
| 752 |
-
categories = data.categories;
|
| 753 |
-
const categoryList = document.getElementById('categoryList');
|
| 754 |
-
|
| 755 |
-
let html = `<div class="category-item active" data-category="" onclick="selectCategory('')">
|
| 756 |
-
<span class="category-icon">🌐</span><span class="category-name">${t('all')}</span>
|
| 757 |
-
</div>`;
|
| 758 |
-
|
| 759 |
-
for (const [key, cat] of Object.entries(data.categories)) {
|
| 760 |
-
const name = currentLang === 'kr' ? cat.name_kr : cat.name;
|
| 761 |
-
html += `<div class="category-item" data-category="${key}" onclick="selectCategory('${key}')">
|
| 762 |
-
<span class="category-icon">${cat.icon}</span><span class="category-name">${name}</span>
|
| 763 |
-
</div>`;
|
| 764 |
-
}
|
| 765 |
-
categoryList.innerHTML = html;
|
| 766 |
-
|
| 767 |
-
// Populate create market category select
|
| 768 |
-
const categorySelect = document.getElementById('marketCategory');
|
| 769 |
-
categorySelect.innerHTML = Object.entries(data.categories).map(([key, cat]) =>
|
| 770 |
`<option value="${key}">${cat.icon} ${currentLang === 'kr' ? cat.name_kr : cat.name}</option>`
|
| 771 |
).join('');
|
| 772 |
-
|
| 773 |
-
} catch (error) { console.error('Failed to load categories:', error); }
|
| 774 |
}
|
| 775 |
|
| 776 |
function selectCategory(category) {
|
|
@@ -782,14 +768,30 @@
|
|
| 782 |
}
|
| 783 |
|
| 784 |
// ==================== Markets ====================
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 793 |
}
|
| 794 |
|
| 795 |
function renderMarkets(markets) {
|
|
@@ -805,18 +807,14 @@
|
|
| 805 |
grid.innerHTML = markets.map(market => {
|
| 806 |
const statusClass = market.status === 'resolved'
|
| 807 |
? (market.resolved_outcome === 'yes' ? 'resolved-yes' : 'resolved-no')
|
| 808 |
-
:
|
| 809 |
-
|
| 810 |
-
const statusText = market.status === 'resolved'
|
| 811 |
-
? `✅ ${market.resolved_outcome.toUpperCase()}`
|
| 812 |
-
: market.time_remaining;
|
| 813 |
|
| 814 |
const catName = currentLang === 'kr' ? market.category_info?.name_kr : market.category_info?.name;
|
| 815 |
|
| 816 |
return `<div class="market-card glass ${market.status === 'resolved' ? 'resolved' : ''}" onclick="openMarketModal(${market.id})">
|
| 817 |
<div class="market-header">
|
| 818 |
<div class="market-category"><span>${market.category_info?.icon || '📊'}</span><span>${catName || market.category}</span></div>
|
| 819 |
-
<span class="market-status ${statusClass}">${
|
| 820 |
</div>
|
| 821 |
<h3 class="market-title">${market.title}</h3>
|
| 822 |
<div class="prob-bar-container">
|
|
@@ -834,54 +832,37 @@
|
|
| 834 |
}
|
| 835 |
|
| 836 |
// ==================== Rankings ====================
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
|
|
|
|
|
|
| 841 |
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
<div class="ranking-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
<div class="ranking-value">${user.gen_balance.toLocaleString()} GEN</div>
|
| 852 |
-
</div>
|
| 853 |
-
</div>`;
|
| 854 |
-
}).join('');
|
| 855 |
-
} catch (error) { console.error('Failed to load rankings:', error); }
|
| 856 |
-
}
|
| 857 |
-
|
| 858 |
-
// ==================== Prices ====================
|
| 859 |
-
async function loadPrices() {
|
| 860 |
-
try {
|
| 861 |
-
const data = await fetchAPI('/prices');
|
| 862 |
-
if (data.prices.BTC?.price) {
|
| 863 |
-
document.getElementById('btcPrice').textContent = `$${data.prices.BTC.price.toLocaleString(undefined, {maximumFractionDigits: 0})}`;
|
| 864 |
-
const btcChange = document.getElementById('btcChange');
|
| 865 |
-
btcChange.textContent = `${data.prices.BTC.change_24h >= 0 ? '+' : ''}${data.prices.BTC.change_24h}%`;
|
| 866 |
-
btcChange.className = `ticker-change ${data.prices.BTC.change_24h >= 0 ? 'up' : 'down'}`;
|
| 867 |
-
}
|
| 868 |
-
if (data.prices.ETH?.price) {
|
| 869 |
-
document.getElementById('ethPrice').textContent = `$${data.prices.ETH.price.toLocaleString(undefined, {maximumFractionDigits: 0})}`;
|
| 870 |
-
const ethChange = document.getElementById('ethChange');
|
| 871 |
-
ethChange.textContent = `${data.prices.ETH.change_24h >= 0 ? '+' : ''}${data.prices.ETH.change_24h}%`;
|
| 872 |
-
ethChange.className = `ticker-change ${data.prices.ETH.change_24h >= 0 ? 'up' : 'down'}`;
|
| 873 |
-
}
|
| 874 |
-
} catch (error) { console.error('Failed to load prices:', error); }
|
| 875 |
}
|
| 876 |
|
| 877 |
-
// ==================== Market Modal ====================
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 885 |
}
|
| 886 |
|
| 887 |
function renderMarketModal(market) {
|
|
@@ -903,6 +884,25 @@
|
|
| 903 |
</div>`;
|
| 904 |
}
|
| 905 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 906 |
content.innerHTML = `
|
| 907 |
<button class="modal-close" onclick="closeModal()"><i class="fas fa-times"></i></button>
|
| 908 |
<div class="market-category" style="display: inline-flex; margin-bottom: 1rem;">
|
|
@@ -920,12 +920,6 @@
|
|
| 920 |
|
| 921 |
${aiSection}
|
| 922 |
|
| 923 |
-
<div style="text-align: center; margin: 1rem 0;">
|
| 924 |
-
<button class="btn btn-ai" onclick="getAIPrediction(${market.id})" id="aiPredictBtn">
|
| 925 |
-
<i class="fas fa-robot"></i> ${t('getAIPrediction')}
|
| 926 |
-
</button>
|
| 927 |
-
</div>
|
| 928 |
-
|
| 929 |
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin: 1.5rem 0;">
|
| 930 |
<div style="text-align: center; padding: 1rem; background: rgba(255,255,255,0.05); border-radius: 12px;">
|
| 931 |
<div style="font-family: 'JetBrains Mono'; font-size: 1.25rem; color: var(--accent-cyan);">${market.total_volume.toLocaleString()}</div>
|
|
@@ -937,7 +931,7 @@
|
|
| 937 |
</div>
|
| 938 |
<div style="text-align: center; padding: 1rem; background: rgba(255,255,255,0.05); border-radius: 12px;">
|
| 939 |
<div style="font-size: 1rem; color: ${market.status === 'resolved' ? 'var(--success)' : 'var(--text-primary)'};">
|
| 940 |
-
${market.
|
| 941 |
</div>
|
| 942 |
<div style="font-size: 0.8rem; color: var(--text-muted);">${t('status')}</div>
|
| 943 |
</div>
|
|
@@ -985,18 +979,7 @@
|
|
| 985 |
</div>
|
| 986 |
` : ''}
|
| 987 |
<div class="comment-list">
|
| 988 |
-
${
|
| 989 |
-
<div class="comment-item">
|
| 990 |
-
<img class="comment-avatar" src="${comment.avatar_url || 'https://api.dicebear.com/7.x/avataaars/svg?seed=' + comment.username}" onerror="this.src='https://api.dicebear.com/7.x/avataaars/svg?seed=default'">
|
| 991 |
-
<div class="comment-content">
|
| 992 |
-
<div class="comment-header">
|
| 993 |
-
<span class="comment-author">@${comment.username}</span>
|
| 994 |
-
<span class="comment-time">${formatTime(comment.created_at)}</span>
|
| 995 |
-
</div>
|
| 996 |
-
<p class="comment-text">${escapeHtml(comment.content)}</p>
|
| 997 |
-
</div>
|
| 998 |
-
</div>
|
| 999 |
-
`).join('') || `<p style="color: var(--text-muted); text-align: center; padding: 1rem;">${t('noComments')}</p>`}
|
| 1000 |
</div>
|
| 1001 |
</div>
|
| 1002 |
`;
|
|
@@ -1009,64 +992,38 @@
|
|
| 1009 |
|
| 1010 |
function setBetAmount(amount) { document.getElementById('betAmount').value = amount; }
|
| 1011 |
|
| 1012 |
-
|
| 1013 |
if (!currentUser) { showToast(t('loginRequired'), 'error'); return; }
|
| 1014 |
const amount = parseInt(document.getElementById('betAmount').value);
|
| 1015 |
if (isNaN(amount) || amount < 10) { showToast('Min bet: 10 GEN', 'error'); return; }
|
| 1016 |
if (amount > currentUser.gen_balance) { showToast(t('insufficientBalance'), 'error'); return; }
|
| 1017 |
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
});
|
| 1023 |
-
|
| 1024 |
-
if (data) {
|
| 1025 |
-
currentUser = data.user;
|
| 1026 |
-
localStorage.setItem(USER_KEY, JSON.stringify(currentUser));
|
| 1027 |
-
updateUserCard();
|
| 1028 |
-
showToast(data.message, 'success');
|
| 1029 |
-
closeModal();
|
| 1030 |
-
loadMarkets();
|
| 1031 |
-
}
|
| 1032 |
-
} catch (error) { showToast(error.message, 'error'); }
|
| 1033 |
-
}
|
| 1034 |
-
|
| 1035 |
-
async function getAIPrediction(marketId) {
|
| 1036 |
-
const btn = document.getElementById('aiPredictBtn');
|
| 1037 |
-
btn.innerHTML = `<span class="loading-spinner"></span> ${t('analyzing')}`;
|
| 1038 |
-
btn.disabled = true;
|
| 1039 |
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
// Refresh modal with new prediction
|
| 1044 |
-
const marketData = await fetchAPI(`/markets/${marketId}`);
|
| 1045 |
-
selectedMarket = marketData.market;
|
| 1046 |
-
renderMarketModal(marketData.market);
|
| 1047 |
-
|
| 1048 |
-
showToast('AI prediction updated!', 'success');
|
| 1049 |
-
} catch (error) {
|
| 1050 |
-
showToast('AI prediction failed: ' + error.message, 'error');
|
| 1051 |
-
btn.innerHTML = `<i class="fas fa-robot"></i> ${t('getAIPrediction')}`;
|
| 1052 |
-
btn.disabled = false;
|
| 1053 |
-
}
|
| 1054 |
}
|
| 1055 |
|
| 1056 |
-
|
| 1057 |
if (!currentUser || !selectedMarket) return;
|
| 1058 |
const input = document.getElementById('commentInput');
|
| 1059 |
const content = input.value.trim();
|
| 1060 |
if (!content) return;
|
| 1061 |
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
|
|
|
|
|
|
| 1070 |
}
|
| 1071 |
|
| 1072 |
// ==================== Create Market ====================
|
|
@@ -1085,7 +1042,7 @@
|
|
| 1085 |
document.getElementById('createMarketModal').classList.remove('active');
|
| 1086 |
}
|
| 1087 |
|
| 1088 |
-
|
| 1089 |
if (!currentUser) { showToast(t('loginRequired'), 'error'); return; }
|
| 1090 |
|
| 1091 |
const title = document.getElementById('marketTitle').value.trim();
|
|
@@ -1098,88 +1055,69 @@
|
|
| 1098 |
if (!endDate) { showToast('End date is required', 'error'); return; }
|
| 1099 |
if (currentUser.gen_balance < 100) { showToast('Need 100 GEN to create market', 'error'); return; }
|
| 1100 |
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
|
| 1113 |
-
|
| 1114 |
-
|
| 1115 |
-
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
|
| 1122 |
-
|
| 1123 |
-
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
} catch (error) { showToast(error.message, 'error'); }
|
| 1128 |
}
|
| 1129 |
|
| 1130 |
-
// ====================
|
| 1131 |
-
|
| 1132 |
-
if (
|
| 1133 |
-
|
| 1134 |
-
|
| 1135 |
-
|
| 1136 |
-
|
| 1137 |
-
|
| 1138 |
-
if (count > 0) {
|
| 1139 |
-
badge.textContent = count > 99 ? '99+' : count;
|
| 1140 |
-
badge.style.display = 'flex';
|
| 1141 |
-
} else {
|
| 1142 |
-
badge.style.display = 'none';
|
| 1143 |
-
}
|
| 1144 |
-
}
|
| 1145 |
-
} catch (error) { console.error('Failed to load notifications:', error); }
|
| 1146 |
}
|
| 1147 |
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
try {
|
| 1152 |
-
const data = await fetchAPI(`/users/${currentUser.id}/notifications`);
|
| 1153 |
-
if (!data) return;
|
| 1154 |
-
|
| 1155 |
-
const list = document.getElementById('notificationsList');
|
| 1156 |
-
|
| 1157 |
-
if (!data.notifications || data.notifications.length === 0) {
|
| 1158 |
-
list.innerHTML = `<p style="color: var(--text-muted); text-align: center; padding: 2rem;">${t('noNotifications')}</p>`;
|
| 1159 |
-
} else {
|
| 1160 |
-
list.innerHTML = data.notifications.map(notif => {
|
| 1161 |
-
const bgColor = notif.is_read ? 'transparent' : 'rgba(0, 245, 212, 0.1)';
|
| 1162 |
-
return `<div style="padding: 1rem; background: ${bgColor}; border-radius: 12px; margin-bottom: 0.75rem; border-left: 3px solid ${notif.is_read ? 'var(--glass-border)' : 'var(--accent-cyan)'};">
|
| 1163 |
-
<div style="font-weight: 600; margin-bottom: 0.25rem;">${notif.title}</div>
|
| 1164 |
-
<div style="color: var(--text-secondary); font-size: 0.9rem;">${notif.message || ''}</div>
|
| 1165 |
-
<div style="color: var(--text-muted); font-size: 0.75rem; margin-top: 0.5rem;">${formatTime(notif.created_at)}</div>
|
| 1166 |
-
</div>`;
|
| 1167 |
-
}).join('');
|
| 1168 |
-
}
|
| 1169 |
-
|
| 1170 |
-
document.getElementById('notificationsModal').classList.add('active');
|
| 1171 |
-
|
| 1172 |
-
// Mark as read
|
| 1173 |
-
await fetchAPI(`/users/${currentUser.id}/notifications/read`, { method: 'POST' });
|
| 1174 |
-
loadNotificationCount();
|
| 1175 |
-
} catch (error) { showToast('Failed to load notifications', 'error'); }
|
| 1176 |
}
|
| 1177 |
|
| 1178 |
-
function
|
| 1179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1180 |
}
|
| 1181 |
|
| 1182 |
-
// ==================== User ====================
|
| 1183 |
function updateUserCard() {
|
| 1184 |
const card = document.getElementById('userCard');
|
| 1185 |
const loginBtn = document.getElementById('loginBtn');
|
|
@@ -1192,26 +1130,13 @@
|
|
| 1192 |
</div>`;
|
| 1193 |
|
| 1194 |
loginBtn.innerHTML = `<i class="fas fa-user"></i> <span data-i18n="login">${t('login')}</span>`;
|
| 1195 |
-
loginBtn.onclick = handleLoginClick;
|
| 1196 |
return;
|
| 1197 |
}
|
| 1198 |
|
| 1199 |
const winRate = currentUser.wins + currentUser.losses > 0 ? Math.round(currentUser.wins * 100 / (currentUser.wins + currentUser.losses)) : 0;
|
| 1200 |
|
| 1201 |
-
// Badges section
|
| 1202 |
-
let badgesHtml = '';
|
| 1203 |
-
if (currentUser.badges && currentUser.badges.length > 0) {
|
| 1204 |
-
badgesHtml = `
|
| 1205 |
-
<div class="badges-section">
|
| 1206 |
-
<div class="badges-title">${t('badges')}</div>
|
| 1207 |
-
<div class="badges-list">
|
| 1208 |
-
${currentUser.badges.map(b => `<span class="badge-item" title="${b.description}">${b.icon}</span>`).join('')}
|
| 1209 |
-
</div>
|
| 1210 |
-
</div>`;
|
| 1211 |
-
}
|
| 1212 |
-
|
| 1213 |
card.innerHTML = `
|
| 1214 |
-
<img class="user-avatar" src="
|
| 1215 |
<div class="user-name">@${currentUser.username}</div>
|
| 1216 |
<div class="user-balance">
|
| 1217 |
<span class="balance-icon">💎</span>
|
|
@@ -1222,42 +1147,39 @@
|
|
| 1222 |
<div class="user-stat"><div class="user-stat-value" style="color: var(--success);">${currentUser.wins}</div><div class="user-stat-label">${t('wins')}</div></div>
|
| 1223 |
<div class="user-stat"><div class="user-stat-value">${winRate}%</div><div class="user-stat-label">${t('winRate')}</div></div>
|
| 1224 |
</div>
|
| 1225 |
-
${badgesHtml}
|
| 1226 |
`;
|
| 1227 |
|
| 1228 |
loginBtn.innerHTML = `
|
| 1229 |
-
<img src="
|
| 1230 |
${currentUser.username}
|
| 1231 |
`;
|
| 1232 |
-
|
| 1233 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1234 |
}
|
| 1235 |
|
| 1236 |
function showProfile() {
|
| 1237 |
document.getElementById('userDropdownMenu').classList.remove('active');
|
| 1238 |
-
// Could open a profile modal here
|
| 1239 |
showToast('Profile feature coming soon!', 'success');
|
| 1240 |
}
|
| 1241 |
|
| 1242 |
function showMyBets() {
|
| 1243 |
document.getElementById('userDropdownMenu').classList.remove('active');
|
| 1244 |
-
// Could open a bets modal here
|
| 1245 |
showToast('My Bets feature coming soon!', 'success');
|
| 1246 |
}
|
| 1247 |
|
| 1248 |
-
|
| 1249 |
if (!currentUser) { showToast(t('loginRequired'), 'error'); return; }
|
| 1250 |
-
|
| 1251 |
-
|
| 1252 |
-
|
| 1253 |
-
|
| 1254 |
-
|
| 1255 |
-
|
| 1256 |
-
updateUserCard();
|
| 1257 |
-
}
|
| 1258 |
-
showToast(data.message, data.success ? 'success' : 'error');
|
| 1259 |
-
}
|
| 1260 |
-
} catch (error) { showToast(error.message, 'error'); }
|
| 1261 |
}
|
| 1262 |
|
| 1263 |
// ==================== Utility ====================
|
|
@@ -1270,20 +1192,9 @@
|
|
| 1270 |
setTimeout(() => { toast.style.animation = 'slideIn 0.3s ease reverse'; setTimeout(() => toast.remove(), 300); }, 3000);
|
| 1271 |
}
|
| 1272 |
|
| 1273 |
-
function formatTime(dateStr) {
|
| 1274 |
-
if (!dateStr) return '';
|
| 1275 |
-
const date = new Date(dateStr);
|
| 1276 |
-
const now = new Date();
|
| 1277 |
-
const diff = now - date;
|
| 1278 |
-
if (diff < 60000) return currentLang === 'kr' ? '방금 전' : 'just now';
|
| 1279 |
-
if (diff < 3600000) return currentLang === 'kr' ? `${Math.floor(diff / 60000)}분 전` : `${Math.floor(diff / 60000)}m ago`;
|
| 1280 |
-
if (diff < 86400000) return currentLang === 'kr' ? `${Math.floor(diff / 3600000)}시간 전` : `${Math.floor(diff / 3600000)}h ago`;
|
| 1281 |
-
return currentLang === 'kr' ? `${Math.floor(diff / 86400000)}일 전` : `${Math.floor(diff / 86400000)}d ago`;
|
| 1282 |
-
}
|
| 1283 |
-
|
| 1284 |
function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }
|
| 1285 |
|
| 1286 |
-
function refreshMarkets() { loadMarkets(); loadRankings();
|
| 1287 |
|
| 1288 |
// ==================== Events ====================
|
| 1289 |
document.querySelectorAll('.sort-tab').forEach(tab => {
|
|
@@ -1305,7 +1216,6 @@
|
|
| 1305 |
}
|
| 1306 |
});
|
| 1307 |
|
| 1308 |
-
// Close dropdown when clicking outside
|
| 1309 |
document.addEventListener('click', (e) => {
|
| 1310 |
const dropdown = document.getElementById('userDropdown');
|
| 1311 |
const menu = document.getElementById('userDropdownMenu');
|
|
@@ -1320,16 +1230,12 @@
|
|
| 1320 |
document.getElementById('loginModal').addEventListener('click', (e) => { if (e.target.id === 'loginModal') closeLoginModal(); });
|
| 1321 |
|
| 1322 |
// ==================== Init ====================
|
| 1323 |
-
|
| 1324 |
-
|
| 1325 |
-
|
| 1326 |
-
|
| 1327 |
-
await checkAuthStatus();
|
| 1328 |
-
}
|
| 1329 |
-
|
| 1330 |
updateUserCard();
|
| 1331 |
-
|
| 1332 |
-
setInterval(loadPrices, 30000);
|
| 1333 |
}
|
| 1334 |
|
| 1335 |
init();
|
|
|
|
| 345 |
<div class="price-ticker glass" id="priceTicker">
|
| 346 |
<div class="ticker-item">
|
| 347 |
<span class="ticker-symbol">BTC</span>
|
| 348 |
+
<span class="ticker-price" id="btcPrice">$98,500</span>
|
| 349 |
+
<span class="ticker-change up" id="btcChange">+2.3%</span>
|
| 350 |
</div>
|
| 351 |
<div class="ticker-item">
|
| 352 |
<span class="ticker-symbol">ETH</span>
|
| 353 |
+
<span class="ticker-price" id="ethPrice">$3,450</span>
|
| 354 |
+
<span class="ticker-change up" id="ethChange">+1.8%</span>
|
| 355 |
</div>
|
| 356 |
</div>
|
| 357 |
|
|
|
|
| 525 |
<div class="toast-container" id="toastContainer"></div>
|
| 526 |
|
| 527 |
<script>
|
| 528 |
+
// ==================== Demo Data ====================
|
| 529 |
+
const DEMO_MARKETS = [
|
| 530 |
+
{
|
| 531 |
+
id: 1,
|
| 532 |
+
title: "Bitcoin이 2025년 3월까지 $150K를 돌파할 것인가?",
|
| 533 |
+
description: "BTC가 Q1 2025에 $150,000를 돌파할지 예측하세요.",
|
| 534 |
+
category: "crypto",
|
| 535 |
+
category_info: { icon: "💰", name: "Crypto", name_kr: "암호화폐" },
|
| 536 |
+
yes_pool: 25000, no_pool: 18000,
|
| 537 |
+
yes_pct: 58, no_pct: 42,
|
| 538 |
+
yes_odds: 1.72, no_odds: 2.39,
|
| 539 |
+
total_volume: 43000, participant_count: 86,
|
| 540 |
+
status: "active", time_remaining: "85일 남음",
|
| 541 |
+
resolution_source: "CoinGecko BTC/USD 가격",
|
| 542 |
+
ai_prediction_yes: 0.62,
|
| 543 |
+
ai_prediction_reason: "기관 투자 증가와 ETF 승인으로 상승 모멘텀",
|
| 544 |
+
comments: [
|
| 545 |
+
{ username: "CryptoWhale", content: "ETF 승인 이후 상승 추세가 강해질 것 같습니다.", created_at: "2025-01-04 10:30" },
|
| 546 |
+
{ username: "TraderKim", content: "조금 더 지켜봐야 할 것 같아요.", created_at: "2025-01-03 15:20" }
|
| 547 |
+
]
|
| 548 |
+
},
|
| 549 |
+
{
|
| 550 |
+
id: 2,
|
| 551 |
+
title: "Tesla 주가가 2025년 6월까지 $500를 넘길까?",
|
| 552 |
+
description: "테슬라 주식이 $500를 초과할지 예측하세요.",
|
| 553 |
+
category: "stocks",
|
| 554 |
+
category_info: { icon: "📈", name: "Stocks", name_kr: "주식" },
|
| 555 |
+
yes_pool: 12000, no_pool: 15000,
|
| 556 |
+
yes_pct: 44, no_pct: 56,
|
| 557 |
+
yes_odds: 2.25, no_odds: 1.80,
|
| 558 |
+
total_volume: 27000, participant_count: 54,
|
| 559 |
+
status: "active", time_remaining: "176일 남음",
|
| 560 |
+
resolution_source: "NASDAQ TSLA 종가",
|
| 561 |
+
ai_prediction_yes: 0.45,
|
| 562 |
+
ai_prediction_reason: "EV 경쟁 심화로 불확실성 존재",
|
| 563 |
+
comments: []
|
| 564 |
+
},
|
| 565 |
+
{
|
| 566 |
+
id: 3,
|
| 567 |
+
title: "USD/KRW 환율이 2025년 상반기에 1,500원을 넘길까?",
|
| 568 |
+
description: "달러-원 환율 예측",
|
| 569 |
+
category: "forex",
|
| 570 |
+
category_info: { icon: "💱", name: "Forex", name_kr: "외환" },
|
| 571 |
+
yes_pool: 8500, no_pool: 6500,
|
| 572 |
+
yes_pct: 57, no_pct: 43,
|
| 573 |
+
yes_odds: 1.76, no_odds: 2.31,
|
| 574 |
+
total_volume: 15000, participant_count: 30,
|
| 575 |
+
status: "active", time_remaining: "176일 남음",
|
| 576 |
+
resolution_source: "한국은행 기준환율",
|
| 577 |
+
ai_prediction_yes: 0.55,
|
| 578 |
+
ai_prediction_reason: "미국 금리 인하 지연으로 달러 강세 지속 가능",
|
| 579 |
+
comments: []
|
| 580 |
+
},
|
| 581 |
+
{
|
| 582 |
+
id: 4,
|
| 583 |
+
title: "AI 기업이 2025년 말까지 시가총액 1위가 될까?",
|
| 584 |
+
description: "AI 회사가 세계 시가총액 1위가 될지 예측",
|
| 585 |
+
category: "tech",
|
| 586 |
+
category_info: { icon: "⚡", name: "Tech & AI", name_kr: "테크/AI" },
|
| 587 |
+
yes_pool: 18000, no_pool: 12000,
|
| 588 |
+
yes_pct: 60, no_pct: 40,
|
| 589 |
+
yes_odds: 1.67, no_odds: 2.50,
|
| 590 |
+
total_volume: 30000, participant_count: 60,
|
| 591 |
+
status: "active", time_remaining: "360일 남음",
|
| 592 |
+
resolution_source: "Bloomberg 시가총액 순위",
|
| 593 |
+
ai_prediction_yes: 0.68,
|
| 594 |
+
ai_prediction_reason: "NVIDIA의 AI 칩 독점으로 급성장 중",
|
| 595 |
+
comments: []
|
| 596 |
+
},
|
| 597 |
+
{
|
| 598 |
+
id: 5,
|
| 599 |
+
title: "2025년 Fed가 3회 이상 금리를 인하할까?",
|
| 600 |
+
description: "연준의 금리 정책 예측",
|
| 601 |
+
category: "economy",
|
| 602 |
+
category_info: { icon: "🏦", name: "Economy", name_kr: "경제" },
|
| 603 |
+
yes_pool: 22000, no_pool: 15000,
|
| 604 |
+
yes_pct: 59, no_pct: 41,
|
| 605 |
+
yes_odds: 1.68, no_odds: 2.47,
|
| 606 |
+
total_volume: 37000, participant_count: 74,
|
| 607 |
+
status: "active", time_remaining: "360일 남음",
|
| 608 |
+
resolution_source: "Federal Reserve 공식 발표",
|
| 609 |
+
ai_prediction_yes: 0.52,
|
| 610 |
+
ai_prediction_reason: "인플레이션 둔화 속도에 따라 결정될 전망",
|
| 611 |
+
comments: []
|
| 612 |
+
},
|
| 613 |
+
{
|
| 614 |
+
id: 6,
|
| 615 |
+
title: "Bitcoin이 2024년 12월에 $100K를 돌파했다",
|
| 616 |
+
description: "BTC $100K 달성 여부",
|
| 617 |
+
category: "crypto",
|
| 618 |
+
category_info: { icon: "💰", name: "Crypto", name_kr: "암호화폐" },
|
| 619 |
+
yes_pool: 45000, no_pool: 12000,
|
| 620 |
+
yes_pct: 79, no_pct: 21,
|
| 621 |
+
yes_odds: 1.27, no_odds: 4.75,
|
| 622 |
+
total_volume: 57000, participant_count: 114,
|
| 623 |
+
status: "resolved", resolved_outcome: "yes",
|
| 624 |
+
time_remaining: "✅ YES",
|
| 625 |
+
resolution_source: "Official Source",
|
| 626 |
+
ai_prediction_yes: null,
|
| 627 |
+
comments: []
|
| 628 |
+
},
|
| 629 |
+
{
|
| 630 |
+
id: 7,
|
| 631 |
+
title: "Trump가 2024년 대선에서 승리했다",
|
| 632 |
+
description: "2024 미국 대선 결과",
|
| 633 |
+
category: "politics",
|
| 634 |
+
category_info: { icon: "🏛️", name: "Politics", name_kr: "정치" },
|
| 635 |
+
yes_pool: 38000, no_pool: 25000,
|
| 636 |
+
yes_pct: 60, no_pct: 40,
|
| 637 |
+
yes_odds: 1.66, no_odds: 2.52,
|
| 638 |
+
total_volume: 63000, participant_count: 126,
|
| 639 |
+
status: "resolved", resolved_outcome: "yes",
|
| 640 |
+
time_remaining: "✅ YES",
|
| 641 |
+
resolution_source: "Official Source",
|
| 642 |
+
ai_prediction_yes: null,
|
| 643 |
+
comments: []
|
| 644 |
+
}
|
| 645 |
+
];
|
| 646 |
+
|
| 647 |
+
const DEMO_USERS = [
|
| 648 |
+
{ id: 1, username: 'CryptoWhale', gen_balance: 12500, total_bets: 125, wins: 62, losses: 41 },
|
| 649 |
+
{ id: 2, username: 'TraderKim', gen_balance: 8750, total_bets: 87, wins: 43, losses: 29 },
|
| 650 |
+
{ id: 3, username: 'AI_Master', gen_balance: 6200, total_bets: 62, wins: 31, losses: 20 },
|
| 651 |
+
{ id: 4, username: 'ForexPro', gen_balance: 5100, total_bets: 51, wins: 25, losses: 17 },
|
| 652 |
+
{ id: 5, username: 'StockGuru', gen_balance: 4800, total_bets: 48, wins: 24, losses: 16 },
|
| 653 |
+
];
|
| 654 |
+
|
| 655 |
+
const CATEGORIES = {
|
| 656 |
+
all: { icon: "🌐", name: "All", name_kr: "전체" },
|
| 657 |
+
crypto: { icon: "💰", name: "Crypto", name_kr: "암호화폐" },
|
| 658 |
+
stocks: { icon: "📈", name: "Stocks", name_kr: "주식" },
|
| 659 |
+
forex: { icon: "💱", name: "Forex", name_kr: "외환" },
|
| 660 |
+
futures: { icon: "📊", name: "Futures", name_kr: "선물" },
|
| 661 |
+
economy: { icon: "🏦", name: "Economy", name_kr: "경제" },
|
| 662 |
+
tech: { icon: "⚡", name: "Tech & AI", name_kr: "테크/AI" },
|
| 663 |
+
geopolitics: { icon: "🌍", name: "Geopolitics", name_kr: "지정학" },
|
| 664 |
+
politics: { icon: "🏛️", name: "Politics", name_kr: "정치" },
|
| 665 |
+
sports: { icon: "🏆", name: "Sports", name_kr: "스포츠" },
|
| 666 |
+
korea: { icon: "🇰🇷", name: "Korea", name_kr: "한국" }
|
| 667 |
+
};
|
| 668 |
|
| 669 |
// ==================== i18n ====================
|
| 670 |
const i18n = {
|
|
|
|
| 712 |
}
|
| 713 |
};
|
| 714 |
|
| 715 |
+
let currentLang = 'kr';
|
| 716 |
let currentUser = null;
|
|
|
|
| 717 |
let currentSort = 'hot';
|
| 718 |
let currentCategory = '';
|
| 719 |
let selectedMarket = null;
|
|
|
|
| 720 |
|
| 721 |
function t(key) { return i18n[currentLang][key] || i18n.en[key] || key; }
|
| 722 |
|
|
|
|
| 733 |
});
|
| 734 |
}
|
| 735 |
|
| 736 |
+
// ==================== Categories ====================
|
| 737 |
+
function loadCategories() {
|
| 738 |
+
const categoryList = document.getElementById('categoryList');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 739 |
|
| 740 |
+
let html = `<div class="category-item active" data-category="" onclick="selectCategory('')">
|
| 741 |
+
<span class="category-icon">🌐</span><span class="category-name">${t('all')}</span>
|
| 742 |
+
</div>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 743 |
|
| 744 |
+
for (const [key, cat] of Object.entries(CATEGORIES)) {
|
| 745 |
+
if (key === 'all') continue;
|
| 746 |
+
const name = currentLang === 'kr' ? cat.name_kr : cat.name;
|
| 747 |
+
html += `<div class="category-item" data-category="${key}" onclick="selectCategory('${key}')">
|
| 748 |
+
<span class="category-icon">${cat.icon}</span><span class="category-name">${name}</span>
|
| 749 |
+
</div>`;
|
|
|
|
|
|
|
|
|
|
| 750 |
}
|
| 751 |
+
categoryList.innerHTML = html;
|
| 752 |
|
| 753 |
+
// Populate create market category select
|
| 754 |
+
const categorySelect = document.getElementById('marketCategory');
|
| 755 |
+
categorySelect.innerHTML = Object.entries(CATEGORIES)
|
| 756 |
+
.filter(([k]) => k !== 'all')
|
| 757 |
+
.map(([key, cat]) =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
`<option value="${key}">${cat.icon} ${currentLang === 'kr' ? cat.name_kr : cat.name}</option>`
|
| 759 |
).join('');
|
|
|
|
|
|
|
| 760 |
}
|
| 761 |
|
| 762 |
function selectCategory(category) {
|
|
|
|
| 768 |
}
|
| 769 |
|
| 770 |
// ==================== Markets ====================
|
| 771 |
+
function loadMarkets() {
|
| 772 |
+
let markets = [...DEMO_MARKETS];
|
| 773 |
+
|
| 774 |
+
// Filter by category
|
| 775 |
+
if (currentCategory) {
|
| 776 |
+
markets = markets.filter(m => m.category === currentCategory);
|
| 777 |
+
}
|
| 778 |
+
|
| 779 |
+
// Sort
|
| 780 |
+
switch (currentSort) {
|
| 781 |
+
case 'volume':
|
| 782 |
+
markets.sort((a, b) => b.total_volume - a.total_volume);
|
| 783 |
+
break;
|
| 784 |
+
case 'newest':
|
| 785 |
+
markets.sort((a, b) => b.id - a.id);
|
| 786 |
+
break;
|
| 787 |
+
case 'ending':
|
| 788 |
+
markets.sort((a, b) => (a.status === 'active' ? 0 : 1) - (b.status === 'active' ? 0 : 1));
|
| 789 |
+
break;
|
| 790 |
+
default: // hot
|
| 791 |
+
markets.sort((a, b) => b.participant_count - a.participant_count);
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
renderMarkets(markets);
|
| 795 |
}
|
| 796 |
|
| 797 |
function renderMarkets(markets) {
|
|
|
|
| 807 |
grid.innerHTML = markets.map(market => {
|
| 808 |
const statusClass = market.status === 'resolved'
|
| 809 |
? (market.resolved_outcome === 'yes' ? 'resolved-yes' : 'resolved-no')
|
| 810 |
+
: 'active';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 811 |
|
| 812 |
const catName = currentLang === 'kr' ? market.category_info?.name_kr : market.category_info?.name;
|
| 813 |
|
| 814 |
return `<div class="market-card glass ${market.status === 'resolved' ? 'resolved' : ''}" onclick="openMarketModal(${market.id})">
|
| 815 |
<div class="market-header">
|
| 816 |
<div class="market-category"><span>${market.category_info?.icon || '📊'}</span><span>${catName || market.category}</span></div>
|
| 817 |
+
<span class="market-status ${statusClass}">${market.time_remaining}</span>
|
| 818 |
</div>
|
| 819 |
<h3 class="market-title">${market.title}</h3>
|
| 820 |
<div class="prob-bar-container">
|
|
|
|
| 832 |
}
|
| 833 |
|
| 834 |
// ==================== Rankings ====================
|
| 835 |
+
function loadRankings() {
|
| 836 |
+
const container = document.getElementById('genRanking');
|
| 837 |
+
|
| 838 |
+
container.innerHTML = DEMO_USERS.map((user, index) => {
|
| 839 |
+
const posClass = index === 0 ? 'gold' : index === 1 ? 'silver' : index === 2 ? 'bronze' : '';
|
| 840 |
+
const posText = index < 3 ? ['🥇', '🥈', '🥉'][index] : (index + 1);
|
| 841 |
|
| 842 |
+
return `<div class="ranking-item">
|
| 843 |
+
<div class="ranking-position ${posClass}">${posText}</div>
|
| 844 |
+
<img class="ranking-avatar" src="https://api.dicebear.com/7.x/avataaars/svg?seed=${user.username}" onerror="this.src='https://api.dicebear.com/7.x/avataaars/svg?seed=default'">
|
| 845 |
+
<div class="ranking-info">
|
| 846 |
+
<div class="ranking-name">@${user.username}</div>
|
| 847 |
+
<div class="ranking-value">${user.gen_balance.toLocaleString()} GEN</div>
|
| 848 |
+
</div>
|
| 849 |
+
</div>`;
|
| 850 |
+
}).join('');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 851 |
}
|
| 852 |
|
| 853 |
+
// ==================== Market Modal - 핵심 수정 ====================
|
| 854 |
+
function openMarketModal(marketId) {
|
| 855 |
+
// 데모 데이터에서 마켓 찾기
|
| 856 |
+
const market = DEMO_MARKETS.find(m => m.id === marketId);
|
| 857 |
+
|
| 858 |
+
if (!market) {
|
| 859 |
+
showToast(t('marketNotFound'), 'error');
|
| 860 |
+
return;
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
selectedMarket = market;
|
| 864 |
+
renderMarketModal(market);
|
| 865 |
+
document.getElementById('marketModal').classList.add('active');
|
| 866 |
}
|
| 867 |
|
| 868 |
function renderMarketModal(market) {
|
|
|
|
| 884 |
</div>`;
|
| 885 |
}
|
| 886 |
|
| 887 |
+
// Comments HTML
|
| 888 |
+
let commentsHtml = '';
|
| 889 |
+
if (market.comments && market.comments.length > 0) {
|
| 890 |
+
commentsHtml = market.comments.map(comment => `
|
| 891 |
+
<div class="comment-item">
|
| 892 |
+
<img class="comment-avatar" src="https://api.dicebear.com/7.x/avataaars/svg?seed=${comment.username}" onerror="this.src='https://api.dicebear.com/7.x/avataaars/svg?seed=default'">
|
| 893 |
+
<div class="comment-content">
|
| 894 |
+
<div class="comment-header">
|
| 895 |
+
<span class="comment-author">@${comment.username}</span>
|
| 896 |
+
<span class="comment-time">${comment.created_at}</span>
|
| 897 |
+
</div>
|
| 898 |
+
<p class="comment-text">${escapeHtml(comment.content)}</p>
|
| 899 |
+
</div>
|
| 900 |
+
</div>
|
| 901 |
+
`).join('');
|
| 902 |
+
} else {
|
| 903 |
+
commentsHtml = `<p style="color: var(--text-muted); text-align: center; padding: 1rem;">${t('noComments')}</p>`;
|
| 904 |
+
}
|
| 905 |
+
|
| 906 |
content.innerHTML = `
|
| 907 |
<button class="modal-close" onclick="closeModal()"><i class="fas fa-times"></i></button>
|
| 908 |
<div class="market-category" style="display: inline-flex; margin-bottom: 1rem;">
|
|
|
|
| 920 |
|
| 921 |
${aiSection}
|
| 922 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 923 |
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin: 1.5rem 0;">
|
| 924 |
<div style="text-align: center; padding: 1rem; background: rgba(255,255,255,0.05); border-radius: 12px;">
|
| 925 |
<div style="font-family: 'JetBrains Mono'; font-size: 1.25rem; color: var(--accent-cyan);">${market.total_volume.toLocaleString()}</div>
|
|
|
|
| 931 |
</div>
|
| 932 |
<div style="text-align: center; padding: 1rem; background: rgba(255,255,255,0.05); border-radius: 12px;">
|
| 933 |
<div style="font-size: 1rem; color: ${market.status === 'resolved' ? 'var(--success)' : 'var(--text-primary)'};">
|
| 934 |
+
${market.time_remaining || 'Active'}
|
| 935 |
</div>
|
| 936 |
<div style="font-size: 0.8rem; color: var(--text-muted);">${t('status')}</div>
|
| 937 |
</div>
|
|
|
|
| 979 |
</div>
|
| 980 |
` : ''}
|
| 981 |
<div class="comment-list">
|
| 982 |
+
${commentsHtml}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 983 |
</div>
|
| 984 |
</div>
|
| 985 |
`;
|
|
|
|
| 992 |
|
| 993 |
function setBetAmount(amount) { document.getElementById('betAmount').value = amount; }
|
| 994 |
|
| 995 |
+
function placeBet(choice) {
|
| 996 |
if (!currentUser) { showToast(t('loginRequired'), 'error'); return; }
|
| 997 |
const amount = parseInt(document.getElementById('betAmount').value);
|
| 998 |
if (isNaN(amount) || amount < 10) { showToast('Min bet: 10 GEN', 'error'); return; }
|
| 999 |
if (amount > currentUser.gen_balance) { showToast(t('insufficientBalance'), 'error'); return; }
|
| 1000 |
|
| 1001 |
+
// Demo: Deduct and show success
|
| 1002 |
+
currentUser.gen_balance -= amount;
|
| 1003 |
+
const odds = choice === 'yes' ? selectedMarket.yes_odds : selectedMarket.no_odds;
|
| 1004 |
+
const potentialWin = Math.round(amount * odds);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1005 |
|
| 1006 |
+
showToast(`${t('betPlaced')} ${amount} GEN → ${potentialWin} GEN (${odds}x)`, 'success');
|
| 1007 |
+
updateUserCard();
|
| 1008 |
+
closeModal();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1009 |
}
|
| 1010 |
|
| 1011 |
+
function submitComment() {
|
| 1012 |
if (!currentUser || !selectedMarket) return;
|
| 1013 |
const input = document.getElementById('commentInput');
|
| 1014 |
const content = input.value.trim();
|
| 1015 |
if (!content) return;
|
| 1016 |
|
| 1017 |
+
// Add to demo comments
|
| 1018 |
+
if (!selectedMarket.comments) selectedMarket.comments = [];
|
| 1019 |
+
selectedMarket.comments.unshift({
|
| 1020 |
+
username: currentUser.username,
|
| 1021 |
+
content: content,
|
| 1022 |
+
created_at: new Date().toLocaleString()
|
| 1023 |
+
});
|
| 1024 |
+
|
| 1025 |
+
showToast(t('commentPosted'), 'success');
|
| 1026 |
+
renderMarketModal(selectedMarket);
|
| 1027 |
}
|
| 1028 |
|
| 1029 |
// ==================== Create Market ====================
|
|
|
|
| 1042 |
document.getElementById('createMarketModal').classList.remove('active');
|
| 1043 |
}
|
| 1044 |
|
| 1045 |
+
function submitCreateMarket() {
|
| 1046 |
if (!currentUser) { showToast(t('loginRequired'), 'error'); return; }
|
| 1047 |
|
| 1048 |
const title = document.getElementById('marketTitle').value.trim();
|
|
|
|
| 1055 |
if (!endDate) { showToast('End date is required', 'error'); return; }
|
| 1056 |
if (currentUser.gen_balance < 100) { showToast('Need 100 GEN to create market', 'error'); return; }
|
| 1057 |
|
| 1058 |
+
// Demo: Create market
|
| 1059 |
+
currentUser.gen_balance -= 100;
|
| 1060 |
+
|
| 1061 |
+
const newMarket = {
|
| 1062 |
+
id: DEMO_MARKETS.length + 1,
|
| 1063 |
+
title: title,
|
| 1064 |
+
description: description,
|
| 1065 |
+
category: category,
|
| 1066 |
+
category_info: CATEGORIES[category],
|
| 1067 |
+
yes_pool: 0, no_pool: 0,
|
| 1068 |
+
yes_pct: 50, no_pct: 50,
|
| 1069 |
+
yes_odds: 2.0, no_odds: 2.0,
|
| 1070 |
+
total_volume: 0, participant_count: 0,
|
| 1071 |
+
status: "active",
|
| 1072 |
+
time_remaining: "7일 남음",
|
| 1073 |
+
resolution_source: resolution || 'N/A',
|
| 1074 |
+
ai_prediction_yes: null,
|
| 1075 |
+
comments: []
|
| 1076 |
+
};
|
| 1077 |
+
|
| 1078 |
+
DEMO_MARKETS.unshift(newMarket);
|
| 1079 |
+
|
| 1080 |
+
showToast('✅ Market created!', 'success');
|
| 1081 |
+
closeCreateMarketModal();
|
| 1082 |
+
updateUserCard();
|
| 1083 |
+
loadMarkets();
|
|
|
|
| 1084 |
}
|
| 1085 |
|
| 1086 |
+
// ==================== User & Auth ====================
|
| 1087 |
+
function handleLoginClick() {
|
| 1088 |
+
if (currentUser) {
|
| 1089 |
+
const menu = document.getElementById('userDropdownMenu');
|
| 1090 |
+
menu.classList.toggle('active');
|
| 1091 |
+
} else {
|
| 1092 |
+
document.getElementById('loginModal').classList.add('active');
|
| 1093 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1094 |
}
|
| 1095 |
|
| 1096 |
+
function closeLoginModal() {
|
| 1097 |
+
document.getElementById('loginModal').classList.remove('active');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1098 |
}
|
| 1099 |
|
| 1100 |
+
function loginWithHuggingFace() {
|
| 1101 |
+
showToast('HuggingFace OAuth requires deployment on HF Spaces', 'error');
|
| 1102 |
+
}
|
| 1103 |
+
|
| 1104 |
+
function demoLogin(userId) {
|
| 1105 |
+
const user = DEMO_USERS.find(u => u.id === userId);
|
| 1106 |
+
if (user) {
|
| 1107 |
+
currentUser = { ...user };
|
| 1108 |
+
updateUserCard();
|
| 1109 |
+
closeLoginModal();
|
| 1110 |
+
showToast(`${t('welcome')}, ${user.username}!`, 'success');
|
| 1111 |
+
}
|
| 1112 |
+
}
|
| 1113 |
+
|
| 1114 |
+
function handleLogout() {
|
| 1115 |
+
currentUser = null;
|
| 1116 |
+
updateUserCard();
|
| 1117 |
+
document.getElementById('userDropdownMenu').classList.remove('active');
|
| 1118 |
+
showToast(t('loggedOut'), 'success');
|
| 1119 |
}
|
| 1120 |
|
|
|
|
| 1121 |
function updateUserCard() {
|
| 1122 |
const card = document.getElementById('userCard');
|
| 1123 |
const loginBtn = document.getElementById('loginBtn');
|
|
|
|
| 1130 |
</div>`;
|
| 1131 |
|
| 1132 |
loginBtn.innerHTML = `<i class="fas fa-user"></i> <span data-i18n="login">${t('login')}</span>`;
|
|
|
|
| 1133 |
return;
|
| 1134 |
}
|
| 1135 |
|
| 1136 |
const winRate = currentUser.wins + currentUser.losses > 0 ? Math.round(currentUser.wins * 100 / (currentUser.wins + currentUser.losses)) : 0;
|
| 1137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1138 |
card.innerHTML = `
|
| 1139 |
+
<img class="user-avatar" src="https://api.dicebear.com/7.x/avataaars/svg?seed=${currentUser.username}" onerror="this.src='https://api.dicebear.com/7.x/avataaars/svg?seed=default'">
|
| 1140 |
<div class="user-name">@${currentUser.username}</div>
|
| 1141 |
<div class="user-balance">
|
| 1142 |
<span class="balance-icon">💎</span>
|
|
|
|
| 1147 |
<div class="user-stat"><div class="user-stat-value" style="color: var(--success);">${currentUser.wins}</div><div class="user-stat-label">${t('wins')}</div></div>
|
| 1148 |
<div class="user-stat"><div class="user-stat-value">${winRate}%</div><div class="user-stat-label">${t('winRate')}</div></div>
|
| 1149 |
</div>
|
|
|
|
| 1150 |
`;
|
| 1151 |
|
| 1152 |
loginBtn.innerHTML = `
|
| 1153 |
+
<img src="https://api.dicebear.com/7.x/avataaars/svg?seed=${currentUser.username}" style="width: 24px; height: 24px; border-radius: 6px;" onerror="this.src='https://api.dicebear.com/7.x/avataaars/svg?seed=default'">
|
| 1154 |
${currentUser.username}
|
| 1155 |
`;
|
| 1156 |
+
}
|
| 1157 |
+
|
| 1158 |
+
function claimDailyBonus() {
|
| 1159 |
+
if (!currentUser) { showToast(t('loginRequired'), 'error'); return; }
|
| 1160 |
+
currentUser.gen_balance += 50;
|
| 1161 |
+
updateUserCard();
|
| 1162 |
+
showToast(`🎁 ${t('bonusReceived')} +50 GEN`, 'success');
|
| 1163 |
}
|
| 1164 |
|
| 1165 |
function showProfile() {
|
| 1166 |
document.getElementById('userDropdownMenu').classList.remove('active');
|
|
|
|
| 1167 |
showToast('Profile feature coming soon!', 'success');
|
| 1168 |
}
|
| 1169 |
|
| 1170 |
function showMyBets() {
|
| 1171 |
document.getElementById('userDropdownMenu').classList.remove('active');
|
|
|
|
| 1172 |
showToast('My Bets feature coming soon!', 'success');
|
| 1173 |
}
|
| 1174 |
|
| 1175 |
+
function showNotifications() {
|
| 1176 |
if (!currentUser) { showToast(t('loginRequired'), 'error'); return; }
|
| 1177 |
+
document.getElementById('notificationsList').innerHTML = `<p style="color: var(--text-muted); text-align: center; padding: 2rem;">${t('noNotifications')}</p>`;
|
| 1178 |
+
document.getElementById('notificationsModal').classList.add('active');
|
| 1179 |
+
}
|
| 1180 |
+
|
| 1181 |
+
function closeNotificationsModal() {
|
| 1182 |
+
document.getElementById('notificationsModal').classList.remove('active');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1183 |
}
|
| 1184 |
|
| 1185 |
// ==================== Utility ====================
|
|
|
|
| 1192 |
setTimeout(() => { toast.style.animation = 'slideIn 0.3s ease reverse'; setTimeout(() => toast.remove(), 300); }, 3000);
|
| 1193 |
}
|
| 1194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1195 |
function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }
|
| 1196 |
|
| 1197 |
+
function refreshMarkets() { loadMarkets(); loadRankings(); showToast('Refreshed!', 'success'); }
|
| 1198 |
|
| 1199 |
// ==================== Events ====================
|
| 1200 |
document.querySelectorAll('.sort-tab').forEach(tab => {
|
|
|
|
| 1216 |
}
|
| 1217 |
});
|
| 1218 |
|
|
|
|
| 1219 |
document.addEventListener('click', (e) => {
|
| 1220 |
const dropdown = document.getElementById('userDropdown');
|
| 1221 |
const menu = document.getElementById('userDropdownMenu');
|
|
|
|
| 1230 |
document.getElementById('loginModal').addEventListener('click', (e) => { if (e.target.id === 'loginModal') closeLoginModal(); });
|
| 1231 |
|
| 1232 |
// ==================== Init ====================
|
| 1233 |
+
function init() {
|
| 1234 |
+
loadCategories();
|
| 1235 |
+
loadMarkets();
|
| 1236 |
+
loadRankings();
|
|
|
|
|
|
|
|
|
|
| 1237 |
updateUserCard();
|
| 1238 |
+
updateAllTranslations();
|
|
|
|
| 1239 |
}
|
| 1240 |
|
| 1241 |
init();
|