Files
DailyMeals/meal-plan-frontend-mobile/app/(main)/category/[category].tsx
Piotr Kus f15bb7a916 * Extended functionalities
* Added different UIs
2026-06-24 21:43:40 +02:00

153 lines
4.8 KiB
TypeScript

import { Stack, useLocalSearchParams, 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 { Screen } from '@/src/components/Screen';
import { useAppearance } from '@/src/context/AppearanceContext';
import { categoryLabel, parseCategoryParam } from '@/src/utils/recipe';
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 CategoryScreen() {
const { category: categoryParam } = useLocalSearchParams<{ category: string }>();
const category = parseCategoryParam(categoryParam ?? '');
const { t } = useTranslation();
const { colors } = useAppearance();
const [recipeSort, setRecipeSort] = useState<{ column: RecipeSortColumn; direction: SortDirection }>({
column: 'name',
direction: 'asc',
});
const recipesQuery = useQuery({
queryKey: ['recipes', 'category', category],
queryFn: () => fetchRecipes({ category: category! }),
enabled: category != null,
});
const title = category != null ? categoryLabel(category, t) : t('categories.unknown');
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]);
if (category == null) {
return (
<Screen>
<ErrorView message={t('errors.loadRecipesFailed')} />
</Screen>
);
}
return (
<>
<Stack.Screen options={{ title, headerShown: true }} />
<Screen subtitle={t('common.recipeCount', { count: recipesQuery.data?.length ?? 0 })}>
{(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.emptyCategoryTitle', { category: title })}
description={t('recipes.emptyCategoryDescription')}
/>
}
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({
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,
},
});