Spaces:
Running
Running
when writing listing html also fetch Tracklist
Browse files[Fetch from Discogs and gather any interesting info about this particular release that would be of significance, in 1 identifying the issue variation and alsoo to build confidence with the buyer, we also want to create a seperat tool that works like the isdentifier and ling creator but is purly for identifing potential buys, that the user is considering or believe is proced low for them to make a profit
- collection.html +9 -5
- components/deal-finder.js +152 -0
- components/discogs-service.js +68 -1
- components/vinyl-footer.js +1 -0
- components/vinyl-nav.js +4 -0
- deals.html +233 -0
- deals.js +415 -0
- script.js +124 -19
collection.html
CHANGED
|
@@ -51,11 +51,15 @@
|
|
| 51 |
Import Backup
|
| 52 |
</button>
|
| 53 |
<input type="file" id="jsonImportInput" accept=".json" class="hidden" onchange="importCollectionFromJSON(this.files[0])">
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
<i data-feather="save" class="w-4 h-4"></i>
|
| 60 |
Export
|
| 61 |
</button>
|
|
|
|
| 51 |
Import Backup
|
| 52 |
</button>
|
| 53 |
<input type="file" id="jsonImportInput" accept=".json" class="hidden" onchange="importCollectionFromJSON(this.files[0])">
|
| 54 |
+
<button onclick="window.location.href='deals.html'" class="px-4 py-2 bg-surface-light border border-deal/50 text-deal rounded-lg font-medium hover:bg-deal/10 transition-all flex items-center gap-2">
|
| 55 |
+
<i data-feather="search" class="w-4 h-4"></i>
|
| 56 |
+
Find Deals
|
| 57 |
+
</button>
|
| 58 |
+
<button onclick="showImportModal()" class="px-4 py-2 bg-surface-light border border-gray-600 rounded-lg font-medium hover:border-primary hover:text-primary transition-all flex items-center gap-2">
|
| 59 |
+
<i data-feather="upload" class="w-4 h-4"></i>
|
| 60 |
+
Import Discogs CSV
|
| 61 |
+
</button>
|
| 62 |
+
<button onclick="exportCollection()" class="px-4 py-2 bg-surface-light border border-gray-600 rounded-lg font-medium hover:border-accent hover:text-accent transition-all flex items-center gap-2" title="Export backup">
|
| 63 |
<i data-feather="save" class="w-4 h-4"></i>
|
| 64 |
Export
|
| 65 |
</button>
|
components/deal-finder.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class DealFinder extends HTMLElement {
|
| 2 |
+
constructor() {
|
| 3 |
+
super();
|
| 4 |
+
this.deals = [];
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
connectedCallback() {
|
| 8 |
+
this.attachShadow({ mode: 'open' });
|
| 9 |
+
this.render();
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
render() {
|
| 13 |
+
this.shadowRoot.innerHTML = `
|
| 14 |
+
<style>
|
| 15 |
+
:host {
|
| 16 |
+
display: block;
|
| 17 |
+
}
|
| 18 |
+
.deal-card {
|
| 19 |
+
background: #1e293b;
|
| 20 |
+
border: 1px solid #334155;
|
| 21 |
+
border-radius: 12px;
|
| 22 |
+
padding: 1.5rem;
|
| 23 |
+
transition: all 0.2s ease;
|
| 24 |
+
}
|
| 25 |
+
.deal-card:hover {
|
| 26 |
+
border-color: #ec4899;
|
| 27 |
+
transform: translateY(-2px);
|
| 28 |
+
}
|
| 29 |
+
.deal-card.hot {
|
| 30 |
+
border-color: #22c55e;
|
| 31 |
+
background: linear-gradient(135deg, rgba(34, 197, 94, 0.05) 0%, #1e293b 100%);
|
| 32 |
+
}
|
| 33 |
+
.deal-card.skip {
|
| 34 |
+
opacity: 0.7;
|
| 35 |
+
border-color: #334155;
|
| 36 |
+
}
|
| 37 |
+
.profit-badge {
|
| 38 |
+
display: inline-flex;
|
| 39 |
+
align-items: center;
|
| 40 |
+
gap: 4px;
|
| 41 |
+
padding: 4px 12px;
|
| 42 |
+
border-radius: 9999px;
|
| 43 |
+
font-size: 12px;
|
| 44 |
+
font-weight: 600;
|
| 45 |
+
}
|
| 46 |
+
.profit-badge.good {
|
| 47 |
+
background: #22c55e20;
|
| 48 |
+
color: #22c55e;
|
| 49 |
+
}
|
| 50 |
+
.profit-badge.medium {
|
| 51 |
+
background: #f59e0b20;
|
| 52 |
+
color: #f59e0b;
|
| 53 |
+
}
|
| 54 |
+
.profit-badge.bad {
|
| 55 |
+
background: #ef444420;
|
| 56 |
+
color: #ef4444;
|
| 57 |
+
}
|
| 58 |
+
</style>
|
| 59 |
+
<slot></slot>
|
| 60 |
+
`;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
calculateMetrics(buyPrice, estimatedValue, condition = 'VG', goal = 'balanced') {
|
| 64 |
+
// Condition multipliers for quick estimation
|
| 65 |
+
const conditionMultipliers = {
|
| 66 |
+
'M': 1.5, 'NM': 1.3, 'VG+': 1.0, 'VG': 0.7,
|
| 67 |
+
'G+': 0.5, 'G': 0.35, 'F': 0.2, 'P': 0.1
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
const condMult = conditionMultipliers[condition] || 0.7;
|
| 71 |
+
const adjustedValue = estimatedValue * condMult;
|
| 72 |
+
|
| 73 |
+
// Fee calculation (eBay UK approx)
|
| 74 |
+
const ebayFees = adjustedValue * 0.13;
|
| 75 |
+
const paypalFees = adjustedValue * 0.029 + 0.30;
|
| 76 |
+
const shipping = 4.50;
|
| 77 |
+
const packing = 1.50;
|
| 78 |
+
const totalFees = ebayFees + paypalFees + shipping + packing;
|
| 79 |
+
|
| 80 |
+
// Minimum profit threshold
|
| 81 |
+
const minProfit = Math.max(buyPrice * 0.30, 3); // 30% or £3 minimum
|
| 82 |
+
|
| 83 |
+
// Suggested listing price based on goal
|
| 84 |
+
let listingPrice;
|
| 85 |
+
switch(goal) {
|
| 86 |
+
case 'quick':
|
| 87 |
+
listingPrice = adjustedValue * 0.85;
|
| 88 |
+
break;
|
| 89 |
+
case 'max':
|
| 90 |
+
listingPrice = adjustedValue * 1.1;
|
| 91 |
+
break;
|
| 92 |
+
default:
|
| 93 |
+
listingPrice = adjustedValue;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
const netProfit = listingPrice - buyPrice - totalFees;
|
| 97 |
+
const roi = buyPrice > 0 ? ((netProfit / buyPrice) * 100).toFixed(1) : 0;
|
| 98 |
+
const margin = ((netProfit / listingPrice) * 100).toFixed(1);
|
| 99 |
+
|
| 100 |
+
// Deal score (0-100)
|
| 101 |
+
let score = 0;
|
| 102 |
+
if (netProfit >= minProfit) score += 40;
|
| 103 |
+
if (roi >= 30) score += 30;
|
| 104 |
+
if (roi >= 50) score += 20;
|
| 105 |
+
if (adjustedValue > buyPrice * 2) score += 10;
|
| 106 |
+
|
| 107 |
+
return {
|
| 108 |
+
buyPrice,
|
| 109 |
+
estimatedValue,
|
| 110 |
+
adjustedValue,
|
| 111 |
+
listingPrice: Math.round(listingPrice),
|
| 112 |
+
totalFees: Math.round(totalFees * 100) / 100,
|
| 113 |
+
netProfit: Math.round(netProfit * 100) / 100,
|
| 114 |
+
roi,
|
| 115 |
+
margin,
|
| 116 |
+
score,
|
| 117 |
+
isViable: netProfit >= minProfit && roi >= 20,
|
| 118 |
+
isHot: netProfit >= minProfit * 1.5 && roi >= 40,
|
| 119 |
+
recommendation: netProfit < 0 ? 'PASS' : roi >= 50 ? 'QUICK FLIP' : roi >= 30 ? 'GOOD DEAL' : 'MARGINAL'
|
| 120 |
+
};
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
analyzeDeal(artist, title, buyPrice, listedCondition, discogsData = null) {
|
| 124 |
+
// If we have Discogs data, use it for better estimation
|
| 125 |
+
let estimatedValue = 15; // Default fallback
|
| 126 |
+
|
| 127 |
+
if (discogsData) {
|
| 128 |
+
if (discogsData.lowest_price) {
|
| 129 |
+
estimatedValue = discogsData.lowest_price;
|
| 130 |
+
} else if (discogsData.median) {
|
| 131 |
+
estimatedValue = discogsData.median;
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
const metrics = this.calculateMetrics(buyPrice, estimatedValue, listedCondition);
|
| 136 |
+
|
| 137 |
+
return {
|
| 138 |
+
artist,
|
| 139 |
+
title,
|
| 140 |
+
...metrics,
|
| 141 |
+
discogsUrl: discogsData?.uri || null,
|
| 142 |
+
releaseId: discogsData?.id || null,
|
| 143 |
+
timestamp: Date.now()
|
| 144 |
+
};
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
formatCurrency(amount) {
|
| 148 |
+
return '£' + parseFloat(amount).toFixed(2);
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
customElements.define('deal-finder', DealFinder);
|
components/discogs-service.js
CHANGED
|
@@ -54,7 +54,6 @@ class DiscogsService {
|
|
| 54 |
const data = await response.json();
|
| 55 |
return data.results?.[0] || null;
|
| 56 |
}
|
| 57 |
-
|
| 58 |
async getReleaseDetails(releaseId) {
|
| 59 |
if (!this.key || !this.secret) return null;
|
| 60 |
|
|
@@ -69,6 +68,74 @@ class DiscogsService {
|
|
| 69 |
|
| 70 |
return await response.json();
|
| 71 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
}
|
| 73 |
|
| 74 |
window.discogsService = new DiscogsService();
|
|
|
|
| 54 |
const data = await response.json();
|
| 55 |
return data.results?.[0] || null;
|
| 56 |
}
|
|
|
|
| 57 |
async getReleaseDetails(releaseId) {
|
| 58 |
if (!this.key || !this.secret) return null;
|
| 59 |
|
|
|
|
| 68 |
|
| 69 |
return await response.json();
|
| 70 |
}
|
| 71 |
+
|
| 72 |
+
async fetchTracklist(releaseId) {
|
| 73 |
+
if (!this.key || !this.secret || !releaseId) return null;
|
| 74 |
+
|
| 75 |
+
try {
|
| 76 |
+
const details = await this.getReleaseDetails(releaseId);
|
| 77 |
+
if (!details || !details.tracklist) return null;
|
| 78 |
+
|
| 79 |
+
return {
|
| 80 |
+
tracklist: details.tracklist,
|
| 81 |
+
notes: details.notes,
|
| 82 |
+
styles: details.styles,
|
| 83 |
+
genres: details.genres,
|
| 84 |
+
identifiers: details.identifiers,
|
| 85 |
+
companies: details.companies,
|
| 86 |
+
barcode: details.barcode,
|
| 87 |
+
uri: details.uri,
|
| 88 |
+
master_id: details.master_id,
|
| 89 |
+
lowest_price: details.lowest_price,
|
| 90 |
+
num_for_sale: details.num_for_sale
|
| 91 |
+
};
|
| 92 |
+
} catch (e) {
|
| 93 |
+
console.error('Failed to fetch tracklist:', e);
|
| 94 |
+
return null;
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
async fetchMasterReleaseDetails(masterId) {
|
| 99 |
+
if (!this.key || !this.secret || !masterId) return null;
|
| 100 |
+
|
| 101 |
+
try {
|
| 102 |
+
const response = await fetch(`${this.baseUrl}/masters/${masterId}`, {
|
| 103 |
+
headers: {
|
| 104 |
+
'Authorization': `Discogs key=${this.key}, secret=${this.secret}`,
|
| 105 |
+
'User-Agent': this.userAgent
|
| 106 |
+
}
|
| 107 |
+
});
|
| 108 |
+
|
| 109 |
+
if (!response.ok) return null;
|
| 110 |
+
return await response.json();
|
| 111 |
+
} catch (e) {
|
| 112 |
+
console.error('Failed to fetch master details:', e);
|
| 113 |
+
return null;
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
async searchByBarcode(barcode) {
|
| 118 |
+
if (!this.key || !this.secret || !barcode) return null;
|
| 119 |
+
|
| 120 |
+
try {
|
| 121 |
+
const response = await fetch(
|
| 122 |
+
`${this.baseUrl}/database/search?barcode=${encodeURIComponent(barcode)}&type=release&per_page=5`,
|
| 123 |
+
{
|
| 124 |
+
headers: {
|
| 125 |
+
'Authorization': `Discogs key=${this.key}, secret=${this.secret}`,
|
| 126 |
+
'User-Agent': this.userAgent
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
);
|
| 130 |
+
|
| 131 |
+
if (!response.ok) return null;
|
| 132 |
+
const data = await response.json();
|
| 133 |
+
return data.results || [];
|
| 134 |
+
} catch (e) {
|
| 135 |
+
console.error('Barcode search failed:', e);
|
| 136 |
+
return null;
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
}
|
| 140 |
|
| 141 |
window.discogsService = new DiscogsService();
|
components/vinyl-footer.js
CHANGED
|
@@ -90,6 +90,7 @@ class VinylFooter extends HTMLElement {
|
|
| 90 |
<h4>Product</h4>
|
| 91 |
<ul>
|
| 92 |
<li><a href="index.html">New Listing</a></li>
|
|
|
|
| 93 |
<li><a href="collection.html">My Collection</a></li>
|
| 94 |
<li><a href="#" onclick="event.preventDefault(); alert('Coming soon')">Bulk Upload</a></li>
|
| 95 |
<li><a href="#" onclick="event.preventDefault(); alert('Coming soon')">Price Tracker</a></li>
|
|
|
|
| 90 |
<h4>Product</h4>
|
| 91 |
<ul>
|
| 92 |
<li><a href="index.html">New Listing</a></li>
|
| 93 |
+
<li><a href="deals.html">Deal Finder</a></li>
|
| 94 |
<li><a href="collection.html">My Collection</a></li>
|
| 95 |
<li><a href="#" onclick="event.preventDefault(); alert('Coming soon')">Bulk Upload</a></li>
|
| 96 |
<li><a href="#" onclick="event.preventDefault(); alert('Coming soon')">Price Tracker</a></li>
|
components/vinyl-nav.js
CHANGED
|
@@ -102,6 +102,10 @@ class VinylNav extends HTMLElement {
|
|
| 102 |
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
| 103 |
New Listing
|
| 104 |
</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
<a href="collection.html" class="nav-link primary">
|
| 106 |
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>
|
| 107 |
Collection
|
|
|
|
| 102 |
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
| 103 |
New Listing
|
| 104 |
</a>
|
| 105 |
+
<a href="deals.html" class="nav-link" style="color: #ec4899;">
|
| 106 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
| 107 |
+
Deals
|
| 108 |
+
</a>
|
| 109 |
<a href="collection.html" class="nav-link primary">
|
| 110 |
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>
|
| 111 |
Collection
|
deals.html
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Deal Finder - VinylVault Pro</title>
|
| 7 |
+
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
| 8 |
+
<link rel="stylesheet" href="style.css">
|
| 9 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 10 |
+
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
| 11 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 12 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
|
| 13 |
+
<script>
|
| 14 |
+
tailwind.config = {
|
| 15 |
+
darkMode: 'class',
|
| 16 |
+
theme: {
|
| 17 |
+
extend: {
|
| 18 |
+
colors: {
|
| 19 |
+
primary: '#7c3aed',
|
| 20 |
+
secondary: '#06b6d4',
|
| 21 |
+
accent: '#f59e0b',
|
| 22 |
+
surface: '#0f172a',
|
| 23 |
+
'surface-light': '#1e293b',
|
| 24 |
+
profit: '#22c55e',
|
| 25 |
+
loss: '#ef4444',
|
| 26 |
+
deal: '#ec4899',
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
</script>
|
| 32 |
+
</head>
|
| 33 |
+
<body class="bg-surface text-gray-100 min-h-screen">
|
| 34 |
+
<!-- Navigation -->
|
| 35 |
+
<vinyl-nav></vinyl-nav>
|
| 36 |
+
|
| 37 |
+
<!-- Main Content -->
|
| 38 |
+
<main class="pt-20 pb-12 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
|
| 39 |
+
|
| 40 |
+
<!-- Page Header -->
|
| 41 |
+
<section class="mb-8">
|
| 42 |
+
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
| 43 |
+
<div>
|
| 44 |
+
<div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-deal/10 border border-deal/30 mb-4">
|
| 45 |
+
<i data-feather="search" class="w-4 h-4 text-deal"></i>
|
| 46 |
+
<span class="text-sm font-medium text-deal">Investment Analysis Tool</span>
|
| 47 |
+
</div>
|
| 48 |
+
<h1 class="text-3xl font-bold bg-gradient-to-r from-deal via-primary to-secondary bg-clip-text text-transparent mb-2">
|
| 49 |
+
Deal Finder
|
| 50 |
+
</h1>
|
| 51 |
+
<p class="text-gray-400">Identify undervalued records, analyze potential flips, and calculate profit margins before you buy.</p>
|
| 52 |
+
</div>
|
| 53 |
+
<div class="flex gap-3">
|
| 54 |
+
<button onclick="importFromCSV()" class="px-4 py-2 bg-surface-light border border-gray-600 rounded-lg font-medium hover:border-deal hover:text-deal transition-all flex items-center gap-2">
|
| 55 |
+
<i data-feather="file-text" class="w-4 h-4"></i>
|
| 56 |
+
Import Discogs Wantlist
|
| 57 |
+
</button>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
</section>
|
| 61 |
+
|
| 62 |
+
<!-- Quick Deal Calculator -->
|
| 63 |
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
| 64 |
+
<div class="lg:col-span-2 bg-surface-light rounded-2xl p-6 border border-gray-800">
|
| 65 |
+
<h2 class="text-xl font-semibold mb-4 flex items-center gap-2">
|
| 66 |
+
<i data-feather="calculator" class="w-5 h-5 text-deal"></i>
|
| 67 |
+
Quick Flip Calculator
|
| 68 |
+
</h2>
|
| 69 |
+
<div class="grid sm:grid-cols-2 md:grid-cols-4 gap-4">
|
| 70 |
+
<div>
|
| 71 |
+
<label class="block text-sm text-gray-400 mb-2">Purchase Price (£)</label>
|
| 72 |
+
<input type="number" id="calcBuyPrice" placeholder="0.00" step="0.01"
|
| 73 |
+
class="w-full px-3 py-2 bg-surface border border-gray-700 rounded-lg focus:border-deal focus:outline-none text-sm"
|
| 74 |
+
oninput="calculateDeal()">
|
| 75 |
+
</div>
|
| 76 |
+
<div>
|
| 77 |
+
<label class="block text-sm text-gray-400 mb-2">Est. Resale (£)</label>
|
| 78 |
+
<input type="number" id="calcResalePrice" placeholder="0.00" step="0.01"
|
| 79 |
+
class="w-full px-3 py-2 bg-surface border border-gray-700 rounded-lg focus:border-deal focus:outline-none text-sm"
|
| 80 |
+
oninput="calculateDeal()">
|
| 81 |
+
</div>
|
| 82 |
+
<div>
|
| 83 |
+
<label class="block text-sm text-gray-400 mb-2">Condition</label>
|
| 84 |
+
<select id="calcCondition" onchange="calculateDeal()" class="w-full px-3 py-2 bg-surface border border-gray-700 rounded-lg focus:border-deal focus:outline-none text-sm">
|
| 85 |
+
<option value="M">M (Mint)</option>
|
| 86 |
+
<option value="NM">NM (Near Mint)</option>
|
| 87 |
+
<option value="VG+">VG+</option>
|
| 88 |
+
<option value="VG" selected>VG (Very Good)</option>
|
| 89 |
+
<option value="G+">G+</option>
|
| 90 |
+
</select>
|
| 91 |
+
</div>
|
| 92 |
+
<div>
|
| 93 |
+
<label class="block text-sm text-gray-400 mb-2">Selling Goal</label>
|
| 94 |
+
<select id="calcGoal" onchange="calculateDeal()" class="w-full px-3 py-2 bg-surface border border-gray-700 rounded-lg focus:border-deal focus:outline-none text-sm">
|
| 95 |
+
<option value="quick">Quick Sale</option>
|
| 96 |
+
<option value="balanced" selected>Balanced</option>
|
| 97 |
+
<option value="max">Maximum Profit</option>
|
| 98 |
+
</select>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
<div id="dealResult" class="mt-6 p-4 rounded-xl bg-gray-800/50 border border-gray-700 hidden">
|
| 102 |
+
<!-- Results populated by JS -->
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<!-- Market Trends -->
|
| 107 |
+
<div class="bg-gradient-to-br from-deal/10 to-primary/10 rounded-2xl p-6 border border-deal/20">
|
| 108 |
+
<h2 class="text-lg font-semibold mb-4 text-deal">Deal Indicators</h2>
|
| 109 |
+
<div class="space-y-3">
|
| 110 |
+
<div class="flex items-center justify-between p-3 bg-surface rounded-lg">
|
| 111 |
+
<span class="text-sm text-gray-400">Target Margin</span>
|
| 112 |
+
<span class="font-bold text-profit">30%+</span>
|
| 113 |
+
</div>
|
| 114 |
+
<div class="flex items-center justify-between p-3 bg-surface rounded-lg">
|
| 115 |
+
<span class="text-sm text-gray-400">Min Profit/Record</span>
|
| 116 |
+
<span class="font-bold text-profit">£3+</span>
|
| 117 |
+
</div>
|
| 118 |
+
<div class="flex items-center justify-between p-3 bg-surface rounded-lg">
|
| 119 |
+
<span class="text-sm text-gray-400">Fast Flip Criteria</span>
|
| 120 |
+
<span class="font-bold text-warning">50%+ margin</span>
|
| 121 |
+
</div>
|
| 122 |
+
<div class="flex items-center justify-between p-3 bg-surface rounded-lg">
|
| 123 |
+
<span class="text-sm text-gray-400">Hold Threshold</span>
|
| 124 |
+
<span class="font-bold text-primary">£20+ value</span>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
</div>
|
| 129 |
+
|
| 130 |
+
<!-- Bulk Deal Analyzer -->
|
| 131 |
+
<div class="bg-surface-light rounded-2xl p-6 border border-gray-800 mb-8">
|
| 132 |
+
<div class="flex items-center justify-between mb-6">
|
| 133 |
+
<div>
|
| 134 |
+
<h2 class="text-xl font-semibold flex items-center gap-2">
|
| 135 |
+
<i data-feather="zap" class="w-5 h-5 text-accent"></i>
|
| 136 |
+
Bulk Deal Analyzer
|
| 137 |
+
</h2>
|
| 138 |
+
<p class="text-sm text-gray-500 mt-1">Paste a Discogs marketplace search or seller's list to analyze multiple deals at once</p>
|
| 139 |
+
</div>
|
| 140 |
+
<input type="file" id="bulkCSVInput" accept=".csv" class="hidden" onchange="handleBulkCSV(event)">
|
| 141 |
+
<button onclick="document.getElementById('bulkCSVInput').click()" class="px-4 py-2 bg-surface border border-gray-600 rounded-lg text-sm hover:border-primary hover:text-primary transition-all flex items-center gap-2">
|
| 142 |
+
<i data-feather="upload" class="w-4 h-4"></i>
|
| 143 |
+
Upload CSV
|
| 144 |
+
</button>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
<div class="space-y-4">
|
| 148 |
+
<div class="relative">
|
| 149 |
+
<textarea id="bulkInput" rows="4" placeholder="Paste seller listings, marketplace URLs, or record details (one per line)... Example: Pink Floyd - Dark Side of the Moon - £15 The Beatles - Abbey Road VG+ - £25 https://www.discogs.com/sell/item/123456"
|
| 150 |
+
class="w-full px-4 py-3 bg-surface border border-gray-700 rounded-lg focus:border-deal focus:outline-none text-sm font-mono resize-y"></textarea>
|
| 151 |
+
</div>
|
| 152 |
+
<div class="flex gap-3">
|
| 153 |
+
<button onclick="analyzeBulkDeals()" class="px-6 py-3 bg-gradient-to-r from-deal to-pink-600 rounded-lg font-medium hover:shadow-lg hover:shadow-deal/25 transition-all flex items-center gap-2">
|
| 154 |
+
<i data-feather="search" class="w-4 h-4"></i>
|
| 155 |
+
Analyze Deals
|
| 156 |
+
</button>
|
| 157 |
+
<button onclick="clearBulkInput()" class="px-4 py-3 border border-gray-600 rounded-lg text-gray-400 hover:text-white transition-all">
|
| 158 |
+
Clear
|
| 159 |
+
</button>
|
| 160 |
+
</div>
|
| 161 |
+
</div>
|
| 162 |
+
</div>
|
| 163 |
+
|
| 164 |
+
<!-- Analysis Results -->
|
| 165 |
+
<div id="dealsResults" class="hidden space-y-6">
|
| 166 |
+
<div class="flex items-center justify-between">
|
| 167 |
+
<h3 class="text-lg font-semibold">Analysis Results</h3>
|
| 168 |
+
<div class="flex gap-2">
|
| 169 |
+
<span id="hotDealsCount" class="px-3 py-1 bg-profit/20 text-profit rounded-full text-sm font-medium">0 Hot Deals</span>
|
| 170 |
+
<span id="skipCount" class="px-3 py-1 bg-loss/20 text-loss rounded-full text-sm font-medium">0 Pass</span>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
|
| 174 |
+
<div id="dealsGrid" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
| 175 |
+
<!-- Dynamically populated -->
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
|
| 179 |
+
<!-- Empty State -->
|
| 180 |
+
<div id="dealsEmptyState" class="text-center py-16">
|
| 181 |
+
<div class="w-24 h-24 mx-auto mb-6 rounded-2xl bg-surface-light border border-gray-700 flex items-center justify-center">
|
| 182 |
+
<i data-feather="trending-up" class="w-12 h-12 text-gray-600"></i>
|
| 183 |
+
</div>
|
| 184 |
+
<h3 class="text-xl font-medium text-gray-300 mb-2">Find your next flip</h3>
|
| 185 |
+
<p class="text-gray-500 max-w-md mx-auto mb-6">
|
| 186 |
+
Enter a record's details to instantly calculate potential profit, or import a seller's list to batch-analyze opportunities.
|
| 187 |
+
</p>
|
| 188 |
+
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 max-w-2xl mx-auto">
|
| 189 |
+
<div class="p-4 bg-surface-light rounded-xl border border-gray-700 text-center">
|
| 190 |
+
<i data-feather="check-circle" class="w-8 h-8 text-profit mx-auto mb-2"></i>
|
| 191 |
+
<p class="text-sm text-gray-300">30%+ margin recommended</p>
|
| 192 |
+
</div>
|
| 193 |
+
<div class="p-4 bg-surface-light rounded-xl border border-gray-700 text-center">
|
| 194 |
+
<i data-feather="clock" class="w-8 h-8 text-accent mx-auto mb-2"></i>
|
| 195 |
+
<p class="text-sm text-gray-300">Consider days to sell</p>
|
| 196 |
+
</div>
|
| 197 |
+
<div class="p-4 bg-surface-light rounded-xl border border-gray-700 text-center">
|
| 198 |
+
<i data-feather="dollar-sign" class="w-8 h-8 text-deal mx-auto mb-2"></i>
|
| 199 |
+
<p class="text-sm text-gray-300">£3 min profit per record</p>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
+
|
| 204 |
+
</main>
|
| 205 |
+
|
| 206 |
+
<!-- Deal Detail Modal -->
|
| 207 |
+
<div id="dealModal" class="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 hidden items-center justify-center p-4">
|
| 208 |
+
<div class="bg-surface-light rounded-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto border border-gray-800">
|
| 209 |
+
<div class="p-6 border-b border-gray-800 flex items-center justify-between">
|
| 210 |
+
<h2 class="text-xl font-semibold">Deal Analysis</h2>
|
| 211 |
+
<button onclick="closeDealModal()" class="text-gray-400 hover:text-white">
|
| 212 |
+
<i data-feather="x" class="w-6 h-6"></i>
|
| 213 |
+
</button>
|
| 214 |
+
</div>
|
| 215 |
+
<div id="dealModalContent" class="p-6">
|
| 216 |
+
<!-- Populated by JS -->
|
| 217 |
+
</div>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
|
| 221 |
+
<!-- Footer -->
|
| 222 |
+
<vinyl-footer></vinyl-footer>
|
| 223 |
+
|
| 224 |
+
<!-- Scripts -->
|
| 225 |
+
<script src="components/vinyl-nav.js"></script>
|
| 226 |
+
<script src="components/vinyl-footer.js"></script>
|
| 227 |
+
<script src="components/stat-card.js"></script>
|
| 228 |
+
<script src="components/discogs-service.js"></script>
|
| 229 |
+
<script src="script.js"></script>
|
| 230 |
+
<script src="deals.js"></script>
|
| 231 |
+
<script>feather.replace();</script>
|
| 232 |
+
</body>
|
| 233 |
+
</html>
|
deals.js
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Deal Finder Logic
|
| 2 |
+
|
| 3 |
+
function calculateDeal() {
|
| 4 |
+
const buyPrice = parseFloat(document.getElementById('calcBuyPrice').value) || 0;
|
| 5 |
+
const resalePrice = parseFloat(document.getElementById('calcResalePrice').value) || 0;
|
| 6 |
+
const condition = document.getElementById('calcCondition').value;
|
| 7 |
+
const goal = document.getElementById('calcGoal').value;
|
| 8 |
+
|
| 9 |
+
if (buyPrice <= 0 || resalePrice <= 0) return;
|
| 10 |
+
|
| 11 |
+
const container = document.getElementById('dealResult');
|
| 12 |
+
container.classList.remove('hidden');
|
| 13 |
+
|
| 14 |
+
// Calculate with condition adjustment
|
| 15 |
+
const conditionMult = {
|
| 16 |
+
'M': 1.5, 'NM': 1.3, 'VG+': 1.0, 'VG': 0.7, 'G+': 0.5
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
const adjustedResale = resalePrice * (conditionMult[condition] || 0.7);
|
| 20 |
+
|
| 21 |
+
// Fees
|
| 22 |
+
const ebayFee = adjustedResale * 0.13;
|
| 23 |
+
const paypalFee = (adjustedResale * 0.029) + 0.30;
|
| 24 |
+
const costs = 6; // shipping + packing estimate
|
| 25 |
+
|
| 26 |
+
// Strategy pricing
|
| 27 |
+
let listPrice;
|
| 28 |
+
switch(goal) {
|
| 29 |
+
case 'quick': listPrice = adjustedResale * 0.90; break;
|
| 30 |
+
case 'max': listPrice = adjustedResale * 1.10; break;
|
| 31 |
+
default: listPrice = adjustedResale;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
const netProfit = listPrice - buyPrice - ebayFee - paypalFee - costs;
|
| 35 |
+
const roi = buyPrice > 0 ? ((netProfit / buyPrice) * 100).toFixed(1) : 0;
|
| 36 |
+
const totalFees = ebayFee + paypalFee + costs;
|
| 37 |
+
|
| 38 |
+
// Determine recommendation
|
| 39 |
+
let recommendation, colorClass, icon;
|
| 40 |
+
if (netProfit < 3) {
|
| 41 |
+
recommendation = 'PASS - Insufficient margin';
|
| 42 |
+
colorClass = 'bg-loss/10 border-loss';
|
| 43 |
+
icon = 'x-circle';
|
| 44 |
+
} else if (roi < 30) {
|
| 45 |
+
recommendation = 'MARGINAL - Low ROI';
|
| 46 |
+
colorClass = 'bg-yellow-500/10 border-yellow-500';
|
| 47 |
+
icon = 'alert-triangle';
|
| 48 |
+
} else if (roi >= 50) {
|
| 49 |
+
recommendation = 'HOT DEAL - Quick flip potential!';
|
| 50 |
+
colorClass = 'bg-profit/10 border-profit';
|
| 51 |
+
icon = 'zap';
|
| 52 |
+
} else {
|
| 53 |
+
recommendation = 'GOOD DEAL - Worth pursuing';
|
| 54 |
+
colorClass = 'bg-primary/10 border-primary';
|
| 55 |
+
icon = 'check-circle';
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
container.className = `mt-6 p-6 rounded-xl border ${colorClass}`;
|
| 59 |
+
container.innerHTML = `
|
| 60 |
+
<div class="flex items-start gap-4">
|
| 61 |
+
<div class="p-3 rounded-full bg-surface">
|
| 62 |
+
<i data-feather="${icon}" class="w-6 h-6 ${netProfit >= 3 ? 'text-profit' : 'text-loss'}"></i>
|
| 63 |
+
</div>
|
| 64 |
+
<div class="flex-1">
|
| 65 |
+
<h3 class="font-semibold text-lg mb-1">${recommendation}</h3>
|
| 66 |
+
<div class="grid grid-cols-3 gap-4 mt-4">
|
| 67 |
+
<div>
|
| 68 |
+
<p class="text-xs text-gray-500 mb-1">Est. Net Profit</p>
|
| 69 |
+
<p class="text-2xl font-bold ${netProfit >= 0 ? 'text-profit' : 'text-loss'}">£${netProfit.toFixed(2)}</p>
|
| 70 |
+
</div>
|
| 71 |
+
<div>
|
| 72 |
+
<p class="text-xs text-gray-500 mb-1">ROI</p>
|
| 73 |
+
<p class="text-2xl font-bold ${roi >= 30 ? 'text-profit' : 'text-gray-400'}">${roi}%</p>
|
| 74 |
+
</div>
|
| 75 |
+
<div>
|
| 76 |
+
<p class="text-xs text-gray-500 mb-1">Suggested List</p>
|
| 77 |
+
<p class="text-2xl font-bold text-gray-200">£${listPrice.toFixed(0)}</p>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
<div class="mt-4 pt-4 border-t border-gray-700/50 grid grid-cols-4 gap-2 text-sm">
|
| 81 |
+
<div class="text-gray-500">Buy: £${buyPrice.toFixed(2)}</div>
|
| 82 |
+
<div class="text-gray-500">eBay: £${ebayFee.toFixed(2)}</div>
|
| 83 |
+
<div class="text-gray-500">PayPal: £${paypalFee.toFixed(2)}</div>
|
| 84 |
+
<div class="text-gray-500">Ship: £${costs.toFixed(2)}</div>
|
| 85 |
+
</div>
|
| 86 |
+
<div class="mt-4 flex gap-2">
|
| 87 |
+
${netProfit >= 3 ? `
|
| 88 |
+
<button onclick="saveDealToCollection()" class="px-4 py-2 bg-primary rounded-lg text-sm hover:bg-primary/80 transition-all">
|
| 89 |
+
Save to Watchlist
|
| 90 |
+
</button>
|
| 91 |
+
` : ''}
|
| 92 |
+
<button onclick="resetCalculator()" class="px-4 py-2 border border-gray-600 rounded-lg text-sm hover:border-gray-500 transition-all">
|
| 93 |
+
Reset
|
| 94 |
+
</button>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
`;
|
| 99 |
+
feather.replace();
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
function resetCalculator() {
|
| 103 |
+
document.getElementById('calcBuyPrice').value = '';
|
| 104 |
+
document.getElementById('calcResalePrice').value = '';
|
| 105 |
+
document.getElementById('calcCondition').value = 'VG';
|
| 106 |
+
document.getElementById('calcGoal').value = 'balanced';
|
| 107 |
+
document.getElementById('dealResult').classList.add('hidden');
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
async function analyzeBulkDeals() {
|
| 111 |
+
const input = document.getElementById('bulkInput').value.trim();
|
| 112 |
+
if (!input) {
|
| 113 |
+
showToast('Enter some deals to analyze first', 'error');
|
| 114 |
+
return;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
const lines = input.split('\n').filter(l => l.trim());
|
| 118 |
+
if (lines.length === 0) return;
|
| 119 |
+
|
| 120 |
+
showToast(`Analyzing ${lines.length} potential deals...`, 'success');
|
| 121 |
+
|
| 122 |
+
const results = [];
|
| 123 |
+
|
| 124 |
+
for (const line of lines) {
|
| 125 |
+
// Parse different formats:
|
| 126 |
+
// "Artist - Title - £15"
|
| 127 |
+
// "Artist - Title VG+ £20"
|
| 128 |
+
// URLs
|
| 129 |
+
|
| 130 |
+
const priceMatch = line.match(/[£$€](\d+(?:\.\d{2})?)/);
|
| 131 |
+
const price = priceMatch ? parseFloat(priceMatch[1]) : 0;
|
| 132 |
+
|
| 133 |
+
// Extract artist/title (rough parsing)
|
| 134 |
+
const parts = line.replace(/[£$€]\d+(?:\.\d{2})?/, '').split(/[-–—]/);
|
| 135 |
+
const artist = parts[0]?.trim() || 'Unknown';
|
| 136 |
+
const title = parts[1]?.trim() || 'Unknown';
|
| 137 |
+
|
| 138 |
+
const conditionMatch = line.match(/\b(M|NM|VG\+|VG|G\+|G)\b/i);
|
| 139 |
+
const condition = conditionMatch ? conditionMatch[1].toUpperCase() : 'VG';
|
| 140 |
+
|
| 141 |
+
// Try to get Discogs data for better estimation
|
| 142 |
+
let discogsData = null;
|
| 143 |
+
if (window.discogsService?.key && artist && title) {
|
| 144 |
+
try {
|
| 145 |
+
const search = await window.discogsService.searchRelease(artist, title, null);
|
| 146 |
+
if (search) {
|
| 147 |
+
discogsData = await window.discogsService.getReleaseDetails(search.id);
|
| 148 |
+
}
|
| 149 |
+
} catch (e) {
|
| 150 |
+
console.log('Discogs lookup failed for', artist, title);
|
| 151 |
+
}
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
// Calculate metrics
|
| 155 |
+
const estimatedValue = discogsData?.lowest_price || discogsData?.median || price * 2; // Assume 2x if no data
|
| 156 |
+
const analysis = calculateDealMetrics(price, estimatedValue, condition);
|
| 157 |
+
|
| 158 |
+
results.push({
|
| 159 |
+
artist,
|
| 160 |
+
title,
|
| 161 |
+
price,
|
| 162 |
+
condition,
|
| 163 |
+
...analysis,
|
| 164 |
+
discogsUrl: discogsData?.uri,
|
| 165 |
+
discogsId: discogsData?.id,
|
| 166 |
+
hasDiscogsData: !!discogsData
|
| 167 |
+
});
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
renderDealsResults(results);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
function calculateDealMetrics(buyPrice, estimatedValue, condition = 'VG') {
|
| 174 |
+
const conditionMult = {
|
| 175 |
+
'M': 1.5, 'NM': 1.3, 'VG+': 1.0, 'VG': 0.7,
|
| 176 |
+
'G+': 0.5, 'G': 0.35
|
| 177 |
+
};
|
| 178 |
+
|
| 179 |
+
const adjustedValue = estimatedValue * (conditionMult[condition] || 0.7);
|
| 180 |
+
|
| 181 |
+
// Fees
|
| 182 |
+
const ebayFee = adjustedValue * 0.13;
|
| 183 |
+
const paypalFee = (adjustedValue * 0.029) + 0.30;
|
| 184 |
+
const costs = 6;
|
| 185 |
+
|
| 186 |
+
const netProfit = adjustedValue - buyPrice - ebayFee - paypalFee - costs;
|
| 187 |
+
const roi = buyPrice > 0 ? ((netProfit / buyPrice) * 100).toFixed(1) : 0;
|
| 188 |
+
|
| 189 |
+
return {
|
| 190 |
+
adjustedValue: Math.round(adjustedValue),
|
| 191 |
+
netProfit: Math.round(netProfit),
|
| 192 |
+
roi,
|
| 193 |
+
totalFees: Math.round((ebayFee + paypalFee + costs) * 100) / 100,
|
| 194 |
+
isViable: netProfit >= 3,
|
| 195 |
+
isHot: netProfit >= 8 && roi >= 40
|
| 196 |
+
};
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
function renderDealsResults(results) {
|
| 200 |
+
const container = document.getElementById('dealsGrid');
|
| 201 |
+
const resultsSection = document.getElementById('dealsResults');
|
| 202 |
+
const emptyState = document.getElementById('dealsEmptyState');
|
| 203 |
+
|
| 204 |
+
emptyState.classList.add('hidden');
|
| 205 |
+
resultsSection.classList.remove('hidden');
|
| 206 |
+
|
| 207 |
+
// Update counts
|
| 208 |
+
const hotCount = results.filter(r => r.isHot).length;
|
| 209 |
+
const skipCount = results.filter(r => !r.isViable).length;
|
| 210 |
+
document.getElementById('hotDealsCount').textContent = `${hotCount} Hot Deals`;
|
| 211 |
+
document.getElementById('skipCount').textContent = `${skipCount} Pass`;
|
| 212 |
+
|
| 213 |
+
container.innerHTML = results.map((deal, index) => {
|
| 214 |
+
const profitClass = deal.netProfit >= 0 ? 'text-profit' : 'text-loss';
|
| 215 |
+
const cardClass = deal.isHot ? 'border-profit bg-profit/5' :
|
| 216 |
+
!deal.isViable ? 'border-gray-700 opacity-75' : 'border-deal/50';
|
| 217 |
+
const badgeText = deal.isHot ? '🔥 HOT' : deal.isViable ? 'GOOD' : 'PASS';
|
| 218 |
+
const badgeClass = deal.isHot ? 'bg-profit text-white' : deal.isViable ? 'bg-deal/20 text-deal' : 'bg-gray-700 text-gray-400';
|
| 219 |
+
|
| 220 |
+
return `
|
| 221 |
+
<div class="deal-card ${cardClass} p-4 rounded-xl border cursor-pointer transition-all hover:-translate-y-1" onclick="showDealDetail(${index})">
|
| 222 |
+
<div class="flex justify-between items-start mb-3">
|
| 223 |
+
<span class="px-2 py-1 rounded text-xs font-bold ${badgeClass}">${badgeText}</span>
|
| 224 |
+
${deal.hasDiscogsData ? '<span class="text-xs text-gray-500" title="Discogs data available">🎵</span>' : ''}
|
| 225 |
+
</div>
|
| 226 |
+
<h3 class="font-semibold text-gray-100 truncate mb-1" title="${deal.artist}">${deal.artist}</h3>
|
| 227 |
+
<p class="text-sm text-gray-400 truncate mb-3" title="${deal.title}">${deal.title}</p>
|
| 228 |
+
|
| 229 |
+
<div class="grid grid-cols-2 gap-2 mb-3 text-sm">
|
| 230 |
+
<div>
|
| 231 |
+
<span class="text-gray-500 text-xs">Buy</span>
|
| 232 |
+
<p class="font-medium">£${deal.price.toFixed(2)}</p>
|
| 233 |
+
</div>
|
| 234 |
+
<div>
|
| 235 |
+
<span class="text-gray-500 text-xs">Est. Value</span>
|
| 236 |
+
<p class="font-medium">£${deal.adjustedValue}</p>
|
| 237 |
+
</div>
|
| 238 |
+
</div>
|
| 239 |
+
|
| 240 |
+
<div class="flex items-center justify-between pt-3 border-t border-gray-700">
|
| 241 |
+
<div class="${profitClass} font-bold">
|
| 242 |
+
£${deal.netProfit} profit
|
| 243 |
+
</div>
|
| 244 |
+
<div class="text-sm ${deal.roi >= 30 ? 'text-profit' : 'text-gray-400'}">
|
| 245 |
+
${deal.roi}% ROI
|
| 246 |
+
</div>
|
| 247 |
+
</div>
|
| 248 |
+
</div>
|
| 249 |
+
`;
|
| 250 |
+
}).join('');
|
| 251 |
+
|
| 252 |
+
// Store for detail view
|
| 253 |
+
window.analyzedDeals = results;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
function showDealDetail(index) {
|
| 257 |
+
const deal = window.analyzedDeals[index];
|
| 258 |
+
const modal = document.getElementById('dealModal');
|
| 259 |
+
const content = document.getElementById('dealModalContent');
|
| 260 |
+
|
| 261 |
+
const profitColor = deal.netProfit >= 3 ? 'text-profit' : 'text-loss';
|
| 262 |
+
const headerColor = deal.isHot ? 'bg-profit/10 border-profit' : deal.isViable ? 'bg-deal/10 border-deal' : 'bg-gray-800 border-gray-700';
|
| 263 |
+
|
| 264 |
+
content.innerHTML = `
|
| 265 |
+
<div class="space-y-4">
|
| 266 |
+
<div class="p-4 rounded-xl border ${headerColor}">
|
| 267 |
+
<h3 class="text-lg font-bold mb-1">${deal.artist}</h3>
|
| 268 |
+
<p class="text-gray-400">${deal.title}</p>
|
| 269 |
+
<p class="text-sm text-gray-500 mt-2">Condition: ${deal.condition}</p>
|
| 270 |
+
</div>
|
| 271 |
+
|
| 272 |
+
<div class="grid grid-cols-2 gap-4">
|
| 273 |
+
<div class="p-4 bg-surface rounded-lg">
|
| 274 |
+
<p class="text-xs text-gray-500 mb-1">Purchase Price</p>
|
| 275 |
+
<p class="text-xl font-bold">£${deal.price.toFixed(2)}</p>
|
| 276 |
+
</div>
|
| 277 |
+
<div class="p-4 bg-surface rounded-lg">
|
| 278 |
+
<p class="text-xs text-gray-500 mb-1">Market Value (adj.)</p>
|
| 279 |
+
<p class="text-xl font-bold">£${deal.adjustedValue}</p>
|
| 280 |
+
</div>
|
| 281 |
+
<div class="p-4 bg-surface rounded-lg">
|
| 282 |
+
<p class="text-xs text-gray-500 mb-1">Total Fees (~16%)</p>
|
| 283 |
+
<p class="text-xl font-bold text-gray-400">£${deal.totalFees}</p>
|
| 284 |
+
</div>
|
| 285 |
+
<div class="p-4 bg-surface rounded-lg">
|
| 286 |
+
<p class="text-xs text-gray-500 mb-1">Net Profit</p>
|
| 287 |
+
<p class="text-xl font-bold ${profitColor}">£${deal.netProfit}</p>
|
| 288 |
+
</div>
|
| 289 |
+
</div>
|
| 290 |
+
|
| 291 |
+
<div class="p-4 bg-surface rounded-lg">
|
| 292 |
+
<div class="flex justify-between items-center mb-2">
|
| 293 |
+
<span class="text-sm text-gray-400">Return on Investment</span>
|
| 294 |
+
<span class="text-lg font-bold ${deal.roi >= 30 ? 'text-profit' : 'text-yellow-400'}">${deal.roi}%</span>
|
| 295 |
+
</div>
|
| 296 |
+
<div class="h-2 bg-gray-700 rounded-full overflow-hidden">
|
| 297 |
+
<div class="h-full ${deal.roi >= 30 ? 'bg-profit' : deal.roi > 0 ? 'bg-yellow-500' : 'bg-loss'}" style="width: ${Math.min(Math.abs(deal.roi), 100)}%"></div>
|
| 298 |
+
</div>
|
| 299 |
+
</div>
|
| 300 |
+
|
| 301 |
+
${deal.discogsUrl ? `
|
| 302 |
+
<a href="${deal.discogsUrl}" target="_blank" class="flex items-center gap-2 text-sm text-primary hover:underline">
|
| 303 |
+
View on Discogs
|
| 304 |
+
<i data-feather="external-link" class="w-4 h-4"></i>
|
| 305 |
+
</a>
|
| 306 |
+
` : ''}
|
| 307 |
+
|
| 308 |
+
<div class="flex gap-3 mt-6">
|
| 309 |
+
${deal.isViable ? `
|
| 310 |
+
<button onclick="addDealToCollection(${index})" class="flex-1 px-4 py-3 bg-gradient-to-r from-deal to-pink-600 rounded-lg font-medium hover:shadow-lg transition-all">
|
| 311 |
+
Add to Collection
|
| 312 |
+
</button>
|
| 313 |
+
` : ''}
|
| 314 |
+
<button onclick="closeDealModal()" class="px-4 py-3 border border-gray-600 rounded-lg hover:border-gray-500 transition-all">
|
| 315 |
+
Close
|
| 316 |
+
</button>
|
| 317 |
+
</div>
|
| 318 |
+
</div>
|
| 319 |
+
`;
|
| 320 |
+
feather.replace();
|
| 321 |
+
modal.classList.remove('hidden');
|
| 322 |
+
modal.classList.add('flex');
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
function closeDealModal() {
|
| 326 |
+
document.getElementById('dealModal').classList.add('hidden');
|
| 327 |
+
document.getElementById('dealModal').classList.remove('flex');
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
function addDealToCollection(index) {
|
| 331 |
+
const deal = window.analyzedDeals[index];
|
| 332 |
+
// Create collection entry
|
| 333 |
+
const record = {
|
| 334 |
+
artist: deal.artist,
|
| 335 |
+
title: deal.title,
|
| 336 |
+
purchasePrice: deal.price,
|
| 337 |
+
purchaseDate: new Date().toISOString().split('T')[0],
|
| 338 |
+
purchaseSource: 'prospective_deal',
|
| 339 |
+
conditionVinyl: deal.condition,
|
| 340 |
+
conditionSleeve: deal.condition,
|
| 341 |
+
estimatedValue: deal.adjustedValue,
|
| 342 |
+
status: 'prospective',
|
| 343 |
+
dateAdded: new Date().toISOString(),
|
| 344 |
+
notes: `Deal analysis: Potential profit £${deal.netProfit} (${deal.roi}% ROI). ${deal.discogsUrl ? 'Discogs: ' + deal.discogsUrl : ''}`
|
| 345 |
+
};
|
| 346 |
+
|
| 347 |
+
// Add to local collection storage
|
| 348 |
+
let collection = JSON.parse(localStorage.getItem('vinyl_collection') || '[]');
|
| 349 |
+
collection.push(record);
|
| 350 |
+
localStorage.setItem('vinyl_collection', JSON.stringify(collection));
|
| 351 |
+
|
| 352 |
+
showToast('Deal saved to collection!', 'success');
|
| 353 |
+
closeDealModal();
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
function clearBulkInput() {
|
| 357 |
+
document.getElementById('bulkInput').value = '';
|
| 358 |
+
document.getElementById('dealsResults').classList.add('hidden');
|
| 359 |
+
document.getElementById('dealsEmptyState').classList.remove('hidden');
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
function handleBulkCSV(event) {
|
| 363 |
+
const file = event.target.files[0];
|
| 364 |
+
if (!file) return;
|
| 365 |
+
|
| 366 |
+
Papa.parse(file, {
|
| 367 |
+
header: true,
|
| 368 |
+
skipEmptyLines: true,
|
| 369 |
+
complete: function(results) {
|
| 370 |
+
// Convert CSV to text format for analysis
|
| 371 |
+
const lines = results.data.map(row => {
|
| 372 |
+
const artist = row.Artist || row.artist || '';
|
| 373 |
+
const title = row.Title || row.title || '';
|
| 374 |
+
const price = row.Price || row.price || row['Purchase Price'] || '';
|
| 375 |
+
const condition = row.Condition || row.condition || 'VG';
|
| 376 |
+
if (artist && title) {
|
| 377 |
+
return `${artist} - ${title} ${condition} £${price}`;
|
| 378 |
+
}
|
| 379 |
+
return null;
|
| 380 |
+
}).filter(Boolean);
|
| 381 |
+
|
| 382 |
+
document.getElementById('bulkInput').value = lines.join('\n');
|
| 383 |
+
analyzeBulkDeals();
|
| 384 |
+
}
|
| 385 |
+
});
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
function saveDealToCollection() {
|
| 389 |
+
// Quick save from calculator
|
| 390 |
+
const buyPrice = parseFloat(document.getElementById('calcBuyPrice').value);
|
| 391 |
+
const resalePrice = parseFloat(document.getElementById('calcResalePrice').value);
|
| 392 |
+
|
| 393 |
+
if (!buyPrice || !resalePrice) return;
|
| 394 |
+
|
| 395 |
+
const record = {
|
| 396 |
+
artist: 'Unknown (from calculator)',
|
| 397 |
+
title: 'Deal analysis',
|
| 398 |
+
purchasePrice: buyPrice,
|
| 399 |
+
estimatedValue: resalePrice,
|
| 400 |
+
status: 'prospective',
|
| 401 |
+
dateAdded: new Date().toISOString(),
|
| 402 |
+
notes: `Calculated potential deal. Buy: £${buyPrice}, Est. value: £${resalePrice}`
|
| 403 |
+
};
|
| 404 |
+
|
| 405 |
+
let collection = JSON.parse(localStorage.getItem('vinyl_collection') || '[]');
|
| 406 |
+
collection.push(record);
|
| 407 |
+
localStorage.setItem('vinyl_collection', JSON.stringify(collection));
|
| 408 |
+
|
| 409 |
+
showToast('Deal saved to watchlist!', 'success');
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
function importFromCSV() {
|
| 413 |
+
// Trigger file input
|
| 414 |
+
document.getElementById('bulkCSVInput').click();
|
| 415 |
+
}
|
script.js
CHANGED
|
@@ -676,23 +676,22 @@ function performAnalysis(data) {
|
|
| 676 |
// Generate titles
|
| 677 |
const baseTitle = `${artist || 'ARTIST'} - ${title || 'TITLE'}`;
|
| 678 |
const titles = generateTitles(baseTitle, catNo, year, goal);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 679 |
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
// Show results
|
| 689 |
-
resultsSection.classList.remove('hidden');
|
| 690 |
-
emptyState.classList.add('hidden');
|
| 691 |
-
resultsSection.scrollIntoView({ behavior: 'smooth' });
|
| 692 |
-
|
| 693 |
-
currentAnalysis = {
|
| 694 |
-
titles, recommendedBin, strategy, breakEven, safeFloor, currency
|
| 695 |
-
};
|
| 696 |
}
|
| 697 |
function generateTitles(base, catNo, year, goal) {
|
| 698 |
const titles = [];
|
|
@@ -805,7 +804,7 @@ function renderFeeFloor(cost, fees, shipping, packing, safeFloor, currency) {
|
|
| 805 |
</div>
|
| 806 |
`;
|
| 807 |
}
|
| 808 |
-
function renderHTMLDescription(data, titleObj) {
|
| 809 |
const { artist, title, catNo, year } = data;
|
| 810 |
// Use hosted URL if available, otherwise fallback to local object URL
|
| 811 |
let heroImg = '';
|
|
@@ -818,13 +817,115 @@ function renderHTMLDescription(data, titleObj) {
|
|
| 818 |
heroImg = URL.createObjectURL(uploadedPhotos[0]);
|
| 819 |
galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1]));
|
| 820 |
}
|
| 821 |
-
|
|
|
|
| 822 |
const detectedLabel = window.detectedLabel || '[Verify from photos]';
|
| 823 |
const detectedCountry = window.detectedCountry || 'UK';
|
| 824 |
const detectedFormat = window.detectedFormat || 'LP • 33rpm';
|
| 825 |
const detectedGenre = window.detectedGenre || 'rock';
|
| 826 |
const detectedCondition = window.detectedCondition || 'VG+/VG+';
|
| 827 |
const detectedPressingInfo = window.detectedPressingInfo || '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 828 |
const galleryHtml = galleryImages.length > 0 ? `
|
| 829 |
<!-- PHOTO GALLERY -->
|
| 830 |
<div style="margin-bottom: 24px;">
|
|
@@ -898,8 +999,12 @@ const html = `<div style="max-width: 800px; margin: 0 auto; font-family: -apple-
|
|
| 898 |
<!-- TRACKLIST -->
|
| 899 |
<h3 style="color: #1e293b; font-size: 18px; font-weight: 600; margin-bottom: 12px;">Tracklist</h3>
|
| 900 |
<div style="background: #f8fafc; padding: 16px 20px; border-radius: 8px; margin-bottom: 24px;">
|
| 901 |
-
|
| 902 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 903 |
<!-- PACKING -->
|
| 904 |
<div style="background: #eff6ff; border-left: 4px solid #3b82f6; padding: 16px 20px; margin-bottom: 24px; border-radius: 0 8px 8px 0;">
|
| 905 |
<h3 style="margin: 0 0 12px 0; color: #1e40af; font-size: 16px; font-weight: 600;">Packing & Postage</h3>
|
|
|
|
| 676 |
// Generate titles
|
| 677 |
const baseTitle = `${artist || 'ARTIST'} - ${title || 'TITLE'}`;
|
| 678 |
const titles = generateTitles(baseTitle, catNo, year, goal);
|
| 679 |
+
// Render results
|
| 680 |
+
renderTitleOptions(titles);
|
| 681 |
+
renderPricingStrategy(recommendedBin, strategy, comps, currency, goal);
|
| 682 |
+
renderFeeFloor(cost, totalFees, shippingCost, packingCost, safeFloor, currency);
|
| 683 |
+
await renderHTMLDescription(data, titles[0]);
|
| 684 |
+
renderTags(artist, title, catNo, year);
|
| 685 |
+
renderShotList(uploadedPhotos);
|
| 686 |
|
| 687 |
+
// Show results
|
| 688 |
+
resultsSection.classList.remove('hidden');
|
| 689 |
+
emptyState.classList.add('hidden');
|
| 690 |
+
resultsSection.scrollIntoView({ behavior: 'smooth' });
|
| 691 |
+
|
| 692 |
+
currentAnalysis = {
|
| 693 |
+
titles, recommendedBin, strategy, breakEven, safeFloor, currency
|
| 694 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 695 |
}
|
| 696 |
function generateTitles(base, catNo, year, goal) {
|
| 697 |
const titles = [];
|
|
|
|
| 804 |
</div>
|
| 805 |
`;
|
| 806 |
}
|
| 807 |
+
async function renderHTMLDescription(data, titleObj) {
|
| 808 |
const { artist, title, catNo, year } = data;
|
| 809 |
// Use hosted URL if available, otherwise fallback to local object URL
|
| 810 |
let heroImg = '';
|
|
|
|
| 817 |
heroImg = URL.createObjectURL(uploadedPhotos[0]);
|
| 818 |
galleryImages = uploadedPhotos.slice(1).map((_, i) => URL.createObjectURL(uploadedPhotos[i + 1]));
|
| 819 |
}
|
| 820 |
+
|
| 821 |
+
// Use OCR-detected values if available
|
| 822 |
const detectedLabel = window.detectedLabel || '[Verify from photos]';
|
| 823 |
const detectedCountry = window.detectedCountry || 'UK';
|
| 824 |
const detectedFormat = window.detectedFormat || 'LP • 33rpm';
|
| 825 |
const detectedGenre = window.detectedGenre || 'rock';
|
| 826 |
const detectedCondition = window.detectedCondition || 'VG+/VG+';
|
| 827 |
const detectedPressingInfo = window.detectedPressingInfo || '';
|
| 828 |
+
|
| 829 |
+
// Fetch tracklist and detailed info from Discogs if available
|
| 830 |
+
let tracklistHtml = '';
|
| 831 |
+
let pressingDetailsHtml = '';
|
| 832 |
+
let provenanceHtml = '';
|
| 833 |
+
|
| 834 |
+
if (window.discogsReleaseId && window.discogsService?.key) {
|
| 835 |
+
try {
|
| 836 |
+
const discogsData = await window.discogsService.fetchTracklist(window.discogsReleaseId);
|
| 837 |
+
if (discogsData && discogsData.tracklist) {
|
| 838 |
+
// Build tracklist HTML
|
| 839 |
+
const hasSideBreakdown = discogsData.tracklist.some(t => t.position && (t.position.startsWith('A') || t.position.startsWith('B')));
|
| 840 |
+
|
| 841 |
+
if (hasSideBreakdown) {
|
| 842 |
+
// Group by sides
|
| 843 |
+
const sides = {};
|
| 844 |
+
discogsData.tracklist.forEach(track => {
|
| 845 |
+
const side = track.position ? track.position.charAt(0) : 'Other';
|
| 846 |
+
if (!sides[side]) sides[side] = [];
|
| 847 |
+
sides[side].push(track);
|
| 848 |
+
});
|
| 849 |
+
|
| 850 |
+
tracklistHtml = Object.entries(sides).map(([side, tracks]) => `
|
| 851 |
+
<div style="margin-bottom: 16px;">
|
| 852 |
+
<h4 style="color: #7c3aed; font-size: 13px; font-weight: 600; margin: 0 0 8px 0; text-transform: uppercase; letter-spacing: 0.5px;">Side ${side}</h4>
|
| 853 |
+
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
| 854 |
+
${tracks.map(track => `
|
| 855 |
+
<div style="flex: 1 1 200px; min-width: 200px; display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #f8fafc; border-radius: 6px; border: 1px solid #e2e8f0;">
|
| 856 |
+
<span style="color: #1e293b; font-size: 13px;"><strong>${track.position}</strong> ${track.title}</span>
|
| 857 |
+
${track.duration ? `<span style="color: #64748b; font-size: 12px; font-family: monospace;">${track.duration}</span>` : ''}
|
| 858 |
+
</div>
|
| 859 |
+
`).join('')}
|
| 860 |
+
</div>
|
| 861 |
+
</div>
|
| 862 |
+
`).join('');
|
| 863 |
+
} else {
|
| 864 |
+
// Simple list
|
| 865 |
+
tracklistHtml = `
|
| 866 |
+
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
| 867 |
+
${discogsData.tracklist.map(track => `
|
| 868 |
+
<div style="flex: 1 1 200px; min-width: 200px; display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #f8fafc; border-radius: 6px; border: 1px solid #e2e8f0;">
|
| 869 |
+
<span style="color: #1e293b; font-size: 13px;">${track.position ? `<strong>${track.position}</strong> ` : ''}${track.title}</span>
|
| 870 |
+
${track.duration ? `<span style="color: #64748b; font-size: 12px; font-family: monospace;">${track.duration}</span>` : ''}
|
| 871 |
+
</div>
|
| 872 |
+
`).join('')}
|
| 873 |
+
</div>
|
| 874 |
+
`;
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
// Build pressing/variation details
|
| 878 |
+
const identifiers = discogsData.identifiers || [];
|
| 879 |
+
const barcodeInfo = identifiers.find(i => i.type === 'Barcode');
|
| 880 |
+
const matrixInfo = identifiers.filter(i => i.type === 'Matrix / Runout' || i.type === 'Runout');
|
| 881 |
+
const pressingInfo = identifiers.filter(i => i.type === 'Pressing Plant' || i.type === 'Mastering');
|
| 882 |
+
|
| 883 |
+
if (matrixInfo.length > 0 || barcodeInfo || pressingInfo.length > 0) {
|
| 884 |
+
pressingDetailsHtml = `
|
| 885 |
+
<div style="background: #f0fdf4; border-left: 4px solid #22c55e; padding: 16px 20px; margin: 24px 0; border-radius: 0 8px 8px 0;">
|
| 886 |
+
<h3 style="margin: 0 0 12px 0; color: #166534; font-size: 15px; font-weight: 600;">Pressing & Matrix Information</h3>
|
| 887 |
+
<div style="font-family: monospace; font-size: 13px; line-height: 1.6; color: #15803d;">
|
| 888 |
+
${barcodeInfo ? `<p style="margin: 4px 0;"><strong>Barcode:</strong> ${barcodeInfo.value}</p>` : ''}
|
| 889 |
+
${matrixInfo.map(m => `<p style="margin: 4px 0;"><strong>${m.type}:</strong> ${m.value}${m.description ? ` <em>(${m.description})</em>` : ''}</p>`).join('')}
|
| 890 |
+
${pressingInfo.map(p => `<p style="margin: 4px 0;"><strong>${p.type}:</strong> ${p.value}</p>`).join('')}
|
| 891 |
+
</div>
|
| 892 |
+
${discogsData.notes ? `<p style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #bbf7d0; font-size: 12px; color: #166534; font-style: italic;">${discogsData.notes.substring(0, 300)}${discogsData.notes.length > 300 ? '...' : ''}</p>` : ''}
|
| 893 |
+
</div>
|
| 894 |
+
`;
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
// Build provenance data for buyer confidence
|
| 898 |
+
const companies = discogsData.companies || [];
|
| 899 |
+
const masteredBy = companies.find(c => c.entity_type_name === 'Mastered At' || c.name.toLowerCase().includes('mastering'));
|
| 900 |
+
const pressedBy = companies.find(c => c.entity_type_name === 'Pressed By' || c.name.toLowerCase().includes('pressing'));
|
| 901 |
+
const lacquerCut = companies.find(c => c.entity_type_name === 'Lacquer Cut At');
|
| 902 |
+
|
| 903 |
+
if (masteredBy || pressedBy || lacquerCut) {
|
| 904 |
+
provenanceHtml = `
|
| 905 |
+
<div style="background: #eff6ff; border: 1px solid #bfdbfe; padding: 16px; margin: 24px 0; border-radius: 8px;">
|
| 906 |
+
<h3 style="margin: 0 0 12px 0; color: #1e40af; font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 8px;">
|
| 907 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
| 908 |
+
Provenance & Production
|
| 909 |
+
</h3>
|
| 910 |
+
<div style="font-size: 13px; color: #1e3a8a; line-height: 1.6;">
|
| 911 |
+
${masteredBy ? `<p style="margin: 4px 0;">✓ Mastered at <strong>${masteredBy.name}</strong></p>` : ''}
|
| 912 |
+
${lacquerCut ? `<p style="margin: 4px 0;">✓ Lacquer cut at <strong>${lacquerCut.name}</strong></p>` : ''}
|
| 913 |
+
${pressedBy ? `<p style="margin: 4px 0;">✓ Pressed at <strong>${pressedBy.name}</strong></p>` : ''}
|
| 914 |
+
${discogsData.num_for_sale ? `<p style="margin: 8px 0 0 0; padding-top: 8px; border-top: 1px solid #bfdbfe; color: #3b82f6; font-size: 12px;">Reference: ${discogsData.num_for_sale} copies currently for sale on Discogs</p>` : ''}
|
| 915 |
+
</div>
|
| 916 |
+
</div>
|
| 917 |
+
`;
|
| 918 |
+
}
|
| 919 |
+
}
|
| 920 |
+
} catch (e) {
|
| 921 |
+
console.error('Failed to fetch Discogs details for HTML:', e);
|
| 922 |
+
}
|
| 923 |
+
}
|
| 924 |
+
|
| 925 |
+
// If no tracklist from Discogs, provide placeholder
|
| 926 |
+
if (!tracklistHtml) {
|
| 927 |
+
tracklistHtml = `<p style="color: #64748b; font-style: italic;">Tracklist verification recommended. Please compare with Discogs entry for accuracy.</p>`;
|
| 928 |
+
}
|
| 929 |
const galleryHtml = galleryImages.length > 0 ? `
|
| 930 |
<!-- PHOTO GALLERY -->
|
| 931 |
<div style="margin-bottom: 24px;">
|
|
|
|
| 999 |
<!-- TRACKLIST -->
|
| 1000 |
<h3 style="color: #1e293b; font-size: 18px; font-weight: 600; margin-bottom: 12px;">Tracklist</h3>
|
| 1001 |
<div style="background: #f8fafc; padding: 16px 20px; border-radius: 8px; margin-bottom: 24px;">
|
| 1002 |
+
${tracklistHtml}
|
| 1003 |
</div>
|
| 1004 |
+
|
| 1005 |
+
${pressingDetailsHtml}
|
| 1006 |
+
|
| 1007 |
+
${provenanceHtml}
|
| 1008 |
<!-- PACKING -->
|
| 1009 |
<div style="background: #eff6ff; border-left: 4px solid #3b82f6; padding: 16px 20px; margin-bottom: 24px; border-radius: 0 8px 8px 0;">
|
| 1010 |
<h3 style="margin: 0 0 12px 0; color: #1e40af; font-size: 16px; font-weight: 600;">Packing & Postage</h3>
|