381 lines
11 KiB
TypeScript
381 lines
11 KiB
TypeScript
import React, { useEffect, useMemo, useState } from 'react';
|
||
import {
|
||
View,
|
||
Text,
|
||
TouchableOpacity,
|
||
ScrollView,
|
||
Image,
|
||
ImageBackground,
|
||
} from 'react-native';
|
||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||
import { StackNavigationProp } from '@react-navigation/stack';
|
||
import { getStyles } from './providerDetailsScreen.styles';
|
||
import { CatalogItemRow } from '@components';
|
||
import { useAppTheme } from '@theme';
|
||
import { addItem } from '../../../store/commonreducers/cart';
|
||
import {
|
||
getProviderCatalogApi,
|
||
getProvidersApi,
|
||
} from '../../../api/deliveryApi';
|
||
import { CatalogItem, Provider } from '../../../interfaces';
|
||
import { AppStackParamList } from '../../../navigation/appStack';
|
||
import { useAppDispatch, useAppSelector } from '@store';
|
||
|
||
type ProviderDetailsNavProp = StackNavigationProp<
|
||
AppStackParamList,
|
||
'ProviderDetailsScreen'
|
||
>;
|
||
type ProviderDetailsRouteProp = RouteProp<
|
||
AppStackParamList,
|
||
'ProviderDetailsScreen'
|
||
>;
|
||
|
||
const FALLBACK_CATEGORIES = ['Pizza', 'Sides', 'Beverages'];
|
||
|
||
const OFFERS = [
|
||
{ key: 'o1', icon: '🏷️', label: '50% OFF up to ₹100' },
|
||
{ key: 'o2', icon: '🚚', label: 'Free delivery above ₹299' },
|
||
{ key: 'o3', icon: '🎁', label: 'Buy 1 Get 1 on combos' },
|
||
];
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Presentational subcomponents
|
||
// Kept local to this screen since none are reused elsewhere yet. Promote to
|
||
// @components if a second screen needs them.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
interface HeroSectionProps {
|
||
styles: ReturnType<typeof getStyles>;
|
||
imageUrl?: string;
|
||
onBack: () => void;
|
||
}
|
||
|
||
const HeroSection: React.FC<HeroSectionProps> = ({
|
||
styles,
|
||
imageUrl,
|
||
onBack,
|
||
}) => (
|
||
<View style={styles.heroWrap}>
|
||
{imageUrl ? (
|
||
<ImageBackground source={{ uri: imageUrl }} style={styles.heroImage}>
|
||
<View style={styles.heroOverlay} />
|
||
</ImageBackground>
|
||
) : (
|
||
<View style={styles.heroFallback}>
|
||
<Text style={styles.heroFallbackEmoji}>🍽️</Text>
|
||
<View style={styles.heroOverlay} />
|
||
</View>
|
||
)}
|
||
|
||
<View style={styles.topIconRow}>
|
||
<TouchableOpacity
|
||
style={styles.iconButton}
|
||
activeOpacity={0.75}
|
||
onPress={onBack}
|
||
>
|
||
<Text style={styles.iconButtonText}>←</Text>
|
||
</TouchableOpacity>
|
||
<View style={styles.iconButtonGroup}>
|
||
<TouchableOpacity
|
||
style={[styles.iconButton, { marginRight: 10 }]}
|
||
activeOpacity={0.75}
|
||
>
|
||
<Text style={styles.iconButtonText}>🔗</Text>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity style={styles.iconButton} activeOpacity={0.75}>
|
||
<Text style={styles.iconButtonText}>🤍</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
);
|
||
|
||
interface InfoCardProps {
|
||
styles: ReturnType<typeof getStyles>;
|
||
name: string;
|
||
rating: number;
|
||
deliveryTime: string;
|
||
tag: string;
|
||
}
|
||
|
||
const InfoCard: React.FC<InfoCardProps> = ({
|
||
styles,
|
||
name,
|
||
rating,
|
||
deliveryTime,
|
||
tag,
|
||
}) => (
|
||
<View style={styles.infoCard}>
|
||
<View style={styles.infoTopRow}>
|
||
<Text style={styles.heroName} numberOfLines={1}>
|
||
{name}
|
||
</Text>
|
||
<View style={styles.ratingBadge}>
|
||
<Text style={{ fontSize: 11 }}>⭐</Text>
|
||
<Text style={styles.ratingBadgeText}>{rating.toFixed(1)}</Text>
|
||
</View>
|
||
</View>
|
||
|
||
<Text style={styles.cuisineText}>{tag} • Multi-cuisine</Text>
|
||
|
||
<View style={styles.metaRow}>
|
||
<View style={styles.metaItem}>
|
||
<Text style={styles.metaIcon}>🕐</Text>
|
||
<Text style={styles.metaText}>{deliveryTime}</Text>
|
||
</View>
|
||
<View style={styles.metaDivider} />
|
||
<View style={styles.metaItem}>
|
||
<Text style={styles.metaIcon}>📍</Text>
|
||
<Text style={styles.metaText}>2.4 km away</Text>
|
||
</View>
|
||
</View>
|
||
|
||
<View style={styles.statusRow}>
|
||
<View style={styles.statusDot} />
|
||
<Text style={styles.statusText}>Open now</Text>
|
||
<Text style={styles.statusTextMuted}> • Closes 11:30 PM</Text>
|
||
</View>
|
||
</View>
|
||
);
|
||
|
||
interface OffersRowProps {
|
||
styles: ReturnType<typeof getStyles>;
|
||
}
|
||
|
||
const OffersRow: React.FC<OffersRowProps> = ({ styles }) => (
|
||
<View style={styles.offersSection}>
|
||
<ScrollView
|
||
horizontal
|
||
showsHorizontalScrollIndicator={false}
|
||
contentContainerStyle={styles.offersScrollContent}
|
||
>
|
||
{OFFERS.map(offer => (
|
||
<View key={offer.key} style={styles.offerChip}>
|
||
<Text style={styles.offerIcon}>{offer.icon}</Text>
|
||
<Text style={styles.offerText}>{offer.label}</Text>
|
||
</View>
|
||
))}
|
||
</ScrollView>
|
||
</View>
|
||
);
|
||
|
||
interface CategoryTabsProps {
|
||
styles: ReturnType<typeof getStyles>;
|
||
categories: string[];
|
||
activeCategory: string;
|
||
onSelect: (category: string) => void;
|
||
}
|
||
|
||
const CategoryTabs: React.FC<CategoryTabsProps> = ({
|
||
styles,
|
||
categories,
|
||
activeCategory,
|
||
onSelect,
|
||
}) => (
|
||
<ScrollView
|
||
horizontal
|
||
showsHorizontalScrollIndicator={false}
|
||
contentContainerStyle={styles.categoryRow}
|
||
>
|
||
{categories.map(cat => {
|
||
const isActive = activeCategory === cat;
|
||
return (
|
||
<TouchableOpacity
|
||
key={cat}
|
||
style={[styles.categoryTab, isActive && styles.categoryTabActive]}
|
||
onPress={() => onSelect(cat)}
|
||
activeOpacity={0.75}
|
||
>
|
||
<Text
|
||
style={[
|
||
styles.categoryTabText,
|
||
isActive && styles.categoryTabTextActive,
|
||
]}
|
||
>
|
||
{cat}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
);
|
||
})}
|
||
</ScrollView>
|
||
);
|
||
|
||
interface CartStripProps {
|
||
styles: ReturnType<typeof getStyles>;
|
||
totalItems: number;
|
||
totalPrice: number;
|
||
onPress: () => void;
|
||
}
|
||
|
||
const CartStrip: React.FC<CartStripProps> = ({
|
||
styles,
|
||
totalItems,
|
||
totalPrice,
|
||
onPress,
|
||
}) => (
|
||
<View style={styles.cartStripWrap}>
|
||
<TouchableOpacity
|
||
style={styles.cartStrip}
|
||
activeOpacity={0.85}
|
||
onPress={onPress}
|
||
>
|
||
<View style={styles.cartStripLeft}>
|
||
<Text style={styles.cartBagIcon}>🛍️</Text>
|
||
<View style={styles.cartStripTextWrap}>
|
||
<Text style={styles.cartStripCount}>
|
||
{totalItems} item{totalItems === 1 ? '' : 's'}
|
||
</Text>
|
||
<Text style={styles.cartStripPrice}>₹{totalPrice}</Text>
|
||
</View>
|
||
</View>
|
||
<View style={styles.viewCartButton}>
|
||
<Text style={styles.viewCartText}>View Cart</Text>
|
||
<Text style={styles.viewCartArrow}>→</Text>
|
||
</View>
|
||
</TouchableOpacity>
|
||
</View>
|
||
);
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Screen
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export const ProviderDetailsScreen: React.FC = () => {
|
||
const { colors } = useAppTheme();
|
||
const styles = getStyles(colors);
|
||
const dispatch = useAppDispatch();
|
||
const navigation = useNavigation<ProviderDetailsNavProp>();
|
||
const route = useRoute<ProviderDetailsRouteProp>();
|
||
const { providerId, providerName } = route.params;
|
||
|
||
const cartItems = useAppSelector(state => state.cart.items);
|
||
|
||
const [provider, setProvider] = useState<Provider | null>(null);
|
||
const [catalog, setCatalog] = useState<CatalogItem[]>([]);
|
||
const [activeCategory, setActiveCategory] = useState(FALLBACK_CATEGORIES[0]);
|
||
|
||
const resolvedProviderId = providerId || 'p1';
|
||
const resolvedProviderName = providerName || 'Pizza Planet';
|
||
|
||
useEffect(() => {
|
||
getProviderCatalogApi(resolvedProviderId).then(setCatalog);
|
||
}, [resolvedProviderId]);
|
||
|
||
useEffect(() => {
|
||
getProvidersApi().then(providers => {
|
||
const match = providers.find(p => p.id === resolvedProviderId);
|
||
if (match) setProvider(match);
|
||
});
|
||
}, [resolvedProviderId]);
|
||
|
||
const categories = useMemo(() => {
|
||
const fromCatalog = Array.from(new Set(catalog.map(item => item.category)));
|
||
return fromCatalog.length > 0 ? fromCatalog : FALLBACK_CATEGORIES;
|
||
}, [catalog]);
|
||
|
||
useEffect(() => {
|
||
if (!categories.includes(activeCategory)) {
|
||
setActiveCategory(categories[0]);
|
||
}
|
||
}, [categories, activeCategory]);
|
||
|
||
const filteredItems = useMemo(
|
||
() => catalog.filter(item => item.category === activeCategory),
|
||
[catalog, activeCategory],
|
||
);
|
||
|
||
const getItemQuantity = (itemId: string) => {
|
||
const match = cartItems.find(i => i.item.id === itemId);
|
||
return match ? match.quantity : 0;
|
||
};
|
||
|
||
const totalItems = cartItems.reduce((sum, i) => sum + i.quantity, 0);
|
||
const totalPrice = cartItems.reduce(
|
||
(sum, i) => sum + i.item.price * i.quantity,
|
||
0,
|
||
);
|
||
|
||
const handleAddItem = (item: CatalogItem) => {
|
||
dispatch(
|
||
addItem({
|
||
providerId: resolvedProviderId,
|
||
providerName: resolvedProviderName,
|
||
item,
|
||
}),
|
||
);
|
||
};
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
<ScrollView
|
||
style={styles.content}
|
||
contentContainerStyle={styles.contentBody}
|
||
showsVerticalScrollIndicator={false}
|
||
>
|
||
<HeroSection
|
||
styles={styles}
|
||
imageUrl={provider?.imageUrl}
|
||
onBack={() => navigation.goBack()}
|
||
/>
|
||
|
||
<InfoCard
|
||
styles={styles}
|
||
name={resolvedProviderName}
|
||
rating={provider?.rating ?? 4.5}
|
||
deliveryTime={provider?.deliveryTime ?? '25 min'}
|
||
tag={provider?.tag ?? 'Italian'}
|
||
/>
|
||
|
||
<OffersRow styles={styles} />
|
||
|
||
<View style={styles.sectionDivider} />
|
||
|
||
<View style={styles.menuHeaderRow}>
|
||
<Text style={styles.menuTitle}>Menu</Text>
|
||
<Text style={styles.menuCount}>{catalog.length} items</Text>
|
||
</View>
|
||
|
||
<CategoryTabs
|
||
styles={styles}
|
||
categories={categories}
|
||
activeCategory={activeCategory}
|
||
onSelect={setActiveCategory}
|
||
/>
|
||
|
||
<View style={styles.menuList}>
|
||
{filteredItems.length === 0 && (
|
||
<View style={styles.emptyMenuWrap}>
|
||
<Text style={styles.emptyMenuEmoji}>🍽️</Text>
|
||
<Text style={styles.emptyMenuText}>
|
||
No items in this category yet
|
||
</Text>
|
||
</View>
|
||
)}
|
||
|
||
{filteredItems.map(item => (
|
||
<CatalogItemRow
|
||
key={item.id}
|
||
imageUrl={item.imageUrl}
|
||
title={item.title}
|
||
description={item.description}
|
||
price={item.price}
|
||
quantity={getItemQuantity(item.id)}
|
||
onAdd={() => handleAddItem(item)}
|
||
onRemove={() => {}}
|
||
/>
|
||
))}
|
||
</View>
|
||
</ScrollView>
|
||
|
||
{totalItems > 0 && (
|
||
<CartStrip
|
||
styles={styles}
|
||
totalItems={totalItems}
|
||
totalPrice={totalPrice}
|
||
onPress={() => navigation.navigate('CartScreen')}
|
||
/>
|
||
)}
|
||
</View>
|
||
);
|
||
};
|