fix: currency suggestion uses region detection, seed adds market prices
All checks were successful
CI / ci (push) Successful in 1m24s
CI / e2e (push) Has been skipped
CI / deploy (push) Successful in 15s

- 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:
2026-04-13 21:27:57 +02:00
parent 51c8703a3d
commit 23027551b4
3 changed files with 356 additions and 30 deletions

View File

@@ -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 = [

View File

@@ -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);