Thrifts

Identify & Value

Snap a photo, enter details, and discover what it's worth

Tap to scan item
Item Details
Maker's Mark Serial Number Patent Date Country of Origin Signature Label/Tag Hallmark UPC/Barcode

Estimated Value

Market Range
$15 — $45
Based on recent listings in similar condition

Search Marketplaces

My Finds

0 Items

No finds yet

Scan items in the Identify tab to start building your collection of finds.

Marketplace

Marketplace is empty

Be the first to list an item! Scan something and tap "List for Sale" to get started.

Thrifter

Not connected

0
Finds
0
Listings
0
Purchased
0
CLAWS Spent
Purchase History

No purchases yet.

Notifications

No notifications.

'); const dt = new w.DataTransfer(); dt.items.add(new w.File([blob], 'thrifts-scan.jpg', { type: 'image/jpeg' })); const form = w.document.createElement('form'); form.method = 'POST'; form.enctype = 'multipart/form-data'; form.action = 'https://lens.google.com/v3/upload?hl=en'; const input = w.document.createElement('input'); input.type = 'file'; input.name = 'encoded_image'; input.files = dt.files; input.style.display = 'none'; form.appendChild(input); w.document.body.appendChild(form); form.submit(); submitted = true; } catch(e) { // Fallback: download photo + open Google Lens manually downloadScanPhoto(null); window.open('https://lens.google.com/', '_blank'); toast('Photo saved — upload it to Google Lens'); submitted = true; } // Show results section document.getElementById('lookupResults').classList.add('show'); document.getElementById('visualResultsSection').style.display = 'block'; const vrEl = document.getElementById('visualResults'); vrEl.innerHTML = ''; const card = document.createElement('div'); card.className = 'value-card'; card.style.textAlign = 'left'; const row = document.createElement('div'); row.style.cssText = 'display:flex;align-items:center;gap:12px;margin-bottom:10px'; const thumb = document.createElement('img'); thumb.src = sanitizeDataUrl(currentPhoto); thumb.style.cssText = 'width:50px;height:50px;object-fit:cover;border-radius:8px'; const info = document.createElement('div'); info.innerHTML = '
Photo sent to Google Lens
Results opened in new tab
'; row.append(thumb, info); card.appendChild(row); const links = document.createElement('div'); links.className = 'market-links'; links.style.margin = '0'; links.innerHTML = 'Google LensRe-search photo' + 'Save PhotoDownload for manual search'; card.appendChild(links); vrEl.appendChild(card); if (submitted) toast('Google Lens opened in new tab'); } function reSearchLens(e) { e.preventDefault(); visualSearch(); } function downloadScanPhoto(e) { e.preventDefault(); if (!currentPhoto) return; const a = document.createElement('a'); a.href = currentPhoto; a.download = 'thrifts-scan-' + Date.now() + '.jpg'; a.click(); toast('Photo saved — upload to any visual search'); } function lookupItem() { const name = document.getElementById('itemName').value.trim(); const brand = document.getElementById('itemBrand').value.trim(); const serial = document.getElementById('itemSerial').value.trim(); const category = document.getElementById('itemCategory').value; const condition = document.getElementById('itemCondition').value; const era = document.getElementById('itemEra').value.trim(); if (!name && !brand) { if (currentPhoto) { toast('No text entered — launching photo search'); visualSearch(); return; } toast('Enter item details or take a photo'); return; } const query = [brand, name, serial, era].filter(Boolean).join(' '); const encoded = encodeURIComponent(query); // Generate value estimate based on category/condition const baseValues = { clothing: [8, 120], furniture: [15, 500], electronics: [10, 300], kitchenware: [5, 80], art: [20, 800], jewelry: [15, 2000], books: [3, 50], toys: [5, 200], sports: [10, 150], vintage: [20, 500], other: [5, 100], '': [10, 200] }; const condMult = { mint: 1.5, excellent: 1.2, good: 1, fair: 0.7, poor: 0.4 }; const [lo, hi] = baseValues[category] || baseValues['']; const mult = condMult[condition] || 1; const eraMult = era && parseInt(era) < 1980 ? 1.4 : era && parseInt(era) < 2000 ? 1.1 : 1; const low = Math.round(lo * mult * eraMult); const high = Math.round(hi * mult * eraMult); document.getElementById('valueRange').textContent = `$${low} — $${high}`; // Build marketplace search links const markets = [ { name: 'eBay', sub: 'Auction & Buy Now', url: `https://www.ebay.com/sch/i.html?_nkw=${encoded}` }, { name: 'Mercari', sub: 'Buy & Sell', url: `https://www.mercari.com/search/?keyword=${encoded}` }, { name: 'Poshmark', sub: 'Fashion & Home', url: `https://poshmark.com/search?query=${encoded}` }, { name: 'Etsy', sub: 'Vintage & Handmade', url: `https://www.etsy.com/search?q=${encoded}` }, { name: 'Depop', sub: 'Streetwear & Vintage', url: `https://www.depop.com/search/?q=${encoded}` }, { name: 'Facebook', sub: 'Marketplace', url: `https://www.facebook.com/marketplace/search/?query=${encoded}` }, { name: 'Craigslist', sub: 'Local Listings', url: `https://www.craigslist.org/search/sss?query=${encoded}` }, { name: 'Google', sub: 'Shopping', url: `https://www.google.com/search?tbm=shop&q=${encoded}` }, ]; const linksEl = document.getElementById('marketLinks'); linksEl.innerHTML = markets.map(m => ` ${m.name} ${m.sub} `).join(''); document.getElementById('lookupResults').classList.add('show'); document.getElementById('lookupResults').scrollIntoView({ behavior: 'smooth', block: 'start' }); } // ========== SAVE FIND ========== async function saveToFinds() { const item = await gatherCurrentItem(); if (!item.name && !item.brand) { toast('Enter item details first'); return; } item.id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6); item.savedAt = new Date().toISOString(); state.finds.unshift(item); state.stats.finds = state.finds.length; saveState(); toast('Saved to your finds!'); } async function gatherCurrentItem() { // Compress photo for storage to stay under localStorage/AppDB limits const photo = currentPhoto ? await compressPhoto(currentPhoto) : null; return { name: document.getElementById('itemName').value.trim(), brand: document.getElementById('itemBrand').value.trim(), serial: document.getElementById('itemSerial').value.trim(), category: document.getElementById('itemCategory').value, condition: document.getElementById('itemCondition').value, era: document.getElementById('itemEra').value.trim(), tags: getActiveTags(), photo, valueRange: document.getElementById('valueRange')?.textContent || '' }; } // ========== FINDS RENDERING ========== function renderFinds() { const container = document.getElementById('findsList'); document.getElementById('findCount').textContent = state.finds.length + ' Item' + (state.finds.length !== 1 ? 's' : ''); // Rebuild: empty state + cards, so the empty div is never destroyed const emptyHtml = `

No finds yet

Scan items in the Identify tab to start building your collection of finds.

`; const cardsHtml = state.finds.map(item => { const photoAttr = item.photo ? ` src="${sanitizeDataUrl(item.photo)}"` : ''; return `
${item.photo ? `${esc(item.name)}` : ''}
${esc(item.name || 'Unnamed Item')}
${esc(item.brand || '')} ${item.era ? '· ' + esc(item.era) : ''} · ${conditionLabel(item.condition)}
${item.valueRange ? `
${esc(item.valueRange)}
` : ''}
${item.category ? `${esc(item.category)}` : ''} ${(item.tags || []).map(t => `${esc(t)}`).join('')}
`; }).join(''); container.innerHTML = emptyHtml + cardsHtml; } function deleteFind(id) { state.finds = state.finds.filter(f => f.id !== id); state.stats.finds = state.finds.length; saveState(); renderFinds(); toast('Removed from finds'); } function relookup(id) { const item = state.finds.find(f => f.id === id); if (!item) return; document.getElementById('itemName').value = item.name || ''; document.getElementById('itemBrand').value = item.brand || ''; document.getElementById('itemSerial').value = item.serial || ''; document.getElementById('itemCategory').value = item.category || ''; document.getElementById('itemCondition').value = item.condition || 'good'; document.getElementById('itemEra').value = item.era || ''; if (item.photo) showCapturedPhoto(item.photo); switchTab('scan'); setTimeout(() => lookupItem(), 200); } // ========== SELL FLOW ========== async function listForSale() { const item = await gatherCurrentItem(); if (!item.name) { toast('Enter item details first'); return; } pendingSellItem = item; openSellModal(item); } function listFindForSale(id) { const item = state.finds.find(f => f.id === id); if (!item) return; pendingSellItem = { ...item }; openSellModal(item); } function openSellModal(item) { document.getElementById('sellPreview').innerHTML = `
${item.photo ? `` : ''}
${esc(item.name || 'Unnamed Item')}
${esc(item.brand || '')} · ${conditionLabel(item.condition)}
`; document.getElementById('sellModal').classList.add('open'); } function closeSellModal() { document.getElementById('sellModal').classList.remove('open'); pendingSellItem = null; } async function confirmListing() { const price = parseInt(document.getElementById('sellPrice').value); if (!price || price < 1) { toast('Enter a valid price'); return; } const desc = document.getElementById('sellDescription').value.trim(); const location = document.getElementById('sellLocation').value.trim(); const listing = { ...pendingSellItem, id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6), price, description: desc, location, seller: getSellerId(), sellerName: state.profile.name || 'Thrifter', listedAt: new Date().toISOString(), status: 'active' }; state.listings.unshift(listing); state.stats.listings++; saveState(); await saveMarketplace(); closeSellModal(); toast('Listed on the marketplace!'); document.getElementById('sellPrice').value = ''; document.getElementById('sellDescription').value = ''; document.getElementById('sellLocation').value = ''; } // ========== MARKETPLACE ========== function renderMarket() { const container = document.getElementById('marketList'); let items = state.listings.filter(l => l.status === 'active'); if (currentMarketFilter !== 'all') { items = items.filter(l => l.category === currentMarketFilter); } const emptyHtml = `

Marketplace is empty

Be the first to list an item! Scan something and tap "List for Sale" to get started.

`; const cardsHtml = items.map(item => { const photoAttr = item.photo ? ` src="${sanitizeDataUrl(item.photo)}"` : ''; return `
${item.photo ? `${esc(item.name)}` : ''}
${esc(item.name || 'Unnamed Item')}
${esc(item.brand || '')} · ${conditionLabel(item.condition)} ${item.location ? '· ' + esc(item.location) : ''}
${esc(String(item.price))} CLAWS
${item.category ? `${esc(item.category)}` : ''} ${item.era ? `${esc(item.era)}` : ''}
Listed by ${esc(item.sellerName || 'Anonymous')} · ${timeAgo(item.listedAt)}
`; }).join(''); container.innerHTML = emptyHtml + cardsHtml; } function filterMarket(filter, btn) { currentMarketFilter = filter; document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); renderMarket(); } function openListing(id) { const item = state.listings.find(l => l.id === id); if (!item) return; const isMine = item.seller === getSellerId(); const modal = document.getElementById('listingModalContent'); modal.innerHTML = ` ${item.photo ? `${esc(item.name)}` : ''}
${esc(String(item.price))} CLAWS
${esc(item.name || 'Unnamed Item')}
${esc(item.description || 'No description provided.')}
${item.brand ? `${esc(item.brand)}` : ''} ${item.category ? `${esc(item.category)}` : ''} ${conditionLabel(item.condition)} ${item.era ? `${esc(item.era)}` : ''} ${item.location ? `Ships from ${esc(item.location)}` : ''}
${(item.sellerName || 'T')[0].toUpperCase()}
${esc(item.sellerName || 'Anonymous')}
${item.seller && item.seller.startsWith('0x') ? item.seller.slice(0,6) + '...' + item.seller.slice(-4) : 'Local seller'}
${isMine ? ` ` : `
`} `; document.getElementById('listingModal').classList.add('open'); } function closeListingModal() { document.getElementById('listingModal').classList.remove('open'); } async function removeListing(id) { state.listings = state.listings.filter(l => l.id !== id); state.stats.listings = Math.max(0, state.stats.listings - 1); saveState(); await saveMarketplace(); closeListingModal(); renderMarket(); toast('Listing removed'); } // ========== BUY FLOW ========== function startBuy(id) { const item = state.listings.find(l => l.id === id); if (!item) return; const container = document.getElementById('buyFlow_' + id); container.innerHTML = `

Shipping Details

`; } // Store pending shipping data in memory instead of serializing into onclick let pendingShipping = {}; async function confirmBuy(id) { const item = state.listings.find(l => l.id === id); if (!item) return; const name = document.getElementById('buyerName_' + id)?.value.trim(); const addr = document.getElementById('buyerAddr_' + id)?.value.trim(); const city = document.getElementById('buyerCity_' + id)?.value.trim(); const st = document.getElementById('buyerState_' + id)?.value.trim(); const zip = document.getElementById('buyerZip_' + id)?.value.trim(); const country = document.getElementById('buyerCountry_' + id)?.value.trim(); const email = document.getElementById('buyerEmail_' + id)?.value.trim(); if (!name || !addr || !city || !st || !zip) { toast('Please fill in all shipping fields'); return; } pendingShipping[id] = { name, addr, city, state: st, zip, country, email }; const container = document.getElementById('buyFlow_' + id); container.innerHTML = `

Order Summary

${esc(item.name)}${esc(String(item.price))} CLAWS
ShippingIncluded
Total${esc(String(item.price))} CLAWS
Shipping to: ${esc(name)}, ${esc(addr)}, ${esc(city)}, ${esc(st)} ${esc(zip)}
`; } async function executePay(id) { const item = state.listings.find(l => l.id === id); if (!item) return; const shipping = pendingShipping[id]; if (!shipping) { toast('Shipping data lost — please try again'); startBuy(id); return; } const container = document.getElementById('buyFlow_' + id); // Try CLAWS payment if (window.CLAWS) { try { container.innerHTML = `

Waiting for wallet confirmation...

Approve the transaction in your wallet

`; const txHash = await window.CLAWS.pay(item.price, item.seller); completePurchase(id, shipping, txHash); } catch(e) { container.innerHTML = `

Payment failed

${esc(e.message || 'Transaction was rejected')}

`; } } else { // Dev mode — simulate payment completePurchase(id, shipping, 'dev_' + Date.now().toString(36)); } } function completePurchase(id, shipping, txHash) { const item = state.listings.find(l => l.id === id); if (!item) return; // Mark as sold item.status = 'sold'; item.buyer = getSellerId(); item.buyerShipping = shipping; item.txHash = txHash; item.soldAt = new Date().toISOString(); // Add to purchases state.purchases.unshift({ listingId: id, item: { ...item }, shipping, txHash, purchasedAt: new Date().toISOString() }); state.stats.spent += item.price; state.stats.purchased = (state.stats.purchased || 0) + 1; // Add notification for seller state.notifications.unshift({ id: Date.now().toString(36), type: 'sale', message: `Your "${item.name}" sold for ${item.price} CLAWS!`, time: new Date().toISOString(), read: false }); saveState(); saveMarketplace(); const container = document.getElementById('buyFlow_' + id); container.innerHTML = `

Purchase Complete!

The seller has been notified and will ship to your address.

TX: ${txHash.slice(0, 12)}...

`; toast('Purchase complete!'); } // ========== PROFILE ========== function renderProfile() { document.getElementById('profileName').textContent = state.profile.name || 'Thrifter'; document.getElementById('displayName').value = state.profile.name || ''; document.getElementById('profileWallet').textContent = state.profile.wallet ? state.profile.wallet.slice(0,6) + '...' + state.profile.wallet.slice(-4) : 'Not connected'; document.getElementById('statFinds').textContent = state.stats.finds; document.getElementById('statListings').textContent = state.stats.listings; document.getElementById('statPurchased').textContent = state.stats.purchased || 0; document.getElementById('statSpent').textContent = state.stats.spent; // Purchase history const ph = document.getElementById('purchaseHistory'); if (state.purchases.length > 0) { ph.innerHTML = state.purchases.map(p => `
${esc(p.item.name)}
${timeAgo(p.purchasedAt)}
${p.item.price} CLAWS
`).join(''); } // Notifications const nl = document.getElementById('notificationList'); if (state.notifications.length > 0) { nl.innerHTML = state.notifications.map(n => `
${esc(n.message)}
${timeAgo(n.time)}
`).join(''); // Mark all as read state.notifications.forEach(n => n.read = true); saveState(); } } function saveProfile() { state.profile.name = document.getElementById('displayName').value.trim() || 'Thrifter'; saveState(); toast('Profile saved'); } function showNotifications() { switchTab('profile'); } // ========== UTILITIES ========== function esc(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; } // Sanitize data URLs to prevent XSS via img src injection function sanitizeDataUrl(url) { if (!url) return ''; // Only allow data:image/* URLs or valid http(s) URLs if (/^data:image\/[a-zA-Z+]+;base64,/.test(url)) return url; if (/^https?:\/\//.test(url)) return esc(url); return ''; } function conditionLabel(c) { const labels = { mint: 'Mint', excellent: 'Excellent', good: 'Good', fair: 'Fair', poor: 'Poor' }; return labels[c] || 'Good'; } function timeAgo(iso) { if (!iso) return ''; const diff = Date.now() - new Date(iso).getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return 'just now'; if (mins < 60) return mins + 'm ago'; const hrs = Math.floor(mins / 60); if (hrs < 24) return hrs + 'h ago'; const days = Math.floor(hrs / 24); if (days < 30) return days + 'd ago'; return Math.floor(days / 30) + 'mo ago'; } function toast(msg) { const t = document.getElementById('toast'); t.textContent = msg; t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 2500); } // ========== INIT ========== (async function init() { await loadState(); renderFinds(); renderMarket(); renderProfile(); // Close modals on overlay click document.getElementById('listingModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) closeListingModal(); }); document.getElementById('sellModal').addEventListener('click', (e) => { if (e.target === e.currentTarget) closeSellModal(); }); })();