mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 14:35:22 +02:00
feat: add Supabase vendor data integration (BSR, ratings, buy box)
- Create vendor_daily_data table schema and Supabase client service - Add upload API route for Vendor Central CSV parsing and upsert - Add VendorDataView with BSR trend, ratings, and buy box charts - Integrate new Vendor tab into app navigation (desktop + mobile) - Add vendor CSV upload card to FileUpload modal Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
fed39b18be
commit
2bce0a839b
@@ -0,0 +1,108 @@
|
||||
# Supabase Vendor Daily Data Integration
|
||||
|
||||
## Overview
|
||||
|
||||
Store daily Vendor Central export data (BSR, ratings, buy box) in Supabase. Upload via CSV, display aggregated to weekly in a new "Vendor Data" view.
|
||||
|
||||
## Database Schema
|
||||
|
||||
Single flat table with composite unique key `(date, market, asin)`:
|
||||
|
||||
```sql
|
||||
CREATE TABLE vendor_daily_data (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
date DATE NOT NULL,
|
||||
market TEXT NOT NULL,
|
||||
asin TEXT NOT NULL,
|
||||
product_title TEXT,
|
||||
tags TEXT,
|
||||
bsr_top_rank INTEGER,
|
||||
bsr_top_category TEXT,
|
||||
bsr_detail_rank INTEGER,
|
||||
bsr_detail_category TEXT,
|
||||
avg_rating NUMERIC(3,1),
|
||||
num_reviews INTEGER,
|
||||
buybox_owner TEXT,
|
||||
buybox_price NUMERIC(10,2),
|
||||
amazon_has_buybox BOOLEAN,
|
||||
glance_views INTEGER,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(date, market, asin)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_vendor_daily_market_date ON vendor_daily_data(market, date);
|
||||
CREATE INDEX idx_vendor_daily_asin ON vendor_daily_data(asin);
|
||||
CREATE INDEX idx_vendor_daily_tags ON vendor_daily_data(tags);
|
||||
```
|
||||
|
||||
## CSV Column Mapping
|
||||
|
||||
| CSV Column | DB Column | Transform |
|
||||
|------------|-----------|-----------|
|
||||
| Date | date | Parse as DATE |
|
||||
| Market | market | Direct (DE, UK, IT, FR, ES) |
|
||||
| ASIN | asin | Direct |
|
||||
| Product Title | product_title | Direct |
|
||||
| Tags | tags | Direct (product line) |
|
||||
| Top Level Category (Rank) | bsr_top_rank | Parse INT |
|
||||
| Top Level Category (Name) | bsr_top_category | Direct |
|
||||
| Detail Level Category (Rank) | bsr_detail_rank | Parse INT |
|
||||
| Detail Level Category (Name) | bsr_detail_category | Direct |
|
||||
| Average Rating | avg_rating | EU decimal (3,8 -> 3.8) |
|
||||
| Number of Reviews | num_reviews | Parse INT |
|
||||
| Buybox Seller Name | buybox_owner | Direct |
|
||||
| Buybox Price | buybox_price | EU decimal (2,49 -> 2.49) |
|
||||
| Amazon Has Buybox | amazon_has_buybox | 1/0 -> boolean |
|
||||
| Glance Views | glance_views | Parse INT |
|
||||
|
||||
## New Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `services/supabase.ts` | Client init + query helpers |
|
||||
| `api/upload-vendor-data.ts` | Parse CSV, upsert to Supabase |
|
||||
| `components/VendorDataView.tsx` | BSR/ratings/buybox charts |
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Upload
|
||||
1. User clicks "Upload Vendor CSV" in FileUpload.tsx
|
||||
2. Client sends file to `/api/upload-vendor-data` (POST)
|
||||
3. API parses CSV, extracts 15 columns, converts EU numbers
|
||||
4. Upserts to Supabase in batches of 500
|
||||
5. Returns `{ inserted, updated }` counts
|
||||
|
||||
### Display
|
||||
1. VendorDataView queries Supabase directly (anon key, read-only)
|
||||
2. Client aggregates daily -> weekly (AVG for ranks/ratings, latest for buybox)
|
||||
3. Renders Recharts charts: BSR trend, ratings, buy box status
|
||||
|
||||
## View: VendorDataView.tsx
|
||||
|
||||
Three chart panels:
|
||||
1. **BSR Trend** - Line chart, Y-axis inverted, lines per marketplace
|
||||
2. **Ratings & Reviews** - Dual axis: avg rating + review count
|
||||
3. **Buy Box Status** - % days Amazon vs Seller vs Other
|
||||
|
||||
Filters: marketplace, product line, ASIN, date range (reuse MultiSelectDropdown).
|
||||
|
||||
## Integration Points
|
||||
|
||||
- **App.tsx**: New `vendor` view state + nav button
|
||||
- **FileUpload.tsx**: 4th upload card for Vendor CSV
|
||||
- **package.json**: Add `@supabase/supabase-js`
|
||||
- **Vercel env vars**: `SUPABASE_URL`, `SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_KEY`
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Where Used | Purpose |
|
||||
|----------|-----------|---------|
|
||||
| SUPABASE_URL | Client + API | Supabase project URL |
|
||||
| SUPABASE_ANON_KEY | Client | Read-only access |
|
||||
| SUPABASE_SERVICE_KEY | API routes only | Write access for upserts |
|
||||
|
||||
## Security
|
||||
|
||||
- Anon key for client reads, service key for server writes
|
||||
- No RLS (private internal tool)
|
||||
- Env vars in Vercel, not hardcoded
|
||||
@@ -0,0 +1,568 @@
|
||||
# Supabase Vendor Data Integration — Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Upload Vendor Central daily CSV data to Supabase and display BSR, ratings, and buy box trends in a new "Vendor Data" view.
|
||||
|
||||
**Architecture:** Client-side CSV parsing sends data to a Vercel API route that upserts to Supabase. A new React view queries Supabase directly (anon key) and renders Recharts charts with daily→weekly aggregation.
|
||||
|
||||
**Tech Stack:** React 19, TypeScript, Supabase (Postgres), Recharts, PapaParse, Vercel serverless functions, Tailwind CSS (CDN)
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Create Supabase Table via SQL Editor
|
||||
|
||||
**Context:** Run this SQL in the Supabase Dashboard SQL Editor at https://qjioywarwdbxmdihyrti.supabase.co
|
||||
|
||||
**Step 1: Run the CREATE TABLE SQL**
|
||||
|
||||
Open Supabase Dashboard → SQL Editor → New Query → paste and run:
|
||||
|
||||
```sql
|
||||
CREATE TABLE vendor_daily_data (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
date DATE NOT NULL,
|
||||
market TEXT NOT NULL,
|
||||
asin TEXT NOT NULL,
|
||||
product_title TEXT,
|
||||
tags TEXT,
|
||||
bsr_top_rank INTEGER,
|
||||
bsr_top_category TEXT,
|
||||
bsr_detail_rank INTEGER,
|
||||
bsr_detail_category TEXT,
|
||||
avg_rating NUMERIC(3,1),
|
||||
num_reviews INTEGER,
|
||||
buybox_owner TEXT,
|
||||
buybox_price NUMERIC(10,2),
|
||||
amazon_has_buybox BOOLEAN,
|
||||
glance_views INTEGER,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(date, market, asin)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_vendor_daily_market_date ON vendor_daily_data(market, date);
|
||||
CREATE INDEX idx_vendor_daily_asin ON vendor_daily_data(asin);
|
||||
CREATE INDEX idx_vendor_daily_tags ON vendor_daily_data(tags);
|
||||
```
|
||||
|
||||
**Step 2: Verify table exists**
|
||||
|
||||
Run: `SELECT count(*) FROM vendor_daily_data;` — should return 0.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Install Supabase Client + Configure Env Vars
|
||||
|
||||
**Files:**
|
||||
- Modify: `package.json`
|
||||
- Create: `.env.local` (add Supabase vars)
|
||||
- Modify: `vite.config.ts` (expose env vars to client)
|
||||
|
||||
**Step 1: Install @supabase/supabase-js**
|
||||
|
||||
```bash
|
||||
npm install @supabase/supabase-js
|
||||
```
|
||||
|
||||
**Step 2: Add env vars to `.env.local`**
|
||||
|
||||
```
|
||||
SUPABASE_URL=https://qjioywarwdbxmdihyrti.supabase.co
|
||||
SUPABASE_ANON_KEY=<anon-key-from-supabase-dashboard>
|
||||
SUPABASE_SERVICE_KEY=<service-role-key-from-supabase-dashboard>
|
||||
```
|
||||
|
||||
**Step 3: Expose SUPABASE_URL and SUPABASE_ANON_KEY to the Vite client**
|
||||
|
||||
In `vite.config.ts`, add to the `define` block:
|
||||
|
||||
```ts
|
||||
'process.env.SUPABASE_URL': JSON.stringify(
|
||||
env.SUPABASE_URL || process.env.SUPABASE_URL || ""
|
||||
),
|
||||
'process.env.SUPABASE_ANON_KEY': JSON.stringify(
|
||||
env.SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY || ""
|
||||
),
|
||||
```
|
||||
|
||||
**Step 4: Add Supabase env vars to Vercel**
|
||||
|
||||
In Vercel Dashboard → Settings → Environment Variables, add:
|
||||
- `SUPABASE_URL` = `https://qjioywarwdbxmdihyrti.supabase.co`
|
||||
- `SUPABASE_ANON_KEY` = (anon key)
|
||||
- `SUPABASE_SERVICE_KEY` = (service role key)
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add package.json package-lock.json vite.config.ts
|
||||
git commit -m "chore: add @supabase/supabase-js and configure env vars"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Create Supabase Client Service
|
||||
|
||||
**Files:**
|
||||
- Create: `services/supabase.ts`
|
||||
|
||||
**Step 1: Write the Supabase client module**
|
||||
|
||||
```ts
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const supabaseUrl = process.env.SUPABASE_URL || '';
|
||||
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY || '';
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
||||
|
||||
export interface VendorDailyRow {
|
||||
id?: number;
|
||||
date: string;
|
||||
market: string;
|
||||
asin: string;
|
||||
product_title: string | null;
|
||||
tags: string | null;
|
||||
bsr_top_rank: number | null;
|
||||
bsr_top_category: string | null;
|
||||
bsr_detail_rank: number | null;
|
||||
bsr_detail_category: string | null;
|
||||
avg_rating: number | null;
|
||||
num_reviews: number | null;
|
||||
buybox_owner: string | null;
|
||||
buybox_price: number | null;
|
||||
amazon_has_buybox: boolean | null;
|
||||
glance_views: number | null;
|
||||
}
|
||||
|
||||
export interface VendorFilters {
|
||||
markets?: string[];
|
||||
tags?: string[];
|
||||
asins?: string[];
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
}
|
||||
|
||||
export const fetchVendorData = async (filters: VendorFilters): Promise<VendorDailyRow[]> => {
|
||||
let query = supabase
|
||||
.from('vendor_daily_data')
|
||||
.select('*')
|
||||
.order('date', { ascending: true });
|
||||
|
||||
if (filters.markets?.length) {
|
||||
query = query.in('market', filters.markets);
|
||||
}
|
||||
if (filters.tags?.length) {
|
||||
query = query.in('tags', filters.tags);
|
||||
}
|
||||
if (filters.asins?.length) {
|
||||
query = query.in('asin', filters.asins);
|
||||
}
|
||||
if (filters.dateFrom) {
|
||||
query = query.gte('date', filters.dateFrom);
|
||||
}
|
||||
if (filters.dateTo) {
|
||||
query = query.lte('date', filters.dateTo);
|
||||
}
|
||||
|
||||
// Supabase default limit is 1000, paginate if needed
|
||||
const allRows: VendorDailyRow[] = [];
|
||||
let offset = 0;
|
||||
const pageSize = 1000;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const { data, error } = await query.range(offset, offset + pageSize - 1);
|
||||
if (error) throw error;
|
||||
if (data) {
|
||||
allRows.push(...data);
|
||||
hasMore = data.length === pageSize;
|
||||
offset += pageSize;
|
||||
} else {
|
||||
hasMore = false;
|
||||
}
|
||||
}
|
||||
|
||||
return allRows;
|
||||
};
|
||||
|
||||
export const fetchVendorFilterOptions = async (): Promise<{
|
||||
markets: string[];
|
||||
tags: string[];
|
||||
asins: string[];
|
||||
dateRange: { min: string; max: string } | null;
|
||||
}> => {
|
||||
// Get distinct markets
|
||||
const { data: marketData } = await supabase
|
||||
.from('vendor_daily_data')
|
||||
.select('market')
|
||||
.order('market');
|
||||
|
||||
// Get distinct tags
|
||||
const { data: tagData } = await supabase
|
||||
.from('vendor_daily_data')
|
||||
.select('tags')
|
||||
.order('tags');
|
||||
|
||||
// Get date range
|
||||
const { data: dateData } = await supabase
|
||||
.from('vendor_daily_data')
|
||||
.select('date')
|
||||
.order('date', { ascending: true })
|
||||
.limit(1);
|
||||
|
||||
const { data: dateDataMax } = await supabase
|
||||
.from('vendor_daily_data')
|
||||
.select('date')
|
||||
.order('date', { ascending: false })
|
||||
.limit(1);
|
||||
|
||||
const markets = [...new Set((marketData || []).map(r => r.market))];
|
||||
const tags = [...new Set((tagData || []).map(r => r.tags).filter(Boolean))];
|
||||
|
||||
return {
|
||||
markets,
|
||||
tags,
|
||||
asins: [], // loaded on-demand when filters narrow
|
||||
dateRange: dateData?.[0] && dateDataMax?.[0]
|
||||
? { min: dateData[0].date, max: dateDataMax[0].date }
|
||||
: null,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add services/supabase.ts
|
||||
git commit -m "feat: add Supabase client with vendor data queries"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Create Upload API Route
|
||||
|
||||
**Files:**
|
||||
- Create: `api/upload-vendor-data.ts`
|
||||
|
||||
**Step 1: Write the serverless upload function**
|
||||
|
||||
This route receives CSV text in the POST body, parses it with papaparse, maps columns, and upserts to Supabase using the service role key.
|
||||
|
||||
```ts
|
||||
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import Papa from 'papaparse';
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.SUPABASE_URL || '',
|
||||
process.env.SUPABASE_SERVICE_KEY || ''
|
||||
);
|
||||
|
||||
function parseEUNumber(val: string | undefined | null): number | null {
|
||||
if (!val || val.trim() === '') return null;
|
||||
// EU format: "1.234,56" → 1234.56 or "2,49" → 2.49
|
||||
const cleaned = val.replace(/\./g, '').replace(',', '.');
|
||||
const num = parseFloat(cleaned);
|
||||
return isNaN(num) ? null : num;
|
||||
}
|
||||
|
||||
function parseIntSafe(val: string | undefined | null): number | null {
|
||||
if (!val || val.trim() === '') return null;
|
||||
const cleaned = val.replace(/\./g, '').replace(',', '.');
|
||||
const num = parseInt(cleaned, 10);
|
||||
return isNaN(num) ? null : num;
|
||||
}
|
||||
|
||||
interface CSVRow {
|
||||
Date: string;
|
||||
Market: string;
|
||||
ASIN: string;
|
||||
'Product Title': string;
|
||||
Tags: string;
|
||||
'Top Level Category (Rank)': string;
|
||||
'Top Level Category (Name)': string;
|
||||
'Detail Level Category (Rank)': string;
|
||||
'Detail Level Category (Name)': string;
|
||||
'Average Rating': string;
|
||||
'Number of Reviews': string;
|
||||
'Buybox Seller Name': string;
|
||||
'Buybox Price': string;
|
||||
'Amazon Has Buybox': string;
|
||||
'Glance Views': string;
|
||||
}
|
||||
|
||||
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
|
||||
if (req.method === 'OPTIONS') return res.status(200).end();
|
||||
if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' });
|
||||
|
||||
try {
|
||||
const csvText = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);
|
||||
|
||||
const parsed = Papa.parse<CSVRow>(csvText, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
});
|
||||
|
||||
if (parsed.errors.length > 0) {
|
||||
console.error('[upload-vendor-data] Parse errors:', parsed.errors.slice(0, 5));
|
||||
}
|
||||
|
||||
const rows = parsed.data
|
||||
.filter(row => row.Date && row.Market && row.ASIN)
|
||||
.map(row => ({
|
||||
date: row.Date,
|
||||
market: row.Market,
|
||||
asin: row.ASIN,
|
||||
product_title: row['Product Title'] || null,
|
||||
tags: row.Tags || null,
|
||||
bsr_top_rank: parseIntSafe(row['Top Level Category (Rank)']),
|
||||
bsr_top_category: row['Top Level Category (Name)'] || null,
|
||||
bsr_detail_rank: parseIntSafe(row['Detail Level Category (Rank)']),
|
||||
bsr_detail_category: row['Detail Level Category (Name)'] || null,
|
||||
avg_rating: parseEUNumber(row['Average Rating']),
|
||||
num_reviews: parseIntSafe(row['Number of Reviews']),
|
||||
buybox_owner: row['Buybox Seller Name'] || null,
|
||||
buybox_price: parseEUNumber(row['Buybox Price']),
|
||||
amazon_has_buybox: row['Amazon Has Buybox'] === '1',
|
||||
glance_views: parseIntSafe(row['Glance Views']),
|
||||
}));
|
||||
|
||||
// Upsert in batches of 500
|
||||
const BATCH_SIZE = 500;
|
||||
let totalUpserted = 0;
|
||||
|
||||
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
||||
const batch = rows.slice(i, i + BATCH_SIZE);
|
||||
const { error } = await supabase
|
||||
.from('vendor_daily_data')
|
||||
.upsert(batch, { onConflict: 'date,market,asin' });
|
||||
|
||||
if (error) {
|
||||
console.error('[upload-vendor-data] Upsert error at batch', i, error);
|
||||
throw error;
|
||||
}
|
||||
totalUpserted += batch.length;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
rowsParsed: parsed.data.length,
|
||||
rowsUpserted: totalUpserted,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[upload-vendor-data] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add api/upload-vendor-data.ts
|
||||
git commit -m "feat: add vendor data upload API route with CSV parsing"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Add Vendor Upload to FileUpload.tsx
|
||||
|
||||
**Files:**
|
||||
- Modify: `components/FileUpload.tsx` (add 4th upload card)
|
||||
- Modify: `App.tsx` (add handler + pass prop)
|
||||
|
||||
**Step 1: Add `onVendorUpload` prop to FileUpload**
|
||||
|
||||
In `components/FileUpload.tsx`, add to interface:
|
||||
```ts
|
||||
onVendorUpload?: (file: File) => void;
|
||||
```
|
||||
|
||||
Destructure it in the component props. Add a 4th upload card after the Traffic card, styled with a green/emerald theme.
|
||||
|
||||
**Step 2: Add vendor upload handler in App.tsx**
|
||||
|
||||
```ts
|
||||
const handleVendorUpload = async (file: File) => {
|
||||
setSyncing(true);
|
||||
try {
|
||||
const text = await file.text();
|
||||
const response = await fetch('/api/upload-vendor-data', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/csv' },
|
||||
body: text,
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.error);
|
||||
alert(`Vendor data uploaded: ${result.rowsUpserted} rows processed.`);
|
||||
setIsDataModalOpen(false);
|
||||
} catch (error: any) {
|
||||
console.error("Failed to upload vendor data", error);
|
||||
alert(`Error uploading vendor data: ${error.message}`);
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Pass `onVendorUpload={handleVendorUpload}` to `<FileUpload>` in the modal.
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add components/FileUpload.tsx App.tsx
|
||||
git commit -m "feat: add vendor CSV upload button and handler"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Create VendorDataView Component
|
||||
|
||||
**Files:**
|
||||
- Create: `components/VendorDataView.tsx`
|
||||
|
||||
**Step 1: Write the component**
|
||||
|
||||
The component:
|
||||
1. On mount, fetches filter options from Supabase (markets, tags, date range)
|
||||
2. Has filter dropdowns for market, product line (tags), and ASIN search
|
||||
3. Fetches vendor data based on filters
|
||||
4. Aggregates daily → weekly client-side
|
||||
5. Renders 3 Recharts chart panels:
|
||||
- BSR Detail Rank trend (Y-axis inverted, lower = better), one line per market
|
||||
- Avg Rating + Review Count (dual Y-axis)
|
||||
- Buy Box: % days Amazon has buybox per week, per market
|
||||
|
||||
Uses existing UI patterns from the codebase: dark slate theme, Tailwind classes, `MultiSelectDropdown` for filters.
|
||||
|
||||
Weekly aggregation logic:
|
||||
- Group rows by ISO week (`YYYY-WW`)
|
||||
- BSR rank → use MIN (best rank that week)
|
||||
- Avg rating → use latest day's value
|
||||
- Reviews → use MAX (cumulative)
|
||||
- Buy box → % of days `amazon_has_buybox === true`
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add components/VendorDataView.tsx
|
||||
git commit -m "feat: add VendorDataView with BSR, ratings, and buybox charts"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Integrate VendorDataView into App.tsx
|
||||
|
||||
**Files:**
|
||||
- Modify: `App.tsx`
|
||||
|
||||
**Step 1: Add imports and view state**
|
||||
|
||||
Add lazy import:
|
||||
```ts
|
||||
const VendorDataView = lazy(() => import('./components/VendorDataView'));
|
||||
```
|
||||
|
||||
Update view type to include `'vendor'`:
|
||||
```ts
|
||||
const [view, setView] = useState<'dashboard' | 'table' | 'weekly' | 'movers' | 'ads' | 'forecast' | 'vendor'>('dashboard');
|
||||
```
|
||||
|
||||
**Step 2: Add nav button (desktop)**
|
||||
|
||||
After the Forecast button in the desktop nav bar (around line 731), add:
|
||||
```tsx
|
||||
<button
|
||||
onClick={() => setView('vendor')}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
||||
${view === 'vendor' ? '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 lg:inline">Vendor</span>
|
||||
</button>
|
||||
```
|
||||
|
||||
**Step 3: Add nav button (mobile)**
|
||||
|
||||
Add to the mobile bottom nav array (around line 855):
|
||||
```ts
|
||||
{ key: 'vendor' as const, icon: <ChartIcon />, label: 'Vendor' },
|
||||
```
|
||||
|
||||
Update `grid-cols-6` to `grid-cols-7`.
|
||||
|
||||
**Step 4: Add view rendering**
|
||||
|
||||
After the forecast Suspense block (around line 843), add:
|
||||
```tsx
|
||||
<Suspense fallback={<LoadingSpinner />}>
|
||||
<div className={view === 'vendor' ? '' : 'hidden'}>
|
||||
<VendorDataView />
|
||||
</div>
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add App.tsx
|
||||
git commit -m "feat: integrate Vendor Data view into app navigation"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Add Vite Dev Proxy for Upload Route
|
||||
|
||||
**Files:**
|
||||
- Modify: `vite.config.ts`
|
||||
|
||||
**Step 1: Add proxy for upload-vendor-data**
|
||||
|
||||
The upload API route needs a dev proxy. Since it's a POST to our own Vercel function (not Dropbox), we need to proxy to a local Vercel dev server or handle it differently.
|
||||
|
||||
For local dev, add a note that `vercel dev` should be used, or configure the proxy to forward to Supabase directly. Since the API route uses `@supabase/supabase-js` server-side, the simplest approach is to use `vercel dev` for testing the upload route locally.
|
||||
|
||||
No vite proxy needed for the upload route — it works in production via Vercel's `/api/*` routing. For local dev, use `vercel dev` instead of `npm run dev` when testing uploads.
|
||||
|
||||
**Step 2: Commit (if changes made)**
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Build, Test, Deploy
|
||||
|
||||
**Step 1: Build**
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Fix any TypeScript errors.
|
||||
|
||||
**Step 2: Test locally**
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Verify:
|
||||
- New "Vendor" tab appears in nav
|
||||
- VendorDataView loads (empty state with no data)
|
||||
- Upload modal shows Vendor CSV upload card
|
||||
|
||||
**Step 3: Commit and push**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "feat: complete Supabase vendor data integration"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
Vercel auto-deploys from main. After deploy, set env vars in Vercel dashboard if not already done.
|
||||
|
||||
**Step 4: Test upload on production**
|
||||
|
||||
Upload the sample CSV via the Vendor upload card. Verify data appears in Supabase table and charts render.
|
||||
Reference in New Issue
Block a user