feat: Automate Dropbox data loading with Vite proxy

This commit is contained in:
Christian Vidal Wolf
2026-01-16 10:20:22 +01:00
parent 135e0d78ed
commit 470db20458
9 changed files with 406 additions and 1664 deletions
+48 -95
View File
@@ -23,7 +23,7 @@ const RefreshIcon = ({ className }: { className?: string }) => (
// Hardcoded Permanent URL for Auto-Loading
// Using the original share link to leverage Dropbox's redirect for robust fetching.
const PERMANENT_DROPBOX_URL = "https://www.dropbox.com/scl/fi/b9zxn4z5i7sxwfakk5g5y/Amazon-Sell-Out-2023-2025.csv?rlkey=uoto6v0mm99py8nszy8ldtez8&dl=0";
const PERMANENT_DROPBOX_URL = "https://www.dropbox.com/scl/fi/b9zxn4z5i7sxwfakk5g5y/Amazon-Sell-Out-2023-2025.csv?rlkey=uoto6v0mm99py8nszy8ldtez8&st=7vk22iod&dl=0";
const App: React.FC = () => {
const [rawData, setRawData] = useState<SalesRecord[]>([]);
@@ -52,15 +52,7 @@ const App: React.FC = () => {
// Handle URL Fetch (Auto/Manual)
const handleUrlFetch = useCallback(async (url: string) => {
setSyncing(true);
setLoading(true);
// Reset previous errors on new attempt
// setLastUpdated(null);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000); // 15s Timeout
try {
console.log(`[Sync] Starting fetch from: ${url}`);
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')) {
@@ -69,28 +61,28 @@ const App: React.FC = () => {
directUrl = urlObject.toString();
}
// Use a CORS proxy to bypass browser's same-origin policy restrictions.
const proxyUrl = `https://corsproxy.io/?${encodeURIComponent(directUrl)}`;
let fetchUrl = directUrl;
console.log(`[Sync] Fetching via proxy: ${proxyUrl}`);
// Check if we are in a local environment to use the Vite proxy
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
const response = await fetch(proxyUrl, {
signal: controller.signal
});
if (!response.ok) {
throw new Error(`Failed to fetch CSV: ${response.status} ${response.statusText}`);
if (isLocal && url.includes('dropbox.com')) {
// Extract path and query from the direct URL
const urlObj = new URL(directUrl);
// Pass the necessary query params including st and dl=1
const searchParams = urlObj.search;
fetchUrl = `/api/dropbox${urlObj.pathname}${searchParams}`;
} else {
// Production or non-local fallback: Use CORS proxy
// Using allorigins.win as a fallback if not local
fetchUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(directUrl)}`;
}
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();
console.log(`[Sync] Download complete. Bytes: ${csvText.length}`);
if (!csvText || csvText.trim().length === 0) {
throw new Error("Downloaded file is empty");
}
const data = await processCSV(csvText);
console.log(`[Sync] Processing complete. Rows: ${data.length}`);
await saveSalesData(data);
@@ -101,20 +93,12 @@ const App: React.FC = () => {
localStorage.setItem('craze_last_updated', now);
localStorage.setItem('craze_csv_url', url);
setIsDataModalOpen(false); // Close modal on success
} catch (error: any) {
console.error("[Sync] Error during data fetch:", error);
let msg = "Failed to sync data.";
if (error.name === 'AbortError') {
msg = "Connection timed out (15s). Proxy might be slow.";
} else if (error.message) {
msg = error.message;
}
alert(`Error: ${msg}\n\nSwitching to manual mode.`);
// IMPORTANT: If fetch fails, we MUST stop loading so the user can interact
setRawData([]);
setActiveUrl(null); // Clear active URL so valid manual upload is required or retry
} 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
} finally {
clearTimeout(timeoutId);
setSyncing(false);
setLoading(false);
}
@@ -137,31 +121,25 @@ const App: React.FC = () => {
useEffect(() => {
const initApp = async () => {
setLoading(true);
console.log("[Init] App starting...");
try {
const currentStoredUrl = localStorage.getItem('craze_csv_url');
if (currentStoredUrl !== PERMANENT_DROPBOX_URL) {
console.log("[Init] Updating stored URL to default permanent URL");
localStorage.setItem('craze_csv_url', PERMANENT_DROPBOX_URL);
setActiveUrl(PERMANENT_DROPBOX_URL);
}
const currentStoredUrl = localStorage.getItem('craze_csv_url');
if (currentStoredUrl !== PERMANENT_DROPBOX_URL) {
localStorage.setItem('craze_csv_url', PERMANENT_DROPBOX_URL);
setActiveUrl(PERMANENT_DROPBOX_URL);
}
const { data, lastUpdated: date } = await loadSalesData();
const { data, lastUpdated: date } = await loadSalesData();
if (data && data.length > 0) {
console.log("[Init] Loaded data from cache:", data.length, "rows. Last updated:", date);
initializeData(data);
setLastUpdated(date);
setLoading(false);
} else {
console.log("[Init] No cache found. Auto-fetching from Permanent URL...");
// Catch error here so initApp doesn't crash, handleUrlFetch handles UI
await handleUrlFetch(PERMANENT_DROPBOX_URL);
}
} catch (e) {
console.error("[Init] Critical failure during initialization:", e);
setLoading(false); // Ensure we never get stuck in infinite load
if (data && data.length > 0) {
console.log("Loaded data from cache:", data.length, "rows");
initializeData(data);
setLastUpdated(date);
setLoading(false);
} else {
console.log("No cache found. Auto-fetching from Permanent URL...");
handleUrlFetch(PERMANENT_DROPBOX_URL).catch(e => {
console.error("Initial fetch failed.");
});
}
};
initApp();
@@ -211,9 +189,6 @@ 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...");
// Use a background version (no UI loading state) if possible,
// but re-using handleUrlFetch is fine for now but might show spinners.
// Ideally split the "silent sync" logic.
handleUrlFetch(PERMANENT_DROPBOX_URL).then(() => {
localStorage.setItem('craze_last_refresh_date', today);
console.log("Daily refresh successful.");
@@ -224,11 +199,12 @@ const App: React.FC = () => {
};
// Check immediately on load in case the user opens the app after 7 AM
// checkAndRefresh(); // Disabled for now to prevent double-fetch on startup logic conflict
checkAndRefresh();
// And then check periodically (e.g., every 15 minutes) in case app is left open across midnight
// const interval = setInterval(checkAndRefresh, 15 * 60 * 1000);
// return () => clearInterval(interval);
const interval = setInterval(checkAndRefresh, 15 * 60 * 1000);
return () => clearInterval(interval);
}, [handleUrlFetch]);
@@ -298,12 +274,6 @@ const App: React.FC = () => {
setRawData([]);
};
const handleSkipLoading = () => {
console.log("User skipped loading.");
setLoading(false);
setSyncing(false);
};
return (
<div className="min-h-screen flex flex-col bg-background text-slate-200">
@@ -390,29 +360,11 @@ const App: React.FC = () => {
{/* Main Content */}
<main className="flex-1 relative">
{loading ? (
// Initial loading spinner with SKIP Option
<div className="flex flex-col items-center justify-center h-[80vh] gap-6 text-center animate-fade-in">
<div className="relative">
<div className="w-20 h-20 border-4 border-indigo-500/30 border-t-indigo-500 rounded-full animate-spin"></div>
<div className="absolute inset-0 flex items-center justify-center">
<UploadIcon className="w-8 h-8 text-indigo-400 animate-pulse" />
</div>
</div>
<div>
<h2 className="text-2xl font-bold text-white mb-2">Syncing Data</h2>
<p className="text-slate-400 max-w-md mx-auto">
Connecting to Dropbox to fetch the latest analytics...
</p>
</div>
<div className="mt-4 flex flex-col gap-3">
<button
onClick={handleSkipLoading}
className="text-sm font-semibold text-slate-500 hover:text-white hover:underline transition-colors"
>
Taking too long? Skip Sync
</button>
</div>
// Initial loading spinner
<div className="flex flex-col items-center justify-center h-[80vh] gap-4">
<div className="w-16 h-16 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>
<h2 className="text-xl font-bold text-slate-300">Loading Dashboard...</h2>
<p className="text-sm text-slate-500">Syncing with Dropbox...</p>
</div>
) : (
<>
@@ -464,4 +416,5 @@ const App: React.FC = () => {
</div>
);
};
export default App;