Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions apps/expo/features/pack-templates/components/FeaturedPacksSection.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Pressable
className="mr-4 w-64 overflow-hidden rounded-xl bg-card shadow-sm"
onPress={() => onPress(template)}
style={({ pressed }) => ({ opacity: pressed ? 0.85 : 1 })}
>
{template.image ? (
<Image source={{ uri: template.image }} className="h-32 w-full" resizeMode="cover" />
) : (
<View className="h-32 w-full items-center justify-center bg-primary/10">
<Text className="text-4xl">🎒</Text>
</View>
)}

<View className="p-3">
<Text className="mb-1 text-sm font-semibold text-foreground" numberOfLines={1}>
{template.name}
</Text>

{template.category && (
<Text className="mb-2 text-xs capitalize text-muted-foreground">{template.category}</Text>
)}

{template.tags && isArray(template.tags) && template.tags.length > 0 && (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
className="mb-2"
scrollEnabled={false}
>
<View className="flex-row flex-wrap gap-1">
{template.tags.slice(0, 3).map((tag) => (
<View key={tag} className="rounded-full bg-primary/10 px-2 py-0.5">
<Text className="text-xs text-primary">{tag}</Text>
</View>
))}
</View>
</ScrollView>
)}

<View className="flex-row items-center justify-between">
<View className="flex-row gap-1">
<WeightBadge weight={template.baseWeight ?? 0} unit="g" type="base" />
</View>
<Text className="text-xs text-muted-foreground">
{template.items && isArray(template.items)
? `${template.items.length} ${t('packTemplates.items')}`
: `0 ${t('packTemplates.items')}`}
</Text>
Comment on lines +66 to +70
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The item count label always uses the plural translation (t('packTemplates.items')), which can render grammatically incorrect strings like "1 items". Match the pluralization logic used in PackTemplateCard by choosing item vs items based on template.items.length.

Copilot uses AI. Check for mistakes.
</View>
</View>
</Pressable>
);
}

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 (
<View className="mb-2">
<View className="flex-row items-center justify-between px-4 py-2">
<Text className="text-base font-semibold text-foreground">
{t('packTemplates.featuredPacks')}
</Text>
<Pressable onPress={() => router.push('/pack-templates')}>
<Text className="text-sm text-primary">{t('packTemplates.viewAll')}</Text>
</Pressable>
Comment on lines +96 to +98
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"View All" currently does router.push('/pack-templates'), which navigates to the same screen the section is rendered on (and may push a duplicate route onto the stack). If the intent is to show all featured templates, this should instead switch the segmented control to the Featured tab (index 1) and reset filters/search, or accept an onViewAll callback from the parent to perform that state change.

Copilot uses AI. Check for mistakes.
</View>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingLeft: 16, paddingRight: 8 }}
className="pb-2"
>
{featuredTemplates.map((template) => (
<FeaturedPackCard key={template.id} templateId={template.id} onPress={onTemplatePress} />
))}
</ScrollView>
</View>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -167,13 +168,16 @@ export function PackTemplateListScreen() {
stickyHeaderHiddenOnScroll
ListHeaderComponent={
selectedTemplateTypeIndex === 0 ? (
<View className="bg-background px-4 pb-2">
<Text className="text-muted-foreground">
{filteredTemplates.length}{' '}
{filteredTemplates.length === 1
? t('packTemplates.template')
: t('packTemplates.templates')}
</Text>
<View className="bg-background">
<FeaturedPacksSection onTemplatePress={handleTemplatePress} />
<View className="px-4 pb-2">
<Text className="text-muted-foreground">
{filteredTemplates.length}{' '}
{filteredTemplates.length === 1
? t('packTemplates.template')
: t('packTemplates.templates')}
</Text>
</View>
</View>
) : undefined
}
Expand Down
13 changes: 12 additions & 1 deletion apps/expo/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading
Loading