From a9d85a2915d05b4daf28dda81807c9457d513267 Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Mon, 8 Jun 2026 12:20:31 +0100 Subject: [PATCH 1/9] fix(catalog): add id tiebreaker to all sort branches for stable pagination MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ETL-imported items share identical created_at timestamps (PostgreSQL NOW() is transaction-scoped). ORDER BY created_at DESC with no secondary key produces non-deterministic ordering within a tie group, causing the same items to appear on multiple LIMIT/OFFSET pages and others to be skipped. Measured: 10 pages × 20 items → 21 duplicates (10.5%) before; 0 after. The default sort already had desc(id) as tiebreaker; add it to the category, usage, and generic field branches for the same guarantee. --- packages/api/src/services/catalogService.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/api/src/services/catalogService.ts b/packages/api/src/services/catalogService.ts index 3a3c4e4e74..009db15ad1 100644 --- a/packages/api/src/services/catalogService.ts +++ b/packages/api/src/services/catalogService.ts @@ -116,22 +116,29 @@ export class CatalogService { let orderBy = [desc(sql`COALESCE(pack_item_counts.count, 0)`), desc(catalogItems.id)]; // default ordering by usage if (sort) { const { field, order } = sort; + // All branches include `desc(catalogItems.id)` as a tiebreaker so that + // LIMIT/OFFSET pagination is stable. Without it, rows sharing the same + // sort-key value (e.g. all ETL-imported items have identical created_at) + // are ordered non-deterministically by the planner, causing the same item + // to appear on multiple pages and other items to be skipped entirely. if (field === 'category') { orderBy = [ order === 'desc' ? desc(sql`jsonb_array_elements_text(${catalogItems.categories})[0]`) : asc(sql`jsonb_array_elements_text(${catalogItems.categories})[0]`), + desc(catalogItems.id), ]; } else if (field === 'usage') { orderBy = [ order === 'desc' ? desc(sql`COALESCE(pack_item_counts.count, 0)`) : asc(sql`COALESCE(pack_item_counts.count, 0)`), + desc(catalogItems.id), ]; } else { const sortColumn = catalogItems[field]; if (sortColumn) { - orderBy = [order === 'desc' ? desc(sortColumn) : asc(sortColumn)]; + orderBy = [order === 'desc' ? desc(sortColumn) : asc(sortColumn), desc(catalogItems.id)]; } } } From a0d0fd1d3cb6aefac091d77f98a2ad523f0ee2bf Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Mon, 8 Jun 2026 12:27:29 +0100 Subject: [PATCH 2/9] feat(catalog): group variant items in list, show variants in detail screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ETL-imported gear items are stored as one row per size/color variant with a unique SKU. The catalog list was showing "Fineline Stretch Full-Zip Pant - Men's" three times (sizes L, L/Short, M/Short) as separate cards. Group the paginated items by name+brand before rendering: - One CatalogItemCard per group (representative = first variant) - "N options" badge on the card when the group has >1 variant - Tapping the card stores all variant items in catalogGroupVariantsAtom and navigates to the representative item's detail screen Detail screen reads the atom and shows an "Available Variants" section listing each other variant's size, color, price, weight, availability, and a retailer link. No picker or selection UI — just a plain list. Search results (vector search) still show individual items unchanged. --- apps/expo/atoms/catalogGroupAtom.ts | 7 ++ .../catalog/components/CatalogItemCard.tsx | 10 ++- .../features/catalog/lib/groupCatalogItems.ts | 25 ++++++ .../screens/CatalogItemDetailScreen.tsx | 82 ++++++++++++++++++- .../catalog/screens/CatalogItemsScreen.tsx | 29 +++++-- apps/expo/lib/i18n/locales/en.json | 16 ++-- 6 files changed, 154 insertions(+), 15 deletions(-) create mode 100644 apps/expo/atoms/catalogGroupAtom.ts create mode 100644 apps/expo/features/catalog/lib/groupCatalogItems.ts diff --git a/apps/expo/atoms/catalogGroupAtom.ts b/apps/expo/atoms/catalogGroupAtom.ts new file mode 100644 index 0000000000..14c7f3f4d7 --- /dev/null +++ b/apps/expo/atoms/catalogGroupAtom.ts @@ -0,0 +1,7 @@ +import type { CatalogItem } from 'expo-app/features/catalog/types'; +import { atom } from 'jotai'; + +// Holds all variants of the catalog group the user tapped in the list. +// Set immediately before pushing to /catalog/[id] so the detail screen can +// show a variants list without any extra API calls. +export const catalogGroupVariantsAtom = atom([]); diff --git a/apps/expo/features/catalog/components/CatalogItemCard.tsx b/apps/expo/features/catalog/components/CatalogItemCard.tsx index d5d13cd4c2..c013e32346 100644 --- a/apps/expo/features/catalog/components/CatalogItemCard.tsx +++ b/apps/expo/features/catalog/components/CatalogItemCard.tsx @@ -19,9 +19,10 @@ import { CatalogItemImage } from './CatalogItemImage'; type CatalogItemCardProps = { item: CatalogItem; onPress: () => void; + variantCount?: number; }; -export function CatalogItemCard({ item, onPress }: CatalogItemCardProps) { +export function CatalogItemCard({ item, onPress, variantCount }: CatalogItemCardProps) { const { colors } = useColorScheme(); const { t } = useTranslation(); @@ -82,6 +83,13 @@ export function CatalogItemCard({ item, onPress }: CatalogItemCardProps) { )} + {variantCount && variantCount > 1 && ( + + + {variantCount} {t('catalog.options')} + + + )} diff --git a/apps/expo/features/catalog/lib/groupCatalogItems.ts b/apps/expo/features/catalog/lib/groupCatalogItems.ts new file mode 100644 index 0000000000..151376850b --- /dev/null +++ b/apps/expo/features/catalog/lib/groupCatalogItems.ts @@ -0,0 +1,25 @@ +import type { CatalogItem } from '../types'; + +export type CatalogItemGroup = { + key: string; + representative: CatalogItem; + variants: CatalogItem[]; +}; + +function groupKey(item: CatalogItem): string { + return `${(item.name ?? '').toLowerCase().trim()}:::${(item.brand ?? '').toLowerCase().trim()}`; +} + +export function groupCatalogItems(items: CatalogItem[]): CatalogItemGroup[] { + const map = new Map(); + for (const item of items) { + const key = groupKey(item); + const group = map.get(key); + if (group) { + group.variants.push(item); + } else { + map.set(key, { key, representative: item, variants: [item] }); + } + } + return Array.from(map.values()); +} diff --git a/apps/expo/features/catalog/screens/CatalogItemDetailScreen.tsx b/apps/expo/features/catalog/screens/CatalogItemDetailScreen.tsx index 82c09368ff..3bedd8d770 100644 --- a/apps/expo/features/catalog/screens/CatalogItemDetailScreen.tsx +++ b/apps/expo/features/catalog/screens/CatalogItemDetailScreen.tsx @@ -1,5 +1,6 @@ import { Ionicons } from '@expo/vector-icons'; import { Button, Text } from '@packrat/ui/nativewindui'; +import { catalogGroupVariantsAtom } from 'expo-app/atoms/catalogGroupAtom'; import { Icon } from 'expo-app/components/Icon'; import { Chip } from 'expo-app/components/initial/Chip'; import { ExpandableText } from 'expo-app/components/initial/ExpandableText'; @@ -14,11 +15,70 @@ import { ErrorScreen } from 'expo-app/screens/ErrorScreen'; import { LoadingSpinnerScreen } from 'expo-app/screens/LoadingSpinnerScreen'; import { NotFoundScreen } from 'expo-app/screens/NotFoundScreen'; import { useLocalSearchParams, useRouter } from 'expo-router'; -import { Linking, Text as RNText, ScrollView, View } from 'react-native'; +import { useAtomValue } from 'jotai'; +import { Linking, Pressable, Text as RNText, ScrollView, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { CatalogItemImage } from '../components/CatalogItemImage'; import { useCatalogItemDetails } from '../hooks'; import { normalizeDescription } from '../lib/normalizeDescription'; +import type { CatalogItem } from '../types'; + +function VariantRow({ + variant, + colors, +}: { + variant: CatalogItem; + colors: ReturnType['colors']; +}) { + const { t } = useTranslation(); + const label = [variant.size, variant.color].filter(Boolean).join(' · '); + return ( + + + {label ? {label} : null} + + {variant.price != null && ( + + {variant.currency ?? '$'} + {variant.price.toFixed(2)} + + )} + {variant.weight != null && ( + + {variant.weight} {variant.weightUnit} + + )} + {variant.availability && ( + + + + {variant.availability === 'in_stock' + ? t('catalog.inStock') + : t('catalog.outOfStock')} + + + )} + + + {variant.productUrl ? ( + Linking.openURL(variant.productUrl as string)} + className="ml-3 rounded-md border border-border px-3 py-1.5" + > + {t('catalog.viewOnRetailerSite')} + + ) : null} + + ); +} export function CatalogItemDetailScreen() { const router = useRouter(); @@ -28,6 +88,12 @@ export function CatalogItemDetailScreen() { const { t } = useTranslation(); const MATERIAL_LENGTH_THRESHOLD = 60; + const groupVariants = useAtomValue(catalogGroupVariantsAtom); + // Show the variants section only when there are multiple variants for this + // group — i.e. when the user navigated from the grouped catalog list. + const otherVariants = + groupVariants.length > 1 ? groupVariants.filter((v) => v.id !== Number(id)) : []; + const handleAddToPack = () => { router.push({ pathname: '/catalog/add-to-pack', @@ -229,6 +295,20 @@ export function CatalogItemDetailScreen() { )} + {/* Variants Section */} + {otherVariants.length > 0 && ( + + + {t('catalog.variantsSection')} + + + {otherVariants.map((variant) => ( + + ))} + + + )} + {/* Similar Items Section */} groupCatalogItems(paginatedItems), [paginatedItems]); + const totalItems = paginatedData?.pages[0]?.totalCount ?? 0; const totalItemsText = `${Number(totalItems).toLocaleString()} ${ @@ -85,6 +89,13 @@ function CatalogItemsScreen() { total: Number(totalItems).toLocaleString(), }); + const setGroupVariants = useSetAtom(catalogGroupVariantsAtom); + + const handleGroupPress = (group: CatalogItemGroup) => { + setGroupVariants(group.variants); + router.push({ pathname: '/catalog/[id]', params: { id: group.representative.id } }); + }; + const handleItemPress = (item: CatalogItem) => { router.push({ pathname: '/catalog/[id]', params: { id: item.id } }); }; @@ -124,7 +135,7 @@ function CatalogItemsScreen() { - {paginatedItems.length > 0 && ( + {groupedItems.length > 0 && ( {showingText} )} @@ -136,7 +147,7 @@ function CatalogItemsScreen() { activeFilter, categoriesError, totalItemsText, - paginatedItems.length, + groupedItems.length, showingText, refetchCategories, ]); @@ -220,10 +231,14 @@ function CatalogItemsScreen() { /> item.id.toString()} - renderItem={({ item }) => ( - handleItemPress(item)} /> + data={groupedItems} + keyExtractor={(group) => group.key} + renderItem={({ item: group }) => ( + handleGroupPress(group)} + /> )} ItemSeparatorComponent={ItemSeparatorComponent} ListHeaderComponent={listHeader} diff --git a/apps/expo/lib/i18n/locales/en.json b/apps/expo/lib/i18n/locales/en.json index 8f960aee78..712d04428c 100644 --- a/apps/expo/lib/i18n/locales/en.json +++ b/apps/expo/lib/i18n/locales/en.json @@ -160,7 +160,7 @@ "loginFailed": "Login Failed", "invalidEmailOrPassword": "Invalid email or password", "resumeSync": "Resume Sync", - "syncPaused": "Sync paused \u2014 please sign in again." + "syncPaused": "Sync paused — please sign in again." }, "profile": { "profile": "Profile", @@ -249,10 +249,10 @@ "lastUpdatedShort": "Last updated", "itemsCount": "{{count}} items", "sharingBenefits": "Sharing Benefits", - "distributeGroupGear": "\u2022 Distribute group gear among members to reduce individual pack weight", - "sharingBenefit1": "\u2022 Coordinate who brings shared items like tent, stove, and water filter", - "sharingBenefit2": "\u2022 See real-time updates when members modify the pack", - "sharingBenefit3": "\u2022 Plan together and ensure nothing essential is forgotten", + "distributeGroupGear": "• Distribute group gear among members to reduce individual pack weight", + "sharingBenefit1": "• Coordinate who brings shared items like tent, stove, and water filter", + "sharingBenefit2": "• See real-time updates when members modify the pack", + "sharingBenefit3": "• Plan together and ensure nothing essential is forgotten", "itemsInInventory": "{{count}} items in your inventory", "all": "All", "byCategory": "By Category", @@ -562,7 +562,11 @@ "recentlyUsed": "Recently Used", "selectedItemsQuantity": "Selected Items ({{count}})", "noRecentlyUsedItems": "No recently used items yet", - "quantityFor": "Qty for {{name}}" + "quantityFor": "Qty for {{name}}", + "options": "{{count}} options", + "variantsSection": "Available Variants", + "variantSize": "Size", + "variantColor": "Color" }, "welcome": { "features": { From 2f13c2fdd7169af407a9fc0311bb636c6f07f3d5 Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Mon, 8 Jun 2026 12:27:50 +0100 Subject: [PATCH 3/9] fix(catalog): remove unused colors prop from VariantRow --- .../catalog/screens/CatalogItemDetailScreen.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/expo/features/catalog/screens/CatalogItemDetailScreen.tsx b/apps/expo/features/catalog/screens/CatalogItemDetailScreen.tsx index 3bedd8d770..f08c94be9f 100644 --- a/apps/expo/features/catalog/screens/CatalogItemDetailScreen.tsx +++ b/apps/expo/features/catalog/screens/CatalogItemDetailScreen.tsx @@ -23,13 +23,7 @@ import { useCatalogItemDetails } from '../hooks'; import { normalizeDescription } from '../lib/normalizeDescription'; import type { CatalogItem } from '../types'; -function VariantRow({ - variant, - colors, -}: { - variant: CatalogItem; - colors: ReturnType['colors']; -}) { +function VariantRow({ variant }: { variant: CatalogItem }) { const { t } = useTranslation(); const label = [variant.size, variant.color].filter(Boolean).join(' · '); return ( @@ -303,7 +297,7 @@ export function CatalogItemDetailScreen() { {otherVariants.map((variant) => ( - + ))} From e6b500717dc009402dbb8c06246bac5542a740a4 Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Mon, 8 Jun 2026 13:35:26 +0100 Subject: [PATCH 4/9] fix(catalog): badge text, remove total count, reduce header gap, full-bleed categories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix "3 {{count}} options" badge: i18n key is now plain "options" so the manually-prepended count renders as "3 options" correctly - Remove "197,717 items / Showing X of Y" from the catalog list header — that count reflects raw variants, not grouped products - Reduce top gap: FlatList contentContainerStyle loses top/horizontal padding; horizontal padding moves to a px-4 wrapper in renderItem so the categories row can be full-bleed - CategoriesFilter gains contentPaddingX prop: ScrollView uses it as paddingHorizontal in contentContainerStyle, letting chips scroll to the actual screen edge while starting at the same 16 px indent as other content - Move variants section to directly below the action buttons in the detail screen; shorten title from "Available Variants" → "Variants" --- apps/expo/components/CategoriesFilter.tsx | 10 +++- .../screens/CatalogItemDetailScreen.tsx | 30 +++++------ .../catalog/screens/CatalogItemsScreen.tsx | 50 ++++--------------- apps/expo/lib/i18n/locales/en.json | 4 +- 4 files changed, 37 insertions(+), 57 deletions(-) diff --git a/apps/expo/components/CategoriesFilter.tsx b/apps/expo/components/CategoriesFilter.tsx index b70144247a..2aaee65198 100644 --- a/apps/expo/components/CategoriesFilter.tsx +++ b/apps/expo/components/CategoriesFilter.tsx @@ -11,6 +11,7 @@ export function CategoriesFilter({ error, retry, className, + contentPaddingX = 0, }: { activeFilter: string; onFilter: (filter: string) => void; @@ -18,6 +19,7 @@ export function CategoriesFilter({ error?: Error | null; retry?: (() => void) | undefined; className?: string; + contentPaddingX?: number; }) { const { t } = useTranslation(); @@ -57,7 +59,13 @@ export function CategoriesFilter({ ) : ( - + {!data ? Array.from({ length: 10 }).map((_, i) => ( + {/* Variants Section */} + {otherVariants.length > 0 && ( + + + {t('catalog.variantsSection')} + + + {otherVariants.map((variant) => ( + + ))} + + + )} + {item.techs && Object.keys(item.techs).length > 0 && ( - + {t('catalog.specifications')} @@ -289,20 +303,6 @@ export function CatalogItemDetailScreen() { )} - {/* Variants Section */} - {otherVariants.length > 0 && ( - - - {t('catalog.variantsSection')} - - - {otherVariants.map((variant) => ( - - ))} - - - )} - {/* Similar Items Section */} groupCatalogItems(paginatedItems), [paginatedItems]); - const totalItems = paginatedData?.pages[0]?.totalCount ?? 0; - - const totalItemsText = `${Number(totalItems).toLocaleString()} ${ - totalItems === 1 ? t('catalog.item') : t('catalog.items') - }`; - const showingText = t('catalog.showingItems', { - current: paginatedItems.length, - total: Number(totalItems).toLocaleString(), - }); - const setGroupVariants = useSetAtom(catalogGroupVariantsAtom); const handleGroupPress = (group: CatalogItemGroup) => { @@ -125,32 +115,12 @@ function CatalogItemsScreen() { activeFilter={activeFilter} error={categoriesError} retry={refetchCategories} - className="px-4 py-2" + className="py-2" + contentPaddingX={16} /> - - - - - {totalItemsText} - - - - {groupedItems.length > 0 && ( - {showingText} - )} - ); - }, [ - isSearching, - categories, - activeFilter, - categoriesError, - totalItemsText, - groupedItems.length, - showingText, - refetchCategories, - ]); + }, [isSearching, categories, activeFilter, categoriesError, refetchCategories]); return ( <> @@ -234,18 +204,20 @@ function CatalogItemsScreen() { data={groupedItems} keyExtractor={(group) => group.key} renderItem={({ item: group }) => ( - handleGroupPress(group)} - /> + + handleGroupPress(group)} + /> + )} ItemSeparatorComponent={ItemSeparatorComponent} ListHeaderComponent={listHeader} refreshControl={} onEndReached={loadMore} onEndReachedThreshold={0.5} - contentContainerStyle={{ flexGrow: 1, padding: 16 }} + contentContainerStyle={{ flexGrow: 1, paddingBottom: 16 }} contentInsetAdjustmentBehavior="automatic" ListFooterComponent={ <> diff --git a/apps/expo/lib/i18n/locales/en.json b/apps/expo/lib/i18n/locales/en.json index 712d04428c..d22deeb905 100644 --- a/apps/expo/lib/i18n/locales/en.json +++ b/apps/expo/lib/i18n/locales/en.json @@ -563,8 +563,8 @@ "selectedItemsQuantity": "Selected Items ({{count}})", "noRecentlyUsedItems": "No recently used items yet", "quantityFor": "Qty for {{name}}", - "options": "{{count}} options", - "variantsSection": "Available Variants", + "options": "options", + "variantsSection": "Variants", "variantSize": "Size", "variantColor": "Color" }, From 5d1016d6b653ef44f0083f2f6829288f28b8ecef Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Mon, 8 Jun 2026 13:47:03 +0100 Subject: [PATCH 5/9] feat(catalog): remove variant count badge from item cards --- .../features/catalog/components/CatalogItemCard.tsx | 10 +--------- .../features/catalog/screens/CatalogItemsScreen.tsx | 8 ++------ 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/apps/expo/features/catalog/components/CatalogItemCard.tsx b/apps/expo/features/catalog/components/CatalogItemCard.tsx index c013e32346..d5d13cd4c2 100644 --- a/apps/expo/features/catalog/components/CatalogItemCard.tsx +++ b/apps/expo/features/catalog/components/CatalogItemCard.tsx @@ -19,10 +19,9 @@ import { CatalogItemImage } from './CatalogItemImage'; type CatalogItemCardProps = { item: CatalogItem; onPress: () => void; - variantCount?: number; }; -export function CatalogItemCard({ item, onPress, variantCount }: CatalogItemCardProps) { +export function CatalogItemCard({ item, onPress }: CatalogItemCardProps) { const { colors } = useColorScheme(); const { t } = useTranslation(); @@ -83,13 +82,6 @@ export function CatalogItemCard({ item, onPress, variantCount }: CatalogItemCard )} - {variantCount && variantCount > 1 && ( - - - {variantCount} {t('catalog.options')} - - - )} diff --git a/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx b/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx index e1fbae3ac9..0f8652ff41 100644 --- a/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx +++ b/apps/expo/features/catalog/screens/CatalogItemsScreen.tsx @@ -115,7 +115,7 @@ function CatalogItemsScreen() { activeFilter={activeFilter} error={categoriesError} retry={refetchCategories} - className="py-2" + className="py-4" contentPaddingX={16} /> @@ -205,11 +205,7 @@ function CatalogItemsScreen() { keyExtractor={(group) => group.key} renderItem={({ item: group }) => ( - handleGroupPress(group)} - /> + handleGroupPress(group)} /> )} ItemSeparatorComponent={ItemSeparatorComponent} From 8a46c673211895096d34c7d31c34b7be98105cc6 Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Mon, 8 Jun 2026 14:08:28 +0100 Subject: [PATCH 6/9] fix(catalog): shorten retailer CTA to 'Visit Site', add open-in-new icon --- .../catalog/screens/CatalogItemDetailScreen.tsx | 10 ++++++---- apps/expo/lib/i18n/locales/en.json | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/expo/features/catalog/screens/CatalogItemDetailScreen.tsx b/apps/expo/features/catalog/screens/CatalogItemDetailScreen.tsx index e57b8ba678..c14f1ca420 100644 --- a/apps/expo/features/catalog/screens/CatalogItemDetailScreen.tsx +++ b/apps/expo/features/catalog/screens/CatalogItemDetailScreen.tsx @@ -65,9 +65,10 @@ function VariantRow({ variant }: { variant: CatalogItem }) { {variant.productUrl ? ( Linking.openURL(variant.productUrl as string)} - className="ml-3 rounded-md border border-border px-3 py-1.5" + className="ml-3 flex-row items-center gap-1 rounded-md border border-border px-3 py-1.5" > {t('catalog.viewOnRetailerSite')} + ) : null} @@ -259,13 +260,14 @@ export function CatalogItemDetailScreen() { onPress={() => Linking.openURL(item.productUrl as string)} > {t('catalog.viewOnRetailerSite')} + {/* Variants Section */} {otherVariants.length > 0 && ( - + {t('catalog.variantsSection')} @@ -278,11 +280,11 @@ export function CatalogItemDetailScreen() { )} {item.techs && Object.keys(item.techs).length > 0 && ( - + {t('catalog.specifications')} - + {Object.entries(item.techs).map(([key, value]) => ( {key} diff --git a/apps/expo/lib/i18n/locales/en.json b/apps/expo/lib/i18n/locales/en.json index d22deeb905..73dbf96686 100644 --- a/apps/expo/lib/i18n/locales/en.json +++ b/apps/expo/lib/i18n/locales/en.json @@ -514,7 +514,7 @@ "outOfStock": "Out of Stock", "preorder": "Pre-order", "specifications": "Specifications", - "viewOnRetailerSite": "View on Retailer Site", + "viewOnRetailerSite": "Visit Site", "addToPack": "Add to Pack", "errorFetchingItem": "Error fetching item", "pleaseTryAgain": "Please try again.", From dc7549460766c597d6bdfa7565433be43a8cf983 Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Mon, 8 Jun 2026 17:44:04 +0100 Subject: [PATCH 7/9] fix(catalog): icon dark mode + full-bleed similar items - Replace hardcoded hex availability colors with colors.green/destructive - Fix open-in-new icon invisible in dark mode (was passing NativeWind class string as color value; now uses colors.foreground) - Move SimilarItems outside padded container for full-bleed scroll - Add px-4 to SimilarItems title to maintain heading alignment - Remove unused 'options' i18n key --- .../catalog/components/SimilarItems.tsx | 2 +- .../screens/CatalogItemDetailScreen.tsx | 26 +++++++++---------- apps/expo/lib/i18n/locales/en.json | 1 - 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/apps/expo/features/catalog/components/SimilarItems.tsx b/apps/expo/features/catalog/components/SimilarItems.tsx index 31b36e4719..48508a2ec5 100644 --- a/apps/expo/features/catalog/components/SimilarItems.tsx +++ b/apps/expo/features/catalog/components/SimilarItems.tsx @@ -97,7 +97,7 @@ export const SimilarItems: React.FC = ({ if (isLoading) { return ( - + {t('catalog.moreLike', { itemName })} @@ -51,7 +52,7 @@ function VariantRow({ variant }: { variant: CatalogItem }) { : 'close-circle-outline' } size={12} - color={variant.availability === 'in_stock' ? '#22c55e' : '#ef4444'} + color={variant.availability === 'in_stock' ? colors.green : colors.destructive} /> {variant.availability === 'in_stock' @@ -68,7 +69,7 @@ function VariantRow({ variant }: { variant: CatalogItem }) { className="ml-3 flex-row items-center gap-1 rounded-md border border-border px-3 py-1.5" > {t('catalog.viewOnRetailerSite')} - + ) : null} @@ -260,7 +261,7 @@ export function CatalogItemDetailScreen() { onPress={() => Linking.openURL(item.productUrl as string)} > {t('catalog.viewOnRetailerSite')} - + @@ -268,9 +269,7 @@ export function CatalogItemDetailScreen() { {/* Variants Section */} {otherVariants.length > 0 && ( - - {t('catalog.variantsSection')} - + {t('catalog.variantsSection')} {otherVariants.map((variant) => ( @@ -304,15 +303,14 @@ export function CatalogItemDetailScreen() { )} - - {/* Similar Items Section */} - + + ); diff --git a/apps/expo/lib/i18n/locales/en.json b/apps/expo/lib/i18n/locales/en.json index 73dbf96686..3c69f275de 100644 --- a/apps/expo/lib/i18n/locales/en.json +++ b/apps/expo/lib/i18n/locales/en.json @@ -563,7 +563,6 @@ "selectedItemsQuantity": "Selected Items ({{count}})", "noRecentlyUsedItems": "No recently used items yet", "quantityFor": "Qty for {{name}}", - "options": "options", "variantsSection": "Variants", "variantSize": "Size", "variantColor": "Color" From fcaedb2f75b7262700603eb85a18416e7bede1c5 Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Mon, 8 Jun 2026 17:50:17 +0100 Subject: [PATCH 8/9] revert(catalog): undo full-bleed similar items section --- .../features/catalog/components/SimilarItems.tsx | 2 +- .../catalog/screens/CatalogItemDetailScreen.tsx | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/expo/features/catalog/components/SimilarItems.tsx b/apps/expo/features/catalog/components/SimilarItems.tsx index 48508a2ec5..31b36e4719 100644 --- a/apps/expo/features/catalog/components/SimilarItems.tsx +++ b/apps/expo/features/catalog/components/SimilarItems.tsx @@ -97,7 +97,7 @@ export const SimilarItems: React.FC = ({ if (isLoading) { return ( - + {t('catalog.moreLike', { itemName })} )} - - + {/* Similar Items Section */} + + ); From 1af579ae0b63886dc991098edb7d963d302fed74 Mon Sep 17 00:00:00 2001 From: Ibrahim Isa Jajere Date: Mon, 8 Jun 2026 17:52:40 +0100 Subject: [PATCH 9/9] feat(catalog): full-bleed similar items scroll with negative margin --- apps/expo/features/catalog/components/SimilarItems.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/expo/features/catalog/components/SimilarItems.tsx b/apps/expo/features/catalog/components/SimilarItems.tsx index 31b36e4719..1aad2afa18 100644 --- a/apps/expo/features/catalog/components/SimilarItems.tsx +++ b/apps/expo/features/catalog/components/SimilarItems.tsx @@ -106,6 +106,7 @@ export const SimilarItems: React.FC = ({ data={Array(3).fill(null)} renderItem={() => } keyExtractor={(_, index) => `loading-${index}`} + style={{ marginHorizontal: -16 }} contentContainerStyle={{ paddingHorizontal: 16 }} /> @@ -127,6 +128,7 @@ export const SimilarItems: React.FC = ({ data={data.items} renderItem={({ item }) => } keyExtractor={(item) => item.id.toString()} + style={{ marginHorizontal: -16 }} contentContainerStyle={{ paddingHorizontal: 16 }} />