From 35b20e7cccfd755b272390730178049d2d0389a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:24:03 +0000 Subject: [PATCH 1/3] Initial plan From 389a488e6a79ae9d5f03beaae6af310f603fc852 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:54:36 +0000 Subject: [PATCH 2/3] feat: Add Featured Packs section with curated pack templates and seed data Co-authored-by: andrew-bierman <94939237+andrew-bierman@users.noreply.github.com> --- .../components/FeaturedPacksSection.tsx | 112 + .../screens/PackTemplateListScreen.tsx | 18 +- apps/expo/lib/i18n/locales/en.json | 13 +- packages/api/package.json | 1 + packages/api/src/db/seed.ts | 1960 +++++++++++++++++ 5 files changed, 2096 insertions(+), 8 deletions(-) create mode 100644 apps/expo/features/pack-templates/components/FeaturedPacksSection.tsx create mode 100644 packages/api/src/db/seed.ts diff --git a/apps/expo/features/pack-templates/components/FeaturedPacksSection.tsx b/apps/expo/features/pack-templates/components/FeaturedPacksSection.tsx new file mode 100644 index 0000000000..f035251956 --- /dev/null +++ b/apps/expo/features/pack-templates/components/FeaturedPacksSection.tsx @@ -0,0 +1,112 @@ +import { Text } from '@packrat/ui/nativewindui'; +import { WeightBadge } from 'expo-app/components/initial/WeightBadge'; +import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; +import { useRouter } from 'expo-router'; +import { isArray } from 'radash'; +import { Image, Pressable, ScrollView, View } from 'react-native'; +import { usePackTemplates } from '../hooks'; +import { usePackTemplateDetails } from '../hooks/usePackTemplatesDetails'; +import type { PackTemplate } from '../types'; + +type FeaturedPackCardProps = { + templateId: string; + onPress: (template: PackTemplate) => void; +}; + +function FeaturedPackCard({ templateId, onPress }: FeaturedPackCardProps) { + const template = usePackTemplateDetails(templateId); + const { t } = useTranslation(); + + if (!template) return null; + + return ( + onPress(template)} + style={({ pressed }) => ({ opacity: pressed ? 0.85 : 1 })} + > + {template.image ? ( + + ) : ( + + šŸŽ’ + + )} + + + + {template.name} + + + {template.category && ( + {template.category} + )} + + {template.tags && isArray(template.tags) && template.tags.length > 0 && ( + + + {template.tags.slice(0, 3).map((tag) => ( + + {tag} + + ))} + + + )} + + + + + + + {template.items && isArray(template.items) + ? `${template.items.length} ${t('packTemplates.items')}` + : `0 ${t('packTemplates.items')}`} + + + + + ); +} + +type FeaturedPacksSectionProps = { + onTemplatePress: (template: PackTemplate) => void; +}; + +export function FeaturedPacksSection({ onTemplatePress }: FeaturedPacksSectionProps) { + const templates = usePackTemplates(); + const { t } = useTranslation(); + const router = useRouter(); + + const featuredTemplates = templates.filter((template) => template.isAppTemplate); + + if (featuredTemplates.length === 0) return null; + + return ( + + + + {t('packTemplates.featuredPacks')} + + router.push('/pack-templates')}> + {t('packTemplates.viewAll')} + + + + {featuredTemplates.map((template) => ( + + ))} + + + ); +} diff --git a/apps/expo/features/pack-templates/screens/PackTemplateListScreen.tsx b/apps/expo/features/pack-templates/screens/PackTemplateListScreen.tsx index 4f7624e4a6..8bab147808 100644 --- a/apps/expo/features/pack-templates/screens/PackTemplateListScreen.tsx +++ b/apps/expo/features/pack-templates/screens/PackTemplateListScreen.tsx @@ -21,6 +21,7 @@ import { View, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { FeaturedPacksSection } from '../components/FeaturedPacksSection'; import { PackTemplateCard } from '../components/PackTemplateCard'; import TemplateCreationOptions from '../components/TemplateCreationOptions'; import { usePackTemplates } from '../hooks'; @@ -167,13 +168,16 @@ export function PackTemplateListScreen() { stickyHeaderHiddenOnScroll ListHeaderComponent={ selectedTemplateTypeIndex === 0 ? ( - - - {filteredTemplates.length}{' '} - {filteredTemplates.length === 1 - ? t('packTemplates.template') - : t('packTemplates.templates')} - + + + + + {filteredTemplates.length}{' '} + {filteredTemplates.length === 1 + ? t('packTemplates.template') + : t('packTemplates.templates')} + + ) : undefined } diff --git a/apps/expo/lib/i18n/locales/en.json b/apps/expo/lib/i18n/locales/en.json index a747dfd5a6..b24456bba3 100644 --- a/apps/expo/lib/i18n/locales/en.json +++ b/apps/expo/lib/i18n/locales/en.json @@ -965,7 +965,18 @@ "goBack": "Go Back", "pleaseTryAgainLater": "Please try again later.", "anErrorOccurred": "An Error Occurred", - "pleaseTryAgain": "Please try again" + "pleaseTryAgain": "Please try again", + "featuredPacks": "Featured Packs", + "viewAll": "View All", + "dayHike": "Day Hike", + "overnight": "Overnight", + "weekendCamping": "Weekend Camping", + "multiDay": "Multi-Day", + "winterHiking": "Winter Hiking", + "winterCamping": "Winter Camping", + "tripDuration": "Trip Duration", + "environment": "Environment", + "intendedUse": "Intended Use" }, "guides": { "guides": "Guides", diff --git a/packages/api/package.json b/packages/api/package.json index 6d25481653..c5687614e4 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -9,6 +9,7 @@ "check-types-watch": "tsc --noEmit --watch", "db:generate": "drizzle-kit generate --dialect=postgresql --schema=src/db/schema.ts --out=./drizzle", "db:migrate": "bun run ./migrate.ts", + "db:seed": "bun run ./src/db/seed.ts", "test": "vitest run", "test:unit": "vitest run --config vitest.unit.config.ts", "test:unit:coverage": "vitest run --config vitest.unit.config.ts --coverage" diff --git a/packages/api/src/db/seed.ts b/packages/api/src/db/seed.ts new file mode 100644 index 0000000000..2a64f3879a --- /dev/null +++ b/packages/api/src/db/seed.ts @@ -0,0 +1,1960 @@ +/** + * Seed script for Featured Pack Templates. + * + * This script populates the database with curated "Featured Packs" (app templates) + * for 6 common outdoor activity types: + * 1. Day Hike Pack + * 2. Overnight Backpacking Pack + * 3. Weekend Camping Pack + * 4. Multi-Day Backpacking Pack + * 5. Winter Day Hike Pack + * 6. Winter Camping Pack + * + * Usage: + * NEON_DATABASE_URL= bun run packages/api/src/db/seed.ts [adminUserId] + * + * If no adminUserId is provided, the script looks up the first ADMIN user in the DB. + */ + +import { neon, neonConfig } from '@neondatabase/serverless'; +import { and, eq } from 'drizzle-orm'; +import { drizzle } from 'drizzle-orm/neon-http'; +import { drizzle as drizzlePg } from 'drizzle-orm/node-postgres'; +import { Client } from 'pg'; +import WebSocket from 'ws'; +import * as schema from './schema'; + +neonConfig.webSocketConstructor = WebSocket; + +const isStandardPostgresUrl = (url: string) => { + try { + const u = new URL(url); + const host = u.hostname.toLowerCase(); + const isNeonTech = host === 'neon.tech' || host.endsWith('.neon.tech'); + const isNeonCom = host === 'neon.com' || host.endsWith('.neon.com'); + return u.protocol === 'postgres:' && !isNeonTech && !isNeonCom; + } catch { + return false; + } +}; + +// ─── Featured Pack Definitions ───────────────────────────────────────────── + +type SeedItem = { + id: string; + name: string; + description?: string; + weight: number; + weightUnit: string; + quantity: number; + category: string; + consumable: boolean; + worn: boolean; + notes?: string; +}; + +type SeedTemplate = { + id: string; + name: string; + description: string; + category: string; + tags: string[]; + items: SeedItem[]; +}; + +const FEATURED_TEMPLATES: SeedTemplate[] = [ + // ── 1. Day Hike Pack ────────────────────────────────────────────────────── + { + id: 'featured_day_hike_pack', + name: 'Day Hike Pack', + description: + 'Essential gear for a single-day hike on established trails. Trip duration: 1 day. Environment: Mountain / Forest. Use: Day hiking.', + category: 'hiking', + tags: ['day hike', '1 day', 'hiking', 'mountain', 'forest'], + items: [ + { + id: 'featured_dhp_backpack', + name: 'Daypack 20-25L', + weight: 800, + weightUnit: 'g', + quantity: 1, + category: 'pack', + consumable: false, + worn: false, + description: 'Lightweight daypack with hip belt and hydration sleeve', + }, + { + id: 'featured_dhp_water_bottles', + name: 'Water Bottles (1L each)', + weight: 200, + weightUnit: 'g', + quantity: 2, + category: 'hydration', + consumable: false, + worn: false, + }, + { + id: 'featured_dhp_water_filter', + name: 'Water Filter', + weight: 88, + weightUnit: 'g', + quantity: 1, + category: 'hydration', + consumable: false, + worn: false, + description: 'Squeeze-style filter for trail water sources', + }, + { + id: 'featured_dhp_rain_jacket', + name: 'Rain Jacket', + weight: 300, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: false, + description: 'Lightweight packable waterproof jacket', + }, + { + id: 'featured_dhp_fleece', + name: 'Insulating Fleece / Mid Layer', + weight: 250, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_dhp_sun_hat', + name: 'Sun Hat', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_dhp_sunglasses', + name: 'Sunglasses', + weight: 30, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_dhp_sunscreen', + name: 'Sunscreen SPF 50', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'protection', + consumable: true, + worn: false, + }, + { + id: 'featured_dhp_snacks', + name: 'Trail Snacks / Energy Bars', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'food', + consumable: true, + worn: false, + description: 'Mix of energy bars, nuts, and dried fruit', + }, + { + id: 'featured_dhp_lunch', + name: 'Packed Lunch', + weight: 350, + weightUnit: 'g', + quantity: 1, + category: 'food', + consumable: true, + worn: false, + }, + { + id: 'featured_dhp_trekking_poles', + name: 'Trekking Poles', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'gear', + consumable: false, + worn: false, + description: 'Collapsible aluminum or carbon poles', + }, + { + id: 'featured_dhp_first_aid', + name: 'First Aid Kit', + weight: 150, + weightUnit: 'g', + quantity: 1, + category: 'safety', + consumable: false, + worn: false, + description: 'Blister care, bandages, antiseptic wipes, pain relief', + }, + { + id: 'featured_dhp_map_compass', + name: 'Map & Compass', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'navigation', + consumable: false, + worn: false, + }, + { + id: 'featured_dhp_headlamp', + name: 'Headlamp', + weight: 90, + weightUnit: 'g', + quantity: 1, + category: 'lighting', + consumable: false, + worn: false, + description: 'LED headlamp with spare batteries', + }, + { + id: 'featured_dhp_whistle', + name: 'Emergency Whistle', + weight: 15, + weightUnit: 'g', + quantity: 1, + category: 'safety', + consumable: false, + worn: true, + }, + { + id: 'featured_dhp_knife', + name: 'Pocket Knife / Multi-tool', + weight: 60, + weightUnit: 'g', + quantity: 1, + category: 'gear', + consumable: false, + worn: false, + }, + ], + }, + + // ── 2. Overnight Backpacking Pack ───────────────────────────────────────── + { + id: 'featured_overnight_pack', + name: 'Overnight Backpacking Pack', + description: + 'All gear for a one-night backcountry trip. Trip duration: 1 night (2 days). Environment: Backcountry trails. Use: Backpacking.', + category: 'backpacking', + tags: ['overnight', '1 night', '2 days', 'backpacking', 'backcountry'], + items: [ + { + id: 'featured_op_backpack', + name: 'Backpack 40-50L', + weight: 1400, + weightUnit: 'g', + quantity: 1, + category: 'pack', + consumable: false, + worn: false, + }, + { + id: 'featured_op_tent', + name: 'Backpacking Tent (2-person)', + weight: 1200, + weightUnit: 'g', + quantity: 1, + category: 'shelter', + consumable: false, + worn: false, + description: 'Freestanding 3-season tent with rainfly', + }, + { + id: 'featured_op_sleeping_bag', + name: 'Sleeping Bag (30°F / 0°C rated)', + weight: 900, + weightUnit: 'g', + quantity: 1, + category: 'sleep system', + consumable: false, + worn: false, + }, + { + id: 'featured_op_sleeping_pad', + name: 'Sleeping Pad', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'sleep system', + consumable: false, + worn: false, + description: 'Inflatable or foam sleeping pad (R-value 2+)', + }, + { + id: 'featured_op_stove', + name: 'Backpacking Stove', + weight: 110, + weightUnit: 'g', + quantity: 1, + category: 'cooking', + consumable: false, + worn: false, + }, + { + id: 'featured_op_fuel', + name: 'Fuel Canister (100g)', + weight: 230, + weightUnit: 'g', + quantity: 1, + category: 'cooking', + consumable: true, + worn: false, + }, + { + id: 'featured_op_cook_pot', + name: 'Cook Pot (750ml)', + weight: 200, + weightUnit: 'g', + quantity: 1, + category: 'cooking', + consumable: false, + worn: false, + }, + { + id: 'featured_op_spork', + name: 'Spork / Utensils', + weight: 20, + weightUnit: 'g', + quantity: 1, + category: 'cooking', + consumable: false, + worn: false, + }, + { + id: 'featured_op_food_d1', + name: 'Day 1 Meals (dehydrated)', + weight: 500, + weightUnit: 'g', + quantity: 1, + category: 'food', + consumable: true, + worn: false, + description: 'Breakfast, snacks, dinner for day 1', + }, + { + id: 'featured_op_food_d2', + name: 'Day 2 Meals (dehydrated)', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'food', + consumable: true, + worn: false, + description: 'Breakfast and snacks for day 2 hike out', + }, + { + id: 'featured_op_water_filter', + name: 'Water Filter', + weight: 88, + weightUnit: 'g', + quantity: 1, + category: 'hydration', + consumable: false, + worn: false, + }, + { + id: 'featured_op_water_bottles', + name: 'Water Bottles (1L each)', + weight: 200, + weightUnit: 'g', + quantity: 2, + category: 'hydration', + consumable: false, + worn: false, + }, + { + id: 'featured_op_headlamp', + name: 'Headlamp', + weight: 90, + weightUnit: 'g', + quantity: 1, + category: 'lighting', + consumable: false, + worn: false, + }, + { + id: 'featured_op_rain_jacket', + name: 'Rain Jacket', + weight: 300, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_op_insulating_layer', + name: 'Insulating Layer (Fleece or Down)', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_op_hiking_pants', + name: 'Hiking Pants', + weight: 250, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_op_hiking_shirt', + name: 'Moisture-Wicking Shirt', + weight: 150, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_op_extra_socks', + name: 'Extra Socks', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_op_underwear', + name: 'Extra Underwear', + weight: 50, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_op_toiletries', + name: 'Toiletries Kit', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'hygiene', + consumable: true, + worn: false, + description: 'Toothbrush, toothpaste, biodegradable soap, toilet paper', + }, + { + id: 'featured_op_trowel', + name: 'Trowel (cat hole)', + weight: 80, + weightUnit: 'g', + quantity: 1, + category: 'hygiene', + consumable: false, + worn: false, + }, + { + id: 'featured_op_first_aid', + name: 'First Aid Kit', + weight: 150, + weightUnit: 'g', + quantity: 1, + category: 'safety', + consumable: false, + worn: false, + }, + { + id: 'featured_op_trekking_poles', + name: 'Trekking Poles', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'gear', + consumable: false, + worn: false, + }, + { + id: 'featured_op_map_compass', + name: 'Map & Compass', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'navigation', + consumable: false, + worn: false, + }, + { + id: 'featured_op_bear_bag', + name: 'Bear Bag / Food Hang Kit', + weight: 200, + weightUnit: 'g', + quantity: 1, + category: 'safety', + consumable: false, + worn: false, + description: 'Stuff sack + 50ft cord for food hanging', + }, + ], + }, + + // ── 3. Weekend Camping Pack ─────────────────────────────────────────────── + { + id: 'featured_weekend_camping_pack', + name: 'Weekend Camping Pack', + description: + 'Complete setup for 2-3 day camping trips at established campgrounds. Trip duration: 2-3 days. Environment: Campground / car camping. Use: Camping.', + category: 'camping', + tags: ['weekend', '2-3 days', 'camping', 'car camping', 'campground'], + items: [ + { + id: 'featured_wcp_backpack', + name: 'Pack or Duffel Bag 60-70L', + weight: 1200, + weightUnit: 'g', + quantity: 1, + category: 'pack', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_tent', + name: 'Camping Tent (2-person)', + weight: 2000, + weightUnit: 'g', + quantity: 1, + category: 'shelter', + consumable: false, + worn: false, + description: '3-season tent with full rainfly and footprint', + }, + { + id: 'featured_wcp_sleeping_bag', + name: 'Sleeping Bag', + weight: 1000, + weightUnit: 'g', + quantity: 1, + category: 'sleep system', + consumable: false, + worn: false, + description: '20°F / -7°C rated sleeping bag', + }, + { + id: 'featured_wcp_sleeping_pad', + name: 'Sleeping Pad or Cot', + weight: 500, + weightUnit: 'g', + quantity: 1, + category: 'sleep system', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_camp_stove', + name: 'Camp Stove (2-burner)', + weight: 1200, + weightUnit: 'g', + quantity: 1, + category: 'cooking', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_fuel', + name: 'Propane Fuel', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'cooking', + consumable: true, + worn: false, + }, + { + id: 'featured_wcp_cook_pot_set', + name: 'Cook Pot Set', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'cooking', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_camp_dishes', + name: 'Camp Dishes & Utensils', + weight: 300, + weightUnit: 'g', + quantity: 1, + category: 'cooking', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_food', + name: 'Food & Snacks (3 days)', + weight: 2000, + weightUnit: 'g', + quantity: 1, + category: 'food', + consumable: true, + worn: false, + }, + { + id: 'featured_wcp_water_filter', + name: 'Water Filter', + weight: 88, + weightUnit: 'g', + quantity: 1, + category: 'hydration', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_water_bottles', + name: 'Water Bottles', + weight: 400, + weightUnit: 'g', + quantity: 2, + category: 'hydration', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_camp_chair', + name: 'Camp Chair', + weight: 800, + weightUnit: 'g', + quantity: 1, + category: 'comfort', + consumable: false, + worn: false, + description: 'Lightweight folding chair', + }, + { + id: 'featured_wcp_headlamp', + name: 'Headlamp', + weight: 90, + weightUnit: 'g', + quantity: 1, + category: 'lighting', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_lantern', + name: 'Camp Lantern', + weight: 200, + weightUnit: 'g', + quantity: 1, + category: 'lighting', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_rain_jacket', + name: 'Rain Jacket', + weight: 300, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_fleece', + name: 'Fleece Jacket', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_pants', + name: 'Camp Pants', + weight: 300, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wcp_shirts', + name: 'T-Shirts', + weight: 300, + weightUnit: 'g', + quantity: 2, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_underwear', + name: 'Underwear', + weight: 100, + weightUnit: 'g', + quantity: 2, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_socks', + name: 'Hiking Socks', + weight: 150, + weightUnit: 'g', + quantity: 2, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_toiletries', + name: 'Toiletries', + weight: 200, + weightUnit: 'g', + quantity: 1, + category: 'hygiene', + consumable: true, + worn: false, + }, + { + id: 'featured_wcp_towel', + name: 'Quick-Dry Towel', + weight: 200, + weightUnit: 'g', + quantity: 1, + category: 'hygiene', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_first_aid', + name: 'First Aid Kit', + weight: 200, + weightUnit: 'g', + quantity: 1, + category: 'safety', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_tarp', + name: 'Tarp / Camp Shelter', + weight: 600, + weightUnit: 'g', + quantity: 1, + category: 'shelter', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_rope', + name: 'Paracord (50ft)', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'gear', + consumable: false, + worn: false, + }, + { + id: 'featured_wcp_knife', + name: 'Camp Knife', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'gear', + consumable: false, + worn: false, + }, + ], + }, + + // ── 4. Multi-Day Backpacking Pack ───────────────────────────────────────── + { + id: 'featured_multiday_pack', + name: 'Multi-Day Backpacking Pack', + description: + 'Comprehensive gear for 4-7 day backcountry trips. Trip duration: 4-7 days. Environment: Backcountry wilderness. Use: Backpacking / thru-hiking.', + category: 'backpacking', + tags: ['multi-day', '4-7 days', 'backpacking', 'backcountry', 'thru-hiking', 'ultralight'], + items: [ + { + id: 'featured_mdp_backpack', + name: 'Backpack 65-75L', + weight: 1600, + weightUnit: 'g', + quantity: 1, + category: 'pack', + consumable: false, + worn: false, + description: 'Internal frame backpack with hip belt and load lifters', + }, + { + id: 'featured_mdp_tent', + name: 'Ultralight Tent (2-person)', + weight: 1000, + weightUnit: 'g', + quantity: 1, + category: 'shelter', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_sleeping_bag', + name: 'Sleeping Bag (3-season, 20°F)', + weight: 900, + weightUnit: 'g', + quantity: 1, + category: 'sleep system', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_sleeping_pad', + name: 'Sleeping Pad (R-value 3+)', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'sleep system', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_stove', + name: 'Backpacking Stove', + weight: 110, + weightUnit: 'g', + quantity: 1, + category: 'cooking', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_fuel', + name: 'Fuel Canisters (100g each)', + weight: 460, + weightUnit: 'g', + quantity: 2, + category: 'cooking', + consumable: true, + worn: false, + }, + { + id: 'featured_mdp_cook_pot', + name: 'Cook Pot (900ml)', + weight: 200, + weightUnit: 'g', + quantity: 1, + category: 'cooking', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_spork', + name: 'Spork / Long Spoon', + weight: 20, + weightUnit: 'g', + quantity: 1, + category: 'cooking', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_food', + name: 'Dehydrated Meals & Trail Food (4-7 days)', + weight: 3500, + weightUnit: 'g', + quantity: 1, + category: 'food', + consumable: true, + worn: false, + description: 'Approx. 500g/day of high-calorie backcountry food', + }, + { + id: 'featured_mdp_water_filter', + name: 'Water Filter', + weight: 88, + weightUnit: 'g', + quantity: 1, + category: 'hydration', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_water_bottles', + name: 'Water Bottles (1L each)', + weight: 200, + weightUnit: 'g', + quantity: 2, + category: 'hydration', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_electrolytes', + name: 'Electrolyte Tabs / Powder', + weight: 50, + weightUnit: 'g', + quantity: 1, + category: 'hydration', + consumable: true, + worn: false, + }, + { + id: 'featured_mdp_headlamp', + name: 'Headlamp + Spare Batteries', + weight: 150, + weightUnit: 'g', + quantity: 1, + category: 'lighting', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_rain_jacket', + name: 'Rain Jacket', + weight: 300, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_rain_pants', + name: 'Rain Pants', + weight: 200, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_insulating_jacket', + name: 'Insulating Jacket (Down or Synthetic)', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_hiking_pants', + name: 'Hiking Pants', + weight: 250, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_mdp_shirts', + name: 'Moisture-Wicking Shirts', + weight: 450, + weightUnit: 'g', + quantity: 3, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_underwear', + name: 'Underwear (merino wool)', + weight: 100, + weightUnit: 'g', + quantity: 2, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_socks', + name: 'Hiking Socks', + weight: 225, + weightUnit: 'g', + quantity: 3, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_base_layer_top', + name: 'Base Layer Top', + weight: 200, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_base_layer_bottom', + name: 'Base Layer Bottom', + weight: 200, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_gloves', + name: 'Lightweight Gloves', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_beanie', + name: 'Wool Beanie', + weight: 80, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_sunglasses', + name: 'Sunglasses', + weight: 30, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_mdp_sun_hat', + name: 'Sun Hat', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_trekking_poles', + name: 'Trekking Poles', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'gear', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_bear_canister', + name: 'Bear Canister', + weight: 900, + weightUnit: 'g', + quantity: 1, + category: 'food', + consumable: false, + worn: false, + description: 'Required in many backcountry areas', + }, + { + id: 'featured_mdp_trowel', + name: 'Trowel', + weight: 80, + weightUnit: 'g', + quantity: 1, + category: 'hygiene', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_toiletries', + name: 'Toiletries', + weight: 150, + weightUnit: 'g', + quantity: 1, + category: 'hygiene', + consumable: true, + worn: false, + }, + { + id: 'featured_mdp_first_aid', + name: 'First Aid Kit', + weight: 200, + weightUnit: 'g', + quantity: 1, + category: 'safety', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_map_compass', + name: 'Map & Compass', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'navigation', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_whistle', + name: 'Emergency Whistle', + weight: 15, + weightUnit: 'g', + quantity: 1, + category: 'safety', + consumable: false, + worn: true, + }, + { + id: 'featured_mdp_satellite', + name: 'Satellite Communicator (e.g. Garmin inReach)', + weight: 160, + weightUnit: 'g', + quantity: 1, + category: 'safety', + consumable: false, + worn: false, + description: 'Two-way messaging and SOS for remote areas', + }, + { + id: 'featured_mdp_power_bank', + name: 'Power Bank (10,000mAh)', + weight: 180, + weightUnit: 'g', + quantity: 1, + category: 'electronics', + consumable: false, + worn: false, + }, + { + id: 'featured_mdp_repair_kit', + name: 'Gear Repair Kit', + weight: 80, + weightUnit: 'g', + quantity: 1, + category: 'gear', + consumable: false, + worn: false, + description: 'Duct tape, needle & thread, tent pole repair sleeve', + }, + ], + }, + + // ── 5. Winter Day Hike Pack ──────────────────────────────────────────────── + { + id: 'featured_winter_day_hike_pack', + name: 'Winter Day Hike Pack', + description: + 'Safe gear for winter day hikes in snow and cold conditions. Trip duration: 1 day. Environment: Snow / mountain trails. Use: Winter hiking (below 32°F / 0°C).', + category: 'winter', + tags: ['winter hiking', 'cold weather', '1 day', 'snow', 'day hike'], + items: [ + { + id: 'featured_wdhp_backpack', + name: 'Backpack 25-35L', + weight: 1000, + weightUnit: 'g', + quantity: 1, + category: 'pack', + consumable: false, + worn: false, + description: 'Waterproof or pack cover included', + }, + { + id: 'featured_wdhp_insulated_jacket', + name: 'Insulated Jacket (Down)', + weight: 600, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wdhp_base_layer_top', + name: 'Merino Wool Base Layer Top', + weight: 200, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wdhp_base_layer_bottom', + name: 'Merino Wool Base Layer Bottom', + weight: 200, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wdhp_mid_layer', + name: 'Fleece Mid Layer', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wdhp_waterproof_pants', + name: 'Waterproof / Softshell Pants', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wdhp_wool_socks', + name: 'Wool Socks', + weight: 200, + weightUnit: 'g', + quantity: 2, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_wdhp_waterproof_gloves', + name: 'Waterproof Insulated Gloves', + weight: 200, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wdhp_balaclava', + name: 'Balaclava', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wdhp_insulated_hat', + name: 'Insulated Winter Hat', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wdhp_gaiters', + name: 'Gaiters', + weight: 300, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + description: 'Keep snow out of boots', + }, + { + id: 'featured_wdhp_microspikes', + name: 'Microspikes / Traction Devices', + weight: 680, + weightUnit: 'g', + quantity: 1, + category: 'gear', + consumable: false, + worn: false, + description: 'Essential for icy or packed snow trails', + }, + { + id: 'featured_wdhp_trekking_poles', + name: 'Trekking Poles', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'gear', + consumable: false, + worn: false, + }, + { + id: 'featured_wdhp_headlamp', + name: 'Headlamp (winter-rated batteries)', + weight: 90, + weightUnit: 'g', + quantity: 1, + category: 'lighting', + consumable: false, + worn: false, + description: 'Days are short in winter — always bring a headlamp', + }, + { + id: 'featured_wdhp_thermos', + name: 'Insulated Thermos', + weight: 500, + weightUnit: 'g', + quantity: 1, + category: 'hydration', + consumable: false, + worn: false, + description: 'Pre-filled with hot water, tea, or broth', + }, + { + id: 'featured_wdhp_snacks', + name: 'High-Calorie Snacks', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'food', + consumable: true, + worn: false, + description: 'Your body burns more calories in cold — eat more', + }, + { + id: 'featured_wdhp_water_filter', + name: 'Water Filter (insulated to prevent freezing)', + weight: 88, + weightUnit: 'g', + quantity: 1, + category: 'hydration', + consumable: false, + worn: false, + }, + { + id: 'featured_wdhp_emergency_bivy', + name: 'Emergency Bivy', + weight: 230, + weightUnit: 'g', + quantity: 1, + category: 'safety', + consumable: false, + worn: false, + description: 'Lightweight reflective emergency shelter', + }, + { + id: 'featured_wdhp_first_aid', + name: 'First Aid Kit', + weight: 200, + weightUnit: 'g', + quantity: 1, + category: 'safety', + consumable: false, + worn: false, + }, + { + id: 'featured_wdhp_map_compass', + name: 'Map & Compass', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'navigation', + consumable: false, + worn: false, + notes: 'GPS can fail in cold; always carry paper map', + }, + { + id: 'featured_wdhp_whistle', + name: 'Emergency Whistle', + weight: 15, + weightUnit: 'g', + quantity: 1, + category: 'safety', + consumable: false, + worn: true, + }, + { + id: 'featured_wdhp_hand_warmers', + name: 'Hand Warmers', + weight: 100, + weightUnit: 'g', + quantity: 4, + category: 'protection', + consumable: true, + worn: false, + }, + { + id: 'featured_wdhp_snow_shovel', + name: 'Avalanche Probe & Shovel', + weight: 700, + weightUnit: 'g', + quantity: 1, + category: 'safety', + consumable: false, + worn: false, + description: 'Required in avalanche terrain', + }, + { + id: 'featured_wdhp_sunglasses', + name: 'Glacier Glasses / Goggles', + weight: 80, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + description: 'Snow reflects UV — protect your eyes', + }, + { + id: 'featured_wdhp_sunscreen', + name: 'Sunscreen SPF 50+', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'protection', + consumable: true, + worn: false, + notes: 'Snow reflection increases UV exposure significantly', + }, + ], + }, + + // ── 6. Winter Camping Pack ───────────────────────────────────────────────── + { + id: 'featured_winter_camping_pack', + name: 'Winter Camping Pack', + description: + 'Comprehensive cold-weather camping gear for sub-zero temperatures. Trip duration: 2-4 nights. Environment: Winter wilderness / snow camping. Use: Winter camping (below 0°F / -18°C).', + category: 'winter', + tags: ['winter camping', 'cold weather', 'below freezing', 'snow camping', 'multi-night'], + items: [ + { + id: 'featured_wcamp_tent', + name: '4-Season Tent', + weight: 2500, + weightUnit: 'g', + quantity: 1, + category: 'shelter', + consumable: false, + worn: false, + description: 'Designed to withstand heavy snow load and strong winds', + }, + { + id: 'featured_wcamp_sleeping_bag', + name: 'Sleeping Bag (-20°F / -29°C rated)', + weight: 2000, + weightUnit: 'g', + quantity: 1, + category: 'sleep system', + consumable: false, + worn: false, + description: 'Expedition-rated down sleeping bag', + }, + { + id: 'featured_wcamp_sleeping_pad', + name: 'Sleeping Pad (R-value 5+)', + weight: 600, + weightUnit: 'g', + quantity: 1, + category: 'sleep system', + consumable: false, + worn: false, + description: 'Closed-cell foam pad for insulation from frozen ground', + }, + { + id: 'featured_wcamp_backpack', + name: 'Backpack 70-80L', + weight: 1800, + weightUnit: 'g', + quantity: 1, + category: 'pack', + consumable: false, + worn: false, + description: 'Large capacity for extra winter gear', + }, + { + id: 'featured_wcamp_insulated_jacket', + name: 'Expedition Down Jacket', + weight: 800, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wcamp_down_vest', + name: 'Down Vest', + weight: 300, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_wcamp_base_layer_top', + name: 'Heavyweight Base Layer Top', + weight: 250, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wcamp_base_layer_bottom', + name: 'Heavyweight Base Layer Bottom', + weight: 250, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wcamp_mid_layer', + name: 'Heavy Fleece Mid Layer', + weight: 500, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wcamp_hardshell_jacket', + name: 'Hardshell Jacket', + weight: 600, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wcamp_hardshell_pants', + name: 'Hardshell Pants', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wcamp_wool_socks', + name: 'Expedition Wool Socks', + weight: 300, + weightUnit: 'g', + quantity: 3, + category: 'clothing', + consumable: false, + worn: false, + }, + { + id: 'featured_wcamp_insulated_gloves', + name: 'Expedition Insulated Gloves', + weight: 250, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wcamp_liner_gloves', + name: 'Liner Gloves', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wcamp_balaclava', + name: 'Balaclava', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wcamp_insulated_hat', + name: 'Expedition Insulated Hat', + weight: 120, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wcamp_gaiters', + name: 'Gaiters (tall)', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + }, + { + id: 'featured_wcamp_crampons', + name: 'Crampons', + weight: 900, + weightUnit: 'g', + quantity: 1, + category: 'gear', + consumable: false, + worn: false, + description: '12-point crampons for steep icy terrain', + }, + { + id: 'featured_wcamp_ice_axe', + name: 'Ice Axe', + weight: 600, + weightUnit: 'g', + quantity: 1, + category: 'gear', + consumable: false, + worn: false, + }, + { + id: 'featured_wcamp_snow_shovel', + name: 'Avalanche Shovel', + weight: 750, + weightUnit: 'g', + quantity: 1, + category: 'safety', + consumable: false, + worn: false, + description: 'For digging out tents and avalanche rescue', + }, + { + id: 'featured_wcamp_trekking_poles', + name: 'Trekking Poles', + weight: 400, + weightUnit: 'g', + quantity: 1, + category: 'gear', + consumable: false, + worn: false, + }, + { + id: 'featured_wcamp_stove', + name: 'Backpacking Stove (cold-rated)', + weight: 110, + weightUnit: 'g', + quantity: 1, + category: 'cooking', + consumable: false, + worn: false, + description: 'Liquid fuel or inverted canister for cold-weather use', + }, + { + id: 'featured_wcamp_fuel', + name: 'Fuel Canisters (100g each)', + weight: 690, + weightUnit: 'g', + quantity: 3, + category: 'cooking', + consumable: true, + worn: false, + notes: 'Stoves use more fuel in cold — bring extra', + }, + { + id: 'featured_wcamp_cook_pot', + name: 'Cook Pot (1.5L)', + weight: 250, + weightUnit: 'g', + quantity: 1, + category: 'cooking', + consumable: false, + worn: false, + }, + { + id: 'featured_wcamp_thermos', + name: 'Insulated Thermos', + weight: 300, + weightUnit: 'g', + quantity: 1, + category: 'hydration', + consumable: false, + worn: false, + description: 'Keep hot liquids warm throughout the day', + }, + { + id: 'featured_wcamp_food', + name: 'High-Calorie Winter Meals (3 days)', + weight: 3000, + weightUnit: 'g', + quantity: 1, + category: 'food', + consumable: true, + worn: false, + description: 'High-fat, high-calorie meals; your body needs extra fuel in the cold', + }, + { + id: 'featured_wcamp_water_filter', + name: 'Water Filter (insulated)', + weight: 88, + weightUnit: 'g', + quantity: 1, + category: 'hydration', + consumable: false, + worn: false, + }, + { + id: 'featured_wcamp_water_bottles', + name: 'Insulated Water Bottles', + weight: 400, + weightUnit: 'g', + quantity: 2, + category: 'hydration', + consumable: false, + worn: false, + description: 'Keep inside sleeping bag at night to prevent freezing', + }, + { + id: 'featured_wcamp_hand_warmers', + name: 'Hand Warmers', + weight: 200, + weightUnit: 'g', + quantity: 8, + category: 'protection', + consumable: true, + worn: false, + }, + { + id: 'featured_wcamp_headlamp', + name: 'Headlamp (lithium batteries)', + weight: 90, + weightUnit: 'g', + quantity: 1, + category: 'lighting', + consumable: false, + worn: false, + notes: 'Lithium batteries perform better in the cold', + }, + { + id: 'featured_wcamp_emergency_bivy', + name: 'Emergency Bivy', + weight: 230, + weightUnit: 'g', + quantity: 1, + category: 'safety', + consumable: false, + worn: false, + }, + { + id: 'featured_wcamp_first_aid', + name: 'Wilderness First Aid Kit', + weight: 250, + weightUnit: 'g', + quantity: 1, + category: 'safety', + consumable: false, + worn: false, + description: 'Include hand warmers, blister kit, and frostbite treatment', + }, + { + id: 'featured_wcamp_map_compass', + name: 'Map & Compass', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'navigation', + consumable: false, + worn: false, + }, + { + id: 'featured_wcamp_satellite', + name: 'Satellite Communicator', + weight: 160, + weightUnit: 'g', + quantity: 1, + category: 'safety', + consumable: false, + worn: false, + description: 'Essential for remote winter trips — SOS capability', + }, + { + id: 'featured_wcamp_power_bank', + name: 'Power Bank (kept warm)', + weight: 180, + weightUnit: 'g', + quantity: 1, + category: 'electronics', + consumable: false, + worn: false, + notes: 'Store close to body to preserve battery in cold', + }, + { + id: 'featured_wcamp_sunglasses', + name: 'Glacier Glasses / Goggles', + weight: 100, + weightUnit: 'g', + quantity: 1, + category: 'clothing', + consumable: false, + worn: true, + description: 'Essential to prevent snow blindness', + }, + ], + }, +]; + +// ─── Seed Function ────────────────────────────────────────────────────────── + +async function seed() { + const dbUrl = process.env.NEON_DATABASE_URL; + if (!dbUrl) { + throw new Error('NEON_DATABASE_URL environment variable is not set'); + } + + // Get optional admin user ID from CLI args + const argUserIdRaw = process.argv[2] ? Number.parseInt(process.argv[2], 10) : undefined; + if (argUserIdRaw !== undefined && Number.isNaN(argUserIdRaw)) { + throw new Error( + 'Invalid user ID provided. Please provide a valid numeric user ID, e.g.: bun run seed.ts 1', + ); + } + const argUserId = argUserIdRaw; + + let db: ReturnType | ReturnType; + let pgClient: Client | undefined; + + if (isStandardPostgresUrl(dbUrl)) { + console.log('Connecting using node-postgres...'); + pgClient = new Client({ connectionString: dbUrl }); + await pgClient.connect(); + db = drizzlePg(pgClient, { schema }); + } else { + console.log('Connecting using Neon serverless...'); + const sql = neon(dbUrl); + db = drizzle(sql, { schema }); + } + + try { + // Use a typed reference to avoid repeating the type cast + const seedDb = db as ReturnType; + + // Resolve admin user ID + let adminUserId: number; + if (argUserId) { + adminUserId = argUserId; + console.log(`Using provided user ID: ${adminUserId}`); + } else { + console.log('No user ID provided, looking up first ADMIN user...'); + const adminUser = await seedDb.query.users.findFirst({ + where: eq(schema.users.role, 'ADMIN'), + orderBy: (users, { asc }) => [asc(users.id)], + }); + if (!adminUser) { + throw new Error( + 'No ADMIN user found in the database. Please provide a user ID as argument: bun run seed.ts ', + ); + } + adminUserId = adminUser.id; + console.log(`Found ADMIN user: ${adminUser.email} (ID: ${adminUserId})`); + } + + const now = new Date(); + + let insertedTemplates = 0; + let skippedTemplates = 0; + let insertedItems = 0; + + for (const templateDef of FEATURED_TEMPLATES) { + // Check if this featured template already exists (idempotent seed) + const existing = await seedDb.query.packTemplates.findFirst({ + where: and( + eq(schema.packTemplates.id, templateDef.id), + eq(schema.packTemplates.deleted, false), + ), + }); + + if (existing) { + console.log(` ↳ Skipping "${templateDef.name}" (already exists)`); + skippedTemplates++; + continue; + } + + // Insert template + await seedDb.insert(schema.packTemplates).values({ + id: templateDef.id, + name: templateDef.name, + description: templateDef.description, + category: templateDef.category, + userId: adminUserId, + tags: templateDef.tags, + isAppTemplate: true, + deleted: false, + localCreatedAt: now, + localUpdatedAt: now, + }); + + console.log(` āœ“ Inserted template: "${templateDef.name}"`); + + // Insert items + for (const item of templateDef.items) { + await seedDb.insert(schema.packTemplateItems).values({ + id: item.id, + name: item.name, + description: item.description, + weight: item.weight, + weightUnit: item.weightUnit, + quantity: item.quantity, + category: item.category, + consumable: item.consumable, + worn: item.worn, + notes: item.notes, + packTemplateId: templateDef.id, + userId: adminUserId, + deleted: false, + }); + insertedItems++; + } + + console.log(` ↳ Inserted ${templateDef.items.length} items`); + insertedTemplates++; + } + + console.log('\nāœ… Seed complete!'); + console.log( + ` Templates: ${insertedTemplates} inserted, ${skippedTemplates} skipped (already exist)`, + ); + console.log(` Items: ${insertedItems} inserted`); + } finally { + if (pgClient) { + await pgClient.end(); + } + } +} + +seed() + .then(() => process.exit(0)) + .catch((err) => { + console.error('āŒ Seed failed:', err); + process.exit(1); + }); From 0007888deb0be8d1546c63a45c53546f5eb12ffc Mon Sep 17 00:00:00 2001 From: Andrew Bierman Date: Fri, 10 Apr 2026 22:47:22 -0600 Subject: [PATCH 3/3] fix(api/seed): type db as NodePgDatabase | NeonHttpDatabase union The previous discriminated union erased the schema generic, so every seedDb.query.* access resolved to {}. The workaround was a lossy 'as ReturnType' cast. Import NodePgDatabase and NeonHttpDatabase generics, parameterize both with typeof schema, and drop the cast. Both query callers (users.findFirst, packTemplates.findFirst) type correctly. --- packages/api/src/db/seed.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/api/src/db/seed.ts b/packages/api/src/db/seed.ts index 2a64f3879a..aa0c5e3f6b 100644 --- a/packages/api/src/db/seed.ts +++ b/packages/api/src/db/seed.ts @@ -18,8 +18,8 @@ import { neon, neonConfig } from '@neondatabase/serverless'; import { and, eq } from 'drizzle-orm'; -import { drizzle } from 'drizzle-orm/neon-http'; -import { drizzle as drizzlePg } from 'drizzle-orm/node-postgres'; +import { drizzle, type NeonHttpDatabase } from 'drizzle-orm/neon-http'; +import { drizzle as drizzlePg, type NodePgDatabase } from 'drizzle-orm/node-postgres'; import { Client } from 'pg'; import WebSocket from 'ws'; import * as schema from './schema'; @@ -1841,7 +1841,9 @@ async function seed() { } const argUserId = argUserIdRaw; - let db: ReturnType | ReturnType; + type SeedDatabase = NodePgDatabase | NeonHttpDatabase; + + let db: SeedDatabase; let pgClient: Client | undefined; if (isStandardPostgresUrl(dbUrl)) { @@ -1856,8 +1858,7 @@ async function seed() { } try { - // Use a typed reference to avoid repeating the type cast - const seedDb = db as ReturnType; + const seedDb = db; // Resolve admin user ID let adminUserId: number;