diff --git a/app/(main)/(authenticated)/create-trip/_layout.tsx b/app/(main)/(authenticated)/create-trip/_layout.tsx index d7dd19b..b700a45 100644 --- a/app/(main)/(authenticated)/create-trip/_layout.tsx +++ b/app/(main)/(authenticated)/create-trip/_layout.tsx @@ -73,6 +73,7 @@ export default function CreateTripLayout() { }} /> + ); } diff --git a/app/(main)/(authenticated)/create-trip/dish-details-modal.tsx b/app/(main)/(authenticated)/create-trip/dish-details-modal.tsx new file mode 100644 index 0000000..3652a94 --- /dev/null +++ b/app/(main)/(authenticated)/create-trip/dish-details-modal.tsx @@ -0,0 +1,7 @@ +import { DishDetailsModalPage } from '@/features/trips/pages'; + +const DishDetailsModal = () => { + return ; +}; + +export default DishDetailsModal; diff --git a/convex/validators/Trips.ts b/convex/validators/Trips.ts index 3b1fc37..2ab054c 100644 --- a/convex/validators/Trips.ts +++ b/convex/validators/Trips.ts @@ -54,6 +54,7 @@ export const TypicalDish = v.object({ ingredients: v.array(v.string()), isGlutenFree: v.boolean(), isVegetarian: v.boolean(), + isVegan: v.boolean(), }); export const Food = v.object({ diff --git a/features/core/navigation/data/services/NavigationService.ts b/features/core/navigation/data/services/NavigationService.ts index 612ab21..550563a 100644 --- a/features/core/navigation/data/services/NavigationService.ts +++ b/features/core/navigation/data/services/NavigationService.ts @@ -61,8 +61,11 @@ export class NavigationService implements INavigationService { toChangeLanguage() { this.client.push(`/${Stacks.Profile}/${Routes.ChangeLanguage}`); } - toTypicalDishesModal({ tripId }: { tripId: string }) { - this.client.push({ pathname: `/${Stacks.CreateTrip}/${Modals.TypicalDishes}`, params: { tripId } }); + toTypicalDishesModal(params: { tripId: string }) { + this.client.push({ pathname: `/${Stacks.CreateTrip}/${Modals.TypicalDishes}`, params }); + } + toDishDetailsModal(params: { tripId: string; searchTerm: string }) { + this.client.push({ pathname: `/${Stacks.CreateTrip}/${Modals.DishDetails}`, params }); } back() { diff --git a/features/core/navigation/domain/entities/Modals.ts b/features/core/navigation/domain/entities/Modals.ts index 5356b1a..bcd73e0 100644 --- a/features/core/navigation/domain/entities/Modals.ts +++ b/features/core/navigation/domain/entities/Modals.ts @@ -1,5 +1,6 @@ export const Modals = { TypicalDishes: 'typical-dishes-modal', + DishDetails: 'dish-details-modal', } as const; export type Modals = (typeof Modals)[keyof typeof Modals]; diff --git a/features/core/navigation/domain/entities/services/INavigationService.ts b/features/core/navigation/domain/entities/services/INavigationService.ts index 64a3b28..c559fc3 100644 --- a/features/core/navigation/domain/entities/services/INavigationService.ts +++ b/features/core/navigation/domain/entities/services/INavigationService.ts @@ -25,6 +25,7 @@ export interface INavigationService { toTripList(): void; toChangeLanguage(): void; toTypicalDishesModal(params: { tripId: string }): void; + toDishDetailsModal(params: { tripId: string; searchTerm: string }): void; back(): void; diff --git a/features/core/translations/libraries/locales/en.json b/features/core/translations/libraries/locales/en.json index 8fd3086..f544eeb 100644 --- a/features/core/translations/libraries/locales/en.json +++ b/features/core/translations/libraries/locales/en.json @@ -35,10 +35,15 @@ "DISHES_one": "dish", "DISHES_other": "dishes", "TYPICAL_DISHES": "Typical Dishes", + "INGREDIENTS": "INGREDIENTS", + "GLUTEN_FREE": "GLUTEN-FREE", + "VEGAN": "VEGAN", + "VEGETARIAN": "VEGETARIAN", "PROFILE": "Profile", "MY_TRIPS": "My Trips", "LOGO": "HolidAI", - "UPCOMING_TRIP": "Upcoming trip:" + "UPCOMING_TRIP": "Upcoming trip:", + "MORE_DETAILS": "More details" }, "ACTIVITY_DETAILS": { "USEFUL_TIPS": "Useful Tips", diff --git a/features/core/translations/libraries/locales/it.json b/features/core/translations/libraries/locales/it.json index c1e8ff4..1997724 100644 --- a/features/core/translations/libraries/locales/it.json +++ b/features/core/translations/libraries/locales/it.json @@ -35,10 +35,15 @@ "DISHES_one": "piatto", "DISHES_other": "piatti", "TYPICAL_DISHES": "Piatti tipici", + "INGREDIENTS": "INGREDIENTI", + "GLUTEN_FREE": "SENZA GLUTINE", + "VEGAN": "VEGANO", + "VEGETARIAN": "VEGETARIANO", "PROFILE": "Profilo", "MY_TRIPS": "Viaggi", "LOGO": "HolidAI", - "UPCOMING_TRIP": "Prossimo viaggio:" + "UPCOMING_TRIP": "Prossimo viaggio:", + "MORE_DETAILS": "Più dettagli" }, "ACTIVITY_DETAILS": { "USEFUL_TIPS": "Consigli utili", diff --git a/features/core/ui/assets/images/gluten_free.png b/features/core/ui/assets/images/gluten_free.png new file mode 100644 index 0000000..9d8686a Binary files /dev/null and b/features/core/ui/assets/images/gluten_free.png differ diff --git a/features/core/ui/assets/images/vegan.png b/features/core/ui/assets/images/vegan.png new file mode 100644 index 0000000..d9b007e Binary files /dev/null and b/features/core/ui/assets/images/vegan.png differ diff --git a/features/core/ui/assets/images/vegetarian.png b/features/core/ui/assets/images/vegetarian.png new file mode 100644 index 0000000..4259caa Binary files /dev/null and b/features/core/ui/assets/images/vegetarian.png differ diff --git a/features/core/ui/components/basic/Cheap/Cheap.tsx b/features/core/ui/components/basic/Cheap/Cheap.tsx index 5920d65..01ee667 100644 --- a/features/core/ui/components/basic/Cheap/Cheap.tsx +++ b/features/core/ui/components/basic/Cheap/Cheap.tsx @@ -10,14 +10,16 @@ type CheapProps = { title: string; color: string; icon?: IoniconsName; + uppercase?: boolean; }; -export const Cheap: FC = ({ title, color, icon }) => { +export const Cheap: FC = ({ title, color, icon, uppercase = true }) => { const styles = cheapStyles(color); + return ( {icon && } - + ); }; diff --git a/features/core/ui/components/composite/Badge/Badge.style.ts b/features/core/ui/components/composite/Badge/Badge.style.ts new file mode 100644 index 0000000..d954a2a --- /dev/null +++ b/features/core/ui/components/composite/Badge/Badge.style.ts @@ -0,0 +1,47 @@ +import { colors } from '@/features/core/ui/style/colors'; +import { components } from '@/features/core/ui/style/dimensions/components'; +import { fontSize } from '@/features/core/ui/style/dimensions/fontSize'; +import { spacing } from '@/features/core/ui/style/dimensions/spacing'; +import { fontFamily } from '@/features/core/ui/style/fontFamily'; +import { StyleSheet } from 'react-native'; + +export const styles = (active: boolean, backgroundColor: string) => + StyleSheet.create({ + container: { + alignItems: 'center', + gap: spacing.Single, + }, + circleWrapper: { + width: components.badgeCircleSize, + height: components.badgeCircleSize, + }, + circle: { + width: components.badgeCircleSize, + height: components.badgeCircleSize, + borderRadius: components.badgeCircleSize / 2, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: active ? backgroundColor : colors.primaryRed, + }, + checkBadge: { + position: 'absolute', + bottom: 0, + right: 0, + width: components.badgeCheckSize, + height: components.badgeCheckSize, + borderRadius: components.badgeCheckSize / 2, + backgroundColor: colors.primaryWhite, + alignItems: 'center', + justifyContent: 'center', + }, + badgeImage: { + width: components.badgeCircleSize * 0.6, + height: components.badgeCircleSize * 0.6, + }, + label: { + fontSize: fontSize.XS, + fontFamily: fontFamily.interBold, + color: colors.primaryBlack, + textAlign: 'center', + }, + }); diff --git a/features/core/ui/components/composite/Badge/Badge.tsx b/features/core/ui/components/composite/Badge/Badge.tsx new file mode 100644 index 0000000..c743e81 --- /dev/null +++ b/features/core/ui/components/composite/Badge/Badge.tsx @@ -0,0 +1,39 @@ +import { CustomIcon } from '@/features/core/ui/components/basic/CustomIcon/CustomIcon'; +import { CustomImage } from '@/features/core/ui/components/basic/CustomImage/CustomImage'; +import { CustomText } from '@/features/core/ui/components/basic/CustomText/CustomText'; +import { styles as badgeStyle } from '@/features/core/ui/components/composite/Badge/Badge.style'; +import { colors } from '@/features/core/ui/style/colors'; +import { spacing } from '@/features/core/ui/style/dimensions/spacing'; +import { icons } from '@/features/core/ui/style/icons'; +import type { FC } from 'react'; +import type { ImageSourcePropType } from 'react-native'; +import { View } from 'react-native'; + +type BadgeProps = { + label: string; + image: ImageSourcePropType; + backgroundColor: string; + active: boolean; +}; + +export const Badge: FC = ({ label, image, backgroundColor, active }) => { + const styles = badgeStyle(active, backgroundColor); + + return ( + + + + + + + + + + + + ); +}; diff --git a/features/core/ui/components/composite/BottomSheetHeader/BottomSheetHeader.style.ts b/features/core/ui/components/composite/BottomSheetHeader/BottomSheetHeader.style.ts new file mode 100644 index 0000000..578ef2c --- /dev/null +++ b/features/core/ui/components/composite/BottomSheetHeader/BottomSheetHeader.style.ts @@ -0,0 +1,19 @@ +import { fontSize } from '@/features/core/ui/style/dimensions/fontSize'; +import { spacing } from '@/features/core/ui/style/dimensions/spacing'; +import { fontFamily } from '@/features/core/ui/style/fontFamily'; +import { StyleSheet } from 'react-native'; + +export const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingBottom: spacing.Double, + marginBottom: spacing.Fourfold, + }, + title: { + fontFamily: fontFamily.interExtraBold, + fontSize: fontSize.XL2, + maxWidth: '90%', + }, +}); diff --git a/features/core/ui/components/composite/BottomSheetHeader/BottomSheetHeader.tsx b/features/core/ui/components/composite/BottomSheetHeader/BottomSheetHeader.tsx new file mode 100644 index 0000000..a28d2d1 --- /dev/null +++ b/features/core/ui/components/composite/BottomSheetHeader/BottomSheetHeader.tsx @@ -0,0 +1,19 @@ +import { ButtonType } from '@/features/core/ui/components/basic/CustomButton/CustomButton.logic'; +import { CustomIconButtonMedium } from '@/features/core/ui/components/basic/CustomIconButton/CustomIconButtonMedium'; +import { CustomText } from '@/features/core/ui/components/basic/CustomText/CustomText'; +import { styles } from '@/features/core/ui/components/composite/BottomSheetHeader/BottomSheetHeader.style'; +import { icons } from '@/features/core/ui/style/icons'; +import type { FC } from 'react'; +import { View } from 'react-native'; + +type BottomSheetHeaderProps = { + title: string; + onClose: () => void; +}; + +export const BottomSheetHeader: FC = ({ title, onClose }) => ( + + + + +); diff --git a/features/core/ui/index.ts b/features/core/ui/index.ts index fb161d6..8f8bda6 100644 --- a/features/core/ui/index.ts +++ b/features/core/ui/index.ts @@ -41,6 +41,8 @@ export * from '@/features/core/ui/components/basic/LinearGradientText/LinearGrad export * from '@/features/core/ui/components/basic/LottieAnimation/LottieAnimation'; // Composite components +export * from '@/features/core/ui/components/composite/Badge/Badge'; +export * from '@/features/core/ui/components/composite/BottomSheetHeader/BottomSheetHeader'; export * from '@/features/core/ui/components/composite/AnimatedHeaderImage/AnimatedHeaderImage'; export * from '@/features/core/ui/components/composite/CardWithImage/CardWithImage'; export * from '@/features/core/ui/components/composite/CustomHeader/CustomHeader'; diff --git a/features/core/ui/style/colors.ts b/features/core/ui/style/colors.ts index cc226c4..2ba4741 100644 --- a/features/core/ui/style/colors.ts +++ b/features/core/ui/style/colors.ts @@ -19,4 +19,5 @@ export const colors = { tertiaryBlue: '#63b3ff', primaryRed: '#FFD9D9', secondaryGreen: '#8AFF8A', + tertiaryGreen: '#57C17B', } as const; diff --git a/features/core/ui/style/dimensions/components.ts b/features/core/ui/style/dimensions/components.ts index 1f9b583..9460c44 100644 --- a/features/core/ui/style/dimensions/components.ts +++ b/features/core/ui/style/dimensions/components.ts @@ -26,6 +26,8 @@ export const components = { buttonNumberHeight: 30, animatedWordsHeight: 30, carouselImageSize: 200, + badgeCircleSize: 80, + badgeCheckSize: 22, welcomeCardHeightSmall: 130, welcomeCardHeightMedium: 180, welcomeCardHeightLarge: 230, diff --git a/features/core/ui/style/dimensions/images.ts b/features/core/ui/style/dimensions/images.ts index fa95a72..3a309ce 100644 --- a/features/core/ui/style/dimensions/images.ts +++ b/features/core/ui/style/dimensions/images.ts @@ -5,4 +5,5 @@ export const images = { logoRoundHeight: 80, logoRoundWidth: 80, dishImageSize: 80, + dishFullImageSize: 110, }; diff --git a/features/core/ui/style/icons.ts b/features/core/ui/style/icons.ts index 0190c46..4f75651 100644 --- a/features/core/ui/style/icons.ts +++ b/features/core/ui/style/icons.ts @@ -2,9 +2,11 @@ import type { Ionicons } from '@expo/vector-icons'; export const icons: Record = { close: 'close', + closeCircle: 'close-circle', location: 'location', locationOutline: 'location-outline', globeOutline: 'globe-outline', + checkmark: 'checkmark', personCircleOutline: 'person-circle-outline', arrowBack: 'arrow-back', arrowRight: 'chevron-forward', diff --git a/features/trip-generation/domain/schemas/GenerateTripSchema.ts b/features/trip-generation/domain/schemas/GenerateTripSchema.ts index f44c3b5..174c5d4 100644 --- a/features/trip-generation/domain/schemas/GenerateTripSchema.ts +++ b/features/trip-generation/domain/schemas/GenerateTripSchema.ts @@ -78,6 +78,7 @@ export const generatedTripSchema = z.object({ ingredients: z.array(z.string()).describe('Main ingredients of the dish.'), isGlutenFree: z.boolean().describe('Whether the dish is gluten free.'), isVegetarian: z.boolean().describe('Whether the dish is vegetarian.'), + isVegan: z.boolean().describe('Whether the dish is vegan.'), }), ), }), diff --git a/features/trips/domain/entities/TypicalDish.ts b/features/trips/domain/entities/TypicalDish.ts index 502a0f4..ba6af8a 100644 --- a/features/trips/domain/entities/TypicalDish.ts +++ b/features/trips/domain/entities/TypicalDish.ts @@ -5,4 +5,5 @@ export interface TypicalDish { ingredients: string[]; isGlutenFree: boolean; isVegetarian: boolean; + isVegan: boolean; } diff --git a/features/trips/pages.ts b/features/trips/pages.ts index 32d8177..c043fac 100644 --- a/features/trips/pages.ts +++ b/features/trips/pages.ts @@ -1,4 +1,5 @@ export { ActivityDetailsPage } from '@/features/trips/ui/pages/ActivityDetailsPage/ActivityDetailsPage'; +export { DishDetailsModalPage } from '@/features/trips/ui/pages/DishDetailsModalPage/DishDetailsModalPage'; export { TripDetailsPage } from '@/features/trips/ui/pages/TripDetailsPage/TripDetailsPage'; export { TripListPage } from '@/features/trips/ui/pages/TripListPage/TripListPage'; export { TypicalDishesModalPage } from '@/features/trips/ui/pages/TypicalDishesModalPage/TypicalDishesModalPage'; diff --git a/features/trips/ui/components/FoodCard/FoodCard.style.ts b/features/trips/ui/components/FoodCard/FoodCard.style.ts index 93a7bfe..3db5914 100644 --- a/features/trips/ui/components/FoodCard/FoodCard.style.ts +++ b/features/trips/ui/components/FoodCard/FoodCard.style.ts @@ -1,4 +1,4 @@ -import { colors, fontFamily, fontSize, spacing } from '@/features/core/ui'; +import { colors, fontFamily, fontSize, opacity, spacing } from '@/features/core/ui'; import { StyleSheet } from 'react-native'; export const styles = StyleSheet.create({ @@ -56,4 +56,31 @@ export const styles = StyleSheet.create({ marginTop: spacing.Double, paddingHorizontal: spacing.Double, }, + titleContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.Single, + }, + typicalDishesBox: { + padding: spacing.Double, + backgroundColor: colors.secondaryGrey, + borderRadius: spacing.Double, + marginTop: spacing.Double, + marginHorizontal: spacing.Double, + }, + pressed: { + opacity: opacity.default, + }, + boxText: { + fontFamily: fontFamily.interMedium, + color: colors.primaryBlack, + }, + boxButton: { + marginTop: spacing.Single, + fontFamily: fontFamily.interBold, + alignSelf: 'flex-end', + backgroundColor: colors.primaryWhite, + padding: spacing.Single, + borderRadius: spacing.Double, + }, }); diff --git a/features/trips/ui/components/FoodCard/FoodCard.tsx b/features/trips/ui/components/FoodCard/FoodCard.tsx index 6037c92..3430d22 100644 --- a/features/trips/ui/components/FoodCard/FoodCard.tsx +++ b/features/trips/ui/components/FoodCard/FoodCard.tsx @@ -1,10 +1,10 @@ -import { CustomButtonMedium, CustomIcon, CustomText, colors, icons, spacing } from '@/features/core/ui'; +import { CustomIcon, CustomText, colors, icons, spacing } from '@/features/core/ui'; import type { Food } from '@/features/trips/domain/entities/Food'; import { useFoodCardLogic } from '@/features/trips/ui/components/FoodCard/FoodCard.logic'; import { styles } from '@/features/trips/ui/components/FoodCard/FoodCard.style'; import { LinearGradient } from 'expo-linear-gradient'; import type { FC } from 'react'; -import { View } from 'react-native'; +import { Pressable, View } from 'react-native'; type FoodCardProps = { food: Food; @@ -36,9 +36,16 @@ export const FoodCard: FC = ({ food, tripId }) => { - - - + [styles.typicalDishesBox, pressed && styles.pressed]} + onPress={handleOpenModal} + > + + + + + + ); diff --git a/features/trips/ui/components/FoodCard/components/DishItem/DishItem.logic.ts b/features/trips/ui/components/FoodCard/components/DishItem/DishItem.logic.ts index d060e70..ba1857c 100644 --- a/features/trips/ui/components/FoodCard/components/DishItem/DishItem.logic.ts +++ b/features/trips/ui/components/FoodCard/components/DishItem/DishItem.logic.ts @@ -1,6 +1,26 @@ import { useGetWikimediaDishImage } from '@/features/core/images'; +import type { TypicalDish } from '@/features/trips'; -export const useDishItemLogic = (searchTerm: string) => { - const { data, isLoading } = useGetWikimediaDishImage(searchTerm); - return { image: data?.url, isLoading }; +const glutenFreeImage = require('@/features/core/ui/assets/images/gluten_free.png'); +const veganImage = require('@/features/core/ui/assets/images/vegan.png'); +const vegetarianImage = require('@/features/core/ui/assets/images/vegetarian.png'); + +export const useDishItemLogic = (dish: TypicalDish) => { + const { data, isLoading } = useGetWikimediaDishImage(dish.searchTerm); + const hasBadge = dish.isGlutenFree || dish.isVegan || dish.isVegetarian; + + return { + image: data?.url, + isLoading, + glutenFreeImage, + veganImage, + vegetarianImage, + hasBadge, + isGlutenFree: dish.isGlutenFree, + isVegan: dish.isVegan, + isVegetarian: dish.isVegetarian, + glutenFreeLabel: 'MY_TRIP.GLUTEN_FREE', + veganLabel: 'MY_TRIP.VEGAN', + vegetarianLabel: 'MY_TRIP.VEGETARIAN', + }; }; diff --git a/features/trips/ui/components/FoodCard/components/DishItem/DishItem.style.ts b/features/trips/ui/components/FoodCard/components/DishItem/DishItem.style.ts index 08c9dc5..384793b 100644 --- a/features/trips/ui/components/FoodCard/components/DishItem/DishItem.style.ts +++ b/features/trips/ui/components/FoodCard/components/DishItem/DishItem.style.ts @@ -1,4 +1,4 @@ -import { colors, fontFamily, images, spacing } from '@/features/core/ui'; +import { colors, fontFamily, images, opacity, spacing } from '@/features/core/ui'; import { StyleSheet } from 'react-native'; export const styles = StyleSheet.create({ @@ -6,11 +6,16 @@ export const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', gap: spacing.Double, + backgroundColor: colors.secondaryGrey, + padding: spacing.Double, + borderRadius: spacing.Fourfold, }, image: { width: images.dishImageSize, height: images.dishImageSize, borderRadius: images.dishImageSize, + borderWidth: spacing.Minimal, + borderColor: colors.primaryBlack, }, skeleton: { width: images.dishImageSize, @@ -31,4 +36,16 @@ export const styles = StyleSheet.create({ flex: 1, alignItems: 'flex-start', }, + pressed: { + opacity: opacity.default, + }, + badge: { + width: spacing.FourfoldAndHalf, + height: spacing.FourfoldAndHalf, + }, + badgeContainer: { + flexDirection: 'row', + columnGap: spacing.Minimal, + left: -spacing.MinimalDouble, + }, }); diff --git a/features/trips/ui/components/FoodCard/components/DishItem/DishItem.tsx b/features/trips/ui/components/FoodCard/components/DishItem/DishItem.tsx index 3262bfa..e867e51 100644 --- a/features/trips/ui/components/FoodCard/components/DishItem/DishItem.tsx +++ b/features/trips/ui/components/FoodCard/components/DishItem/DishItem.tsx @@ -3,15 +3,33 @@ import type { TypicalDish } from '@/features/trips/domain/entities/TypicalDish'; import { useDishItemLogic } from '@/features/trips/ui/components/FoodCard/components/DishItem/DishItem.logic'; import { styles } from '@/features/trips/ui/components/FoodCard/components/DishItem/DishItem.style'; import type { FC } from 'react'; -import { View } from 'react-native'; +import { Pressable, View } from 'react-native'; -type DishItemProps = { dish: TypicalDish }; +type DishItemProps = { dish: TypicalDish; onPress: () => void }; -export const DishItem: FC = ({ dish }) => { - const { image, isLoading } = useDishItemLogic(dish.searchTerm); +export const DishItem: FC = ({ dish, onPress }) => { + const { + image, + isLoading, + glutenFreeImage, + veganImage, + vegetarianImage, + hasBadge, + isGlutenFree, + isVegan, + isVegetarian, + glutenFreeLabel, + veganLabel, + vegetarianLabel, + } = useDishItemLogic(dish); return ( - + [styles.container, pressed && styles.pressed]} + onPress={onPress} + accessibilityRole="button" + accessibilityLabel={dish.name} + > {isLoading ? ( @@ -21,8 +39,24 @@ export const DishItem: FC = ({ dish }) => { - + + {hasBadge && ( + + {isGlutenFree && ( + + )} + {isVegan && } + {isVegetarian && ( + + )} + + )} - + ); }; diff --git a/features/trips/ui/components/IngredientsList/IngredientsList.style.ts b/features/trips/ui/components/IngredientsList/IngredientsList.style.ts new file mode 100644 index 0000000..2632914 --- /dev/null +++ b/features/trips/ui/components/IngredientsList/IngredientsList.style.ts @@ -0,0 +1,20 @@ +import { colors, fontFamily, fontSize, spacing } from '@/features/core/ui'; +import { StyleSheet } from 'react-native'; + +export const styles = StyleSheet.create({ + container: { + flex: 1, + gap: spacing.Double, + }, + title: { + fontFamily: fontFamily.interBold, + fontSize: fontSize.SM, + color: colors.primaryGrey, + }, + chipsRow: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: spacing.SingleAndHalf, + width: '100%', + }, +}); diff --git a/features/trips/ui/components/IngredientsList/IngredientsList.tsx b/features/trips/ui/components/IngredientsList/IngredientsList.tsx new file mode 100644 index 0000000..980719f --- /dev/null +++ b/features/trips/ui/components/IngredientsList/IngredientsList.tsx @@ -0,0 +1,26 @@ +import { Cheap, CustomText, colors, icons } from '@/features/core/ui'; +import { styles } from '@/features/trips/ui/components/IngredientsList/IngredientsList.style'; +import type { FC } from 'react'; +import { View } from 'react-native'; + +type IngredientsListProps = { + title: string; + ingredients: string[]; +}; + +export const IngredientsList: FC = ({ title, ingredients }) => ( + + + + {ingredients.map(ingredient => ( + + ))} + + +); diff --git a/features/trips/ui/components/TypicalDishesList/TypicalDishesList.style.ts b/features/trips/ui/components/TypicalDishesList/TypicalDishesList.style.ts index a350883..b8c00ac 100644 --- a/features/trips/ui/components/TypicalDishesList/TypicalDishesList.style.ts +++ b/features/trips/ui/components/TypicalDishesList/TypicalDishesList.style.ts @@ -4,7 +4,7 @@ import { StyleSheet } from 'react-native'; export const styles = StyleSheet.create({ separator: { height: 1, - backgroundColor: colors.secondaryGreen, - marginVertical: spacing.Fourfold, + backgroundColor: colors.secondaryGrey, + marginVertical: spacing.Double, }, }); diff --git a/features/trips/ui/components/TypicalDishesList/TypicalDishesList.tsx b/features/trips/ui/components/TypicalDishesList/TypicalDishesList.tsx index b3348f8..f7d4eed 100644 --- a/features/trips/ui/components/TypicalDishesList/TypicalDishesList.tsx +++ b/features/trips/ui/components/TypicalDishesList/TypicalDishesList.tsx @@ -7,16 +7,17 @@ import { View } from 'react-native'; type TypicalDishesListProps = { dishItems: TypicalDish[] | undefined; + onDishPress: (searchTerm: string) => void; }; const Separator = () => ; -export const TypicalDishesList: FC = ({ dishItems }) => ( +export const TypicalDishesList: FC = ({ dishItems, onDishPress }) => ( <> {dishItems?.map((item, index) => ( {index > 0 && } - + onDishPress(item.searchTerm)} /> ))} diff --git a/features/trips/ui/components/TypicalDishesModalHeader/TypicalDishesModalHeader.style.ts b/features/trips/ui/components/TypicalDishesModalHeader/TypicalDishesModalHeader.style.ts index 072e1af..3d26e90 100644 --- a/features/trips/ui/components/TypicalDishesModalHeader/TypicalDishesModalHeader.style.ts +++ b/features/trips/ui/components/TypicalDishesModalHeader/TypicalDishesModalHeader.style.ts @@ -7,9 +7,6 @@ export const styles = StyleSheet.create({ justifyContent: 'space-between', alignItems: 'center', marginBottom: spacing.Quintuple, - borderBottomWidth: 1, - borderBottomColor: colors.secondaryGreen, - paddingBottom: spacing.Double, }, headerContent: { gap: spacing.Single, diff --git a/features/trips/ui/pages/DishDetailsModalPage/DishDetailsModalPage.logic.ts b/features/trips/ui/pages/DishDetailsModalPage/DishDetailsModalPage.logic.ts new file mode 100644 index 0000000..0f655a6 --- /dev/null +++ b/features/trips/ui/pages/DishDetailsModalPage/DishDetailsModalPage.logic.ts @@ -0,0 +1,33 @@ +import { useGetWikimediaDishImage } from '@/features/core/images'; +import { navigationService } from '@/features/core/navigation'; +import { useGetTripById } from '@/features/trips/facades/useGetTripById'; +import { useLocalSearchParams } from 'expo-router'; + +const glutenFreeImage = require('@/features/core/ui/assets/images/gluten_free.png'); +const veganImage = require('@/features/core/ui/assets/images/vegan.png'); +const vegetarianImage = require('@/features/core/ui/assets/images/vegetarian.png'); + +export const useDishDetailsModalPageLogic = () => { + const { tripId, searchTerm } = useLocalSearchParams<{ tripId: string; searchTerm: string }>(); + const { data, isLoading } = useGetWikimediaDishImage(searchTerm); + const { trip } = useGetTripById(tripId); + + const dish = trip?.tripAiResp?.food?.typicalDishes.find(d => d.searchTerm === searchTerm); + + const handleClose = () => navigationService.back(); + + return { + dishName: dish?.name ?? '', + dishDescription: dish?.description ?? '', + dishIngredients: dish?.ingredients ?? [], + handleClose, + image: data?.url, + imageIsLoading: isLoading, + isVegetarian: dish?.isVegetarian ?? false, + isGlutenFree: dish?.isGlutenFree ?? false, + isVegan: dish?.isVegan ?? false, + glutenFreeImage, + veganImage, + vegetarianImage, + }; +}; diff --git a/features/trips/ui/pages/DishDetailsModalPage/DishDetailsModalPage.style.ts b/features/trips/ui/pages/DishDetailsModalPage/DishDetailsModalPage.style.ts new file mode 100644 index 0000000..cfccfa6 --- /dev/null +++ b/features/trips/ui/pages/DishDetailsModalPage/DishDetailsModalPage.style.ts @@ -0,0 +1,40 @@ +import { colors, fontFamily, fontSize, images, spacing } from '@/features/core/ui'; +import { StyleSheet } from 'react-native'; + +export const styles = StyleSheet.create({ + contentContainer: { + paddingVertical: spacing.Quintuple, + paddingHorizontal: spacing.Quintuple, + }, + container: { + backgroundColor: colors.primaryWhite, + }, + image: { + width: images.dishFullImageSize, + height: images.dishFullImageSize, + borderRadius: images.dishFullImageSize, + borderWidth: spacing.MinimalDouble, + borderColor: colors.primaryBlack, + }, + bodyContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + columnGap: spacing.Double, + marginBottom: spacing.Triple, + alignItems: 'center', + }, + description: { + flex: 1, + fontFamily: fontFamily.interRegular, + fontSize: fontSize.MD, + lineHeight: fontSize.MD * 1.5, + }, + badgesContainer: { + flexDirection: 'row', + gap: spacing.Single, + marginTop: spacing.Double, + justifyContent: 'space-between', + paddingHorizontal: spacing.Double, + paddingVertical: spacing.Single, + }, +}); diff --git a/features/trips/ui/pages/DishDetailsModalPage/DishDetailsModalPage.tsx b/features/trips/ui/pages/DishDetailsModalPage/DishDetailsModalPage.tsx new file mode 100644 index 0000000..4d5525a --- /dev/null +++ b/features/trips/ui/pages/DishDetailsModalPage/DishDetailsModalPage.tsx @@ -0,0 +1,52 @@ +import { Badge, BaseSkeleton, BottomSheetHeader, CustomImage, CustomText, colors } from '@/features/core/ui'; +import { IngredientsList } from '@/features/trips/ui/components/IngredientsList/IngredientsList'; +import { useDishDetailsModalPageLogic } from '@/features/trips/ui/pages/DishDetailsModalPage/DishDetailsModalPage.logic'; +import { styles } from '@/features/trips/ui/pages/DishDetailsModalPage/DishDetailsModalPage.style'; +import { ScrollView, View } from 'react-native'; + +export const DishDetailsModalPage = () => { + const { + dishName, + dishDescription, + dishIngredients, + handleClose, + image, + imageIsLoading, + isVegetarian, + isGlutenFree, + isVegan, + glutenFreeImage, + veganImage, + vegetarianImage, + } = useDishDetailsModalPageLogic(); + + return ( + + + + {imageIsLoading ? ( + + ) : ( + + )} + + + + + + + + + + ); +}; diff --git a/features/trips/ui/pages/TypicalDishesModalPage/TypicalDishesModalPage.logic.ts b/features/trips/ui/pages/TypicalDishesModalPage/TypicalDishesModalPage.logic.ts index 5b21d14..2ed7cf4 100644 --- a/features/trips/ui/pages/TypicalDishesModalPage/TypicalDishesModalPage.logic.ts +++ b/features/trips/ui/pages/TypicalDishesModalPage/TypicalDishesModalPage.logic.ts @@ -11,6 +11,7 @@ export const useTypicalDishesModalPageLogic = () => { const dishNumber = food?.typicalDishes.length ?? 0; const handleClose = () => navigationService.back(); + const handleDishPress = (searchTerm: string) => navigationService.toDishDetailsModal({ tripId, searchTerm }); - return { handleClose, location, dishNumber, dishItems: food?.typicalDishes }; + return { handleClose, handleDishPress, location, dishNumber, dishItems: food?.typicalDishes }; }; diff --git a/features/trips/ui/pages/TypicalDishesModalPage/TypicalDishesModalPage.tsx b/features/trips/ui/pages/TypicalDishesModalPage/TypicalDishesModalPage.tsx index 1438b31..c306cfc 100644 --- a/features/trips/ui/pages/TypicalDishesModalPage/TypicalDishesModalPage.tsx +++ b/features/trips/ui/pages/TypicalDishesModalPage/TypicalDishesModalPage.tsx @@ -5,12 +5,12 @@ import { styles } from '@/features/trips/ui/pages/TypicalDishesModalPage/Typical import { ScrollView } from 'react-native'; export const TypicalDishesModalPage = () => { - const { handleClose, location, dishNumber, dishItems } = useTypicalDishesModalPageLogic(); + const { handleClose, handleDishPress, location, dishNumber, dishItems } = useTypicalDishesModalPageLogic(); return ( - + ); };