Files
Piotr Kus f15bb7a916 * Extended functionalities
* Added different UIs
2026-06-24 21:43:40 +02:00

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,
},
});