refactor: simplified data loading with direct fetch endpoint

This commit is contained in:
Christian Vidal Wolf
2026-01-17 11:12:39 +01:00
parent 9d2d115bd2
commit 84d0291360
5 changed files with 122 additions and 207 deletions
+23 -37
View File
@@ -49,49 +49,35 @@ const App: React.FC = () => {
title: [], title: [],
}); });
// Handle URL Fetch (Auto/Manual) // Handle Data Fetch (Simplified)
const handleUrlFetch = useCallback(async (url: string) => { const handleDataFetch = useCallback(async () => {
setSyncing(true); setSyncing(true);
try { try {
let directUrl = url; console.log('[App] Fetching data from /api/fetch-data...');
// Create a direct download link for Dropbox if it's a share link. const response = await fetch('/api/fetch-data');
if (url.includes('dropbox.com/') && !url.includes('dl.dropboxusercontent.com')) {
const urlObject = new URL(url); if (!response.ok) {
urlObject.searchParams.set('dl', '1'); throw new Error(`Failed to fetch CSV: ${response.status} ${response.statusText}`);
directUrl = urlObject.toString();
} }
// Unified Fetch Logic for Local (Vite) and Production (Vercel Function)
// Both environments now support the /api/dropbox/... path.
// - Local: Vite proxies /api/dropbox -> https://www.dropbox.com
// - Vercel: api/dropbox.js handles the request -> https://www.dropbox.com
const urlObj = new URL(directUrl);
const searchParams = urlObj.search;
// Construct path relative to root: /api/dropbox/scl/fi/...
const fetchUrl = `/api/dropbox${urlObj.pathname}${searchParams}`;
const response = await fetch(fetchUrl);
if (!response.ok) throw new Error(`Failed to fetch CSV from URL: ${response.status} ${response.statusText}`);
const csvText = await response.text(); const csvText = await response.text();
const data = await processCSV(csvText); const data = await processCSV(csvText);
await saveSalesData(data); await saveSalesData(data);
initializeData(data); initializeData(data);
setActiveUrl(url); // Store the original user-facing URL setActiveUrl(PERMANENT_DROPBOX_URL);
const now = new Date().toISOString(); const now = new Date().toISOString();
setLastUpdated(now); setLastUpdated(now);
localStorage.setItem('craze_last_updated', now); localStorage.setItem('craze_last_updated', now);
localStorage.setItem('craze_csv_url', url); localStorage.setItem('craze_csv_url', PERMANENT_DROPBOX_URL);
setIsDataModalOpen(false); // Close modal on success setIsDataModalOpen(false);
console.log('[App] Successfully loaded', data.length, 'rows');
} catch (error) { } catch (error) {
console.error("Failed to fetch/parse CSV from URL", error); console.error("Failed to fetch/parse CSV", error);
// Don't alert on auto-fetch to avoid spamming the user on startup if offline alert("Error loading data. Please refresh the page.");
// alert("Error syncing data. Please check the URL."); throw error;
throw error; // re-throw to be caught by caller
} finally { } finally {
setSyncing(false); setSyncing(false);
setLoading(false); setLoading(false);
@@ -144,13 +130,13 @@ const App: React.FC = () => {
setLoading(false); setLoading(false);
} else { } else {
console.log("Fetching fresh data from Permanent URL..."); console.log("Fetching fresh data from Permanent URL...");
handleUrlFetch(PERMANENT_DROPBOX_URL).catch(e => { handleDataFetch().catch(e => {
console.error("Initial fetch failed."); console.error("Initial fetch failed.");
}); });
} }
}; };
initApp(); initApp();
}, [handleUrlFetch]); }, [handleDataFetch]);
// Handle uploaded Sales file (Manual) // Handle uploaded Sales file (Manual)
const handleSalesUpload = async (file: File) => { const handleSalesUpload = async (file: File) => {
@@ -196,7 +182,7 @@ const App: React.FC = () => {
// Refresh if it's after 7 AM and we haven't refreshed today // Refresh if it's after 7 AM and we haven't refreshed today
if (now.getHours() >= 7 && lastRefreshDate !== today) { if (now.getHours() >= 7 && lastRefreshDate !== today) {
console.log("Triggering daily data refresh..."); console.log("Triggering daily data refresh...");
handleUrlFetch(PERMANENT_DROPBOX_URL).then(() => { handleDataFetch().then(() => {
localStorage.setItem('craze_last_refresh_date', today); localStorage.setItem('craze_last_refresh_date', today);
console.log("Daily refresh successful."); console.log("Daily refresh successful.");
}).catch(err => { }).catch(err => {
@@ -212,7 +198,7 @@ const App: React.FC = () => {
const interval = setInterval(checkAndRefresh, 15 * 60 * 1000); const interval = setInterval(checkAndRefresh, 15 * 60 * 1000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [handleUrlFetch]); }, [handleDataFetch]);
// Derive Data // Derive Data
@@ -322,7 +308,7 @@ const App: React.FC = () => {
{/* NEW REFRESH BUTTON */} {/* NEW REFRESH BUTTON */}
<button <button
onClick={() => activeUrl && handleUrlFetch(activeUrl)} onClick={handleDataFetch}
disabled={syncing} disabled={syncing}
title="Refresh Data" title="Refresh Data"
className="p-3 rounded-lg bg-slate-800 border border-border text-slate-400 hover:text-white hover:bg-slate-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" className="p-3 rounded-lg bg-slate-800 border border-border text-slate-400 hover:text-white hover:bg-slate-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
@@ -400,9 +386,9 @@ const App: React.FC = () => {
<div className="p-6"> <div className="p-6">
<FileUpload <FileUpload
onSalesUpload={handleSalesUpload} // CORRECTED: Was handleFileUpload onSalesUpload={handleSalesUpload}
onAdsUpload={handleAdsUpload} // ADDED: Missing prop causing error onAdsUpload={handleAdsUpload}
onUrlSubmit={handleUrlFetch} onUrlSubmit={handleDataFetch}
isLoading={syncing} isLoading={syncing}
activeUrl={activeUrl} activeUrl={activeUrl}
onDisconnect={disconnectUrl} onDisconnect={disconnectUrl}
-67
View File
@@ -1,67 +0,0 @@
import type { VercelRequest, VercelResponse } from '@vercel/node';
import fetch from 'node-fetch'; // Ensure fetch is available
export default async function handler(req: VercelRequest, res: VercelResponse) {
// Allow CORS
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,PATCH,DELETE,POST,PUT');
res.setHeader(
'Access-Control-Allow-Headers',
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
);
if (req.method === 'OPTIONS') {
res.status(200).end();
return;
}
try {
// 1. Reconstruct the Target Dropbox URL
// The incoming request is to /api/dropbox/scl/fi/...
// We want to fetch https://www.dropbox.com/scl/fi/...
// req.url in Vercel function might be just the suffix or full path depending on routing
// Typically: /api/dropbox/scl/fi/xyz?dl=1
// We need to strip '/api/dropbox' from the start to get the dropbox path
const requestPath = req.url || '';
const dropboxPath = requestPath.replace(/^\/api\/dropbox/, ''); // e.g. /scl/fi/xyz?dl=1
let targetUrl = `https://www.dropbox.com${dropboxPath}`;
// Ensure dl=1 is present to force download
if (targetUrl.includes('?')) {
if (!targetUrl.includes('dl=1')) {
targetUrl = targetUrl.replace('dl=0', 'dl=1');
if (!targetUrl.includes('dl=1')) {
targetUrl += '&dl=1';
}
}
} else {
targetUrl += '?dl=1';
}
console.log(`[Proxy] Incoming: ${req.url}`);
console.log(`[Proxy] Target: ${targetUrl}`);
const response = await fetch(targetUrl);
if (!response.ok) {
throw new Error(`Dropbox responded with ${response.status} ${response.statusText}`);
}
// Forward the content type (likely text/csv)
const contentType = response.headers.get('content-type');
if (contentType) {
res.setHeader('Content-Type', contentType);
}
const data = await response.text();
res.status(200).send(data);
} catch (error: any) {
console.error('Proxy Error:', error);
res.status(500).json({ error: 'Failed to fetch data from Dropbox', details: error.message });
}
}
+32
View File
@@ -0,0 +1,32 @@
import type { VercelRequest, VercelResponse } from '@vercel/node';
const DROPBOX_URL = "https://www.dropbox.com/scl/fi/b9zxn4z5i7sxwfakk5g5y/Amazon-Sell-Out-2023-2025.csv?rlkey=uoto6v0mm99py8nszy8ldtez8&st=pzn1zkrg&dl=1";
export default async function handler(req: VercelRequest, res: VercelResponse) {
// CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
return res.status(200).end();
}
try {
console.log('[fetch-data] Fetching from Dropbox...');
const response = await fetch(DROPBOX_URL);
if (!response.ok) {
throw new Error(`Dropbox responded with ${response.status}`);
}
const csvData = await response.text();
console.log('[fetch-data] Successfully fetched CSV, size:', csvData.length);
res.setHeader('Content-Type', 'text/csv');
res.status(200).send(csvData);
} catch (error: any) {
console.error('[fetch-data] Error:', error);
res.status(500).json({ error: error.message });
}
}
+64 -100
View File
@@ -1,25 +1,25 @@
import React, { ChangeEvent, useState } from 'react'; import React, { ChangeEvent, useState } from 'react';
import { UploadIcon, MegaphoneIcon } from './Icons'; import { UploadIcon, MegaphoneIcon } from './Icons';
interface FileUploadProps { interface FileUploadProps {
onSalesUpload: (file: File) => void; // Renamed from onFileUpload onSalesUpload: (file: File) => void;
onAdsUpload: (file: File) => void; // New Prop onAdsUpload: (file: File) => void;
onUrlSubmit: (url: string) => void; onUrlSubmit: () => void; // Changed: no longer takes URL parameter
isLoading: boolean; isLoading: boolean;
activeUrl?: string | null; activeUrl?: string | null;
onDisconnect?: () => void; onDisconnect?: () => void;
lastUpdated?: string | null; lastUpdated?: string | null;
} }
const FileUpload: React.FC<FileUploadProps> = ({ const FileUpload: React.FC<FileUploadProps> = ({
onSalesUpload, onSalesUpload,
onAdsUpload, onAdsUpload,
onUrlSubmit, onUrlSubmit,
isLoading, isLoading,
activeUrl, activeUrl,
onDisconnect, onDisconnect,
lastUpdated lastUpdated
}) => { }) => {
const [url, setUrl] = useState(''); const [url, setUrl] = useState('');
@@ -30,37 +30,37 @@ const FileUpload: React.FC<FileUploadProps> = ({
}; };
const handleAdsChange = (e: ChangeEvent<HTMLInputElement>) => { const handleAdsChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) { if (e.target.files && e.target.files.length > 0) {
onAdsUpload(e.target.files[0]); onAdsUpload(e.target.files[0]);
} }
}; };
const handleUrlSubmit = (e: React.FormEvent) => { const handleUrlSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (url.trim()) { if (url.trim()) {
onUrlSubmit(url.trim()); onUrlSubmit(url.trim());
} }
}; };
const handleSyncNow = () => { const handleSyncNow = () => {
if (activeUrl) onUrlSubmit(activeUrl); onUrlSubmit();
}; };
return ( return (
<div className="flex flex-col gap-6 p-2"> <div className="flex flex-col gap-6 p-2">
{/* Active Connection Status */} {/* Active Connection Status */}
{activeUrl && ( {activeUrl && (
<div className="bg-emerald-900/20 border border-emerald-500/30 rounded-xl p-6 text-center animate-fade-in"> <div className="bg-emerald-900/20 border border-emerald-500/30 rounded-xl p-6 text-center animate-fade-in">
<div className="flex items-center justify-center gap-2 mb-2 text-emerald-400"> <div className="flex items-center justify-center gap-2 mb-2 text-emerald-400">
<div className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></div> <div className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></div>
<span className="font-bold text-sm uppercase tracking-wide">Cloud Sync Active</span> <span className="font-bold text-sm uppercase tracking-wide">Cloud Sync Active</span>
</div> </div>
<p className="text-slate-300 text-sm mb-1 truncate max-w-sm mx-auto opacity-80">{activeUrl}</p> <p className="text-slate-300 text-sm mb-1 truncate max-w-sm mx-auto opacity-80">{activeUrl}</p>
{lastUpdated && <p className="text-xs text-slate-500 mb-4">Last updated: {new Date(lastUpdated).toLocaleString()}</p>} {lastUpdated && <p className="text-xs text-slate-500 mb-4">Last updated: {new Date(lastUpdated).toLocaleString()}</p>}
<div className="flex gap-3 justify-center"> <div className="flex gap-3 justify-center">
<button <button
onClick={handleSyncNow} onClick={handleSyncNow}
disabled={isLoading} disabled={isLoading}
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-sm font-medium transition-colors flex items-center gap-2 disabled:opacity-50" className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-sm font-medium transition-colors flex items-center gap-2 disabled:opacity-50"
@@ -77,7 +77,7 @@ const FileUpload: React.FC<FileUploadProps> = ({
</> </>
)} )}
</button> </button>
<button <button
onClick={onDisconnect} onClick={onDisconnect}
className="px-4 py-2 bg-slate-800 hover:bg-red-900/30 text-slate-300 hover:text-red-400 border border-slate-700 hover:border-red-800 rounded-lg text-sm font-medium transition-colors" className="px-4 py-2 bg-slate-800 hover:bg-red-900/30 text-slate-300 hover:text-red-400 border border-slate-700 hover:border-red-800 rounded-lg text-sm font-medium transition-colors"
> >
@@ -89,82 +89,46 @@ const FileUpload: React.FC<FileUploadProps> = ({
{/* Manual File Uploads */} {/* Manual File Uploads */}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="rounded-xl border border-dashed border-slate-700 bg-slate-900/50 hover:bg-slate-900 transition-colors group h-full"> <div className="rounded-xl border border-dashed border-slate-700 bg-slate-900/50 hover:bg-slate-900 transition-colors group h-full">
<label htmlFor="sales-upload" className="cursor-pointer flex flex-col items-center justify-center gap-3 p-6 h-full"> <label htmlFor="sales-upload" className="cursor-pointer flex flex-col items-center justify-center gap-3 p-6 h-full">
<div className="p-3 bg-indigo-500/10 rounded-full text-indigo-400 group-hover:scale-110 transition-transform"> <div className="p-3 bg-indigo-500/10 rounded-full text-indigo-400 group-hover:scale-110 transition-transform">
<UploadIcon /> <UploadIcon />
</div> </div>
<div className="text-center"> <div className="text-center">
<h3 className="font-semibold text-slate-200">Upload Sales CSV</h3> <h3 className="font-semibold text-slate-200">Upload Sales CSV</h3>
<p className="text-[10px] text-slate-500 mt-1">Standard Sales Data</p> <p className="text-[10px] text-slate-500 mt-1">Standard Sales Data</p>
</div> </div>
<input <input
id="sales-upload" id="sales-upload"
type="file" type="file"
accept=".csv,.xlsx" accept=".csv,.xlsx"
onChange={handleSalesChange} onChange={handleSalesChange}
disabled={isLoading} disabled={isLoading}
className="hidden" className="hidden"
/> />
</label> </label>
</div>
<div className="rounded-xl border border-dashed border-slate-700 bg-slate-900/50 hover:bg-slate-900 transition-colors group h-full">
<label htmlFor="ads-upload" className="cursor-pointer flex flex-col items-center justify-center gap-3 p-6 h-full">
<div className="p-3 bg-fuchsia-500/10 rounded-full text-fuchsia-400 group-hover:scale-110 transition-transform">
<MegaphoneIcon />
</div>
<div className="text-center">
<h3 className="font-semibold text-slate-200">Upload Ads CSV</h3>
<p className="text-[10px] text-slate-500 mt-1">Advertising Expenses</p>
</div>
<input
id="ads-upload"
type="file"
accept=".csv,.xlsx"
onChange={handleAdsChange}
disabled={isLoading}
className="hidden"
/>
</label>
</div>
</div>
<div className="flex items-center gap-4">
<div className="h-px bg-slate-800 flex-1"></div>
<span className="text-slate-600 text-xs font-bold uppercase">OR</span>
<div className="h-px bg-slate-800 flex-1"></div>
</div>
{/* URL Connection */}
<div className="bg-slate-900 border border-slate-800 rounded-xl p-5">
<h3 className="text-sm font-bold text-slate-200 mb-1">{activeUrl ? 'Change Source URL' : 'Connect Cloud CSV'}</h3>
<p className="text-xs text-slate-500 mb-3">Direct link to CSV (e.g. Dropbox dl=1). Auto-refreshes daily at 7 AM.</p>
<form onSubmit={handleUrlSubmit} className="flex gap-2">
<input
type="url"
placeholder="https://..."
value={url}
onChange={(e) => setUrl(e.target.value)}
className="flex-1 bg-slate-950 border border-slate-700 text-slate-200 rounded-lg px-3 py-2 focus:outline-none focus:border-indigo-500 text-sm"
required
/>
<button
type="submit"
disabled={isLoading || !url.trim()}
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white font-medium rounded-lg text-sm transition-colors disabled:opacity-50 whitespace-nowrap"
>
Connect
</button>
</form>
</div>
{isLoading && !activeUrl && (
<div className="flex items-center justify-center gap-2 text-indigo-400 py-2">
<div className="w-4 h-4 border-2 border-indigo-400 border-t-transparent rounded-full animate-spin"></div>
<span className="text-sm font-medium">Processing data...</span>
</div> </div>
)}
<div className="rounded-xl border border-dashed border-slate-700 bg-slate-900/50 hover:bg-slate-900 transition-colors group h-full">
<label htmlFor="ads-upload" className="cursor-pointer flex flex-col items-center justify-center gap-3 p-6 h-full">
<div className="p-3 bg-fuchsia-500/10 rounded-full text-fuchsia-400 group-hover:scale-110 transition-transform">
<MegaphoneIcon />
</div>
<div className="text-center">
<h3 className="font-semibold text-slate-200">Upload Ads CSV</h3>
<p className="text-[10px] text-slate-500 mt-1">Advertising Expenses</p>
</div>
<input
id="ads-upload"
type="file"
accept=".csv,.xlsx"
onChange={handleAdsChange}
disabled={isLoading}
className="hidden"
/>
</label>
</div>
</div>
</div> </div>
); );
}; };
+3 -3
View File
@@ -9,10 +9,10 @@ export default defineConfig(({ mode }) => {
port: 3000, port: 3000,
host: '0.0.0.0', host: '0.0.0.0',
proxy: { proxy: {
'/api/dropbox': { '/api/fetch-data': {
target: 'https://www.dropbox.com', target: 'https://www.dropbox.com/scl/fi/b9zxn4z5i7sxwfakk5g5y/Amazon-Sell-Out-2023-2025.csv?rlkey=uoto6v0mm99py8nszy8ldtez8&st=pzn1zkrg&dl=1',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/dropbox/, ''), rewrite: () => '', // Replace entire path with empty string (target has full URL)
followRedirects: true followRedirects: true
} }
} }