Skip to content

Commit 51991a7

Browse files
authored
Merge branch 'main' into copilot/fix-test-environment-setup
Signed-off-by: Asper Beauty Shop <252395498+asperpharma@users.noreply.github.com>
2 parents 7c4ce46 + a444ac3 commit 51991a7

6 files changed

Lines changed: 262 additions & 3 deletions

File tree

eslint.config.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import js from "@eslint/js";
2+
import globals from "globals";
3+
import reactHooks from "eslint-plugin-react-hooks";
4+
import reactRefresh from "eslint-plugin-react-refresh";
5+
import tseslint from "typescript-eslint";
6+
7+
export default tseslint.config(
8+
{ ignores: ["dist"] },
9+
{
10+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
11+
files: ["**/*.{ts,tsx}"],
12+
languageOptions: {
13+
ecmaVersion: 2020,
14+
globals: globals.browser,
15+
},
16+
plugins: {
17+
"react-hooks": reactHooks,
18+
"react-refresh": reactRefresh,
19+
},
20+
rules: {
21+
...reactHooks.configs.recommended.rules,
22+
"react-refresh/only-export-components": [
23+
"warn",
24+
{ allowConstantExport: true },
25+
],
26+
},
27+
}
28+
);

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"health": "node scripts/health-check.js",
2222
"brain": "node scripts/brain-check.js",
2323
"sync": "tsx scripts/sync-shopify-catalog.ts",
24+
"sync:supabase": "tsx scripts/reorganize-catalog.js",
2425
"sync:dry": "tsx scripts/sync-shopify-catalog.ts --dry-run",
2526
"sync:publish": "tsx scripts/sync-shopify-catalog.ts --publish"
2627
},
@@ -78,7 +79,7 @@
7879
"tailwind-merge": "^2.6.0",
7980
"tailwindcss-animate": "^1.0.7",
8081
"vaul": "^0.9.9",
81-
"vite": "5.4.19",
82+
"vite": "^6.2.0",
8283
"zod": "^3.25.76",
8384
"zustand": "^5.0.11"
8485
},
@@ -91,8 +92,9 @@
9192
"@types/node": "^22.16.5",
9293
"@types/react": "^18.3.23",
9394
"@types/react-dom": "^18.3.7",
94-
"@vitejs/plugin-react-swc": "3.11.0",
95+
"@vitejs/plugin-react-swc": "^3.8.0",
9596
"autoprefixer": "^10.4.21",
97+
"csv-parse": "^6.1.0",
9698
"eslint": "^9.39.3",
9799
"eslint-plugin-react-hooks": "^5.2.0",
98100
"eslint-plugin-react-refresh": "^0.4.20",

public/sitemap.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3+
<url><loc>https://asperbeautyshop-com.lovable.app/</loc><priority>1.0</priority><changefreq>daily</changefreq></url>
4+
<url><loc>https://asperbeautyshop-com.lovable.app/shop</loc><priority>0.9</priority><changefreq>daily</changefreq></url>
5+
<url><loc>https://asperbeautyshop-com.lovable.app/products</loc><priority>0.9</priority><changefreq>daily</changefreq></url>
6+
<url><loc>https://asperbeautyshop-com.lovable.app/best-sellers</loc><priority>0.8</priority><changefreq>daily</changefreq></url>
7+
<url><loc>https://asperbeautyshop-com.lovable.app/collections</loc><priority>0.8</priority><changefreq>weekly</changefreq></url>
8+
<url><loc>https://asperbeautyshop-com.lovable.app/brands</loc><priority>0.8</priority><changefreq>weekly</changefreq></url>
9+
<url><loc>https://asperbeautyshop-com.lovable.app/brands/vichy</loc><priority>0.7</priority><changefreq>weekly</changefreq></url>
10+
<url><loc>https://asperbeautyshop-com.lovable.app/offers</loc><priority>0.8</priority><changefreq>daily</changefreq></url>
11+
<url><loc>https://asperbeautyshop-com.lovable.app/skin-concerns</loc><priority>0.8</priority><changefreq>weekly</changefreq></url>
12+
<url><loc>https://asperbeautyshop-com.lovable.app/philosophy</loc><priority>0.5</priority><changefreq>monthly</changefreq></url>
13+
<url><loc>https://asperbeautyshop-com.lovable.app/contact</loc><priority>0.5</priority><changefreq>monthly</changefreq></url>
14+
<url><loc>https://asperbeautyshop-com.lovable.app/track-order</loc><priority>0.4</priority><changefreq>monthly</changefreq></url>
15+
<url><loc>https://asperbeautyshop-com.lovable.app/wishlist</loc><priority>0.4</priority><changefreq>weekly</changefreq></url>
16+
<url><loc>https://asperbeautyshop-com.lovable.app/auth</loc><priority>0.3</priority><changefreq>monthly</changefreq></url>
17+
</urlset>

scripts/reorganize-catalog.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { createClient } from '@supabase/supabase-js';
2+
import fs from 'fs';
3+
import path from 'path';
4+
import { parse } from 'csv-parse/sync';
5+
6+
// Use provided credentials
7+
const SUPABASE_URL = 'https://qqceibvalkoytafynwoc.supabase.co';
8+
const SUPABASE_SERVICE_ROLE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFxY2VpYnZhbGtveXRhZnlud29jIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3MDMzNzU5NSwiZXhwIjoyMDg1OTEzNTk1fQ.bMm74y-fjlz11_pkayGyo5ho2Mgzqfrh-ZNgPTFYxGY';
9+
10+
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
11+
12+
function mapCategory(productType, tags, title) {
13+
const searchStr = `${productType} ${tags} ${title}`.toLowerCase();
14+
15+
if (searchStr.includes('sunscreen') || searchStr.includes('spf') || searchStr.includes('sun protection')) return 'Sun Protection (SPF)';
16+
if (searchStr.includes('cleanser') || searchStr.includes('toner') || searchStr.includes('wash')) return 'Cleansers & Toners';
17+
if (searchStr.includes('serum') || searchStr.includes('active') || searchStr.includes('ampoule')) return 'Clinical Serums & Actives';
18+
if (searchStr.includes('moisturizer') || searchStr.includes('cream') || searchStr.includes('barrier') || searchStr.includes('hydration')) return 'Daily Hydration & Barrier';
19+
if (searchStr.includes('makeup') || searchStr.includes('foundation') || searchStr.includes('concealer') || searchStr.includes('lipstick') || searchStr.includes('mascara') || searchStr.includes('primer')) return 'Evening Radiance & Glamour';
20+
if (searchStr.includes('treatment') || searchStr.includes('mask') || searchStr.includes('peel') || searchStr.includes('spot')) return 'Targeted Treatments';
21+
if (searchStr.includes('hair') || searchStr.includes('shampoo') || searchStr.includes('conditioner')) return 'Hair Care';
22+
if (searchStr.includes('fragrance') || searchStr.includes('perfume') || searchStr.includes('cologne')) return 'Fragrance';
23+
if (searchStr.includes('body') || searchStr.includes('lotion') || searchStr.includes('shower') || searchStr.includes('hand')) return 'Body Care';
24+
25+
return 'Requires_Manual_Review';
26+
}
27+
28+
function mapConcern(title, type) {
29+
const s = `${title} ${type}`.toLowerCase();
30+
if (s.includes('acne') || s.includes('blemish')) return 'Concern_Acne';
31+
if (s.includes('age') || s.includes('wrinkle') || s.includes('firming')) return 'Concern_Aging';
32+
if (s.includes('bright') || s.includes('whitening') || s.includes('glow')) return 'Concern_Brightening';
33+
if (s.includes('dry') || s.includes('hydrate') || s.includes('moist')) return 'Concern_Hydration';
34+
if (s.includes('sensitive') || s.includes('soothe') || s.includes('calm')) return 'Concern_Sensitivity';
35+
if (s.includes('oil') || s.includes('pore') || s.includes('matte')) return 'Concern_Oiliness';
36+
if (s.includes('sun') || s.includes('spf')) return 'Concern_SunProtection';
37+
if (s.includes('pigment') || s.includes('dark spot')) return 'Concern_Pigmentation';
38+
return 'Concern_Hydration'; // Default
39+
}
40+
41+
function mapStep(title, type) {
42+
const s = `${title} ${type}`.toLowerCase();
43+
if (s.includes('cleanser') || s.includes('wash') || s.includes('toner') || s.includes('soap')) return 'Step_1_Cleanser';
44+
if (s.includes('serum') || s.includes('active') || s.includes('treatment') || s.includes('ampoule') || s.includes('mask')) return 'Step_2_Treatment';
45+
if (s.includes('spf') || s.includes('sunscreen') || s.includes('cream') || s.includes('moisturizer') || s.includes('lotion')) return 'Step_3_Protection';
46+
return 'Step_2_Treatment'; // Default
47+
}
48+
49+
async function run() {
50+
console.log('🚀 Starting Catalog Re-organization (with required fields)...');
51+
52+
const csvPath = path.resolve(process.cwd(), 'Orgnized Products/Asper_Catalog_FINAL_READY - Copy.csv');
53+
const fileContent = fs.readFileSync(csvPath, 'utf8');
54+
const records = parse(fileContent, {
55+
columns: true,
56+
skip_empty_lines: true,
57+
relax_column_count: true
58+
});
59+
60+
const products = [];
61+
const seenHandles = new Set();
62+
63+
for (const record of records) {
64+
const handle = record.handle;
65+
if (!handle || seenHandles.has(handle)) continue;
66+
seenHandles.add(handle);
67+
68+
const title = record.title;
69+
const productType = record.productType || '';
70+
const tags = record['tags/0'] || '';
71+
const price = parseFloat(record['variants/0/price']) || 0;
72+
const brand = record.vendor || '';
73+
const image_url = record['images/0/src'] || '';
74+
75+
products.push({
76+
handle,
77+
title,
78+
brand,
79+
price,
80+
image_url,
81+
asper_category: mapCategory(productType, tags, title),
82+
primary_concern: mapConcern(title, productType),
83+
regimen_step: mapStep(title, productType),
84+
inventory_total: parseInt(record['variants/0/inventoryQuantity']) || 0,
85+
updated_at: new Date().toISOString()
86+
});
87+
}
88+
89+
console.log(`✅ Processed ${products.length} products. Updating Supabase...`);
90+
91+
const CHUNK_SIZE = 50;
92+
for (let i = 0; i < products.length; i += CHUNK_SIZE) {
93+
const chunk = products.slice(i, i + CHUNK_SIZE);
94+
const { error } = await supabase
95+
.from('products')
96+
.upsert(chunk, { onConflict: 'handle' });
97+
98+
if (error) {
99+
console.error(`❌ Error in chunk ${i / CHUNK_SIZE + 1}:`, error.message);
100+
// break; // Uncomment to stop on first error
101+
} else {
102+
process.stdout.write(`✔ Updated ${Math.min(i + CHUNK_SIZE, products.length)} / ${products.length} products...\r`);
103+
}
104+
}
105+
106+
console.log('\n🏁 Re-organization complete!');
107+
}
108+
109+
run();

scripts/sync-to-supabase.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { createClient } from '@supabase/supabase-js';
2+
import fs from 'fs';
3+
import path from 'path';
4+
5+
// Use provided credentials
6+
const SUPABASE_URL = 'https://qqceibvalkoytafynwoc.supabase.co';
7+
const SUPABASE_SERVICE_ROLE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFxY2VpYnZhbGtveXRhZnlud29jIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3MDMzNzU5NSwiZXhwIjoyMDg1OTEzNTk1fQ.bMm74y-fjlz11_pkayGyo5ho2Mgzqfrh-ZNgPTFYxGY';
8+
9+
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
10+
11+
function robustSplit(raw) {
12+
const parts = [];
13+
let currentPart = '';
14+
let inString = false;
15+
16+
for (let i = 0; i < raw.length; i++) {
17+
const char = raw[i];
18+
if (char === "'" && (i === 0 || raw[i-1] !== "\\")) {
19+
inString = !inString;
20+
}
21+
if (char === ',' && !inString) {
22+
parts.push(currentPart.trim());
23+
currentPart = '';
24+
} else {
25+
currentPart += char;
26+
}
27+
}
28+
parts.push(currentPart.trim());
29+
return parts;
30+
}
31+
32+
async function sync() {
33+
console.log('🚀 Starting catalog sync to Supabase (Unique-Handle Mode)...');
34+
35+
const sqlPath = path.resolve(process.cwd(), 'catalog-sync.sql');
36+
if (!fs.existsSync(sqlPath)) {
37+
console.error('❌ catalog-sync.sql not found!');
38+
return;
39+
}
40+
41+
const sqlContent = fs.readFileSync(sqlPath, 'utf8');
42+
const lines = sqlContent.split('\n');
43+
44+
console.log('📦 Parsing SQL file lines...');
45+
46+
const productsMap = new Map();
47+
for (let line of lines) {
48+
line = line.trim();
49+
if (line.startsWith('(') && (line.endsWith('),') || line.endsWith(');') || line.endsWith(')'))) {
50+
try {
51+
const startIdx = line.indexOf('(') + 1;
52+
const endIdx = line.lastIndexOf(')');
53+
const raw = line.substring(startIdx, endIdx);
54+
const parts = robustSplit(raw);
55+
56+
if (parts.length >= 10) {
57+
const handle = parts[4].replace(/^'|'$/g, '').replace(/''/g, "'");
58+
productsMap.set(handle, {
59+
title: parts[1].replace(/^'|'$/g, '').replace(/''/g, "'"),
60+
brand: parts[2].replace(/^'|'$/g, '').replace(/''/g, "'"),
61+
price: parseFloat(parts[3]),
62+
handle: handle,
63+
image_url: parts[5].replace(/^'|'$/g, '').replace(/''/g, "'"),
64+
ai_persona_lead: parts[6].replace(/^'|'$/g, '').replace(/''/g, "'"),
65+
primary_concern: parts[7].replace(/^'|'$/g, '').replace(/''/g, "'"),
66+
regimen_step: parts[8].replace(/^'|'$/g, '').replace(/''/g, "'"),
67+
inventory_total: parseInt(parts[9]) || 0,
68+
updated_at: new Date().toISOString()
69+
});
70+
}
71+
} catch (e) {
72+
console.warn('⚠️ Skipping malformed line:', line.substring(0, 50) + '...');
73+
}
74+
}
75+
}
76+
77+
const products = Array.from(productsMap.values());
78+
if (products.length === 0) {
79+
console.error('❌ No products parsed.');
80+
return;
81+
}
82+
83+
console.log(`✅ Parsed ${products.length} unique products. Starting upload...`);
84+
85+
const CHUNK_SIZE = 50;
86+
for (let i = 0; i < products.length; i += CHUNK_SIZE) {
87+
const chunk = products.slice(i, i + CHUNK_SIZE);
88+
const { error } = await supabase
89+
.from('products')
90+
.upsert(chunk, { onConflict: 'handle' });
91+
92+
if (error) {
93+
console.error(`❌ Error uploading chunk ${i / CHUNK_SIZE + 1}:`, error.message);
94+
break;
95+
} else {
96+
process.stdout.write(`✔ Synced ${Math.min(i + CHUNK_SIZE, products.length)} / ${products.length} products...\r`);
97+
}
98+
}
99+
100+
console.log('\n🏁 Sync process finished.');
101+
}
102+
103+
sync();

src/integrations/supabase/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1104,8 +1104,8 @@ export type Database = {
11041104
}
11051105
Update: {
11061106
created_at?: string
1107-
description?: string | null
11081107
id?: string
1108+
description?: string | null
11091109
title?: string
11101110
}
11111111
Relationships: []

0 commit comments

Comments
 (0)