* Extended functionalities
* Added different UIs
This commit is contained in:
152
meal-plan-frontend-mobile/app/(main)/category/[category].tsx
Normal file
152
meal-plan-frontend-mobile/app/(main)/category/[category].tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user