mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 14: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: [],
|
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}
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
-88
@@ -3,9 +3,9 @@ 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;
|
||||||
@@ -30,20 +30,20 @@ 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 (
|
||||||
@@ -53,8 +53,8 @@ const FileUpload: React.FC<FileUploadProps> = ({
|
|||||||
{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>}
|
||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user