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;