// Type declarations for global objects declare global { interface Window { analytics?: { track: (event: string, data: any) => void; }; } } import './style.css'; import { fetchMarketplaceOffers, fetchMarketplaceStats, submitMarketplaceBid, } from './lib/api'; import type { MarketplaceOffer, MarketplaceStats } from './lib/api'; const app = document.querySelector('#app'); if (!app) { throw new Error('Unable to mount marketplace app'); } app.innerHTML = `

Total Offers

-- Listings currently visible

Open Capacity

-- GPU / compute units available

Average Price

-- Credits per unit per hour

Active Bids

-- Open bids awaiting match

Available Offers

Fetching marketplace offers…

Submit a Bid

`; const selectors = { totalOffers: document.querySelector('#stat-total-offers')!, openCapacity: document.querySelector('#stat-open-capacity')!, averagePrice: document.querySelector('#stat-average-price')!, activeBids: document.querySelector('#stat-active-bids')!, statsWrapper: document.querySelector('#stats-grid')!, offersWrapper: document.querySelector('#offers-table-wrapper')!, bidForm: document.querySelector('#bid-form')!, toast: document.querySelector('#toast')!, }; function formatNumber(value: number, options: Intl.NumberFormatOptions = {}): string { return new Intl.NumberFormat(undefined, options).format(value); } function renderStats(stats: MarketplaceStats): void { selectors.totalOffers.textContent = formatNumber(stats.totalOffers); selectors.openCapacity.textContent = `${formatNumber(stats.openCapacity)} units`; selectors.averagePrice.textContent = `${formatNumber(stats.averagePrice, { minimumFractionDigits: 2, maximumFractionDigits: 2, })} credits`; selectors.activeBids.textContent = formatNumber(stats.activeBids); } function statusClass(status: string): string { switch (status.toLowerCase()) { case 'open': return 'status-pill status-open'; case 'reserved': return 'status-pill status-reserved'; default: return 'status-pill'; } } function renderOffers(offers: MarketplaceOffer[]): void { const wrapper = selectors.offersWrapper; if (!wrapper) return; if (offers.length === 0) { wrapper.innerHTML = '

No offers available right now. Check back soon or submit a bid.

'; return; } const cards = offers .map( (offer) => `
${offer.gpu_model || 'Unknown GPU'}
${offer.status}
${offer.provider}${offer.region ? ` · ${offer.region}` : ''}
VRAM ${offer.gpu_memory_gb ? `${offer.gpu_memory_gb} GB` : '—'}
GPUs ${offer.gpu_count ?? 1}×
CUDA ${offer.cuda_version || '—'}
Capacity ${formatNumber(offer.capacity)} units
${offer.attributes?.models?.length ? `
Ollama
Available Models
${offer.attributes.models.map((m: string) => `${m}`).join('')}
` : ''}
${formatNumber(offer.price_per_hour ?? offer.price, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} credits/hr
${offer.sla}
`, ) .join(''); wrapper.innerHTML = `
${cards}
`; } function showToast(message: string, duration = 2500): void { if (!selectors.toast) return; selectors.toast.textContent = message; selectors.toast.classList.add('visible'); window.setTimeout(() => { selectors.toast?.classList.remove('visible'); }, duration); } async function loadDashboard(): Promise { // Show skeleton loading states showSkeletons(); try { const [stats, offers] = await Promise.all([ fetchMarketplaceStats(), fetchMarketplaceOffers(), ]); renderStats(stats); renderOffers(offers); } catch (error) { console.error(error); const wrapper = selectors.offersWrapper; if (wrapper) { wrapper.innerHTML = '

Failed to load offers. Please retry shortly.

'; } showToast('Failed to load marketplace data.'); } } function showSkeletons() { const statsWrapper = selectors.statsWrapper; const offersWrapper = selectors.offersWrapper; if (statsWrapper) { statsWrapper.innerHTML = `
${Array(4).fill('').map(() => `
`).join('')}
`; } if (offersWrapper) { offersWrapper.innerHTML = `
${Array(6).fill('').map(() => `
`).join('')}
`; } } selectors.bidForm?.addEventListener('submit', async (event) => { event.preventDefault(); const form = selectors.bidForm; if (!form) return; const formData = new FormData(form); const provider = formData.get('provider')?.toString().trim(); const capacity = Number(formData.get('capacity')); const price = Number(formData.get('price')); const notes = formData.get('notes')?.toString().trim(); if (!provider || Number.isNaN(capacity) || Number.isNaN(price)) { showToast('Please complete the required fields.'); return; } try { const submitButton = form.querySelector('button'); if (submitButton) { submitButton.setAttribute('disabled', 'disabled'); } await submitMarketplaceBid({ provider, capacity, price, notes }); form.reset(); showToast('Bid submitted successfully!'); } catch (error) { console.error(error); showToast('Unable to submit bid. Please try again.'); } finally { const submitButton = form.querySelector('button'); if (submitButton) { submitButton.removeAttribute('disabled'); } } }); loadDashboard(); // Dark mode functionality with system preference detection function toggleDarkMode() { const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light'; const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; setTheme(newTheme); } function setTheme(theme: string) { // Apply theme immediately if (theme === 'dark') { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } // Save to localStorage for persistence localStorage.setItem('marketplaceTheme', theme); // Update button display updateThemeButton(theme); // Send analytics event if available if (typeof window !== 'undefined' && window.analytics) { window.analytics.track('marketplace_theme_changed', { theme }); } } function updateThemeButton(theme: string) { const emoji = document.getElementById('darkModeEmoji'); const text = document.getElementById('darkModeText'); if (emoji && text) { if (theme === 'dark') { emoji.textContent = '🌙'; text.textContent = 'Dark'; } else { emoji.textContent = '☀️'; text.textContent = 'Light'; } } } function getPreferredTheme(): string { // 1. Check localStorage first (user preference for marketplace) const saved = localStorage.getItem('marketplaceTheme'); if (saved) { return saved; } // 2. Check main site preference for consistency const mainSiteTheme = localStorage.getItem('theme'); if (mainSiteTheme) { return mainSiteTheme; } // 3. Check system preference if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { return 'dark'; } // 4. Default to dark (AITBC brand preference) return 'dark'; } function initializeTheme() { const theme = getPreferredTheme(); setTheme(theme); // Listen for system preference changes if (window.matchMedia) { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { // Only auto-switch if user hasn't manually set a preference if (!localStorage.getItem('marketplaceTheme') && !localStorage.getItem('theme')) { setTheme(e.matches ? 'dark' : 'light'); } }); } } // Initialize theme immediately (before DOM loads) initializeTheme(); // Reference to suppress TypeScript "never used" warning // @ts-ignore - function called from HTML onclick window.toggleDarkMode = toggleDarkMode;