flen-crypto commited on
Commit
982dccf
·
verified ·
1 Parent(s): 5e3c7d8

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 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
- <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">
55
- <i data-feather="upload" class="w-4 h-4"></i>
56
- Import Discogs CSV
57
- </button>
58
- <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">
 
 
 
 
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)... &#10;Example:&#10;Pink Floyd - Dark Side of the Moon - £15&#10;The Beatles - Abbey Road VG+ - £25&#10;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
- // Render results
681
- renderTitleOptions(titles);
682
- renderPricingStrategy(recommendedBin, strategy, comps, currency, goal);
683
- renderFeeFloor(cost, totalFees, shippingCost, packingCost, safeFloor, currency);
684
- renderHTMLDescription(data, titles[0]);
685
- renderTags(artist, title, catNo, year);
686
- renderShotList(uploadedPhotos);
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
- // 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
  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
- <p style="color: #64748b; font-style: italic; margin: 0;">[Fetch from Discogs or verify from label photos. Format as Side A / Side B with track times if available.]</p>
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>