164 lines
4.9 KiB
TypeScript
164 lines
4.9 KiB
TypeScript
import { router } from 'expo-router';
|
|
import { useMemo, useState } from 'react';
|
|
import { ActivityIndicator, FlatList, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { fetchRecipes } from '@/src/api/recipes.api';
|
|
import { EmptyState, RecipeCard } from '@/src/components/RecipeCard';
|
|
import { ErrorView } from '@/src/components/ErrorView';
|
|
import { Input } from '@/src/components/Input';
|
|
import { Screen } from '@/src/components/Screen';
|
|
import { useAppearance } from '@/src/context/AppearanceContext';
|
|
import { nextSort, sortBy, sortIndicator, type SortDirection } from '@/src/utils/sort';
|
|
|
|
type RecipeSortColumn = 'name' | 'calories' | 'prepTime';
|
|
|
|
const RECIPE_SORT_COLUMNS: { id: RecipeSortColumn; labelKey: string }[] = [
|
|
{ id: 'name', labelKey: 'manage.recipeName' },
|
|
{ id: 'calories', labelKey: 'recipes.calories' },
|
|
{ id: 'prepTime', labelKey: 'manage.prepTimeMin' },
|
|
];
|
|
|
|
export default function MealsScreen() {
|
|
const { t } = useTranslation();
|
|
const { colors } = useAppearance();
|
|
const [search, setSearch] = useState('');
|
|
const [ingredient, setIngredient] = useState('');
|
|
const [recipeSort, setRecipeSort] = useState<{ column: RecipeSortColumn; direction: SortDirection }>({
|
|
column: 'name',
|
|
direction: 'asc',
|
|
});
|
|
|
|
const recipesQuery = useQuery({
|
|
queryKey: ['recipes', search, ingredient],
|
|
queryFn: () =>
|
|
fetchRecipes({
|
|
search: search.trim() || undefined,
|
|
ingredient: ingredient.trim() || undefined,
|
|
}),
|
|
});
|
|
|
|
const emptyMessage = useMemo(() => {
|
|
if (ingredient.trim()) return t('recipes.noMatchingIngredient');
|
|
return t('recipes.noMatchingFilter');
|
|
}, [ingredient, t]);
|
|
|
|
const sortedRecipes = useMemo(() => {
|
|
const list = recipesQuery.data ?? [];
|
|
const { column, direction } = recipeSort;
|
|
return sortBy(list, direction, (recipe) => {
|
|
switch (column) {
|
|
case 'name':
|
|
return recipe.name;
|
|
case 'calories':
|
|
return recipe.calories;
|
|
case 'prepTime':
|
|
return recipe.prepTimeMinutes;
|
|
}
|
|
});
|
|
}, [recipesQuery.data, recipeSort]);
|
|
|
|
return (
|
|
<Screen title={t('recipes.allMealsTitle')} subtitle={t('recipes.allMealsSubtitle')}>
|
|
<View style={styles.filters}>
|
|
<Input
|
|
placeholder={t('recipes.searchRecipes')}
|
|
value={search}
|
|
onChangeText={setSearch}
|
|
autoCapitalize="none"
|
|
/>
|
|
<Input
|
|
placeholder={t('recipes.searchByIngredient')}
|
|
value={ingredient}
|
|
onChangeText={setIngredient}
|
|
autoCapitalize="none"
|
|
/>
|
|
</View>
|
|
|
|
{(recipesQuery.data?.length ?? 0) > 0 ? (
|
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.sortRow}>
|
|
{RECIPE_SORT_COLUMNS.map(({ id, labelKey }) => (
|
|
<SortChip
|
|
key={id}
|
|
label={`${t(labelKey)}${sortIndicator(recipeSort, id)}`}
|
|
active={recipeSort.column === id}
|
|
onPress={() => setRecipeSort((current) => nextSort(current, id))}
|
|
/>
|
|
))}
|
|
</ScrollView>
|
|
) : null}
|
|
|
|
{recipesQuery.isLoading ? (
|
|
<View style={styles.loading}>
|
|
<ActivityIndicator size="large" color={colors.brand} />
|
|
</View>
|
|
) : recipesQuery.isError ? (
|
|
<ErrorView
|
|
message={t('errors.loadRecipesFailed')}
|
|
onRetry={() => recipesQuery.refetch()}
|
|
/>
|
|
) : (
|
|
<FlatList
|
|
data={sortedRecipes}
|
|
keyExtractor={(item) => String(item.id)}
|
|
contentContainerStyle={styles.list}
|
|
ListEmptyComponent={
|
|
<EmptyState title={t('recipes.noMatchingTitle')} description={emptyMessage} />
|
|
}
|
|
renderItem={({ item }) => (
|
|
<RecipeCard
|
|
recipe={item}
|
|
onPress={() => router.push(`/(main)/recipe/${item.id}`)}
|
|
/>
|
|
)}
|
|
/>
|
|
)}
|
|
</Screen>
|
|
);
|
|
}
|
|
|
|
function SortChip({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) {
|
|
const { colors } = useAppearance();
|
|
return (
|
|
<Pressable
|
|
onPress={onPress}
|
|
style={[
|
|
styles.sortChip,
|
|
{
|
|
backgroundColor: active ? colors.brand : colors.surface,
|
|
borderColor: active ? colors.brand : colors.border,
|
|
},
|
|
]}
|
|
>
|
|
<Text style={{ color: active ? '#fff' : colors.text, fontWeight: '600', fontSize: 12 }}>{label}</Text>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
filters: {
|
|
gap: 10,
|
|
},
|
|
sortRow: {
|
|
gap: 8,
|
|
paddingBottom: 8,
|
|
},
|
|
sortChip: {
|
|
borderWidth: 1,
|
|
borderRadius: 999,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 6,
|
|
},
|
|
list: {
|
|
gap: 10,
|
|
paddingBottom: 24,
|
|
flexGrow: 1,
|
|
},
|
|
loading: {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
paddingVertical: 40,
|
|
},
|
|
});
|