mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 13:05:24 +02:00
refactor: simplified data loading with direct fetch endpoint
This commit is contained in:
@@ -49,49 +49,35 @@ const App: React.FC = () => {
|
||||
title: [],
|
||||
});
|
||||
|
||||
// Handle URL Fetch (Auto/Manual)
|
||||
const handleUrlFetch = useCallback(async (url: string) => {
|
||||
// Handle Data Fetch (Simplified)
|
||||
const handleDataFetch = useCallback(async () => {
|
||||
setSyncing(true);
|
||||
try {
|
||||
let directUrl = url;
|
||||
// Create a direct download link for Dropbox if it's a share link.
|
||||
if (url.includes('dropbox.com/') && !url.includes('dl.dropboxusercontent.com')) {
|
||||
const urlObject = new URL(url);
|
||||
urlObject.searchParams.set('dl', '1');
|
||||
directUrl = urlObject.toString();
|
||||
console.log('[App] Fetching data from /api/fetch-data...');
|
||||
const response = await fetch('/api/fetch-data');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch CSV: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
// 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 data = await processCSV(csvText);
|
||||
|
||||
await saveSalesData(data);
|
||||
|
||||
initializeData(data);
|
||||
setActiveUrl(url); // Store the original user-facing URL
|
||||
setActiveUrl(PERMANENT_DROPBOX_URL);
|
||||
const now = new Date().toISOString();
|
||||
setLastUpdated(now);
|
||||
localStorage.setItem('craze_last_updated', now);
|
||||
localStorage.setItem('craze_csv_url', url);
|
||||
setIsDataModalOpen(false); // Close modal on success
|
||||
localStorage.setItem('craze_csv_url', PERMANENT_DROPBOX_URL);
|
||||
setIsDataModalOpen(false);
|
||||
|
||||
console.log('[App] Successfully loaded', data.length, 'rows');
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch/parse CSV from URL", error);
|
||||
// Don't alert on auto-fetch to avoid spamming the user on startup if offline
|
||||
// alert("Error syncing data. Please check the URL.");
|
||||
throw error; // re-throw to be caught by caller
|
||||
console.error("Failed to fetch/parse CSV", error);
|
||||
alert("Error loading data. Please refresh the page.");
|
||||
throw error;
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
setLoading(false);
|
||||
@@ -144,13 +130,13 @@ const App: React.FC = () => {
|
||||
setLoading(false);
|
||||
} else {
|
||||
console.log("Fetching fresh data from Permanent URL...");
|
||||
handleUrlFetch(PERMANENT_DROPBOX_URL).catch(e => {
|
||||
handleDataFetch().catch(e => {
|
||||
console.error("Initial fetch failed.");
|
||||
});
|
||||
}
|
||||
};
|
||||
initApp();
|
||||
}, [handleUrlFetch]);
|
||||
}, [handleDataFetch]);
|
||||
|
||||
// Handle uploaded Sales file (Manual)
|
||||
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
|
||||
if (now.getHours() >= 7 && lastRefreshDate !== today) {
|
||||
console.log("Triggering daily data refresh...");
|
||||
handleUrlFetch(PERMANENT_DROPBOX_URL).then(() => {
|
||||
handleDataFetch().then(() => {
|
||||
localStorage.setItem('craze_last_refresh_date', today);
|
||||
console.log("Daily refresh successful.");
|
||||
}).catch(err => {
|
||||
@@ -212,7 +198,7 @@ const App: React.FC = () => {
|
||||
const interval = setInterval(checkAndRefresh, 15 * 60 * 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [handleUrlFetch]);
|
||||
}, [handleDataFetch]);
|
||||
|
||||
|
||||
// Derive Data
|
||||
@@ -322,7 +308,7 @@ const App: React.FC = () => {
|
||||
|
||||
{/* NEW REFRESH BUTTON */}
|
||||
<button
|
||||
onClick={() => activeUrl && handleUrlFetch(activeUrl)}
|
||||
onClick={handleDataFetch}
|
||||
disabled={syncing}
|
||||
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"
|
||||
@@ -400,9 +386,9 @@ const App: React.FC = () => {
|
||||
|
||||
<div className="p-6">
|
||||
<FileUpload
|
||||
onSalesUpload={handleSalesUpload} // CORRECTED: Was handleFileUpload
|
||||
onAdsUpload={handleAdsUpload} // ADDED: Missing prop causing error
|
||||
onUrlSubmit={handleUrlFetch}
|
||||
onSalesUpload={handleSalesUpload}
|
||||
onAdsUpload={handleAdsUpload}
|
||||
onUrlSubmit={handleDataFetch}
|
||||
isLoading={syncing}
|
||||
activeUrl={activeUrl}
|
||||
onDisconnect={disconnectUrl}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@ import React, { ChangeEvent, useState } from 'react';
|
||||
import { UploadIcon, MegaphoneIcon } from './Icons';
|
||||
|
||||
interface FileUploadProps {
|
||||
onSalesUpload: (file: File) => void; // Renamed from onFileUpload
|
||||
onAdsUpload: (file: File) => void; // New Prop
|
||||
onUrlSubmit: (url: string) => void;
|
||||
onSalesUpload: (file: File) => void;
|
||||
onAdsUpload: (file: File) => void;
|
||||
onUrlSubmit: () => void; // Changed: no longer takes URL parameter
|
||||
isLoading: boolean;
|
||||
activeUrl?: string | null;
|
||||
onDisconnect?: () => void;
|
||||
@@ -43,7 +43,7 @@ const FileUpload: React.FC<FileUploadProps> = ({
|
||||
};
|
||||
|
||||
const handleSyncNow = () => {
|
||||
if (activeUrl) onUrlSubmit(activeUrl);
|
||||
onUrlSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -129,42 +129,6 @@ const FileUpload: React.FC<FileUploadProps> = ({
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-3
@@ -9,10 +9,10 @@ export default defineConfig(({ mode }) => {
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api/dropbox': {
|
||||
target: 'https://www.dropbox.com',
|
||||
'/api/fetch-data': {
|
||||
target: 'https://www.dropbox.com/scl/fi/b9zxn4z5i7sxwfakk5g5y/Amazon-Sell-Out-2023-2025.csv?rlkey=uoto6v0mm99py8nszy8ldtez8&st=pzn1zkrg&dl=1',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api\/dropbox/, ''),
|
||||
rewrite: () => '', // Replace entire path with empty string (target has full URL)
|
||||
followRedirects: true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user