- 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>
16 KiB
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:
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
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:
'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.coSUPABASE_ANON_KEY= (anon key)SUPABASE_SERVICE_KEY= (service role key)
Step 5: Commit
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
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
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.
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
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:
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
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
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:
- On mount, fetches filter options from Supabase (markets, tags, date range)
- Has filter dropdowns for market, product line (tags), and ASIN search
- Fetches vendor data based on filters
- Aggregates daily → weekly client-side
- 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
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:
const VendorDataView = lazy(() => import('./components/VendorDataView'));
Update view type to include 'vendor':
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:
<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):
{ 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:
<Suspense fallback={<LoadingSpinner />}>
<div className={view === 'vendor' ? '' : 'hidden'}>
<VendorDataView />
</div>
</Suspense>
Step 5: Commit
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
npm run build
Fix any TypeScript errors.
Step 2: Test locally
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
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.