← back to Handbag Authentication
public/js/app.js
1100 lines
// API base URL
const API_BASE = window.location.origin;
// State
let currentFilters = {
brand: '',
signature: '',
minPrice: '',
maxPrice: '',
type: '',
dealOnly: false,
minDeal: 0,
sort: 'profit'
};
let autoRefreshInterval = null;
let lastListingCount = 0;
// Infinite scroll state
let allListings = [];
let displayedCount = 0;
const ITEMS_PER_PAGE = 20;
let isLoading = false;
// Auto-translate flag
let autoTranslateEnabled = true;
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadStats();
loadListings();
setupEventListeners();
setupInfiniteScroll();
startAutoRefresh();
initializeConditionGuide();
// Add auto-translate toggle to the page
addTranslateToggle();
});
// Setup event listeners
function setupEventListeners() {
document.getElementById('filter-brand').addEventListener('input', debounce(applyFilters, 500));
document.getElementById('filter-signature').addEventListener('change', applyFilters);
document.getElementById('filter-type').addEventListener('change', applyFilters);
document.getElementById('filter-deals-only').addEventListener('change', applyFilters);
document.getElementById('filter-min-price').addEventListener('input', debounce(applyFilters, 500));
document.getElementById('filter-max-price').addEventListener('input', debounce(applyFilters, 500));
document.getElementById('filter-min-deal').addEventListener('input', debounce(applyFilters, 500));
document.getElementById('btn-refresh').addEventListener('click', () => {
loadStats();
loadListings();
});
document.getElementById('btn-export').addEventListener('click', exportToCSV);
const btnCrawl = document.getElementById('btn-crawl');
if (btnCrawl) {
btnCrawl.addEventListener('click', triggerCrawl);
}
// Sort tabs
document.querySelectorAll('.sort-tab').forEach(tab => {
tab.addEventListener('click', () => {
// Remove active from all tabs
document.querySelectorAll('.sort-tab').forEach(t => t.classList.remove('active'));
// Add active to clicked tab
tab.classList.add('active');
// Update sort and reload
currentFilters.sort = tab.getAttribute('data-sort');
loadListings();
});
});
}
// Debounce helper
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Apply filters
function applyFilters() {
const signatureModel = document.getElementById('filter-signature').value;
currentFilters = {
brand: signatureModel ? '' : document.getElementById('filter-brand').value, // Clear brand if signature selected
signature: signatureModel,
minPrice: document.getElementById('filter-min-price').value,
maxPrice: document.getElementById('filter-max-price').value,
type: document.getElementById('filter-type').value,
dealOnly: document.getElementById('filter-deals-only').checked,
minDeal: document.getElementById('filter-min-deal').value || 0,
sort: currentFilters.sort // Keep current sort from tabs
};
loadListings();
}
// Load stats
async function loadStats() {
try {
const response = await fetch(`${API_BASE}/api/stats`);
const stats = await response.json();
document.getElementById('stat-total').textContent = stats.total_listings.toLocaleString();
document.getElementById('stat-deals').textContent = stats.total_deals.toLocaleString();
document.getElementById('stat-avg-deal').textContent = `${stats.avg_deal_percentage}%`;
document.getElementById('stat-best-deal').textContent = `${stats.best_deal_percentage}%`;
} catch (error) {
console.error('Error loading stats:', error);
}
}
// Load listings
async function loadListings() {
const loadingEl = document.getElementById('loading');
const gridEl = document.getElementById('listings-grid');
const emptyEl = document.getElementById('empty-state');
loadingEl.style.display = 'block';
gridEl.innerHTML = '';
emptyEl.style.display = 'none';
try {
const params = new URLSearchParams();
// If signature model selected, use it; otherwise use brand filter
if (currentFilters.signature) {
params.append('model', currentFilters.signature);
} else if (currentFilters.brand) {
params.append('brand', currentFilters.brand);
}
if (currentFilters.minPrice) params.append('minPrice', currentFilters.minPrice);
if (currentFilters.maxPrice) params.append('maxPrice', currentFilters.maxPrice);
if (currentFilters.type) params.append('type', currentFilters.type);
if (currentFilters.dealOnly) params.append('dealOnly', 'true');
if (currentFilters.minDeal) params.append('minDeal', currentFilters.minDeal);
params.append('sort', currentFilters.sort);
params.append('limit', '1000'); // Get more items for infinite scroll
const response = await fetch(`${API_BASE}/api/listings?${params}`);
const data = await response.json();
loadingEl.style.display = 'none';
if (data.listings.length === 0) {
emptyEl.style.display = 'block';
allListings = [];
displayedCount = 0;
return;
}
// Store all listings and render first batch
allListings = data.listings;
displayedCount = 0;
renderNextBatch();
} catch (error) {
console.error('Error loading listings:', error);
loadingEl.textContent = 'Error loading listings. Please try again.';
}
}
// Calculate profit for a listing
function calculateProfit(listing) {
const estimatedUSPrice = listing.avg_us_price || (listing.price_usd * 1.8);
const shippingCost = 50;
const platformFee = estimatedUSPrice * 0.15;
const authenticationCost = 50;
const totalCosts = listing.price_usd + shippingCost + platformFee + authenticationCost;
const grossProfit = estimatedUSPrice - totalCosts;
const profitMargin = ((grossProfit / totalCosts) * 100);
return { grossProfit, profitMargin };
}
// Setup infinite scroll
function setupInfiniteScroll() {
const sentinel = document.createElement('div');
sentinel.id = 'scroll-sentinel';
sentinel.style.height = '1px';
document.querySelector('main').appendChild(sentinel);
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !isLoading && displayedCount < allListings.length) {
renderNextBatch();
}
}, { threshold: 0.5 });
observer.observe(sentinel);
}
// Render next batch of listings
function renderNextBatch() {
if (isLoading || displayedCount >= allListings.length) return;
isLoading = true;
const gridEl = document.getElementById('listings-grid');
// Sort all listings based on current filter
let sortedListings = [...allListings];
if (currentFilters.sort === 'profit') {
sortedListings.sort((a, b) => {
const profitA = calculateProfit(a).grossProfit;
const profitB = calculateProfit(b).grossProfit;
return profitB - profitA;
});
} else if (currentFilters.sort === 'profit_margin') {
sortedListings.sort((a, b) => {
const marginA = calculateProfit(a).profitMargin;
const marginB = calculateProfit(b).profitMargin;
return marginB - marginA;
});
} else if (currentFilters.sort === 'price_low') {
sortedListings.sort((a, b) => a.price_usd - b.price_usd);
} else if (currentFilters.sort === 'date') {
sortedListings.sort((a, b) => new Date(b.crawled_at) - new Date(a.crawled_at));
} else if (currentFilters.sort === 'brand') {
sortedListings.sort((a, b) => {
const brandA = (a.brand || '').toLowerCase();
const brandB = (b.brand || '').toLowerCase();
return brandA.localeCompare(brandB);
});
}
// Update allListings with sorted version
allListings = sortedListings;
// Get next batch
const nextBatch = sortedListings.slice(displayedCount, displayedCount + ITEMS_PER_PAGE);
nextBatch.forEach((listing, index) => {
const card = createListingCard(listing, displayedCount + index);
gridEl.appendChild(card);
});
displayedCount += nextBatch.length;
isLoading = false;
// Show load more indicator
updateLoadMoreIndicator();
}
// Update load more indicator
function updateLoadMoreIndicator() {
let indicator = document.getElementById('load-more-indicator');
if (!indicator) {
indicator = document.createElement('div');
indicator.id = 'load-more-indicator';
indicator.style.cssText = 'text-align: center; padding: 2rem; color: #7f8c8d; font-weight: 600;';
const sentinel = document.getElementById('scroll-sentinel');
sentinel.parentNode.insertBefore(indicator, sentinel);
}
if (displayedCount < allListings.length) {
indicator.innerHTML = `
<div style="display: inline-block; animation: pulse 1.5s ease-in-out infinite;">
📦 Loading more deals... (${displayedCount} of ${allListings.length})
</div>
`;
} else if (allListings.length > 0) {
indicator.innerHTML = `✅ All ${allListings.length} listings loaded!`;
} else {
indicator.innerHTML = '';
}
}
// Create listing card
function createListingCard(listing, index) {
const card = document.createElement('div');
card.className = 'listing-card';
card.style.animationDelay = `${index * 0.05}s`;
// Calculate profit potential with detailed market research logic
const brandMultipliers = {
'Hermes': 2.5, // Hermes bags hold 150%+ value
'Chanel': 2.2, // Classic Chanel appreciates
'Louis Vuitton': 1.9, // Strong resale market
'Gucci': 1.7, // Popular but more supply
'Dior': 1.8,
'Prada': 1.6,
'Fendi': 1.7,
'Balenciaga': 1.6,
'Celine': 1.8,
'Bottega Veneta': 1.7
};
const brandKey = Object.keys(brandMultipliers).find(b =>
listing.brand && listing.brand.toLowerCase().includes(b.toLowerCase())
);
const multiplier = brandKey ? brandMultipliers[brandKey] : 1.8;
const estimatedUSPrice = listing.avg_us_price || (listing.price_usd * multiplier);
const shippingCost = 50; // Average shipping from Japan via Buyee/FromJapan
const platformFee = estimatedUSPrice * 0.15; // 15% avg platform commission
const authenticationCost = 50; // Entrupy or Real Authentication
const totalCosts = listing.price_usd + shippingCost + platformFee + authenticationCost;
const grossProfit = estimatedUSPrice - totalCosts;
const profitMargin = ((grossProfit / totalCosts) * 100).toFixed(0);
// Add deal class based on profit margin
if (profitMargin >= 50) {
card.classList.add('deal-exceptional');
} else if (profitMargin >= 30) {
card.classList.add('deal-great');
} else if (profitMargin >= 20) {
card.classList.add('deal-good');
}
// Profit badge
let profitBadge = '';
if (grossProfit > 0 && profitMargin >= 20) {
const badgeClass = profitMargin >= 50 ? 'badge-exceptional' :
profitMargin >= 30 ? 'badge-great' : 'badge-good';
const emoji = profitMargin >= 50 ? '💰💰💰' :
profitMargin >= 30 ? '💰💰' : '💰';
profitBadge = `<div class="listing-badge ${badgeClass}">${emoji} ${profitMargin}% Profit</div>`;
}
// Image
const imageUrl = listing.thumbnail_url || listing.image_url || 'https://via.placeholder.com/280x250?text=No+Image';
// Listing type
const typeIcon = listing.listing_type === 'auction' ? '⚡' : '🛒';
const typeClass = listing.listing_type === 'auction' ? 'type-auction' : 'type-buy';
const typeText = listing.listing_type === 'auction' ? 'Auction' : 'Buy Now';
// Where to sell with detailed platform info and quick-list buttons
const translatedTitle = translateJapanese(listing.title);
const translatedBrand = translateJapanese(listing.brand || '');
const sellPlatforms = [];
const platformInfo = {
'Fashionphile': {
commission: '15-30%',
speed: 'Fast (24-48h)',
bestFor: 'Hermes, Chanel, LV',
url: 'https://www.fashionphile.com/sell-to-us',
searchUrl: `https://www.fashionphile.com/search?query=${encodeURIComponent(translatedBrand)}`,
color: '#000'
},
'Rebag': {
commission: '15-25%',
speed: 'Instant (Quote)',
bestFor: 'All luxury',
url: 'https://www.rebag.com/sell',
searchUrl: `https://www.rebag.com/shop?q=${encodeURIComponent(translatedBrand)}`,
color: '#FF6B6B'
},
'TheRealReal': {
commission: '15-50%',
speed: 'Medium (3-7 days)',
bestFor: 'High-end vintage',
url: 'https://www.therealreal.com/consign',
searchUrl: `https://www.therealreal.com/designers/${translatedBrand.toLowerCase().replace(/\s+/g, '-')}`,
color: '#2E7D32'
},
'Vestiaire': {
commission: '12-25%',
speed: 'Medium (5-14 days)',
bestFor: 'European brands',
url: 'https://www.vestiairecollective.com/sell/',
searchUrl: `https://www.vestiairecollective.com/search/?q=${encodeURIComponent(translatedBrand)}`,
color: '#8E44AD'
},
'eBay Auth': {
commission: '13%',
speed: 'Variable',
bestFor: 'All brands',
url: 'https://www.ebay.com/sh/sell',
searchUrl: `https://www.ebay.com/sch/i.html?_nkw=${encodeURIComponent(translatedBrand + ' bag')}&LH_Sold=1&LH_Complete=1`,
color: '#E53935'
},
'Poshmark': {
commission: '20%',
speed: 'Fast',
bestFor: 'Mid-tier luxury',
url: 'https://poshmark.com/sell',
searchUrl: `https://poshmark.com/search?query=${encodeURIComponent(translatedBrand)}&department=Women&type=bags`,
color: '#721B65'
}
};
if (translatedBrand && ['Hermes', 'Chanel', 'Louis Vuitton'].some(b => translatedBrand.includes(b))) {
sellPlatforms.push('Fashionphile', 'Rebag', 'TheRealReal');
} else if (translatedBrand && ['Gucci', 'Prada', 'Dior'].some(b => translatedBrand.includes(b))) {
sellPlatforms.push('Rebag', 'Vestiaire', 'eBay Auth');
} else {
sellPlatforms.push('eBay Auth', 'Poshmark', 'Vestiaire');
}
const sellPlatformsHTML = sellPlatforms.length > 0 ? `
<div class="sell-platforms">
<div class="sell-header">
<span class="sell-label">💼 Best Resale Platforms</span>
<span class="sell-subtitle">Research comps or quick-list this item in seconds</span>
</div>
<div class="platform-grid">
${sellPlatforms.map(p => {
const info = platformInfo[p];
return `
<div class="platform-card-compact">
<div class="platform-info">
<div class="platform-name">${p}</div>
<div class="platform-meta">
<span class="platform-commission">${info.commission} fee</span>
<span class="platform-speed">${info.speed}</span>
</div>
</div>
<div class="platform-buttons">
<button class="btn-platform-research" onclick="window.open('${info.searchUrl}', '_blank'); event.stopPropagation();" title="See sold ${translatedBrand} bags on ${p}">
📊 Sold
</button>
<button class="btn-platform-list" onclick="quickListItem('${p}', '${escapeHtml(translatedTitle)}', ${estimatedUSPrice}, '${info.url}', '${listing.image_url}'); event.stopPropagation();" title="Quick-list to ${p}">
🚀 List
</button>
</div>
</div>
`;
}).join('')}
</div>
<div class="pricing-logic">
<strong>💡 Pricing Logic:</strong> ${translatedBrand || 'These'} bags resell at <strong>${multiplier}x</strong> Japan price (${Math.round((multiplier - 1) * 100)}% markup).
<a href="https://www.google.com/search?q=${encodeURIComponent((translatedBrand || 'luxury') + ' bag sold prices')}" target="_blank" rel="noopener noreferrer" class="research-link">See market data →</a>
</div>
</div>
` : '';
// Profit breakdown
const profitBreakdown = grossProfit > 0 ? `
<div class="profit-breakdown">
<div class="profit-header">
<span class="profit-label">Est. Profit</span>
<span class="profit-amount ${grossProfit > 200 ? 'profit-high' : 'profit-moderate'}">$${formatNumber(grossProfit)}</span>
</div>
<div class="profit-details">
<div class="profit-row">
<span>Resale Price:</span>
<span class="profit-value">$${formatNumber(estimatedUSPrice)}</span>
</div>
<div class="profit-row cost">
<span>Your Cost:</span>
<span class="profit-value">-$${formatNumber(listing.price_usd)}</span>
</div>
<div class="profit-row cost">
<span>Shipping:</span>
<span class="profit-value">-$${shippingCost}</span>
</div>
<div class="profit-row cost">
<span>Auth Fee:</span>
<span class="profit-value">-$${authenticationCost}</span>
</div>
<div class="profit-row cost">
<span>Platform Fee (15%):</span>
<span class="profit-value">-$${formatNumber(platformFee)}</span>
</div>
</div>
</div>
` : '';
card.innerHTML = `
${profitBadge}
<img src="${imageUrl}" alt="${translatedTitle}" class="listing-image" loading="lazy" onerror="this.src='https://via.placeholder.com/280x250?text=No+Image'">
<div class="listing-content">
<div class="listing-brand">${escapeHtml(translatedBrand)}</div>
<div class="listing-title">${escapeHtml(translatedTitle)}</div>
<div class="listing-prices">
<div class="jp-price">
<span class="currency">¥</span>${formatNumber(listing.price_jpy)}
</div>
<div class="usd-price">≈ $${formatNumber(listing.price_usd)}</div>
</div>
<!-- DETAILED SPECS -->
<div class="product-specs">
${listing.size ? `<div class="spec-item"><strong>📏 Size:</strong> ${escapeHtml(translateJapanese(listing.size))}</div>` : ''}
${listing.color ? `<div class="spec-item"><strong>🎨 Color:</strong> ${escapeHtml(translateJapanese(listing.color))}</div>` : ''}
${listing.material ? `<div class="spec-item"><strong>🧵 Material:</strong> ${escapeHtml(translateJapanese(listing.material))}</div>` : ''}
${listing.model ? `<div class="spec-item"><strong>👜 Model:</strong> ${escapeHtml(translateJapanese(listing.model))}</div>` : ''}
${listing.external_id ? `<div class="spec-item"><strong>🔢 ID:</strong> ${escapeHtml(listing.external_id)}</div>` : ''}
${listing.auction_end_date ? `<div class="spec-item"><strong>⏰ Ends:</strong> ${escapeHtml(listing.auction_end_date)}</div>` : ''}
</div>
<!-- CONDITION ANALYSIS -->
${getConditionAnalysis(listing)}
${profitBreakdown}
${sellPlatformsHTML}
<div class="listing-meta">
<span class="condition-badge">${escapeHtml(translateJapanese(listing.condition || 'Pre-Owned'))}</span>
<span class="listing-type ${typeClass}">${typeIcon} ${typeText}</span>
</div>
${getListingAge(listing)}
<div class="listing-source">
<span>📍 ${escapeHtml(listing.source)}</span>
</div>
<div class="action-buttons">
<button class="btn-detail" onclick="window.location.href='/product/${listing.id}'; event.stopPropagation();" title="View complete arbitrage analysis with SKU matching, price history, and more!">
📊 Full Analysis
</button>
<button class="btn-presell" onclick="presellItem(${listing.id}, '${escapeHtml(translatedTitle)}', ${estimatedUSPrice}, '${listing.product_url}', '${listing.image_url}'); event.stopPropagation();" title="List this for sale BEFORE purchasing - lock in profit!">
⚡ Presell Strategy
</button>
<button class="btn-view" onclick="window.open('${listing.product_url}', '_blank'); event.stopPropagation();">View Japan Listing →</button>
</div>
</div>
`;
// Make entire card clickable to product detail page
card.style.cursor = 'pointer';
card.addEventListener('click', (e) => {
// Don't navigate if clicking on buttons
if (!e.target.closest('button')) {
window.location.href = `/product/${listing.id}`;
}
});
return card;
}
// Export to CSV
async function exportToCSV() {
const params = new URLSearchParams();
if (currentFilters.dealOnly) params.append('dealOnly', 'true');
if (currentFilters.minDeal) params.append('minDeal', currentFilters.minDeal);
window.open(`${API_BASE}/api/export/csv?${params}`, '_blank');
}
// Trigger manual crawl
async function triggerCrawl() {
const btn = document.getElementById('btn-crawl');
btn.disabled = true;
btn.textContent = 'Crawling...';
try {
await fetch(`${API_BASE}/api/crawl`, { method: 'POST' });
alert('Crawl started! This will take several minutes. Refresh the page in 5-10 minutes to see new results.');
} catch (error) {
console.error('Error starting crawl:', error);
alert('Error starting crawl. Please try again.');
} finally {
btn.disabled = false;
btn.textContent = 'Start Crawl';
}
}
// Utility functions
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatNumber(num) {
if (!num) return '0';
return Math.round(num).toLocaleString();
}
// Initialize Condition Guide Modal
function initializeConditionGuide() {
if (!window.japaneseConditionGuide) return;
const guide = window.japaneseConditionGuide;
// Populate condition words
const wordsHtml = Object.entries(guide.conditionWords).map(([jp, info]) => `
<div class="condition-item ${info.severity}">
<strong>${jp}</strong> (${info.romaji}) → ${info.english}
<span class="impact-badge ${info.severity}">${info.impact}</span>
</div>
`).join('');
document.getElementById('condition-words-list').innerHTML = wordsHtml;
// Populate grades
const gradesHtml = Object.entries(guide.grades).map(([code, info]) => `
<div class="grade-item">
<strong>${code}</strong>: ${info.name} - ${info.description}
<span class="value-badge">${(info.valueMultiplier * 100).toFixed(0)}% of retail</span>
</div>
`).join('');
document.getElementById('condition-grades-list').innerHTML = gradesHtml;
// Populate safe phrases
const safeHtml = Object.entries(guide.safePhrases).map(([jp, en]) => `
<div class="phrase-item safe"><strong>${jp}</strong> → ${en}</div>
`).join('');
document.getElementById('safe-phrases-list').innerHTML = safeHtml;
// Populate caution phrases
const cautionHtml = Object.entries(guide.cautionPhrases).map(([jp, en]) => `
<div class="phrase-item caution"><strong>${jp}</strong> → ${en}</div>
`).join('');
document.getElementById('caution-phrases-list').innerHTML = cautionHtml;
// Populate photo checklist
const checklistHtml = guide.photoChecklist.map(item => `
<div class="checklist-item">${item}</div>
`).join('');
document.getElementById('photo-checklist-list').innerHTML = checklistHtml;
}
function openConditionGuide() {
document.getElementById('condition-guide-modal').style.display = 'flex';
}
function closeConditionGuide() {
document.getElementById('condition-guide-modal').style.display = 'none';
}
// Analyze listing condition using Japanese guide
function getConditionAnalysis(listing) {
if (!window.japaneseConditionGuide) return '';
const analysis = window.japaneseConditionGuide.analyzeTitle(listing.title);
if (!analysis.hasIssues) return '';
let html = '<div class="condition-warnings';
// Check if it's safe or caution
const isSafe = analysis.warnings.some(w => w.type === 'safe') && analysis.issues.length === 0;
if (isSafe) {
html += ' condition-safe"><div class="warning-title">✅ Condition Notes</div>';
} else {
html += '"><div class="warning-title">⚠️ Condition Issues Detected</div>';
}
// Show issues
if (analysis.issues.length > 0) {
analysis.issues.forEach(issue => {
html += `<div class="warning-item">• <strong>${issue.term}</strong> (${issue.english}) - ${issue.impact}</div>`;
});
if (analysis.estimatedPriceAdjustment < 0) {
html += `<div class="warning-item" style="margin-top: 0.5rem; font-weight: 700;">💰 Est. price adjustment: ${analysis.estimatedPriceAdjustment}%</div>`;
}
}
// Show warnings
if (analysis.warnings.length > 0) {
analysis.warnings.forEach(warning => {
html += `<div class="warning-item">• ${warning.text}</div>`;
});
}
html += '</div>';
return html;
}
// Simple Japanese to English translation for common terms
function translateJapanese(text) {
if (!text || !autoTranslateEnabled) return text;
const translations = {
// Brands
'グッチ': 'Gucci',
'ルイヴィトン': 'Louis Vuitton',
'ルイ・ヴィトン': 'Louis Vuitton',
'シャネル': 'Chanel',
'エルメス': 'Hermes',
'プラダ': 'Prada',
'ディオール': 'Dior',
'クリスチャンディオール': 'Christian Dior',
'フェンディ': 'Fendi',
'バレンシアガ': 'Balenciaga',
'セリーヌ': 'Celine',
'ボッテガヴェネタ': 'Bottega Veneta',
'ボッテガ': 'Bottega',
'サンローラン': 'Saint Laurent',
'イヴサンローラン': 'Yves Saint Laurent',
'ロエベ': 'Loewe',
'バーバリー': 'Burberry',
'コーチ': 'Coach',
'マイケルコース': 'Michael Kors',
'トリーバーチ': 'Tory Burch',
'ケイトスペード': 'Kate Spade',
'フルラ': 'Furla',
'マークジェイコブス': 'Marc Jacobs',
'ジバンシィ': 'Givenchy',
'ヴァレンティノ': 'Valentino',
'ミュウミュウ': 'Miu Miu',
// Bag Models
'バーキン': 'Birkin',
'ケリー': 'Kelly',
'コンスタンス': 'Constance',
'ガーデンパーティ': 'Garden Party',
'ピコタン': 'Picotan',
'ボリード': 'Bolide',
'エヴリン': 'Evelyne',
'リンディ': 'Lindy',
'スピーディ': 'Speedy',
'ネヴァーフル': 'Neverfull',
'アルマ': 'Alma',
'カプシーヌ': 'Capucines',
'ツイスト': 'Twist',
'モノグラム': 'Monogram',
'ダミエ': 'Damier',
'エピ': 'Epi',
'マトラッセ': 'Matelasse',
'チェーンショルダー': 'Chain Shoulder',
'フラップバッグ': 'Flap Bag',
'クラシックフラップ': 'Classic Flap',
'ボーイシャネル': 'Boy Chanel',
'レディディオール': 'Lady Dior',
'サドルバッグ': 'Saddle Bag',
'ブックトート': 'Book Tote',
// Common terms
'バッグ': 'Bag',
'ハンドバッグ': 'Handbag',
'ショルダーバッグ': 'Shoulder Bag',
'トートバッグ': 'Tote Bag',
'クラッチバッグ': 'Clutch Bag',
'ポーチ': 'Pouch',
'財布': 'Wallet',
'長財布': 'Long Wallet',
'二つ折り財布': 'Bifold Wallet',
'カードケース': 'Card Case',
'キーケース': 'Key Case',
// Conditions
'美品': 'Excellent',
'極美品': 'Near Mint',
'良品': 'Good',
'中古': 'Used',
'新品': 'New',
'新品同様': 'Like New',
'未使用': 'Unused',
'未使用品': 'Unused Item',
'展示品': 'Display Item',
'訳あり': 'With Issues',
// Authenticity
'正規品': 'Authentic',
'本物': 'Genuine',
'鑑定済み': 'Authenticated',
'真贋鑑定済': 'Authenticity Verified',
// Accessories
'送料無料': 'Free Shipping',
'送料込み': 'Shipping Included',
'即決': 'Buy It Now',
'即決価格': 'Buy It Now Price',
'レア': 'Rare',
'ヴィンテージ': 'Vintage',
'ビンテージ': 'Vintage',
'限定': 'Limited',
'限定品': 'Limited Edition',
'保証書付': 'With Certificate',
'保証書': 'Certificate',
'箱付き': 'With Box',
'箱付': 'With Box',
'付属品完備': 'Complete Set',
'付属品': 'Accessories',
'保存袋': 'Dust Bag',
'ギャランティカード': 'Guarantee Card',
'購入証明': 'Proof of Purchase',
// Colors
'黒': 'Black',
'ブラック': 'Black',
'白': 'White',
'ホワイト': 'White',
'茶': 'Brown',
'ブラウン': 'Brown',
'ベージュ': 'Beige',
'赤': 'Red',
'レッド': 'Red',
'青': 'Blue',
'ブルー': 'Blue',
'ネイビー': 'Navy',
'緑': 'Green',
'グリーン': 'Green',
'ピンク': 'Pink',
'黄': 'Yellow',
'イエロー': 'Yellow',
'オレンジ': 'Orange',
'パープル': 'Purple',
'紫': 'Purple',
'グレー': 'Gray',
'金': 'Gold',
'ゴールド': 'Gold',
'銀': 'Silver',
'シルバー': 'Silver',
// Materials
'レザー': 'Leather',
'革': 'Leather',
'本革': 'Genuine Leather',
'キャンバス': 'Canvas',
'ナイロン': 'Nylon',
'スエード': 'Suede',
'エナメル': 'Patent',
'パテントレザー': 'Patent Leather',
'クロコダイル': 'Crocodile',
'アリゲーター': 'Alligator',
'オーストリッチ': 'Ostrich',
'パイソン': 'Python',
// Sizes
'小': 'Small',
'中': 'Medium',
'大': 'Large',
'ミニ': 'Mini',
'プチ': 'Petit',
'PM': 'PM',
'MM': 'MM',
'GM': 'GM',
// Time
'即日発送': 'Same Day Shipping',
'翌日発送': 'Next Day Shipping'
};
let translated = text;
for (const [jp, en] of Object.entries(translations)) {
translated = translated.replace(new RegExp(jp, 'gi'), en);
}
return translated;
}
// Add translate toggle button to the page
function addTranslateToggle() {
const toggleContainer = document.createElement('div');
toggleContainer.className = 'translate-toggle-container';
toggleContainer.innerHTML = `
<label class="translate-toggle">
<input type="checkbox" id="auto-translate-toggle" ${autoTranslateEnabled ? 'checked' : ''}>
<span class="toggle-slider"></span>
<span class="toggle-label">🌐 Auto-Translate to English</span>
</label>
`;
// Insert after the filters section
const filtersSection = document.querySelector('.filters');
if (filtersSection) {
filtersSection.parentNode.insertBefore(toggleContainer, filtersSection.nextSibling);
}
// Add event listener
document.getElementById('auto-translate-toggle').addEventListener('change', (e) => {
autoTranslateEnabled = e.target.checked;
localStorage.setItem('autoTranslateEnabled', autoTranslateEnabled);
loadListings(); // Reload listings with/without translation
});
// Load saved preference
const saved = localStorage.getItem('autoTranslateEnabled');
if (saved !== null) {
autoTranslateEnabled = saved === 'true';
document.getElementById('auto-translate-toggle').checked = autoTranslateEnabled;
}
}
// Calculate listing age and negotiation potential
function getListingAge(listing) {
if (!listing.crawled_at) return '';
const crawledDate = new Date(listing.crawled_at);
const now = new Date();
const daysOld = Math.floor((now - crawledDate) / (1000 * 60 * 60 * 24));
const hoursOld = Math.floor((now - crawledDate) / (1000 * 60 * 60));
let ageText = '';
let ageClass = '';
let negotiationTip = '';
if (daysOld === 0) {
if (hoursOld === 0) {
ageText = '🆕 Listed just now';
ageClass = 'age-fresh';
negotiationTip = 'Brand new listing! Act fast before others see it.';
} else if (hoursOld === 1) {
ageText = '🆕 Listed 1 hour ago';
ageClass = 'age-fresh';
negotiationTip = 'Very fresh listing! Seller may be motivated.';
} else {
ageText = `🆕 Listed ${hoursOld} hours ago`;
ageClass = 'age-fresh';
negotiationTip = 'Fresh listing! Good time to make an offer.';
}
} else if (daysOld === 1) {
ageText = '📅 Listed 1 day ago';
ageClass = 'age-recent';
negotiationTip = 'Recent listing. Seller still evaluating offers.';
} else if (daysOld <= 7) {
ageText = `📅 Listed ${daysOld} days ago`;
ageClass = 'age-recent';
negotiationTip = 'Listed for a week. Consider making an offer.';
} else if (daysOld <= 30) {
ageText = `⏰ Listed ${daysOld} days ago`;
ageClass = 'age-stale';
negotiationTip = `${daysOld} days on market. Seller likely motivated - try 10-15% below asking!`;
} else if (daysOld <= 60) {
ageText = `⚠️ Listed ${daysOld} days ago`;
ageClass = 'age-old';
negotiationTip = `Over ${daysOld} days! High negotiation potential - try 20-25% below asking price.`;
} else {
ageText = `🔥 Listed ${daysOld} days ago`;
ageClass = 'age-very-old';
negotiationTip = `${daysOld} days on market! Seller very motivated - try 30%+ discount or best offer!`;
}
return `
<div class="listing-age ${ageClass}">
<div class="age-header">
<span class="age-text">${ageText}</span>
</div>
<div class="negotiation-tip">
💡 <strong>Negotiation Strategy:</strong> ${negotiationTip}
</div>
</div>
`;
}
// PRESELL Strategy - List item for sale BEFORE purchasing from Japan
function presellItem(listingId, title, estimatedPrice, japanUrl, imageUrl) {
const presellData = {
listingId,
title,
estimatedPrice,
japanUrl,
imageUrl,
timestamp: Date.now()
};
// Save to presell queue
let presellQueue = JSON.parse(localStorage.getItem('luxarbPresellQueue') || '[]');
presellQueue.push(presellData);
localStorage.setItem('luxarbPresellQueue', JSON.stringify(presellQueue));
const modal = `
<div class="presell-modal" id="presell-modal">
<div class="presell-content">
<div class="presell-header">
<h2>⚡ Presell Strategy Activated</h2>
<button onclick="document.getElementById('presell-modal').remove();" class="close-btn">×</button>
</div>
<div class="presell-body">
<h3>🎯 How This Works:</h3>
<ol class="presell-steps">
<li><strong>List NOW on your platform</strong> (eBay, Poshmark, etc.) at $${Math.round(estimatedPrice)}</li>
<li><strong>Set handling time to 5-7 business days</strong> (gives you time to purchase & ship from Japan)</li>
<li><strong>When item sells</strong> → Purchase from Japan immediately</li>
<li><strong>Ship directly</strong> from Japan to buyer using Buyee/FromJapan forwarding</li>
<li><strong>Pocket the profit</strong> with ZERO inventory risk!</li>
</ol>
<div class="presell-risks">
<h4>⚠️ Important Notes:</h4>
<ul>
<li>Ensure Japan listing is still available before your listing sells</li>
<li>Factor in 7-10 day shipping from Japan</li>
<li>Have backup plan if Japan item sells out</li>
<li>Only presell if Japan listing is "Buy Now" (not auction)</li>
</ul>
</div>
<div class="presell-templates">
<h4>📋 Copy Listing Description:</h4>
<textarea readonly onclick="this.select();" class="presell-textarea">
${title}
✅ Authentic Designer Bag
📦 Ships from USA (7-10 business days)
🔐 100% Authentic - Sourced from Japan
📸 Photos are of actual item
💯 Money-back guarantee if not authentic
Item will be shipped directly from our trusted Japan supplier. All items are professionally authenticated before shipping. Ships within 5-7 business days of purchase.
</textarea>
<button onclick="navigator.clipboard.writeText(this.previousElementSibling.value); alert('Copied to clipboard!');" class="btn-copy">📋 Copy to Clipboard</button>
</div>
<div class="presell-actions">
<button onclick="window.open('${japanUrl}', '_blank');" class="btn-presell-action">🇯🇵 Keep Japan Tab Open</button>
<button onclick="window.open('https://www.ebay.com/sl/sell', '_blank');" class="btn-presell-action">📤 List on eBay</button>
<button onclick="window.open('https://poshmark.com/sell', '_blank');" class="btn-presell-action">📤 List on Poshmark</button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modal);
}
// Quick-list function to auto-draft listings on resale platforms
function quickListItem(platform, title, price, platformUrl, imageUrl) {
// Create a pre-filled listing template
const listingData = {
title: title,
price: Math.round(price),
description: `Authentic ${title} from Japan. Ships from USA. All sales final. Authentication available.`,
images: [imageUrl],
category: 'Luxury Handbags',
condition: 'Pre-Owned'
};
// Store in localStorage for cross-tab communication
localStorage.setItem('luxarbQuickList', JSON.stringify({
platform,
data: listingData,
timestamp: Date.now()
}));
// Open platform in new tab
window.open(platformUrl, '_blank');
// Show confirmation
alert(`✅ Listing template saved!\n\n📋 Title: ${title}\n💵 Suggested Price: $${Math.round(price)}\n\nThe ${platform} listing page will open. Paste the details from your clipboard or use our browser extension for auto-fill!`);
// Copy to clipboard
const clipboardText = `Title: ${title}\nPrice: $${Math.round(price)}\nDescription: ${listingData.description}`;
navigator.clipboard.writeText(clipboardText).catch(() => {});
}
// Auto-refresh functionality
function startAutoRefresh() {
// Show live banner
const banner = document.getElementById('live-banner');
banner.style.display = 'block';
// Refresh every 5 seconds to show new listings as they come in
autoRefreshInterval = setInterval(async () => {
try {
const params = new URLSearchParams();
if (currentFilters.brand) params.append('brand', currentFilters.brand);
if (currentFilters.minPrice) params.append('minPrice', currentFilters.minPrice);
if (currentFilters.maxPrice) params.append('maxPrice', currentFilters.maxPrice);
if (currentFilters.type) params.append('type', currentFilters.type);
if (currentFilters.dealOnly) params.append('dealOnly', 'true');
if (currentFilters.minDeal) params.append('minDeal', currentFilters.minDeal);
params.append('sort', currentFilters.sort);
const response = await fetch(`${API_BASE}/api/listings?${params}`);
const data = await response.json();
// Only update if count changed
if (data.listings.length !== lastListingCount) {
const newCount = data.listings.length;
const addedCount = newCount - lastListingCount;
if (addedCount > 0) {
banner.innerHTML = `✨ ${addedCount} new product${addedCount > 1 ? 's' : ''} added! Total: ${newCount}`;
banner.style.background = 'linear-gradient(135deg, #ffa502 0%, #ff6348 100%)';
// Flash effect
setTimeout(() => {
banner.style.background = 'linear-gradient(135deg, #2ed573 0%, #26de81 100%)';
banner.innerHTML = '🔄 Auto-refreshing every 5 seconds - New products appear as they\'re crawled!';
}, 3000);
}
lastListingCount = newCount;
loadStats();
const gridEl = document.getElementById('listings-grid');
const emptyEl = document.getElementById('empty-state');
const loadingEl = document.getElementById('loading');
loadingEl.style.display = 'none';
if (data.listings.length === 0) {
emptyEl.style.display = 'block';
gridEl.innerHTML = '';
allListings = [];
displayedCount = 0;
} else {
emptyEl.style.display = 'none';
gridEl.innerHTML = '';
allListings = data.listings;
displayedCount = 0;
renderNextBatch();
}
}
} catch (error) {
console.error('Auto-refresh error:', error);
}
}, 5000); // Refresh every 5 seconds
}