← back to Handbag Authentication
api/mobile-api.js
366 lines
/**
* Mobile API Endpoints
* Optimized for mobile devices with compressed responses
*/
const fs = require('fs');
const path = require('path');
const csv = require('csv-parser');
class MobileAPI {
constructor() {
this.dataPath = path.join(__dirname, '..', 'handbag_data', 'processed');
this.cache = {
retailer: null,
luxury: null,
museum: null,
lastUpdate: null
};
this.cacheTimeout = 5 * 60 * 1000; // 5 minutes
}
/**
* Load and cache data
*/
async loadData(type = 'all') {
const now = Date.now();
// Check cache
if (this.cache.lastUpdate && (now - this.cache.lastUpdate) < this.cacheTimeout) {
return this.getCachedData(type);
}
// Load fresh data
await Promise.all([
this.loadRetailerData(),
this.loadLuxuryData(),
this.loadMuseumData()
]);
this.cache.lastUpdate = now;
return this.getCachedData(type);
}
/**
* Load retailer handbags
*/
async loadRetailerData() {
return new Promise((resolve, reject) => {
const results = [];
const filePath = path.join(this.dataPath, 'retailer_handbags.csv');
if (!fs.existsSync(filePath)) {
this.cache.retailer = [];
return resolve([]);
}
fs.createReadStream(filePath)
.pipe(csv())
.on('data', (row) => {
results.push({
id: `retailer_${results.length + 1}`,
source: 'retailer',
brand: row.brand || 'Unknown',
name: row.product_name || 'Unknown',
price: parseFloat(row.price_numeric) || 0,
priceOriginal: row.price_original || '',
type: row.type || 'handbag',
retailer: row.retailer || '',
imageUrl: row.image_url || '',
pageUrl: row.page_url || '',
has360: false // Retailer data doesn't have 360
});
})
.on('end', () => {
this.cache.retailer = results;
resolve(results);
})
.on('error', reject);
});
}
/**
* Load luxury handbags
*/
async loadLuxuryData() {
return new Promise((resolve, reject) => {
const results = [];
const filePath = path.join(this.dataPath, 'luxury_handbags.csv');
if (!fs.existsSync(filePath)) {
this.cache.luxury = [];
return resolve([]);
}
fs.createReadStream(filePath)
.pipe(csv())
.on('data', (row) => {
results.push({
id: `luxury_${results.length + 1}`,
source: 'luxury',
brand: row.brand || 'Unknown',
name: row.bag_style || 'Unknown',
price: parseFloat(row.price) || 0,
bagStyle: row.bag_style || '',
skinType: row.skin_type || '',
hardwareMetal: row.hardware_metal || '',
majorColor: row.major_color || '',
innerMaterial: row.inner_material || '',
type: 'luxury',
has360: false // Mark luxury items as potentially having 360
});
})
.on('end', () => {
this.cache.luxury = results;
resolve(results);
})
.on('error', reject);
});
}
/**
* Load museum data
*/
async loadMuseumData() {
try {
const filePath = path.join(this.dataPath, 'museum_handbags_normalized.json');
if (!fs.existsSync(filePath)) {
this.cache.museum = [];
return [];
}
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
this.cache.museum = data.map((item, index) => ({
id: `museum_${index + 1}`,
source: 'museum',
brand: item.artist_maker || 'Historical',
name: item.title || 'Untitled',
price: null,
dated: item.dated || '',
culture: item.culture || '',
classification: item.classification || '',
imageUrl: item.image_url || '',
museumUrl: item.url || '',
type: 'historical',
has360: false
}));
return this.cache.museum;
} catch (error) {
console.error('Error loading museum data:', error);
this.cache.museum = [];
return [];
}
}
/**
* Get cached data
*/
getCachedData(type) {
switch (type) {
case 'retailer':
return this.cache.retailer || [];
case 'luxury':
return this.cache.luxury || [];
case 'museum':
return this.cache.museum || [];
case 'all':
default:
return [
...(this.cache.retailer || []),
...(this.cache.luxury || []),
...(this.cache.museum || [])
];
}
}
/**
* Get paginated handbags
*/
async getHandbags(options = {}) {
const {
page = 1,
limit = 20,
category = 'all',
brand = null,
minPrice = null,
maxPrice = null,
search = null,
sortBy = 'recent'
} = options;
// Load data
let data = await this.loadData(category);
// Filter by brand
if (brand) {
data = data.filter(item =>
item.brand.toLowerCase().includes(brand.toLowerCase())
);
}
// Filter by price range
if (minPrice !== null) {
data = data.filter(item => item.price >= minPrice);
}
if (maxPrice !== null) {
data = data.filter(item => item.price <= maxPrice);
}
// Search filter
if (search) {
const searchLower = search.toLowerCase();
data = data.filter(item =>
item.brand.toLowerCase().includes(searchLower) ||
item.name.toLowerCase().includes(searchLower) ||
(item.type && item.type.toLowerCase().includes(searchLower))
);
}
// Sort
switch (sortBy) {
case 'price_asc':
data.sort((a, b) => (a.price || 0) - (b.price || 0));
break;
case 'price_desc':
data.sort((a, b) => (b.price || 0) - (a.price || 0));
break;
case 'brand':
data.sort((a, b) => a.brand.localeCompare(b.brand));
break;
default:
// Recent first (default order)
break;
}
// Paginate
const start = (page - 1) * limit;
const end = start + limit;
const paginated = data.slice(start, end);
return {
data: paginated,
pagination: {
page,
limit,
total: data.length,
totalPages: Math.ceil(data.length / limit),
hasMore: end < data.length
}
};
}
/**
* Get statistics
*/
async getStats() {
const data = await this.loadData('all');
const brands = new Set(data.map(item => item.brand));
const prices = data.filter(item => item.price > 0).map(item => item.price);
const stats = {
totalItems: data.length,
retailerItems: (this.cache.retailer || []).length,
luxuryItems: (this.cache.luxury || []).length,
museumItems: (this.cache.museum || []).length,
totalBrands: brands.size,
priceRange: {
min: prices.length > 0 ? Math.min(...prices) : 0,
max: prices.length > 0 ? Math.max(...prices) : 0,
avg: prices.length > 0 ? prices.reduce((a, b) => a + b, 0) / prices.length : 0
},
topBrands: this.getTopBrands(data, 10),
categoryBreakdown: {
retailer: (this.cache.retailer || []).length,
luxury: (this.cache.luxury || []).length,
historical: (this.cache.museum || []).length
}
};
return stats;
}
/**
* Get top brands
*/
getTopBrands(data, limit = 10) {
const brandCounts = {};
data.forEach(item => {
const brand = item.brand || 'Unknown';
brandCounts[brand] = (brandCounts[brand] || 0) + 1;
});
return Object.entries(brandCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, limit)
.map(([brand, count]) => ({ brand, count }));
}
/**
* Get product by ID
*/
async getProductById(id) {
const data = await this.loadData('all');
return data.find(item => item.id === id) || null;
}
/**
* Get brands list
*/
async getBrands() {
const data = await this.loadData('all');
const brands = new Set(data.map(item => item.brand));
return Array.from(brands).sort().map(brand => {
const count = data.filter(item => item.brand === brand).length;
const avgPrice = data
.filter(item => item.brand === brand && item.price > 0)
.reduce((sum, item, _, arr) =>
arr.length > 0 ? sum + item.price / arr.length : 0
, 0);
return {
name: brand,
count,
avgPrice: Math.round(avgPrice)
};
});
}
/**
* Search suggestions
*/
async getSearchSuggestions(query, limit = 10) {
if (!query || query.length < 2) {
return [];
}
const data = await this.loadData('all');
const queryLower = query.toLowerCase();
const suggestions = new Set();
// Add brand matches
data.forEach(item => {
if (item.brand.toLowerCase().includes(queryLower)) {
suggestions.add(item.brand);
}
});
// Add type matches
data.forEach(item => {
if (item.type && item.type.toLowerCase().includes(queryLower)) {
suggestions.add(item.type);
}
});
return Array.from(suggestions).slice(0, limit);
}
}
module.exports = MobileAPI;