feat: Add ESLint configuration and enhance data fetching with timeouts and improved error handling.

This commit is contained in:
Christian Vidal Wolf
2025-12-12 11:46:04 +01:00
parent bde9d323f1
commit 135e0d78ed
7 changed files with 4756 additions and 252 deletions
+239 -180
View File
@@ -51,84 +51,118 @@ const App: React.FC = () => {
// Handle URL Fetch (Auto/Manual) // Handle URL Fetch (Auto/Manual)
const handleUrlFetch = useCallback(async (url: string) => { const handleUrlFetch = useCallback(async (url: string) => {
setSyncing(true); setSyncing(true);
try { setLoading(true);
let directUrl = url; // Reset previous errors on new attempt
// Create a direct download link for Dropbox if it's a share link. // setLastUpdated(null);
if (url.includes('dropbox.com/') && !url.includes('dl.dropboxusercontent.com')) {
const urlObject = new URL(url);
urlObject.searchParams.set('dl', '1');
directUrl = urlObject.toString();
}
// Use a CORS proxy to bypass browser's same-origin policy restrictions. const controller = new AbortController();
// This is necessary because Dropbox does not send the required CORS headers const timeoutId = setTimeout(() => controller.abort(), 15000); // 15s Timeout
// for direct client-side fetching from another domain.
const proxyUrl = `https://corsproxy.io/?${encodeURIComponent(directUrl)}`;
const response = await fetch(proxyUrl); try {
if (!response.ok) throw new Error(`Failed to fetch CSV from URL: ${response.status} ${response.statusText}`); console.log(`[Sync] Starting fetch from: ${url}`);
let directUrl = url;
const csvText = await response.text(); // Create a direct download link for Dropbox if it's a share link.
const data = await processCSV(csvText); if (url.includes('dropbox.com/') && !url.includes('dl.dropboxusercontent.com')) {
const urlObject = new URL(url);
await saveSalesData(data); urlObject.searchParams.set('dl', '1');
directUrl = urlObject.toString();
initializeData(data);
setActiveUrl(url); // Store the original user-facing 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
} 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 {
setSyncing(false);
setLoading(false);
} }
// Use a CORS proxy to bypass browser's same-origin policy restrictions.
const proxyUrl = `https://corsproxy.io/?${encodeURIComponent(directUrl)}`;
console.log(`[Sync] Fetching via proxy: ${proxyUrl}`);
const response = await fetch(proxyUrl, {
signal: controller.signal
});
if (!response.ok) {
throw new Error(`Failed to fetch CSV: ${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);
initializeData(data);
setActiveUrl(url); // Store the original user-facing 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
} 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
} finally {
clearTimeout(timeoutId);
setSyncing(false);
setLoading(false);
}
}, []); }, []);
const initializeData = (data: SalesRecord[]) => { const initializeData = (data: SalesRecord[]) => {
setRawData(data); setRawData(data);
setFilters({ setFilters({
customer: [], customer: [],
year: [], year: [],
month: [], month: [],
line: [], line: [],
asin: [], asin: [],
sku: [], sku: [],
title: [], title: [],
}); });
}; };
// 1. Initial Load from Cache (IndexedDB) or Auto-Fetch Permanent URL // 1. Initial Load from Cache (IndexedDB) or Auto-Fetch Permanent URL
useEffect(() => { useEffect(() => {
const initApp = async () => { const initApp = async () => {
setLoading(true); setLoading(true);
console.log("[Init] App starting...");
try {
const currentStoredUrl = localStorage.getItem('craze_csv_url'); const currentStoredUrl = localStorage.getItem('craze_csv_url');
if (currentStoredUrl !== PERMANENT_DROPBOX_URL) { if (currentStoredUrl !== PERMANENT_DROPBOX_URL) {
localStorage.setItem('craze_csv_url', PERMANENT_DROPBOX_URL); console.log("[Init] Updating stored URL to default permanent URL");
setActiveUrl(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) { if (data && data.length > 0) {
console.log("Loaded data from cache:", data.length, "rows"); console.log("[Init] Loaded data from cache:", data.length, "rows. Last updated:", date);
initializeData(data); initializeData(data);
setLastUpdated(date); setLastUpdated(date);
setLoading(false); setLoading(false);
} else { } else {
console.log("No cache found. Auto-fetching from Permanent URL..."); console.log("[Init] No cache found. Auto-fetching from Permanent URL...");
handleUrlFetch(PERMANENT_DROPBOX_URL).catch(e => { // Catch error here so initApp doesn't crash, handleUrlFetch handles UI
console.error("Initial fetch failed."); 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
}
}; };
initApp(); initApp();
}, [handleUrlFetch]); }, [handleUrlFetch]);
@@ -154,45 +188,47 @@ const App: React.FC = () => {
const handleAdsUpload = async (file: File) => { const handleAdsUpload = async (file: File) => {
setSyncing(true); setSyncing(true);
try { try {
const data = await processAdsCSV(file); const data = await processAdsCSV(file);
setAdsData(data); setAdsData(data);
console.log("Ads loaded:", data.length); console.log("Ads loaded:", data.length);
setIsDataModalOpen(false); setIsDataModalOpen(false);
setView('ads'); // Switch to ads view automatically setView('ads'); // Switch to ads view automatically
} catch (error) { } catch (error) {
console.error("Failed to parse Ads CSV", error); console.error("Failed to parse Ads CSV", error);
alert("Error parsing Ads CSV. Please check the format."); alert("Error parsing Ads CSV. Please check the format.");
} finally { } finally {
setSyncing(false); setSyncing(false);
} }
}; };
// 2. Schedule Auto-Refresh (Background) // 2. Schedule Auto-Refresh (Background)
useEffect(() => { useEffect(() => {
const checkAndRefresh = () => { const checkAndRefresh = () => {
const now = new Date(); const now = new Date();
const today = now.toISOString().split('T')[0]; // YYYY-MM-DD const today = now.toISOString().split('T')[0]; // YYYY-MM-DD
const lastRefreshDate = localStorage.getItem('craze_last_refresh_date'); const lastRefreshDate = localStorage.getItem('craze_last_refresh_date');
// 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(() => { // Use a background version (no UI loading state) if possible,
localStorage.setItem('craze_last_refresh_date', today); // but re-using handleUrlFetch is fine for now but might show spinners.
console.log("Daily refresh successful."); // Ideally split the "silent sync" logic.
}).catch(err => { handleUrlFetch(PERMANENT_DROPBOX_URL).then(() => {
console.error("Daily refresh failed, will retry later.", err); localStorage.setItem('craze_last_refresh_date', today);
}); console.log("Daily refresh successful.");
} }).catch(err => {
}; console.error("Daily refresh failed, will retry later.", err);
});
}
};
// Check immediately on load in case the user opens the app after 7 AM // Check immediately on load in case the user opens the app after 7 AM
checkAndRefresh(); // checkAndRefresh(); // Disabled for now to prevent double-fetch on startup logic conflict
// And then check periodically (e.g., every 15 minutes) in case app is left open across midnight // And then check periodically (e.g., every 15 minutes) in case app is left open across midnight
const interval = setInterval(checkAndRefresh, 15 * 60 * 1000); // const interval = setInterval(checkAndRefresh, 15 * 60 * 1000);
// return () => clearInterval(interval);
return () => clearInterval(interval);
}, [handleUrlFetch]); }, [handleUrlFetch]);
@@ -202,7 +238,7 @@ const App: React.FC = () => {
// Combine Sales & Ads Data dynamically based on current filters // Combine Sales & Ads Data dynamically based on current filters
const combinedAdsData = useMemo(() => { const combinedAdsData = useMemo(() => {
return mergeSalesAndAdsData(filteredData, adsData); return mergeSalesAndAdsData(filteredData, adsData);
}, [filteredData, adsData]); }, [filteredData, adsData]);
// Derive Context Data (Product Line Context when drilling down) // Derive Context Data (Product Line Context when drilling down)
@@ -255,11 +291,17 @@ const App: React.FC = () => {
}; };
const disconnectUrl = async () => { const disconnectUrl = async () => {
// Allow disconnecting to clear data, but the app will likely re-connect on next reload due to "Permanent" requirement // Allow disconnecting to clear data, but the app will likely re-connect on next reload due to "Permanent" requirement
localStorage.removeItem('craze_csv_url'); localStorage.removeItem('craze_csv_url');
await clearSalesData(); await clearSalesData();
setActiveUrl(null); setActiveUrl(null);
setRawData([]); setRawData([]);
};
const handleSkipLoading = () => {
console.log("User skipped loading.");
setLoading(false);
setSyncing(false);
}; };
return ( return (
@@ -269,77 +311,77 @@ const App: React.FC = () => {
<header className="bg-slate-950/90 backdrop-blur border-b border-border py-4 px-6 relative z-40 shadow-2xl"> <header className="bg-slate-950/90 backdrop-blur border-b border-border py-4 px-6 relative z-40 shadow-2xl">
<div className="max-w-7xl mx-auto flex justify-between items-center"> <div className="max-w-7xl mx-auto flex justify-between items-center">
<div className="flex items-center gap-8"> <div className="flex items-center gap-8">
{/* Logo Container (Horizontal Box) - Persistent User Image */} {/* Logo Container (Horizontal Box) - Persistent User Image */}
<div className="h-16 w-64 relative flex-shrink-0"> <div className="h-16 w-64 relative flex-shrink-0">
<CrazeLogo /> <CrazeLogo />
</div> </div>
{/* Title & Status */} {/* Title & Status */}
<div className="hidden lg:block border-l border-slate-700 pl-6"> <div className="hidden lg:block border-l border-slate-700 pl-6">
<h1 className="text-xl font-bold tracking-tight text-white text-shadow-sm">Analytics Dashboard</h1> <h1 className="text-xl font-bold tracking-tight text-white text-shadow-sm">Analytics Dashboard</h1>
{activeUrl && lastUpdated && ( {activeUrl && lastUpdated && (
<p className="text-[10px] text-emerald-400 mt-1 flex items-center gap-1 uppercase font-bold tracking-wider"> <p className="text-[10px] text-emerald-400 mt-1 flex items-center gap-1 uppercase font-bold tracking-wider">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span> <span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
Live Sync Active Live Sync Active
</p> </p>
)} )}
</div> </div>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{/* Main Action: Data Source Button */} {/* Main Action: Data Source Button */}
<button <button
onClick={() => setIsDataModalOpen(true)} onClick={() => setIsDataModalOpen(true)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-bold shadow-lg transition-all border className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-bold shadow-lg transition-all border
${activeUrl ${activeUrl
? 'bg-slate-800 text-emerald-400 border-emerald-500/50 hover:bg-slate-700' ? 'bg-slate-800 text-emerald-400 border-emerald-500/50 hover:bg-slate-700'
: 'bg-indigo-600 text-white border-transparent hover:bg-indigo-500'}`} : 'bg-indigo-600 text-white border-transparent hover:bg-indigo-500'}`}
> >
{syncing ? <div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin"></div> : <UploadIcon />} {syncing ? <div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin"></div> : <UploadIcon />}
<span className="hidden md:inline">{activeUrl ? 'Data Settings' : 'Connect Data'}</span> <span className="hidden md:inline">{activeUrl ? 'Data Settings' : 'Connect Data'}</span>
</button> </button>
{/* NEW REFRESH BUTTON */} {/* NEW REFRESH BUTTON */}
<button <button
onClick={() => activeUrl && handleUrlFetch(activeUrl)} onClick={() => activeUrl && handleUrlFetch(activeUrl)}
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"
> >
<RefreshIcon className={`w-5 h-5 ${syncing ? 'animate-spin' : ''}`} /> <RefreshIcon className={`w-5 h-5 ${syncing ? 'animate-spin' : ''}`} />
</button> </button>
{/* View Switcher */} {/* View Switcher */}
<div className="flex bg-slate-900 rounded-lg p-1 border border-border"> <div className="flex bg-slate-900 rounded-lg p-1 border border-border">
<button <button
onClick={() => setView('dashboard')} onClick={() => setView('dashboard')}
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
${view === 'dashboard' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`} ${view === 'dashboard' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
> >
<ChartIcon /> <span className="hidden sm:inline">Dashboard</span> <ChartIcon /> <span className="hidden sm:inline">Dashboard</span>
</button> </button>
<button <button
onClick={() => setView('table')} onClick={() => setView('table')}
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
${view === 'table' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`} ${view === 'table' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
> >
<TableIcon /> <span className="hidden sm:inline">Grid</span> <TableIcon /> <span className="hidden sm:inline">Grid</span>
</button> </button>
<button <button
onClick={() => setView('movers')} onClick={() => setView('movers')}
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
${view === 'movers' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`} ${view === 'movers' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
> >
<TrendingIcon /> <span className="hidden sm:inline">Movers</span> <TrendingIcon /> <span className="hidden sm:inline">Movers</span>
</button> </button>
<button <button
onClick={() => setView('ads')} onClick={() => setView('ads')}
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
${view === 'ads' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`} ${view === 'ads' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
> >
<MegaphoneIcon /> <span className="hidden sm:inline">Ads</span> <MegaphoneIcon /> <span className="hidden sm:inline">Ads</span>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
@@ -348,27 +390,45 @@ const App: React.FC = () => {
{/* Main Content */} {/* Main Content */}
<main className="flex-1 relative"> <main className="flex-1 relative">
{loading ? ( {loading ? (
// Initial loading spinner // Initial loading spinner with SKIP Option
<div className="flex flex-col items-center justify-center h-[80vh] gap-4"> <div className="flex flex-col items-center justify-center h-[80vh] gap-6 text-center animate-fade-in">
<div className="w-16 h-16 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin"></div> <div className="relative">
<h2 className="text-xl font-bold text-slate-300">Loading Dashboard...</h2> <div className="w-20 h-20 border-4 border-indigo-500/30 border-t-indigo-500 rounded-full animate-spin"></div>
<p className="text-sm text-slate-500">Syncing with Dropbox...</p> <div className="absolute inset-0 flex items-center justify-center">
<UploadIcon className="w-8 h-8 text-indigo-400 animate-pulse" />
</div>
</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>
</div>
) : ( ) : (
<> <>
<FilterBar filters={filters} onFilterChange={handleFilterChange} options={filterOptions} /> <FilterBar filters={filters} onFilterChange={handleFilterChange} options={filterOptions} />
<div className="mt-6"> <div className="mt-6">
{view === 'dashboard' && ( {view === 'dashboard' && (
<Dashboard <Dashboard
data={aggregatedData} data={aggregatedData}
contextData={contextAggregatedData} contextData={contextAggregatedData}
/> />
)} )}
{view === 'table' && <DataGrid data={filteredData} />} {view === 'table' && <DataGrid data={filteredData} />}
{view === 'movers' && <TopMovers data={filteredData} />} {view === 'movers' && <TopMovers data={filteredData} />}
{view === 'ads' && <AdvertisingDashboard data={combinedAdsData} />} {view === 'ads' && <AdvertisingDashboard data={combinedAdsData} />}
</div> </div>
</> </>
)} )}
</main> </main>
@@ -378,31 +438,30 @@ const App: React.FC = () => {
{/* DATA MODAL */} {/* DATA MODAL */}
{isDataModalOpen && ( {isDataModalOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm animate-fade-in p-4"> <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm animate-fade-in p-4">
<div className="bg-slate-950 border border-border rounded-2xl shadow-2xl w-full max-w-lg relative overflow-hidden"> <div className="bg-slate-950 border border-border rounded-2xl shadow-2xl w-full max-w-lg relative overflow-hidden">
<div className="bg-slate-900 px-6 py-4 border-b border-border flex justify-between items-center"> <div className="bg-slate-900 px-6 py-4 border-b border-border flex justify-between items-center">
<h2 className="text-lg font-bold text-white">Data Source Settings</h2> <h2 className="text-lg font-bold text-white">Data Source Settings</h2>
<button onClick={() => setIsDataModalOpen(false)} className="text-slate-400 hover:text-white transition-colors"> <button onClick={() => setIsDataModalOpen(false)} className="text-slate-400 hover:text-white transition-colors">
<CloseIcon /> <CloseIcon />
</button> </button>
</div>
<div className="p-6">
<FileUpload
onSalesUpload={handleSalesUpload} // CORRECTED: Was handleFileUpload
onAdsUpload={handleAdsUpload} // ADDED: Missing prop causing error
onUrlSubmit={handleUrlFetch}
isLoading={syncing}
activeUrl={activeUrl}
onDisconnect={disconnectUrl}
lastUpdated={lastUpdated}
/>
</div>
</div> </div>
<div className="p-6">
<FileUpload
onSalesUpload={handleSalesUpload} // CORRECTED: Was handleFileUpload
onAdsUpload={handleAdsUpload} // ADDED: Missing prop causing error
onUrlSubmit={handleUrlFetch}
isLoading={syncing}
activeUrl={activeUrl}
onDisconnect={disconnectUrl}
lastUpdated={lastUpdated}
/>
</div>
</div>
</div> </div>
)} )}
</div> </div>
); );
}; };
export default App; export default App;
+54
View File
@@ -0,0 +1,54 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
error: null,
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}
public render() {
if (this.state.hasError) {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-950 text-slate-200 p-8">
<div className="bg-red-900/20 border border-red-500/50 rounded-xl p-8 max-w-2xl w-full text-center">
<h1 className="text-3xl font-bold text-red-500 mb-4">Something went wrong</h1>
<p className="text-slate-300 mb-6">The application encountered a critical error during rendering.</p>
<div className="bg-black/50 p-4 rounded-lg text-left overflow-auto max-h-64 font-mono text-sm border border-slate-800">
<p className="text-red-400 font-bold mb-2">Error: {this.state.error?.message}</p>
<pre className="text-slate-500 text-xs">{this.state.error?.stack}</pre>
</div>
<button
onClick={() => window.location.reload()}
className="mt-8 px-6 py-3 bg-red-600 hover:bg-red-500 text-white font-bold rounded-lg transition-colors"
>
Reload Application
</button>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
+28
View File
@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)
+45 -53
View File
@@ -1,60 +1,52 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="dark"> <html lang="en" class="dark">
<head>
<meta charset="UTF-8" /> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta charset="UTF-8" />
<title>SAS Analytics Dashboard</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script> <title>SAS Analytics Dashboard</title>
<script> <script src="https://cdn.tailwindcss.com"></script>
tailwind.config = { <script>
darkMode: 'class', tailwind.config = {
theme: { darkMode: 'class',
extend: { theme: {
colors: { extend: {
background: '#020617', // slate-950 colors: {
surface: '#0f172a', // slate-900 background: '#020617', // slate-950
border: '#1e293b', // slate-800 surface: '#0f172a', // slate-900
primary: '#6366f1', // indigo-500 border: '#1e293b', // slate-800
secondary: '#64748b', // slate-500 primary: '#6366f1', // indigo-500
} secondary: '#64748b', // slate-500
} }
} }
} }
</script> }
<!-- PapaParse for CSV parsing --> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script> <style>
<style> /* Custom Scrollbar */
/* Custom Scrollbar */ ::-webkit-scrollbar {
::-webkit-scrollbar { width: 8px;
width: 8px; height: 8px;
height: 8px; }
}
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: #020617; background: #020617;
} }
::-webkit-scrollbar-thumb {
background: #334155; ::-webkit-scrollbar-thumb {
border-radius: 4px; background: #334155;
} border-radius: 4px;
::-webkit-scrollbar-thumb:hover { }
background: #475569;
} ::-webkit-scrollbar-thumb:hover {
</style> background: #475569;
<script type="importmap"> }
{ </style>
"imports": {
"react": "https://aistudiocdn.com/react@^19.2.0",
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
"react/": "https://aistudiocdn.com/react@^19.2.0/",
"@google/genai": "https://aistudiocdn.com/@google/genai@^1.30.0",
"recharts": "https://aistudiocdn.com/recharts@^3.5.0",
"xlsx": "https://esm.sh/xlsx@^0.18.5",
"papaparse": "https://esm.sh/papaparse@^5.5.3"
}
}
</script>
</head> </head>
<body class="bg-background text-slate-200 antialiased overflow-y-auto">
<div id="root"></div> <body class="bg-background text-slate-200 antialiased overflow-y-auto">
</body> <div id="root"></div>
<script type="module" src="/index.tsx"></script>
</body>
</html> </html>
+4 -1
View File
@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import App from './App'; import App from './App';
import ErrorBoundary from './components/ErrorBoundary';
const rootElement = document.getElementById('root'); const rootElement = document.getElementById('root');
if (!rootElement) { if (!rootElement) {
@@ -10,6 +11,8 @@ if (!rootElement) {
const root = ReactDOM.createRoot(rootElement); const root = ReactDOM.createRoot(rootElement);
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<App /> <ErrorBoundary>
<App />
</ErrorBoundary>
</React.StrictMode> </React.StrictMode>
); );
+4359
View File
File diff suppressed because it is too large Load Diff
+16 -7
View File
@@ -5,21 +5,30 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"react": "^19.2.0", "react": "^18.3.1",
"react-dom": "^19.2.0", "react-dom": "^18.3.1",
"@google/genai": "^1.30.0", "@google/genai": "^1.30.0",
"recharts": "^3.5.0", "recharts": "^3.5.0",
"xlsx": "^0.18.5", "xlsx": "^0.18.5",
"papaparse": "^5.5.3" "papaparse": "^5.5.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.14.0", "@types/node": "^22.5.0",
"@vitejs/plugin-react": "^5.0.0", "@types/react": "^18.3.3",
"typescript": "~5.8.2", "@types/react-dom": "^18.3.0",
"vite": "^6.2.0" "@types/papaparse": "^5.3.14",
"@vitejs/plugin-react": "^4.3.1",
"typescript": "^5.6.2",
"vite": "^5.4.2",
"eslint": "^9.9.1",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"typescript-eslint": "^8.0.0"
} }
} }