mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 14:05:24 +02:00
569 lines
16 KiB
Markdown
569 lines
16 KiB
Markdown
# 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.
|