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
76 changes: 36 additions & 40 deletions apps/expo/features/catalog/screens/AddCatalogItemDetailsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { CatalogItemImage } from '../components/CatalogItemImage';
import { useCatalogItemDetails } from '../hooks';
import { cacheCatalogItemImage } from '../lib/cacheCatalogItemImage';
import type { CatalogItem } from '../types';
Expand Down Expand Up @@ -140,53 +141,48 @@ export function AddCatalogItemDetailsScreen() {
>
<SafeAreaView className="flex-1">
<ScrollView className="flex-1">
<View className="mb-6 rounded-lg bg-card p-4 shadow-sm">
<Text className="mb-2 text-xl font-semibold text-foreground">{catalogItem.name}</Text>
<Text className="mb-4 text-muted-foreground">{catalogItem.description}</Text>
<View className="flex-row gap-4">
<View className="flex-row items-center">
<Icon name="dumbbell" size={16} color={colors.grey} />
<Text className="ml-1 text-muted-foreground">
{catalogItem.weight} {catalogItem.weightUnit}
<View className="border-b border-border bg-card px-4 py-3">
<View className="flex-row items-center">
<CatalogItemImage
imageUrl={catalogItem.images?.[0]}
className="h-24 w-24 rounded-md"
resizeMode="cover"
/>
<View className="ml-3 flex-1">
<Text className="text-xs uppercase text-muted-foreground">
{t('catalog.adding')}
</Text>
</View>
{catalogItem.brand && (
<View className="flex-row items-center flex-nowrap">
<View className="mx-1 h-1 w-1 rounded-full bg-muted-foreground" />
<Text className="text-xs text-muted-foreground">{catalogItem.brand}</Text>
<Text variant="title3" color="primary">
{catalogItem.name}
</Text>
<View className="mt-1 flex-row items-center">
<Icon name="dumbbell" size={14} color={colors.grey2} />
<Text variant="caption2" className="ml-1">
{catalogItem.weight} {catalogItem.weightUnit}
</Text>
{catalogItem.brand && (
<>
<View className="mx-1 h-1 w-1 rounded-full bg-muted-foreground" />
<Text variant="caption2">{catalogItem.brand}</Text>
</>
)}
</View>
)}
</View>
</View>
</View>

{/* Selected Pack */}
<View className="mt-6 border-b border-border bg-card px-4 py-3">
<View className="flex-row items-center justify-between">
<View>
<Text className="text-sm text-muted-foreground">{t('catalog.selectedPack')}</Text>
<Text className="text-base font-medium text-foreground">{pack.name}</Text>
<View className="mt-1 flex-row items-center">
<Icon name="basket-outline" size={14} color={colors.grey} />
<Text className="ml-1 text-xs text-muted-foreground">
{pack.items.length}{' '}
{pack.items.length === 1 ? t('catalog.item') : t('catalog.items')}
</Text>
<View className="mx-1 h-1 w-1 rounded-full bg-muted-foreground" />
<Text className="text-xs capitalize text-muted-foreground">{pack.category}</Text>
</View>
</View>
<Button
variant="secondary"
onPress={() =>
router.push({
pathname: '/catalog/add-to-pack',
params: { catalogItemId },
})
}
disabled={isAdding}
>
<Text className="font-normal">{t('catalog.change')}</Text>
</Button>
<Text className="text-sm text-muted-foreground">{t('catalog.selectedPack')}</Text>
<Text className="text-base font-medium text-foreground">{pack.name}</Text>
<View className="mt-1 flex-row items-center">
<Icon name="basket-outline" size={14} color={colors.grey} />
<Text className="ml-1 text-xs text-muted-foreground">
{pack.items.length}{' '}
{pack.items.length === 1 ? t('catalog.item') : t('catalog.items')}
</Text>
<View className="mx-1 h-1 w-1 rounded-full bg-muted-foreground" />
<Text className="text-xs capitalize text-muted-foreground">{pack.category}</Text>
</View>
</View>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function CatalogItemDetailScreen() {
}

return (
<SafeAreaView className="flex-1 bg-background">
<SafeAreaView className="flex-1 bg-background" edges={['bottom']}>
<ScrollView>
<CatalogItemImage
imageUrl={item.images?.[0]}
Expand Down
195 changes: 83 additions & 112 deletions apps/expo/features/catalog/screens/PackSelectionScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,11 @@ import { useLocalSearchParams, useRouter } from 'expo-router';
import { useMemo, useState } from 'react';
import { FlatList, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { CatalogItemImage } from '../components/CatalogItemImage';
import { useCatalogItemDetails } from '../hooks';

export function PackSelectionScreen() {
const router = useRouter();
const { catalogItemId } = useLocalSearchParams();
const packs = useDetailedPacks();
const { data: catalogItem } = useCatalogItemDetails(catalogItemId as string);
const [searchQuery, setSearchQuery] = useState('');
const { colors } = useColorScheme();
const { t } = useTranslation();
Expand Down Expand Up @@ -47,122 +44,96 @@ export function PackSelectionScreen() {
router.push('/pack/new');
};

const EmptyState = (
<View className="mx-4 mt-4 items-center justify-center rounded-lg bg-card p-8 shadow-sm">
<Icon name="backpack" size={48} color="text-muted-foreground" />
<Text variant="title3" color="primary" className="mt-4 text-center">
{t('catalog.noPacksAvailable')}
</Text>
<Text variant="body" className="mb-4 text-center">
{t('catalog.createPackMessage')}
</Text>
<Button onPress={handleCreatePack}>
<Icon name="plus" size={18} color="text-primary-foreground" />
<Text variant="body" color="primary">
{t('catalog.createPack')}
</Text>
</Button>
</View>
);

return (
<SafeAreaView className="flex-1 bg-background">
{catalogItem && (
<View className="border-b border-border bg-card px-4 py-3">
<View className="flex-row items-center">
<CatalogItemImage
imageUrl={catalogItem.images?.[0]}
className="h-24 w-24 rounded-md"
resizeMode="cover"
/>
<View className="ml-3 flex-1">
<Text className="text-xs text-muted-foreground uppercase">{t('catalog.adding')}</Text>
<Text variant="title3" color="primary">
{catalogItem.name}
<SafeAreaView className="flex-1 bg-background" edges={['bottom']}>
{/* Fixed: search + count label always visible */}
<View className="border-b border-border bg-background px-4 pb-2 pt-3">
<SearchInput
textContentType="none"
autoComplete="off"
value={searchQuery}
onChangeText={setSearchQuery}
/>
{filteredPacks.length > 0 && (
<Text variant="subhead" color="primary" className="mt-2">
{t('catalog.selectPack', { count: filteredPacks.length })}
</Text>
)}
</View>

<FlatList
data={filteredPacks}
keyExtractor={(item) => item.id}
ListEmptyComponent={
searchQuery.trim() !== '' ? (
<View className="items-center justify-center px-4 py-8">
<Text variant="body" className="text-center">
{t('catalog.noPacksFound')}
</Text>
<View className="mt-1 flex-row items-center">
<Icon name="dumbbell" size={14} color={colors.grey2} />
<Text variant="caption2" className="ml-1">
{catalogItem.weight} {catalogItem.weightUnit}
</Text>
{catalogItem.brand && (
<>
<View className="mx-1 h-1 w-1 rounded-full bg-muted-foreground" />
<Text variant="caption2">{catalogItem.brand}</Text>
</>
)}
</View>
</View>
</View>
</View>
)}

<View className="p-4">
<View className="mb-4">
<SearchInput
textContentType="none"
autoComplete="off"
value={searchQuery}
onChangeText={setSearchQuery}
/>
</View>

{filteredPacks && filteredPacks.length > 0 ? (
<>
<Text variant="subhead" color="primary" className="mb-2">
{t('catalog.selectPack', { count: filteredPacks.length })}
</Text>
<FlatList
data={filteredPacks}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TouchableOpacity
className="mb-3 overflow-hidden rounded-lg bg-card shadow-sm"
onPress={() => handlePackSelect(item.id)}
activeOpacity={0.7}
>
<View className="p-4 py-8">
<View className="flex-row items-center justify-between">
<View className="flex-1 gap-4">
<Text variant="title3" color="primary">
{item.name}
</Text>
<View className="mt-1 flex-row flex-wrap items-center">
<View className="mr-3 flex-row items-center">
<Icon name="basket-outline" size={14} color={colors.grey2} />
<Text variant="caption2" className="ml-1">
{item.items?.length}{' '}
{item.items?.length === 1 ? t('catalog.item') : t('catalog.items')}
</Text>
</View>
<View className="mr-3 flex-row items-center">
<Icon name="dumbbell" size={14} color={colors.grey2} />
<Text variant="caption2" className="ml-1">
{item.baseWeight?.toFixed(2)} g
</Text>
</View>
<View className="flex-row items-center">
<Icon name="tag-outline" size={14} color={colors.grey2} />
<Text variant="caption2" className="ml-1 capitalize">
{item.category}
</Text>
</View>
</View>
</View>
<Icon name="chevron-right" size={20} color={colors.grey2} />
) : (
EmptyState
)
}
renderItem={({ item }) => (
<TouchableOpacity
className="mx-4 mb-3 overflow-hidden rounded-lg bg-card shadow-sm"
onPress={() => handlePackSelect(item.id)}
activeOpacity={0.7}
>
<View className="p-4 py-8">
<View className="flex-row items-center justify-between">
<View className="flex-1 gap-4">
<Text variant="title3" color="primary">
{item.name}
</Text>
<View className="mt-1 flex-row flex-wrap items-center">
<View className="mr-3 flex-row items-center">
<Icon name="basket-outline" size={14} color={colors.grey2} />
<Text variant="caption2" className="ml-1">
{item.items?.length}{' '}
{item.items?.length === 1 ? t('catalog.item') : t('catalog.items')}
</Text>
</View>
<View className="mr-3 flex-row items-center">
<Icon name="dumbbell" size={14} color={colors.grey2} />
<Text variant="caption2" className="ml-1">
{item.baseWeight?.toFixed(2)} g
</Text>
</View>
<View className="flex-row items-center">
<Icon name="tag-outline" size={14} color={colors.grey2} />
<Text variant="caption2" className="ml-1 capitalize">
{item.category}
</Text>
</View>
</View>
</TouchableOpacity>
)}
ListEmptyComponent={
<View className="items-center justify-center py-8">
<Text variant="body" className="text-center">
{t('catalog.noPacksFound')}
</Text>
</View>
}
/>
</>
) : (
<View className="items-center justify-center rounded-lg bg-card p-8 shadow-sm">
<Icon name="backpack" size={48} color="text-muted-foreground" />
<Text variant="title3" color="primary" className="mt-4 text-center">
{t('catalog.noPacksAvailable')}
</Text>
<Text variant="body" className="mb-4 text-center">
{t('catalog.createPackMessage')}
</Text>
<Button onPress={handleCreatePack}>
<Icon name="plus" size={18} color="text-primary-foreground" />
<Text variant="body" color="primary">
{t('catalog.createPack')}
</Text>
</Button>
</View>
<Icon name="chevron-right" size={20} color={colors.grey2} />
</View>
</View>
</TouchableOpacity>
)}
</View>
contentContainerStyle={{ paddingBottom: 24 }}
/>
</SafeAreaView>
);
}
10 changes: 10 additions & 0 deletions apps/expo/lib/api/getBaseUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { clientEnvs } from '@packrat/env/expo-client';
import { Platform } from 'react-native';

export function getApiBaseUrl(): string {
const url = clientEnvs.EXPO_PUBLIC_API_URL;
if (Platform.OS === 'android') {
return url.split('localhost').join('10.0.2.2');
}
return url;
}
4 changes: 2 additions & 2 deletions apps/expo/lib/api/packrat.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createApiClient } from '@packrat/api-client';
import { clientEnvs } from '@packrat/env/expo-client';
import { fromZod } from '@packrat/guards';
import { store } from 'expo-app/atoms/store';
import { needsReauthAtom } from 'expo-app/features/auth/atoms/authAtoms';
import { getApiBaseUrl } from 'expo-app/lib/api/getBaseUrl';
import { authClient } from 'expo-app/lib/auth-client';
import * as SecureStore from 'expo-secure-store';
import { z } from 'zod';
Expand All @@ -27,7 +27,7 @@ function parseSessionToken(cookieJson: string | null): string | null {
}

export const apiClient = createApiClient({
baseUrl: clientEnvs.EXPO_PUBLIC_API_URL,
baseUrl: getApiBaseUrl(),
auth: {
// Read the token from SecureStore — no network call on every API request.
getAccessToken: async () => {
Expand Down
4 changes: 2 additions & 2 deletions apps/expo/lib/auth-client.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { expoClient } from '@better-auth/expo/client';
import { clientEnvs } from '@packrat/env/expo-client';
import { createAuthClient } from 'better-auth/react';
import { getApiBaseUrl } from 'expo-app/lib/api/getBaseUrl';
import * as SecureStore from 'expo-secure-store';

export const authClient = createAuthClient({
baseURL: clientEnvs.EXPO_PUBLIC_API_URL,
baseURL: getApiBaseUrl(),
plugins: [
expoClient({
scheme: 'packrat',
Expand Down
2 changes: 1 addition & 1 deletion apps/expo/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@
"createPack": "Create Pack",
"editPack": "Edit Pack",
"noPacks": "No packs available",
"packName": "Pack Name",
"packName": "Choose a Pack",
"packDescription": "Pack Description",
"packWeight": "Pack Weight",
"packItems": "Pack Items",
Expand Down
Loading