feat: Integrate PapaParse for CSV handling

Adds papaparse as a dependency and updates the data processing service to use it for more robust CSV file parsing. This replaces manual CSV parsing logic with a dedicated library, improving reliability and handling of various CSV formats.

Also renames the `MoversIcon` to `TrendingIcon` to better reflect its usage in indicating trending performance metrics.
This commit is contained in:
Christian
2025-12-11 14:03:33 +01:00
parent 9ba63ab8f8
commit 31eed97ec4
14 changed files with 1311 additions and 1333 deletions
+23 -64
View File
@@ -1,15 +1,16 @@
import React, { useState, useMemo, useEffect, useCallback } from 'react';
import FileUpload from './components/FileUpload';
import Dashboard from './components/Dashboard';
import DataGrid from './components/DataGrid';
import TopMovers from './components/TopMovers';
import FilterBar from './components/FilterBar';
import AIChat from './components/AIChat';
import CrazeLogo from './components/CrazeLogo';
import TopMoversPage from './components/TopMoversPage'; // New import
import { SalesRecord, FilterState, AggregatedData } from './types';
import { processCSV, processExcel, filterData, aggregateData, getUniqueValues } from './services/dataProcessor';
import { processCSV, filterData, aggregateData, getUniqueValues } from './services/dataProcessor';
import { queryGemini } from './services/geminiService';
import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon } from './components/Icons';
import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon } from './components/Icons';
import { loadSalesData, saveSalesData, clearSalesData } from './services/storage';
// New Refresh Icon
@@ -19,13 +20,6 @@ const RefreshIcon = ({ className }: { className?: string }) => (
</svg>
);
// New Movers Icon
const MoversIcon = ({ className }: { className?: string }) => (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 18 9 11.25l4.306 4.305a11.164 11.164 0 0 0 5.814-5.815L21.75 6m0 0-3.5-3.5m3.5 3.5-3.5 3.5" />
</svg>
);
// 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";
@@ -34,14 +28,11 @@ const App: React.FC = () => {
const [rawData, setRawData] = useState<SalesRecord[]>([]);
const [loading, setLoading] = useState(true);
const [syncing, setSyncing] = useState(false);
const [view, setView] = useState<'dashboard' | 'table' | 'topMovers'>('dashboard'); // Added 'topMovers'
const [view, setView] = useState<'dashboard' | 'table' | 'movers'>('dashboard');
const [isChatOpen, setIsChatOpen] = useState(false);
const [activeUrl, setActiveUrl] = useState<string | null>(() => localStorage.getItem('craze_csv_url') || PERMANENT_DROPBOX_URL);
const [lastUpdated, setLastUpdated] = useState<string | null>(null);
// API Key State
const [apiKey, setApiKey] = useState<string>(() => localStorage.getItem('gemini_api_key') || '');
// Modal State
const [isDataModalOpen, setIsDataModalOpen] = useState(false);
@@ -56,16 +47,6 @@ const App: React.FC = () => {
title: [],
});
// Handle API Key Change
const handleApiKeyChange = (key: string) => {
setApiKey(key);
if (key) {
localStorage.setItem('gemini_api_key', key);
} else {
localStorage.removeItem('gemini_api_key');
}
};
// Handle URL Fetch (Auto/Manual)
const handleUrlFetch = useCallback(async (url: string) => {
setSyncing(true);
@@ -79,6 +60,8 @@ const App: React.FC = () => {
}
// Use a CORS proxy to bypass browser's same-origin policy restrictions.
// This is necessary because Dropbox does not send the required CORS headers
// for direct client-side fetching from another domain.
const proxyUrl = `https://corsproxy.io/?${encodeURIComponent(directUrl)}`;
const response = await fetch(proxyUrl);
@@ -152,24 +135,14 @@ const App: React.FC = () => {
const handleFileUpload = async (file: File) => {
setSyncing(true);
try {
let data: SalesRecord[] = [];
const lowerName = file.name.toLowerCase();
if (lowerName.endsWith('.csv')) {
data = await processCSV(file);
} else if (lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls')) {
data = await processExcel(file);
} else {
throw new Error("Unsupported file format");
}
const data = await processCSV(file);
await saveSalesData(data);
initializeData(data);
setLastUpdated(new Date().toISOString());
setIsDataModalOpen(false);
} catch (error) {
console.error("Failed to parse file", error);
alert("Error parsing file. Please check format (CSV or Excel).");
console.error("Failed to parse CSV", error);
alert("Error parsing CSV. Please check the format.");
} finally {
setSyncing(false);
}
@@ -254,7 +227,7 @@ const App: React.FC = () => {
};
const handleAskGemini = async (text: string) => {
return await queryGemini(apiKey, text, aggregatedData, filteredData.length);
return await queryGemini(text, aggregatedData, filteredData.length);
};
const disconnectUrl = async () => {
@@ -329,12 +302,12 @@ const App: React.FC = () => {
>
<TableIcon /> <span className="hidden sm:inline">Data Grid</span>
</button>
<button // New Top Movers Button
onClick={() => setView('topMovers')}
<button
onClick={() => setView('movers')}
className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-all
${view === 'topMovers' ? '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'}`}
>
<MoversIcon className="w-5 h-5" /> <span className="hidden sm:inline">Top Movers</span>
<TrendingIcon /> <span className="hidden sm:inline">Top Movers</span>
</button>
</div>
</div>
@@ -352,37 +325,23 @@ const App: React.FC = () => {
</div>
) : (
<>
{/* FilterBar is now relevant for all views including Top Movers */}
{(view === 'dashboard' || view === 'table' || view === 'topMovers') && (
<FilterBar filters={filters} onFilterChange={handleFilterChange} options={filterOptions} />
)}
<FilterBar filters={filters} onFilterChange={handleFilterChange} options={filterOptions} />
<div className="mt-6">
{view === 'dashboard' ? (
{view === 'dashboard' && (
<Dashboard
data={aggregatedData}
contextData={contextAggregatedData} // Pass context data
/>
) : view === 'table' ? (
<DataGrid data={filteredData} />
) : ( // New Top Movers View
<TopMoversPage
filteredData={filteredData} // Pass globally filtered data
contextData={contextAggregatedData}
/>
)}
{view === 'table' && <DataGrid data={filteredData} />}
{view === 'movers' && <TopMovers data={filteredData} />}
</div>
</>
)}
</main>
{/* Chat Assistant - Now with API Key Props */}
<AIChat
onSendMessage={handleAskGemini}
isOpen={isChatOpen}
setIsOpen={setIsChatOpen}
apiKey={apiKey}
onApiKeyChange={handleApiKeyChange}
/>
{/* Chat Assistant */}
<AIChat onSendMessage={handleAskGemini} isOpen={isChatOpen} setIsOpen={setIsChatOpen} />
{/* DATA MODAL */}
{isDataModalOpen && (
@@ -413,4 +372,4 @@ const App: React.FC = () => {
);
};
export default App;
export default App;