fix: currency suggestion uses region detection, seed adds market prices
- Currency auto-suggestion now uses locale region subtag (en-US → US → USD, en-DE → DE → EUR) instead of language prefix. Fixes wrong suggestion for users with English browser locale in European countries. - Added dismiss button (X) to suggestion banner - Dev seed script now clears existing dev data before re-seeding (safe to run repeatedly without manual DB cleanup) - Added DEV_MARKET_PRICES with multi-market UVP data for 10 global items (EU/US/UK prices) and community prices for 5 owned items Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -881,6 +881,212 @@ export const DEV_SETUPS = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
// ── Market Prices ─────────────────────────────────────────────────
|
||||
// Multi-market UVP/MSRP prices for global items.
|
||||
// globalItemIndex references DEV_GLOBAL_ITEMS array positions.
|
||||
|
||||
export const DEV_MARKET_PRICES = [
|
||||
// Revelate Designs Terrapin (index 0)
|
||||
{
|
||||
globalItemIndex: 0,
|
||||
market: "EU",
|
||||
currency: "EUR",
|
||||
priceCents: 18500,
|
||||
source: "manufacturer",
|
||||
},
|
||||
{
|
||||
globalItemIndex: 0,
|
||||
market: "US",
|
||||
currency: "USD",
|
||||
priceCents: 19900,
|
||||
source: "manufacturer",
|
||||
},
|
||||
{
|
||||
globalItemIndex: 0,
|
||||
market: "UK",
|
||||
currency: "GBP",
|
||||
priceCents: 16500,
|
||||
source: "manufacturer",
|
||||
},
|
||||
// Apidura Expedition Handlebar Pack (index 1)
|
||||
{
|
||||
globalItemIndex: 1,
|
||||
market: "EU",
|
||||
currency: "EUR",
|
||||
priceCents: 16000,
|
||||
source: "manufacturer",
|
||||
},
|
||||
{
|
||||
globalItemIndex: 1,
|
||||
market: "US",
|
||||
currency: "USD",
|
||||
priceCents: 17500,
|
||||
source: "manufacturer",
|
||||
},
|
||||
{
|
||||
globalItemIndex: 1,
|
||||
market: "UK",
|
||||
currency: "GBP",
|
||||
priceCents: 14000,
|
||||
source: "manufacturer",
|
||||
},
|
||||
// Ortlieb Frame-Pack RC (index 2)
|
||||
{
|
||||
globalItemIndex: 2,
|
||||
market: "EU",
|
||||
currency: "EUR",
|
||||
priceCents: 12000,
|
||||
source: "manufacturer",
|
||||
},
|
||||
{
|
||||
globalItemIndex: 2,
|
||||
market: "US",
|
||||
currency: "USD",
|
||||
priceCents: 14500,
|
||||
source: "manufacturer",
|
||||
},
|
||||
// Zpacks Duplex (index 6) — US brand, different pricing per market
|
||||
{
|
||||
globalItemIndex: 6,
|
||||
market: "US",
|
||||
currency: "USD",
|
||||
priceCents: 67900,
|
||||
source: "manufacturer",
|
||||
},
|
||||
{
|
||||
globalItemIndex: 6,
|
||||
market: "EU",
|
||||
currency: "EUR",
|
||||
priceCents: 72000,
|
||||
source: "retailer",
|
||||
},
|
||||
{
|
||||
globalItemIndex: 6,
|
||||
market: "UK",
|
||||
currency: "GBP",
|
||||
priceCents: 62000,
|
||||
source: "retailer",
|
||||
},
|
||||
// Tarptent Stratospire Li (index 7)
|
||||
{
|
||||
globalItemIndex: 7,
|
||||
market: "US",
|
||||
currency: "USD",
|
||||
priceCents: 62500,
|
||||
source: "manufacturer",
|
||||
},
|
||||
{
|
||||
globalItemIndex: 7,
|
||||
market: "EU",
|
||||
currency: "EUR",
|
||||
priceCents: 68000,
|
||||
source: "retailer",
|
||||
},
|
||||
// MSR Hubba Hubba (index 8)
|
||||
{
|
||||
globalItemIndex: 8,
|
||||
market: "EU",
|
||||
currency: "EUR",
|
||||
priceCents: 49000,
|
||||
source: "manufacturer",
|
||||
},
|
||||
{
|
||||
globalItemIndex: 8,
|
||||
market: "US",
|
||||
currency: "USD",
|
||||
priceCents: 47000,
|
||||
source: "manufacturer",
|
||||
},
|
||||
{
|
||||
globalItemIndex: 8,
|
||||
market: "UK",
|
||||
currency: "GBP",
|
||||
priceCents: 42000,
|
||||
source: "manufacturer",
|
||||
},
|
||||
// Enlightened Equipment Enigma (index 10)
|
||||
{
|
||||
globalItemIndex: 10,
|
||||
market: "US",
|
||||
currency: "USD",
|
||||
priceCents: 29500,
|
||||
source: "manufacturer",
|
||||
},
|
||||
{
|
||||
globalItemIndex: 10,
|
||||
market: "EU",
|
||||
currency: "EUR",
|
||||
priceCents: 33000,
|
||||
source: "retailer",
|
||||
},
|
||||
// Therm-a-Rest NeoAir XLite (index 13)
|
||||
{
|
||||
globalItemIndex: 13,
|
||||
market: "EU",
|
||||
currency: "EUR",
|
||||
priceCents: 22000,
|
||||
source: "manufacturer",
|
||||
},
|
||||
{
|
||||
globalItemIndex: 13,
|
||||
market: "US",
|
||||
currency: "USD",
|
||||
priceCents: 21000,
|
||||
source: "manufacturer",
|
||||
},
|
||||
{
|
||||
globalItemIndex: 13,
|
||||
market: "UK",
|
||||
currency: "GBP",
|
||||
priceCents: 19000,
|
||||
source: "manufacturer",
|
||||
},
|
||||
// BRS-3000T Stove (index 15) — cheap item, big market variance
|
||||
{
|
||||
globalItemIndex: 15,
|
||||
market: "EU",
|
||||
currency: "EUR",
|
||||
priceCents: 1500,
|
||||
source: "retailer",
|
||||
},
|
||||
{
|
||||
globalItemIndex: 15,
|
||||
market: "US",
|
||||
currency: "USD",
|
||||
priceCents: 2000,
|
||||
source: "retailer",
|
||||
},
|
||||
{
|
||||
globalItemIndex: 15,
|
||||
market: "UK",
|
||||
currency: "GBP",
|
||||
priceCents: 1300,
|
||||
source: "retailer",
|
||||
},
|
||||
// Wahoo ELEMNT BOLT (index 24)
|
||||
{
|
||||
globalItemIndex: 24,
|
||||
market: "EU",
|
||||
currency: "EUR",
|
||||
priceCents: 27900,
|
||||
source: "manufacturer",
|
||||
},
|
||||
{
|
||||
globalItemIndex: 24,
|
||||
market: "US",
|
||||
currency: "USD",
|
||||
priceCents: 27999,
|
||||
source: "manufacturer",
|
||||
},
|
||||
{
|
||||
globalItemIndex: 24,
|
||||
market: "UK",
|
||||
currency: "GBP",
|
||||
priceCents: 24999,
|
||||
source: "manufacturer",
|
||||
},
|
||||
] as const;
|
||||
|
||||
// ── Settings ───────────────────────────────────────────────────────
|
||||
|
||||
export const DEV_SETTINGS = [
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// ── Dev Seed Runner ────────────────────────────────────────────────
|
||||
// Idempotent script to populate a dev database with realistic data.
|
||||
// Clears dev data and re-seeds with fresh realistic data.
|
||||
// Usage: bun run db:seed:dev
|
||||
// Preserves real (non-dev) users. Safe to run repeatedly.
|
||||
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { and, eq, like, sql } from "drizzle-orm";
|
||||
import {
|
||||
DEV_CATEGORIES,
|
||||
DEV_GLOBAL_ITEMS,
|
||||
DEV_MARKET_PRICES,
|
||||
DEV_SETTINGS,
|
||||
DEV_SETUPS,
|
||||
DEV_TAG_ASSIGNMENTS,
|
||||
@@ -18,19 +20,64 @@ import { seedGlobalItems } from "./seed-global-items.ts";
|
||||
|
||||
type Db = typeof db;
|
||||
|
||||
async function seedDevData(database: Db = db) {
|
||||
// ── Idempotency check ──────────────────────────────────────────
|
||||
const existing = await database
|
||||
.select()
|
||||
.from(schema.users)
|
||||
.where(eq(schema.users.logtoSub, "dev-user-seed"))
|
||||
.limit(1);
|
||||
async function clearDevData(database: Db) {
|
||||
console.log("Clearing existing dev seed data...");
|
||||
|
||||
if (existing.length > 0) {
|
||||
console.log("Dev seed data already exists, skipping.");
|
||||
return;
|
||||
// Find dev user(s)
|
||||
const devUsers = await database
|
||||
.select({ id: schema.users.id })
|
||||
.from(schema.users)
|
||||
.where(like(schema.users.logtoSub, "dev-user%"));
|
||||
|
||||
for (const user of devUsers) {
|
||||
// Delete in FK order: setup_items → setups, thread_candidates → threads, items, categories, settings, shares
|
||||
await database
|
||||
.delete(schema.setupItems)
|
||||
.where(
|
||||
sql`${schema.setupItems.setupId} IN (SELECT id FROM setups WHERE user_id = ${user.id})`,
|
||||
);
|
||||
await database
|
||||
.delete(schema.shares)
|
||||
.where(
|
||||
sql`${schema.shares.setupId} IN (SELECT id FROM setups WHERE user_id = ${user.id})`,
|
||||
);
|
||||
await database
|
||||
.delete(schema.setups)
|
||||
.where(eq(schema.setups.userId, user.id));
|
||||
await database
|
||||
.delete(schema.threadCandidates)
|
||||
.where(
|
||||
sql`${schema.threadCandidates.threadId} IN (SELECT id FROM threads WHERE user_id = ${user.id})`,
|
||||
);
|
||||
await database
|
||||
.delete(schema.threads)
|
||||
.where(eq(schema.threads.userId, user.id));
|
||||
await database
|
||||
.delete(schema.communityPrices)
|
||||
.where(eq(schema.communityPrices.userId, user.id));
|
||||
await database.delete(schema.items).where(eq(schema.items.userId, user.id));
|
||||
await database
|
||||
.delete(schema.categories)
|
||||
.where(eq(schema.categories.userId, user.id));
|
||||
await database
|
||||
.delete(schema.settings)
|
||||
.where(eq(schema.settings.userId, user.id));
|
||||
await database.delete(schema.users).where(eq(schema.users.id, user.id));
|
||||
console.log(` Cleared dev user id=${user.id}`);
|
||||
}
|
||||
|
||||
// Clear market prices (these are global, not user-scoped, but seeded by dev)
|
||||
await database.delete(schema.marketPrices);
|
||||
console.log(" Cleared market prices.");
|
||||
|
||||
// Global items and tags are shared — leave them (seedGlobalItems handles idempotency)
|
||||
console.log("Dev data cleared.\n");
|
||||
}
|
||||
|
||||
async function seedDevData(database: Db = db) {
|
||||
// ── Clear previous dev data ────────────────────────────────────
|
||||
await clearDevData(database);
|
||||
|
||||
try {
|
||||
// ── 1. Seed global items and tags ──────────────────────────
|
||||
await seedGlobalItems(database);
|
||||
@@ -286,9 +333,50 @@ async function seedDevData(database: Db = db) {
|
||||
}
|
||||
console.log(` ${DEV_SETTINGS.length} settings created.`);
|
||||
|
||||
// ── 12. Insert market prices ───────────────────────────────
|
||||
let marketPriceCount = 0;
|
||||
for (const mp of DEV_MARKET_PRICES) {
|
||||
const giId = globalItemIds[mp.globalItemIndex];
|
||||
if (!giId) continue;
|
||||
await database.insert(schema.marketPrices).values({
|
||||
globalItemId: giId,
|
||||
market: mp.market,
|
||||
currency: mp.currency,
|
||||
priceCents: mp.priceCents,
|
||||
source: mp.source,
|
||||
});
|
||||
marketPriceCount++;
|
||||
}
|
||||
console.log(` ${marketPriceCount} market prices created.`);
|
||||
|
||||
// ── 13. Insert community prices ────────────────────────────
|
||||
// Seed a few community prices from the dev user for items they own
|
||||
const ownedGlobalItemIds = insertedItems
|
||||
.filter((i) => i.globalItemId !== null)
|
||||
.map((i) => i.globalItemId as number);
|
||||
|
||||
let communityPriceCount = 0;
|
||||
for (const giId of ownedGlobalItemIds.slice(0, 5)) {
|
||||
const item = insertedItems.find((i) => i.globalItemId === giId);
|
||||
if (!item) continue;
|
||||
await database.insert(schema.communityPrices).values({
|
||||
globalItemId: giId,
|
||||
userId,
|
||||
market: "EU",
|
||||
currency: "EUR",
|
||||
priceCents: item.priceCents
|
||||
? Math.round(item.priceCents * 0.85)
|
||||
: 10000,
|
||||
priceDate: new Date("2026-03-15"),
|
||||
sourceType: "purchased",
|
||||
});
|
||||
communityPriceCount++;
|
||||
}
|
||||
console.log(` ${communityPriceCount} community prices created.`);
|
||||
|
||||
// ── Summary ────────────────────────────────────────────────
|
||||
console.log(
|
||||
`\nDev seed complete: ${globalItemIds.length} global items, ${allTags.length} tags, ${insertedItems.length} user items, ${threadResults.length} threads, ${setupResults.length} setups`,
|
||||
`\nDev seed complete: ${globalItemIds.length} global items, ${allTags.length} tags, ${insertedItems.length} user items, ${threadResults.length} threads, ${setupResults.length} setups, ${marketPriceCount} market prices`,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Seed failed:", err);
|
||||
|
||||
Reference in New Issue
Block a user