No active auctions at this time — check back soon
Loading auctions...
Loading your listings...
Vehicle Information
Photos (up to 10)
Auction Settings
Condition Report
Rate each section honestly. Buyers rely on this information before bidding.
Exterior
Interior
Mechanical
Tires
Account Details
Business Information
Bank Account
Payout History
te">Seller Payout
$${(parseFloat(a.current_bid||0) - 150).toLocaleString()}
Payment received after settlement (1-2 business days). Platform fee: $150
`; } // Contact seller let contactForm = ''; if (!isMine && st === 'active') { contactForm = `
Questions about this vehicle? Message the seller:
`; } document.getElementById('modal-content').innerHTML = ` ${photoHtml}
Price & Bidding
Current Bid$${bid.toLocaleString()}
Bid Count${a.current_bid_count||0}
${isMine ? `
Starting Price$${parseFloat(a.starting_price||0).toLocaleString()}
` : ''} ${reserveInfo}
Status${st}
Time Left${ti.label}
Vehicle Details
Year${v.year||'Unknown'}
Make${v.make||'Unknown'}
Model${v.model||'Unknown'}
Mileage${v.mileage ? parseInt(v.mileage).toLocaleString() + ' mi' : 'Unknown'}
Color${v.color||'Unknown'}
Condition${v.condition||'Unknown'}
${v.vin ? `
VIN${v.vin}
` : ''} ${v.transmission ? `
Transmission${v.transmission}
` : ''} ${v.fuel_type ? `
Fuel${v.fuel_type}
` : ''} ${v.body_type ? `
Body Type${v.body_type}
` : ''} ${a.seller_stats ? `
Seller Info
${sellerVerified}
Rating
${a.seller_stats.average_rating ? parseFloat(a.seller_stats.average_rating).toFixed(2) : 'N/A'}
Auctions
${a.seller_stats.total_auctions||0}
Sold
${a.seller_stats.total_sold||0}
State
${a.seller_stats.state||'–'}
` : ''} ${crHtml} ${a.description ? `
Description
${a.description.replace(//g,'>')}
` : ''} ${bidHistHtml} ${payoutStatus} ${contactForm}
${!isMine && st === 'active' ? `` : ''} ${!isMine && st === 'active' ? `` : ''} ${isMine && st === 'draft' ? `` : ''} ${isMine && st === 'ended' && a.current_bid_count > 0 && !a.winning_bid_id ? `` : ''} ${isMine && (st === 'ended' || st === 'unsold') ? `` : ''}
`; document.getElementById('modal').classList.add('open'); } function setModalPhoto(src, idx, photos) { document.getElementById('modal-main-photo').src = src; document.querySelectorAll('.photo-thumb').forEach((t,i) => t.classList.toggle('active', i===idx)); lightboxPhotos = photos; lightboxIndex = idx; } // ============================================================ // BID MODAL // ============================================================ function openBidModal(auctionId, currentBid, title) { const minBid = currentBid + 100; document.getElementById('bid-modal-content').innerHTML = `
Current Status
Current Bid$${parseFloat(currentBid).toLocaleString()}
Minimum Bid$${minBid.toLocaleString()}
All bids are binding. You are obligated to purchase if you win. A $150 platform fee will be added to the final bid.
`; document.getElementById('bid-modal').classList.add('open'); } async function placeBid(auctionId, currentBid, title) { const amount = parseFloat(document.getElementById('bid-amount').value); const err = document.getElementById('bid-error'); err.classList.remove('show'); if (!amount || amount < (currentBid + 100)) { err.textContent = 'Bid too low'; err.classList.add('show'); return; } try { const r = await fetch(API + '/auctions/' + auctionId + '/bid', { method:'POST', headers:{'Content-Type':'application/json', 'Authorization':'Bearer '+token}, body:JSON.stringify({bid_amount:amount}) }); const d = await r.json().catch(()=>({})); if (!r.ok) throw new Error(d.error || 'Bid failed'); toast('Bid placed! You\'re leading on ' + title, 'success'); closeBidModal(); loadAll(); loadActivity(); } catch(e) { err.textContent = e.message; err.classList.add('show'); } } function closeBidModal() { document.getElementById('bid-modal').classList.remove('open'); } // ============================================================ // LIGHTBOX // ============================================================ function openLightbox(photos, idx) { lightboxPhotos = photos; lightboxIndex = idx; updateLightbox(); document.getElementById('lightbox').classList.add('open'); } function updateLightbox() { document.getElementById('lb-img').src = lightboxPhotos[lightboxIndex]; document.getElementById('lb-counter').textContent = (lightboxIndex + 1) + ' / ' + lightboxPhotos.length; } function lightboxPrev() { lightboxIndex = (lightboxIndex - 1 + lightboxPhotos.length) % lightboxPhotos.length; updateLightbox(); } function lightboxNext() { lightboxIndex = (lightboxIndex + 1) % lightboxPhotos.length; updateLightbox(); } function closeLightbox() { document.getElementById('lightbox').classList.remove('open'); } // ============================================================ // PROFILE // ============================================================ async function loadProfile() { try { const r = await fetch(API + '/users/profile', {headers:{'Authorization':'Bearer '+token}}); if (!r.ok) return; const d = await r.json(); document.getElementById('profile-first').value = d.first_name || ''; document.getElementById('profile-last').value = d.last_name || ''; document.getElementById('profile-email').value = d.email || ''; document.getElementById('profile-phone').value = d.phone || ''; if (d.seller) { document.getElementById('profile-business').value = d.seller.business_name || ''; document.getElementById('profile-license').value = d.seller.business_license || ''; document.getElementById('profile-state').value = d.seller.state || ''; document.getElementById('profile-city').value = d.seller.city || ''; if (d.seller.is_verified) { document.getElementById('seller-verified-status').innerHTML = '
Verified Seller
'; if (!document.getElementById('header-verified').textContent) document.getElementById('header-verified').innerHTML = 'Verified'; } } } catch(e) {} } async function saveProfile() { const first = document.getElementById('profile-first').value.trim(); const last = document.getElementById('profile-last').value.trim(); const phone = document.getElementById('profile-phone').value.trim(); const err = document.getElementById('profile-error'); const succ = document.getElementById('profile-success'); err.classList.remove('show'); succ.classList.remove('show'); try { const r = await fetch(API + '/users/profile', { method:'PUT', headers:{'Content-Type':'application/json','Authorization':'Bearer '+token}, body:JSON.stringify({first_name:first, last_name:last, phone}) }); const d = await r.json().catch(()=>({})); if (!r.ok) throw new Error(d.error || 'Failed to save'); succ.textContent = '✓ Profile updated successfully'; succ.classList.add('show'); } catch(e) { err.textContent = e.message; err.classList.add('show'); } } // ============================================================ // BANK ACCOUNT // ============================================================ async function loadBankStatus() { try { const r = await fetch(API + '/payments/bank-status', {headers:{'Authorization':'Bearer '+token}}); const d = await r.json(); const display = document.getElementById('bank-status-display'); if (d.bank_verified) { display.innerHTML = '
✓ Bank verified: ' + (d.bank_name||'Account on file') + '
'; document.getElementById('bank-setup-form').style.display = 'none'; document.getElementById('bank-verify-form').style.display = 'none'; } else if (d.has_funding_source) { display.innerHTML = '
Bank pending verification. Check your account for 2 small deposits.
'; document.getElementById('bank-setup-form').style.display = 'none'; document.getElementById('bank-verify-form').style.display = 'block'; } else { display.innerHTML = '
No bank account on file
'; document.getElementById('bank-setup-form').style.display = 'block'; document.getElementById('bank-verify-form').style.display = 'none'; } } catch(e) {} } async function setupBank() { const name = document.getElementById('b-name').value.trim(); const routing = document.getElementById('b-routing').value.trim(); const account = document.getElementById('b-account').value.trim(); const type = document.getElementById('b-type').value; const err = document.getElementById('bank-error'); const btn = document.getElementById('bank-btn'); err.classList.remove('show'); if (!name || !routing || !account) { err.textContent = 'All fields required'; err.classList.add('show'); return; } if (routing.length !== 9) { err.textContent = 'Routing number must be 9 digits'; err.classList.add('show'); return; } btn.textContent = 'Adding...'; btn.disabled = true; try { const r = await fetch(API + '/payments/setup-bank', { method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+token}, body:JSON.stringify({account_name:name, routing_number:routing, account_number:account, account_type:type}) }); const d = await r.json().catch(()=>({})); if (!r.ok) throw new Error(d.error || 'Failed to add bank account'); toast(d.message || 'Bank account added. Check for deposits in 1-2 business days.', 'success'); setTimeout(() => loadBankStatus(), 1500); } catch(e) { err.textContent = e.message; err.classList.add('show'); } finally { btn.textContent = 'Add Bank Account'; btn.disabled = false; } } async function verifyBank() { const a1 = document.getElementById('v-amount1').value; const a2 = document.getElementById('v-amount2').value; const err = document.getElementById('verify-error'); const btn = document.getElementById('verify-btn'); err.classList.remove('show'); if (!a1 || !a2) { err.textContent = 'Enter both deposit amounts'; err.classList.add('show'); return; } btn.textContent = 'Verifying...'; btn.disabled = true; try { const r = await fetch(API + '/payments/verify-bank', { method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+token}, body:JSON.stringify({amount1:parseFloat(a1), amount2:parseFloat(a2)}) }); const d = await r.json().catch(()=>({})); if (!r.ok) throw new Error(d.error || 'Verification failed'); toast('Bank account verified successfully!', 'success'); setTimeout(() => loadBankStatus(), 1500); } catch(e) { err.textContent = e.message; err.classList.add('show'); } finally { btn.textContent = 'Verify Bank Account'; btn.disabled = false; } } async function loadPayoutHistory() { try { const r = await fetch(API + '/payments/my-payouts', {headers:{'Authorization':'Bearer '+token}}); const d = await r.json(); const payouts = d.payouts || []; const el = document.getElementById('payout-history'); if (!el) return; if (!payouts.length) { el.innerHTML = '
No payouts yet.
'; return; } el.innerHTML = payouts.map(p => `
${p.auction_title}
${p.date}${p.payout_at?' · Paid '+p.payout_at:''}
$${parseFloat(p.your_payout||0).toLocaleString()}
${p.status}
`).join(''); } catch(e) {} } // ============================================================ // CHECK APPROVAL STATUS & SHOW BANNERS // ============================================================ async function checkApprovalStatus() { try { const r = await fetch(API + '/auth/me', {headers:{'Authorization':'Bearer '+token}}); if (!r.ok) return; const d = await r.json(); const bankR = await fetch(API + '/payments/bank-status', {headers:{'Authorization':'Bearer '+token}}); const bankD = await bankR.json().catch(()=>({})); // Remove existing banners ['approval-banner','bank-setup-banner','welcome-banner'].forEach(id => { const el = document.getElementById(id); if (el) el.remove(); }); if (d.seller && d.seller.approval_status === 'pending') { ['nav-listings','nav-activity','nav-saved','nav-list','nav-profile'].forEach(id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; }); const banner = document.createElement('div'); banner.id = 'approval-banner'; banner.className = 'banner banner-pending'; banner.innerHTML = 'Your account is pending approval. You can browse auctions but listing and bidding will be enabled once approved.'; document.querySelector('.livebar').after(banner); const origBid = window.openBidModal; window.openBidModal = function() { toast('Your account is pending approval.', 'error'); }; } else if (d.seller && d.seller.approval_status === 'approved' && !bankD.bank_verified) { const banner = document.createElement('div'); banner.id = 'bank-setup-banner'; banner.className = 'banner banner-bank'; banner.innerHTML = 'One more step: Verify your bank account to start buying and selling. Complete setup in Profile'; document.querySelector('.livebar').after(banner); showTab('profile'); window.openBidModal = function() { toast('Please verify your bank account in Profile before bidding.', 'error'); showTab('profile'); }; } else if (d.seller && d.seller.approval_status === 'approved' && bankD.bank_verified) { const dismissed = localStorage.getItem('dh_welcome_dismissed_' + userId); if (!dismissed) { const banner = document.createElement('div'); banner.id = 'welcome-banner'; banner.className = 'banner banner-welcome'; banner.innerHTML = `Welcome to DealerHub. You are approved and ready to list vehicles and place bids.`; document.querySelector('.livebar').after(banner); } } } catch(e) {} } // ============================================================ // NOTIFICATIONS // ============================================================ async function enableNotifications() { try { const perm = await Notification.requestPermission(); const nb = document.getElementById('notif-banner'); if (nb) nb.remove(); if (perm === 'granted') { toast('Notifications enabled.', 'success'); localStorage.setItem('dh_notif','granted'); } } catch(e) {} } function sendLocalNotification(title, body) { if (Notification.permission === 'granted' && document.hidden) { new Notification(title, {body: body, icon: '/favicon.ico'}); } } // ============================================================ // LISTING FORM AUTOSAVE // ============================================================ function initListingAutosave() { const fields = ['vin-input','vehicle-year','vehicle-make','vehicle-model','vehicle-mileage','vehicle-color','vehicle-condition','listing-description','starting-price','reserve-price','auction-duration','cr-paint','cr-body','cr-rust','cr-glass','cr-seats','cr-dashboard','cr-headliner','cr-odors','cr-engine','cr-transmission','cr-brakes','cr-ac','cr-tire-fl','cr-tire-fr','cr-tire-rl','cr-tire-rr','cr-notes']; fields.forEach(id => { const el = document.getElementById(id); if (!el) return; const saved = localStorage.getItem('dh_draft_' + id); if (saved && !el.value) el.value = saved; el.addEventListener('input', () => localStorage.setItem('dh_draft_' + id, el.value)); }); const notice = document.getElementById('draft-saved-notice'); if (notice) notice.style.display = 'block'; } function clearListingDraft() { const fields = ['vin-input','vehicle-year','vehicle-make','vehicle-model','vehicle-mileage','vehicle-color','vehicle-condition','listing-description','starting-price','reserve-price','auction-duration','cr-paint','cr-body','cr-rust','cr-glass','cr-seats','cr-dashboard','cr-headliner','cr-odors','cr-engine','cr-transmission','cr-brakes','cr-ac','cr-tire-fl','cr-tire-fr','cr-tire-rl','cr-tire-rr','cr-notes']; fields.forEach(id => localStorage.removeItem('dh_draft_' + id)); document.getElementById('edit-auction-id').value = ''; } // ============================================================ // LISTING // ============================================================ async function submitListing() { const vin = document.getElementById('vin-input').value.trim(); const year = document.getElementById('vehicle-year').value; const make = document.getElementById('vehicle-make').value.trim(); const model = document.getElementById('vehicle-model').value.trim(); const mileage = document.getElementById('vehicle-mileage').value || '0'; const color = document.getElementById('vehicle-color').value.trim(); const condition = document.getElementById('vehicle-condition').value; const description = document.getElementById('listing-description').value.trim(); const starting = parseFloat(document.getElementById('starting-price').value); const reserve = parseFloat(document.getElementById('reserve-price').value) || 0; const duration = parseInt(document.getElementById('auction-duration').value) || 7; const err = document.getElementById('list-error'); err.classList.remove('show'); if (!year || !make || !model || !starting) { err.textContent = 'Year, make, model, and starting price are required'; err.classList.add('show'); return; } try { const r = await fetch(API + '/auctions', { method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+token}, body:JSON.stringify({ year: parseInt(year), make, model, mileage: parseInt(mileage), color, condition, vin, starting_price: starting, reserve_price: reserve, duration_days: duration, description }) }); const d = await r.json(); if (!r.ok) throw new Error(d.error || 'Failed to create listing'); toast('Listing created successfully!', 'success'); clearListingDraft(); document.querySelectorAll('#tab-list input, #tab-list textarea, #tab-list select').forEach(f => { f.value = ''; f.selectedIndex = 0; }); setTimeout(() => { showTab('listings'); loadMyListings(); }, 1000); } catch(e) { err.textContent = e.message; err.classList.add('show'); } } async function editListing(id) { try { const r = await fetch(API + '/auctions/' + id, {headers:{'Authorization':'Bearer '+token}}); if (!r.ok) return; const d = await r.json(); const a = d.auction || d; const v = a.vehicle || {}; showTab('list'); setTimeout(() => { document.getElementById('edit-auction-id').value = id; if (v.vin) document.getElementById('vin-input').value = v.vin; if (v.year) document.getElementById('vehicle-year').value = v.year; if (v.make) document.getElementById('vehicle-make').value = v.make; if (v.model) document.getElementById('vehicle-model').value = v.model; if (v.mileage) document.getElementById('vehicle-mileage').value = v.mileage; if (v.color) document.getElementById('vehicle-color').value = v.color; if (v.condition) document.getElementById('vehicle-condition').value = v.condition; if (a.starting_price) document.getElementById('starting-price').value = a.starting_price; if (a.reserve_price) document.getElementById('reserve-price').value = a.reserve_price; if (a.duration_days) document.getElementById('auction-duration').value = a.duration_days; if (a.description) document.getElementById('listing-description').value = a.description; const btn = document.getElementById('list-btn'); if (btn) btn.textContent = 'Update Listing'; toast('Editing ' + (a.title||'listing') + '. Make your changes and save.', 'success'); }, 300); } catch(e) { toast('Could not load listing for editing.', 'error'); } } async function deleteListing(id) { if (!confirm('Delete this listing? This cannot be undone.')) return; try { const r = await fetch(API + '/auctions/' + id, {method:'DELETE',headers:{'Authorization':'Bearer '+token}}); if (!r.ok) throw new Error('Delete failed'); toast('Listing deleted', 'success'); loadMyListings(); closeModal(); } catch(e) { toast(e.message, 'error'); } } async function previewListing(id) { try { const r = await fetch(API + '/auctions/' + id, {headers:{'Authorization':'Bearer '+token}}); if (!r.ok) return; const d = await r.json(); const a = d.auction || d; openDetail(a); setTimeout(() => { const banner = document.createElement('div'); banner.style.cssText = 'background:#1a1200;border-bottom:2px solid #ff8c00;padding:10px 16px;font-size:12px;color:#ff8c00;font-weight:600;text-align:center;letter-spacing:.06em;text-transform:uppercase;'; banner.textContent = 'Preview Mode — This is how buyers will see your listing'; document.querySelector('.modal-box').prepend(banner); }, 100); } catch(e) {} } async function startAuction(id, title) { if (!confirm('Start auction for ' + title + '? It cannot be stopped.')) return; try { const r = await fetch(API + '/auctions/' + id + '/start', {method:'POST',headers:{'Authorization':'Bearer '+token}}); if (!r.ok) throw new Error('Failed to start auction'); toast('Auction started!', 'success'); loadMyListings(); closeModal(); } catch(e) { toast(e.message, 'error'); } } async function acceptBid(id, title, amount) { if (!confirm('Accept winning bid of $' + amount.toLocaleString() + ' on ' + title + '?')) return; try { const r = await fetch(API + '/auctions/' + id + '/accept-bid', {method:'POST',headers:{'Authorization':'Bearer '+token}}); if (!r.ok) throw new Error('Failed to accept'); toast('Bid accepted! Waiting for payment.', 'success'); loadAll(); closeModal(); } catch(e) { toast(e.message, 'error'); } } async function relistAuction(id) { if (!confirm('Relist this auction?')) return; try { const r = await fetch(API + '/auctions/' + id + '/relist', {method:'POST',headers:{'Authorization':'Bearer '+token}}); if (!r.ok) throw new Error('Failed'); toast('Auction relisted!', 'success'); loadMyListings(); closeModal(); } catch(e) { toast(e.message, 'error'); } } async function sendContactMessage(auctionId) { const msg = (document.getElementById('contact-msg-' + auctionId) || {}).value || ''; if (!msg.trim()) { toast('Please enter a message.', 'error'); return; } try { const r = await fetch(API + '/auctions/' + auctionId + '/contact', { method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+token}, body:JSON.stringify({message:msg}) }); if (!r.ok) throw new Error('Failed'); toast('Message sent to seller.', 'success'); document.getElementById('contact-form-' + auctionId).style.display = 'none'; } catch(e) { toast(e.message, 'error'); } } // ============================================================ // UTILITIES // ============================================================ function closeModal() { document.getElementById('modal').classList.remove('open'); } function toast(msg, type='success') { const el = document.getElementById('toast'); el.textContent = msg; el.className = 'toast show ' + (type === 'error' ? 'toast-error' : 'toast-success'); clearTimeout(toastTimer); toastTimer = setTimeout(() => el.classList.remove('show'), 3500); } function showAlert(el, msg) { el.textContent = msg; el.classList.add('show'); } function decodeVin() { const vin = document.getElementById('vin-input').value.trim().toUpperCase(); if (vin.length !== 17) { toast('VIN must be exactly 17 characters', 'error'); return; } fetch('https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVin/' + vin + '?format=json') .then(r => r.json()) .then(d => { const results = d.Results || []; const findVal = (name) => { const item = results.find(r => r.Variable === name); return item ? item.Value : ''; }; const year = findVal('Model Year'); const make = findVal('Make'); const model = findVal('Model'); if (year) document.getElementById('vehicle-year').value = year; if (make) document.getElementById('vehicle-make').value = make; if (model) document.getElementById('vehicle-model').value = model; if (year && make && model) toast('VIN decoded successfully', 'success'); }) .catch(() => toast('Could not decode VIN', 'error')); } function handlePhotoUpload(e) { const files = e.target.files || []; for (let f of files) { if (!f.type.startsWith('image/')) continue; const reader = new FileReader(); reader.onload = (evt) => { const img = evt.target.result; if (!uploadedPhotos) uploadedPhotos = []; uploadedPhotos.push(img); // TODO: Render photo slots }; reader.readAsDataURL(f); } } bel">Payout Status
Checking…
`; } // Seller contact (after payment) let contactInfo = ''; if (a.winner_contact && !isMine) { contactInfo = `
Buyer Contact
Name${a.winner_contact.name||'—'}
Email${a.winner_contact.email||'—'}
Phone${a.winner_contact.phone||'—'}
`; } if (a.seller_contact && isMine) { contactInfo = `
Buyer Contact
Name${a.seller_contact.name||'—'}
Email${a.seller_contact.email||'—'}
Phone${a.seller_contact.phone||'—'}
`; } // Actions let modalActions = ''; if (!isMine && st === 'active') { modalActions += ``; } if (!isMine && st === 'active') { modalActions += `
`; } if (!isMine && !a.payment_initiated && (st === 'sold') && a.winning_bid_id) { const winBid = bids.find(b => b.id === a.winning_bid_id); if (winBid && winBid.user_id === userId) { modalActions += `Complete Payment`; } } if (isMine && st === 'draft') { modalActions += ``; } if (isMine && (st === 'ended' || st === 'unsold') && a.current_bid_count > 0 && !a.winning_bid_id) { modalActions += ``; } if (isMine && (st === 'ended' || st === 'unsold')) { modalActions += ``; } if ((st === 'sold' || st === 'ended') && a.winning_bid_id) { modalActions += `
Report an issue with this transaction
`; } document.getElementById('modal-content').innerHTML = ` ${photoHtml} ${sellerVerified}
Current Bid$${Math.round(bid).toLocaleString()}
Bids${a.current_bid_count||0}
Status${a.status}
Time Left${ti.label}
${reserveInfo} ${v.mileage ?`
Mileage${parseInt(v.mileage).toLocaleString()} mi
`: ''} ${v.color ?`
Color${v.color}
`: ''} ${v.condition ?`
Condition${v.condition}
`: ''} ${v.vin ?`
VIN${v.vin}
`: ''} ${a.starting_price ?`
Starting Price$${parseFloat(a.starting_price).toLocaleString()}
` : ''} ${isMine ? payoutStatus : ''} ${contactInfo} ${bidHistHtml} ${crHtml}
${modalActions}
`; document.getElementById('modal').classList.add('open'); // Load payout status for seller if (isMine && a.winning_bid_id) { fetch(API + '/payments/auction/' + a.id, {headers:{'Authorization':'Bearer '+token}}) .then(r => r.json()).then(d => { const el = document.getElementById('payout-status-' + a.id); if (!el) return; if (d.payment_status === 'completed') el.innerHTML = 'Payout Processed — funds on their way to your bank'; else if (d.payment_status === 'processing') el.innerHTML = 'Payment Received — payout being processed'; else el.innerHTML = 'Awaiting buyer payment'; }).catch(()=>{}); } } function closeModal() { document.getElementById('modal').classList.remove('open'); } function setModalPhoto(src, idx, photos) { document.getElementById('modal-main-photo').src = src; document.querySelectorAll('.photo-thumb').forEach((t,i) => t.classList.toggle('active', i===idx)); } // ============================================================ // LIGHTBOX // ============================================================ function openLightbox(photos, index) { if (typeof photos === 'string') { try { photos = JSON.parse(photos.replace(/'/g,'"')); } catch(e) { photos = [photos]; } } lightboxPhotos = photos; lightboxIndex = index; document.getElementById('lightbox').classList.add('open'); updateLightbox(); } function updateLightbox() { document.getElementById('lb-img').src = lightboxPhotos[lightboxIndex]; document.getElementById('lb-counter').textContent = (lightboxIndex+1) + ' / ' + lightboxPhotos.length; } function closeLightbox() { document.getElementById('lightbox').classList.remove('open'); } function lightboxPrev() { lightboxIndex = (lightboxIndex - 1 + lightboxPhotos.length) % lightboxPhotos.length; updateLightbox(); } function lightboxNext() { lightboxIndex = (lightboxIndex + 1) % lightboxPhotos.length; updateLightbox(); } // ============================================================ // BID MODAL // ============================================================ function openBidModal(auctionId, currentBid, title) { const min = parseFloat(currentBid) + 100; document.getElementById('bid-modal-title').textContent = 'Place Bid — ' + (title||'Auction'); document.getElementById('bid-modal-content').innerHTML = `
Current Bid$${Math.round(parseFloat(currentBid)).toLocaleString()}
Minimum Bid$${Math.round(min).toLocaleString()}
All bids are binding. You are obligated to purchase if you win.
`; document.getElementById('bid-modal').classList.add('open'); setTimeout(() => { const inp = document.getElementById('bid-amount'); if (inp) inp.focus(); }, 100); } function closeBidModal() { document.getElementById('bid-modal').classList.remove('open'); } async function submitBid(auctionId, title) { const amount = parseFloat(document.getElementById('bid-amount').value); const err = document.getElementById('bid-error'); err.classList.remove('show'); if (!amount || isNaN(amount)) { showAlert(err, 'Please enter a bid amount.'); return; } // Check bank verification try { const bk = await fetch(API + '/payments/bank-status', {headers:{'Authorization':'Bearer '+token}}); const bkd = await bk.json(); if (!bkd.bank_verified) { toast('Please verify your bank account in Profile before bidding.', 'error'); closeBidModal(); showTab('profile'); return; } } catch(e) {} try { const r = await fetch(API + '/auctions/' + auctionId + '/bid', { method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+token}, body:JSON.stringify({amount}) }); const d = await r.json(); if (!r.ok) throw new Error(d.error || 'Bid failed'); toast('Bid of $' + amount.toLocaleString() + ' placed successfully.', 'success'); closeBidModal(); loadAll(); } catch(e) { showAlert(err, e.message); } } // ============================================================ // AUCTION ACTIONS // ============================================================ async function startAuction(auctionId, title) { if (!confirm('Start auction for ' + title + '? Once started, the auction cannot be paused or cancelled.')) return; // Check bank verification try { const bk = await fetch(API + '/payments/bank-status', {headers:{'Authorization':'Bearer '+token}}); const bkd = await bk.json(); if (!bkd.bank_verified) { toast('Please verify your bank account in Profile before starting an auction.', 'error'); showTab('profile'); return; } } catch(e) {} try { const r = await fetch(API + '/auctions/' + auctionId + '/start', {method:'POST',headers:{'Authorization':'Bearer '+token}}); const d = await r.json(); if (!r.ok) throw new Error(d.error || 'Failed to start auction'); toast('Auction started.', 'success'); loadAll(); } catch(e) { toast(e.message, 'error'); } } async function relistAuction(auctionId) { try { const r = await fetch(API + '/auctions/' + auctionId + '/relist', {method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+token},body:JSON.stringify({duration_days:7})}); const d = await r.json(); if (!r.ok) throw new Error(d.error || 'Failed to relist'); toast('Auction relisted as draft.', 'success'); loadAll(); } catch(e) { toast(e.message, 'error'); } } async function acceptBid(auctionId, title, amount) { if (!confirm('Accept the highest bid of $' + parseFloat(amount).toLocaleString() + ' for ' + title + '? The buyer will be notified to complete payment.')) return; try { const r = await fetch(API + '/auctions/' + auctionId + '/accept-bid', {method:'POST',headers:{'Authorization':'Bearer '+token}}); const d = await r.json(); if (!r.ok) throw new Error(d.error || 'Failed to accept bid'); toast('Bid accepted. Buyer notified to complete payment.', 'success'); loadAll(); } catch(e) { toast(e.message, 'error'); } } // ============================================================ // CONTACT ADMIN // ============================================================ async function sendContactMessage(auctionId) { const msg = (document.getElementById('contact-msg-' + auctionId)||{}).value||''; if (!msg.trim()) { toast('Please enter a message.', 'error'); return; } try { const r = await fetch(API + '/auctions/' + auctionId + '/contact', {method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+token},body:JSON.stringify({message:msg.trim()})}); const d = await r.json(); if (!r.ok) throw new Error(d.error || 'Failed to send'); toast('Message sent. DealerHub will follow up shortly.', 'success'); const form = document.getElementById('contact-form-' + auctionId); const btn = document.getElementById('contact-btn-' + auctionId); if (form) form.style.display = 'none'; if (btn) btn.style.display = 'block'; const msg_el = document.getElementById('contact-msg-' + auctionId); if (msg_el) msg_el.value = ''; } catch(e) { toast(e.message, 'error'); } } // ============================================================ // LISTING FORM // ============================================================ function initListingAutosave() { const fields = ['vin-input','vehicle-year','vehicle-make','vehicle-model','vehicle-mileage','vehicle-color','vehicle-condition','starting-price','reserve-price','auction-duration','listing-description']; let hasData = false; fields.forEach(id => { const el = document.getElementById(id); if (!el) return; const saved = localStorage.getItem('dh_draft_' + id); if (saved && !el.value) { el.value = saved; hasData = true; } el.addEventListener('input', () => localStorage.setItem('dh_draft_' + id, el.value)); }); const notice = document.getElementById('draft-saved-notice'); if (notice && hasData) notice.style.display = 'block'; // Init photo grid initPhotoGrid(); } function clearListingDraft() { const fields = ['vin-input','vehicle-year','vehicle-make','vehicle-model','vehicle-mileage','vehicle-color','vehicle-condition','starting-price','reserve-price','auction-duration','listing-description']; fields.forEach(id => localStorage.removeItem('dh_draft_' + id)); document.getElementById('edit-auction-id').value = ''; uploadedPhotos = []; initPhotoGrid(); const notice = document.getElementById('draft-saved-notice'); if (notice) notice.style.display = 'none'; // Reset form section title and button const btn = document.getElementById('list-btn'); if (btn) btn.textContent = 'Create Listing'; const heading = document.querySelector('#tab-list .form-section-title'); if (heading) heading.textContent = 'Vehicle Information'; } function initPhotoGrid() { const grid = document.getElementById('photo-grid'); if (!grid) return; grid.innerHTML = ''; for (let i = 0; i < 10; i++) { const slot = document.createElement('div'); slot.className = 'photo-slot'; slot.dataset.index = i; if (uploadedPhotos[i]) { slot.innerHTML = ``; } else if (i === uploadedPhotos.length) { slot.innerHTML = '
+
'; slot.onclick = () => document.getElementById('photo-input').click(); } else { slot.style.opacity = '.3'; } grid.appendChild(slot); } } async function handlePhotoUpload(event) { const files = Array.from(event.target.files); for (const file of files) { if (uploadedPhotos.length >= 10) break; try { const fd = new FormData(); fd.append('photo', file); const r = await fetch(API + '/photos/upload', {method:'POST',headers:{'Authorization':'Bearer '+token},body:fd}); const d = await r.json(); if (d.url) uploadedPhotos.push(d.url); } catch(e) { toast('Photo upload failed.', 'error'); } } initPhotoGrid(); event.target.value = ''; } function removePhoto(index, event) { event.stopPropagation(); uploadedPhotos.splice(index, 1); initPhotoGrid(); } async function decodeVin() { const vin = document.getElementById('vin-input').value.trim(); if (!vin || vin.length < 11) { toast('Enter a valid VIN (at least 11 characters).', 'error'); return; } try { const r = await fetch(`https://vpic.nhtsa.dot.gov/api/vehicles/decodevinvalues/${vin}?format=json`); const d = await r.json(); const result = d.Results && d.Results[0]; if (result) { if (result.ModelYear) document.getElementById('vehicle-year').value = result.ModelYear; if (result.Make) document.getElementById('vehicle-make').value = result.Make; if (result.Model) document.getElementById('vehicle-model').value = result.Model; toast('VIN decoded successfully.', 'success'); } } catch(e) { toast('VIN decode failed. Enter details manually.', 'error'); } } async function submitListing() { const editId = document.getElementById('edit-auction-id').value; const year = document.getElementById('vehicle-year').value; const make = document.getElementById('vehicle-make').value.trim(); const model = document.getElementById('vehicle-model').value.trim(); const mileage = document.getElementById('vehicle-mileage').value; const color = document.getElementById('vehicle-color').value.trim(); const condition = document.getElementById('vehicle-condition').value; const startingPrice = parseFloat(document.getElementById('starting-price').value); const reservePrice = document.getElementById('reserve-price').value ? parseFloat(document.getElementById('reserve-price').value) : null; const duration = parseInt(document.getElementById('auction-duration').value) || 7; const description = document.getElementById('listing-description').value.trim(); const err = document.getElementById('list-error'); err.classList.remove('show'); if (!make || !model) { showAlert(err, 'Make and model are required.'); return; } if (!startingPrice || startingPrice < 100) { showAlert(err, 'Starting price must be at least $100.'); return; } if (editId) { // Update existing draft try { const r = await fetch(API + '/auctions/' + editId, { method:'PUT', headers:{'Content-Type':'application/json','Authorization':'Bearer '+token}, body:JSON.stringify({starting_price:startingPrice,reserve_price:reservePrice,duration_days:duration,mileage,color,condition}) }); const d = await r.json(); if (!r.ok) throw new Error(d.error || 'Update failed'); clearListingDraft(); toast('Listing updated.', 'success'); showTab('listings'); loadAll(); } catch(e) { showAlert(err, e.message); } return; } // Collect condition report const conditionReport = { exterior: {paint:document.getElementById('cr-paint').value,body:document.getElementById('cr-body').value,rust:document.getElementById('cr-rust').value,glass:document.getElementById('cr-glass').value}, interior: {seats:document.getElementById('cr-seats').value,dashboard:document.getElementById('cr-dashboard').value,headliner:document.getElementById('cr-headliner').value,odors:document.getElementById('cr-odors').value}, mechanical: {engine:document.getElementById('cr-engine').value,transmission:document.getElementById('cr-transmission').value,brakes:document.getElementById('cr-brakes').value,ac:document.getElementById('cr-ac').value}, tires: {front_left:document.getElementById('cr-tire-fl').value,front_right:document.getElementById('cr-tire-fr').value,rear_left:document.getElementById('cr-tire-rl').value,rear_right:document.getElementById('cr-tire-rr').value}, notes: document.getElementById('cr-notes').value, certified: document.getElementById('cr-certify').checked, certified_at: new Date().toISOString() }; if (!conditionReport.certified) { showAlert(err, 'Please certify the condition report before submitting.'); return; } const btn = document.getElementById('list-btn'); btn.textContent = 'Creating…'; btn.disabled = true; try { // Create vehicle first const vr = await fetch(API + '/vehicles', { method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+token}, body:JSON.stringify({vin:document.getElementById('vin-input').value.trim()||null,year:year?parseInt(year):null,make,model,mileage:mileage?parseInt(mileage):null,color:color||null,condition:condition||null,photos:uploadedPhotos,condition_report:conditionReport}) }); const vd = await vr.json(); if (!vr.ok) throw new Error(vd.error || 'Failed to create vehicle'); // Create auction const ar = await fetch(API + '/auctions', { method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+token}, body:JSON.stringify({vehicle_id:vd.id||vd.vehicle_id,title:[year,make,model].filter(Boolean).join(' '),starting_price:startingPrice,reserve_price:reservePrice,duration_days:duration,description}) }); const ad = await ar.json(); if (!ar.ok) throw new Error(ad.error || 'Failed to create auction'); clearListingDraft(); toast('Listing created as draft. Go to My Listings to start your auction.', 'success'); showTab('listings'); loadAll(); } catch(e) { showAlert(err, e.message); } finally { btn.textContent = 'Create Listing'; btn.disabled = false; } } function previewListing(auctionId) { const auction = myListings.find(a => a.id === auctionId); if (!auction) return; openDetail(JSON.parse(JSON.stringify(auction))); setTimeout(() => { const box = document.querySelector('.modal-box'); if (box) { const banner = document.createElement('div'); banner.style.cssText = 'background:#1a1200;border-bottom:2px solid #ff8c00;padding:8px 14px;font-size:11px;color:#ff8c00;font-weight:600;text-align:center;letter-spacing:.06em;text-transform:uppercase;border-radius:0;margin:-20px -20px 16px;'; banner.textContent = 'Preview Mode — This is how buyers will see your listing'; box.prepend(banner); } }, 100); } async function editListing(auctionId) { try { const r = await fetch(API + '/auctions/' + auctionId, {headers:{'Authorization':'Bearer '+token}}); const d = await r.json(); const a = d.auction || d; const v = a.vehicle || {}; showTab('list'); setTimeout(() => { document.getElementById('edit-auction-id').value = auctionId; if (v.vin) document.getElementById('vin-input').value = v.vin; if (v.year) document.getElementById('vehicle-year').value = v.year; if (v.make) document.getElementById('vehicle-make').value = v.make; if (v.model) document.getElementById('vehicle-model').value = v.model; if (v.mileage) document.getElementById('vehicle-mileage').value = v.mileage; if (v.color) document.getElementById('vehicle-color').value = v.color; if (v.condition) document.getElementById('vehicle-condition').value = v.condition; if (a.starting_price) document.getElementById('starting-price').value = a.starting_price; if (a.reserve_price) document.getElementById('reserve-price').value = a.reserve_price; if (a.duration_days) document.getElementById('auction-duration').value = a.duration_days; const btn = document.getElementById('list-btn'); if (btn) btn.textContent = 'Update Listing'; toast('Editing ' + (a.title||'listing') + '. Make your changes and save.', 'success'); }, 300); } catch(e) { toast('Could not load listing for editing.', 'error'); } } // ============================================================ // PROFILE // ============================================================ async function loadProfile() { try { const r = await fetch(API + '/users/profile', {headers:{'Authorization':'Bearer '+token}}); const d = await r.json(); if (d.first_name) document.getElementById('profile-first').value = d.first_name; if (d.last_name) document.getElementById('profile-last').value = d.last_name; if (d.email) document.getElementById('profile-email').value = d.email; if (d.phone) document.getElementById('profile-phone').value = d.phone; if (d.seller) { if (d.seller.business_name) document.getElementById('profile-business').value = d.seller.business_name; if (d.seller.business_license) document.getElementById('profile-license').value = d.seller.business_license; if (d.seller.state) document.getElementById('profile-state').value = d.seller.state; if (d.seller.city) document.getElementById('profile-city').value = d.seller.city; } // Verified status const verEl = document.getElementById('seller-verified-status'); if (verEl && d.seller) { verEl.innerHTML = d.seller.is_verified ? '
Verified Seller
' : '
Not yet verified by DealerHub
'; } // Verified badge in header const hv = document.getElementById('header-verified'); if (hv && d.seller && d.seller.is_verified) { hv.innerHTML = 'Verified'; } } catch(e) {} } async function saveProfile() { const first = document.getElementById('profile-first').value.trim(); const last = document.getElementById('profile-last').value.trim(); const phone = document.getElementById('profile-phone').value.trim(); const err = document.getElementById('profile-error'); const succ = document.getElementById('profile-success'); err.classList.remove('show'); succ.classList.remove('show'); try { const r = await fetch(API + '/users/profile', {method:'PUT',headers:{'Content-Type':'application/json','Authorization':'Bearer '+token},body:JSON.stringify({first_name:first,last_name:last,phone})}); const d = await r.json(); if (!r.ok) throw new Error(d.error || 'Update failed'); showAlert(succ, 'Profile updated successfully.'); } catch(e) { showAlert(err, e.message); } } async function loadBankStatus() { try { const r = await fetch(API + '/payments/bank-status', {headers:{'Authorization':'Bearer '+token}}); const d = await r.json(); const display = document.getElementById('bank-status-display'); if (d.bank_verified) { display.innerHTML = `
Bank account verified: ${d.bank_name||'Account on file'}
`; document.getElementById('bank-setup-form').style.display = 'none'; document.getElementById('bank-verify-form').style.display = 'none'; } else if (d.has_funding_source) { display.innerHTML = `
Bank account pending verification. Check your account for 2 small deposits.
`; document.getElementById('bank-setup-form').style.display = 'none'; document.getElementById('bank-verify-form').style.display = 'block'; } else { display.innerHTML = '
No bank account on file. Add your account to receive payments.
'; } } catch(e) {} } async function setupBank() { const name = document.getElementById('b-name').value.trim(); const routing = document.getElementById('b-routing').value.trim(); const account = document.getElementById('b-account').value.trim(); const type = document.getElementById('b-type').value; const err = document.getElementById('bank-error'); const succ = document.getElementById('bank-success'); const btn = document.getElementById('bank-btn'); err.classList.remove('show'); succ.classList.remove('show'); if (!name || !routing || !account) { showAlert(err, 'All fields are required.'); return; } if (routing.length !== 9) { showAlert(err, 'Routing number must be 9 digits.'); return; } btn.textContent = 'Adding…'; btn.disabled = true; try { const r = await fetch(API + '/payments/setup-bank', {method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+token},body:JSON.stringify({account_name:name,routing_number:routing,account_number:account,account_type:type})}); const d = await r.json(); if (!r.ok) throw new Error(d.error || 'Failed to add bank account'); showAlert(succ, d.message || 'Bank account added. Check for micro-deposits in 1-2 business days.'); setTimeout(() => loadBankStatus(), 1500); } catch(e) { showAlert(err, e.message); } finally { btn.textContent = 'Add Bank Account'; btn.disabled = false; } } async function verifyBank() { const a1 = document.getElementById('v-amount1').value; const a2 = document.getElementById('v-amount2').value; const err = document.getElementById('verify-error'); const succ = document.getElementById('verify-success'); const btn = document.getElementById('verify-btn'); err.classList.remove('show'); succ.classList.remove('show'); if (!a1 || !a2) { showAlert(err, 'Enter both deposit amounts.'); return; } btn.textContent = 'Verifying…'; btn.disabled = true; try { const r = await fetch(API + '/payments/verify-bank', {method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+token},body:JSON.stringify({amount1:parseFloat(a1),amount2:parseFloat(a2)})}); const d = await r.json(); if (!r.ok) throw new Error(d.error || 'Verification failed'); showAlert(succ, 'Bank account verified successfully!'); setTimeout(() => loadBankStatus(), 1500); } catch(e) { showAlert(err, e.message); } finally { btn.textContent = 'Verify Bank Account'; btn.disabled = false; } } async function loadPayoutHistory() { try { const r = await fetch(API + '/payments/my-payouts', {headers:{'Authorization':'Bearer '+token}}); const d = await r.json(); const payouts = d.payouts || []; const el = document.getElementById('payout-history'); if (!el) return; if (!payouts.length) { el.innerHTML = '
No payouts yet.
'; return; } el.innerHTML = payouts.map(p => `
${p.auction_title}
${p.date}${p.payout_at?' · Paid '+p.payout_at:''}
$${parseFloat(p.your_payout||0).toLocaleString()}
${p.status}
`).join(''); } catch(e) {} } // ============================================================ // APPROVAL STATUS / BANNERS // ============================================================ async function checkApprovalStatus() { try { const r = await fetch(API + '/auth/me', {headers:{'Authorization':'Bearer '+token}}); if (!r.ok) return; const d = await r.json(); const bankR = await fetch(API + '/payments/bank-status', {headers:{'Authorization':'Bearer '+token}}); const bankD = await bankR.json().catch(()=>({})); // Remove existing banners ['approval-banner','bank-setup-banner','welcome-banner'].forEach(id => { const el = document.getElementById(id); if (el) el.remove(); }); if (d.seller && d.seller.approval_status === 'pending') { ['nav-listings','nav-activity','nav-saved','nav-list','nav-profile'].forEach(id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; }); const banner = document.createElement('div'); banner.id = 'approval-banner'; banner.className = 'banner banner-pending'; banner.innerHTML = 'Your account is pending approval. You can browse auctions but listing and bidding will be enabled once approved.'; document.querySelector('.livebar').after(banner); const origBid = window.openBidModal; window.openBidModal = function() { toast('Your account is pending approval.', 'error'); }; } else if (d.seller && d.seller.approval_status === 'approved' && !bankD.bank_verified) { const banner = document.createElement('div'); banner.id = 'bank-setup-banner'; banner.className = 'banner banner-bank'; banner.innerHTML = 'One more step: Verify your bank account to start buying and selling. Complete setup in Profile'; document.querySelector('.livebar').after(banner); showTab('profile'); window.openBidModal = function() { toast('Please verify your bank account in Profile before bidding.', 'error'); showTab('profile'); }; } else if (d.seller && d.seller.approval_status === 'approved' && bankD.bank_verified) { const dismissed = localStorage.getItem('dh_welcome_dismissed_' + userId); if (!dismissed) { const banner = document.createElement('div'); banner.id = 'welcome-banner'; banner.className = 'banner banner-welcome'; banner.innerHTML = `Welcome to DealerHub. You are approved and ready to list vehicles and place bids.`; document.querySelector('.livebar').after(banner); } } } catch(e) {} } // ============================================================ // NOTIFICATIONS // ============================================================ async function enableNotifications() { try { const perm = await Notification.requestPermission(); const nb = document.getElementById('notif-banner'); if (nb) nb.remove(); if (perm === 'granted') { toast('Notifications enabled.', 'success'); localStorage.setItem('dh_notif','granted'); } } catch(e) {} } function sendLocalNotification(title, body) { if (Notification.permission === 'granted' && document.hidden) { new Notification(title, {body}); } } // ============================================================ // UTILS // ============================================================ function toast(msg, type='success') { const el = document.getElementById('toast'); el.textContent = msg; el.className = 'toast toast-' + type + ' show'; clearTimeout(toastTimer); toastTimer = setTimeout(() => el.classList.remove('show'), 3500); } function showAlert(el, msg) { el.textContent = msg; el.classList.add('show'); } // Load Socket.IO (function() { const s = document.createElement('script'); s.src = 'https://dealerhub.cloud/socket.io/socket.io.js'; s.onload = function() { if (token) initSocket(); }; document.head.appendChild(s); })();