Compare commits
3 Commits
c8e8ea66d0
...
befa166ef5
Author | SHA1 | Date | |
---|---|---|---|
befa166ef5 | |||
83346c7ba5 | |||
b77ccc6c50 |
@ -3,12 +3,15 @@ import { Stack } from 'expo-router';
|
|||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { CartProvider } from './context/cartContext'
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
|
<CartProvider>
|
||||||
<Stack screenOptions={{ headerShown: false}} />
|
<Stack screenOptions={{ headerShown: false}} />
|
||||||
|
</CartProvider>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
);
|
);
|
||||||
|
107
app/components/CartItem.tsx
Normal file
107
app/components/CartItem.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Text, StyleSheet, Image } from 'react-native';
|
||||||
|
import COLORS from "@/app/constants/colors";
|
||||||
|
import { CartItem as CartItemType } from '@/app/constants/types';
|
||||||
|
import QuantityControl from '@/app/components/QuantityControl';
|
||||||
|
import Animated, {
|
||||||
|
useAnimatedStyle,
|
||||||
|
withTiming,
|
||||||
|
useSharedValue,
|
||||||
|
runOnJS
|
||||||
|
} from 'react-native-reanimated';
|
||||||
|
|
||||||
|
interface CartItemProps {
|
||||||
|
item: CartItemType;
|
||||||
|
onUpdateQuantity: (id: string, quantity: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CartItem: React.FC<CartItemProps> = ({ item, onUpdateQuantity }) => {
|
||||||
|
const opacity = useSharedValue(1);
|
||||||
|
const scale = useSharedValue(1);
|
||||||
|
|
||||||
|
const animatedStyle = useAnimatedStyle(() => {
|
||||||
|
return {
|
||||||
|
opacity: opacity.value,
|
||||||
|
transform: [{ scale: scale.value }]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleIncrease = () => {
|
||||||
|
scale.value = withTiming(1.02, { duration: 100 }, () => {
|
||||||
|
scale.value = withTiming(1, { duration: 100 });
|
||||||
|
});
|
||||||
|
onUpdateQuantity(item.id, item.quantity + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDecrease = () => {
|
||||||
|
if (item.quantity > 0) {
|
||||||
|
scale.value = withTiming(0.98, { duration: 100 }, () => {
|
||||||
|
scale.value = withTiming(1, { duration: 100 });
|
||||||
|
});
|
||||||
|
onUpdateQuantity(item.id, item.quantity - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View style={[styles.container, animatedStyle]}>
|
||||||
|
<View style={styles.imageContainer}>
|
||||||
|
<Image source={{ uri: item.image }} style={styles.image} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.contentContainer}>
|
||||||
|
<Text style={styles.name}>{item.name}</Text>
|
||||||
|
|
||||||
|
<View style={styles.controlRow}>
|
||||||
|
<QuantityControl
|
||||||
|
quantity={item.quantity}
|
||||||
|
onIncrease={handleIncrease}
|
||||||
|
onDecrease={handleDecrease}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text style={styles.price}>{item.price}{item.priceUnit}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
backgroundColor: COLORS.secondary,
|
||||||
|
borderRadius: 12,
|
||||||
|
marginBottom: 5,
|
||||||
|
padding: 4,
|
||||||
|
},
|
||||||
|
imageContainer: {
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: 8,
|
||||||
|
overflow: 'hidden',
|
||||||
|
marginRight: 2,
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
contentContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: COLORS.text,
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
controlRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: COLORS.secondary,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default CartItem;
|
@ -1,20 +1,21 @@
|
|||||||
import { View, Text, Image, StyleSheet, TouchableOpacity } from 'react-native';
|
import { View, Text, Image, StyleSheet, TouchableOpacity } from 'react-native';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { ChevronRight } from 'lucide-react-native';
|
import { ChevronRight } from 'lucide-react-native';
|
||||||
import COLORS from "@/app/constants/colors";
|
import COLORS from '@/app/constants/colors';
|
||||||
|
import { Product } from '../constants/types';
|
||||||
|
|
||||||
interface ProductCardProps {
|
interface ProductCardProps {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
productName: string;
|
||||||
image: string;
|
image: string;
|
||||||
description: string;
|
description: string;
|
||||||
price: number;
|
price: string;
|
||||||
inStock?: boolean;
|
inStock?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProductCard({
|
export default function ProductCard({
|
||||||
id,
|
id,
|
||||||
name,
|
productName,
|
||||||
image,
|
image,
|
||||||
description,
|
description,
|
||||||
price,
|
price,
|
||||||
@ -22,27 +23,40 @@ export default function ProductCard({
|
|||||||
}: ProductCardProps) {
|
}: ProductCardProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.card}
|
style={styles.card}
|
||||||
|
onPress={() =>
|
||||||
|
router.push({
|
||||||
|
pathname: '/screens/user/ProductDetailsScreen', // Absolute path for reliability
|
||||||
|
params: { productId: id },
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Image source={{ uri: image }} style={styles.image} />
|
<Image
|
||||||
|
source={{ uri: image }}
|
||||||
|
style={styles.image}
|
||||||
|
resizeMode="cover"
|
||||||
|
onError={(error) => console.log('Image loading error:', error.nativeEvent.error)}
|
||||||
|
/>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<View>
|
<View>
|
||||||
<Text style={styles.title}>{name}</Text>
|
|
||||||
|
<Text style={styles.title}>{productName}</Text>
|
||||||
<Text style={styles.blend}>
|
<Text style={styles.blend}>
|
||||||
{description}
|
{description}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.rightContent}>
|
<View style={styles.rightContent}>
|
||||||
<ChevronRight size={20} color="#666" />
|
<ChevronRight size={20} color="#666" />
|
||||||
<Text style={[styles.stock, { color: inStock ? '#22C55E' : '#EF4444' }]}>
|
<Text style={[styles.stock, { color: inStock ? '#22C55E' : '#EF4444' }]}>
|
||||||
{inStock ? 'En Stock' : 'Hors Stock'}
|
{inStock ? 'En Stock' : 'Hors Stock'}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.priceContainer}>
|
<View style={styles.priceContainer}>
|
||||||
<Text style={styles.price}>{price}</Text>
|
<Text style={styles.price}>{price}</Text>
|
||||||
<Text style={styles.unit}>TND/kg</Text>
|
<Text style={styles.unit}>TND/kg</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@ -70,6 +84,7 @@ const styles = StyleSheet.create({
|
|||||||
width: 75,
|
width: 75,
|
||||||
height: 120,
|
height: 120,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
|
backgroundColor: '#eee', // Fallback while loading
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@ -81,7 +96,7 @@ const styles = StyleSheet.create({
|
|||||||
title: {
|
title: {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: '#333',
|
color: '#000',
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
blend: {
|
blend: {
|
||||||
@ -92,12 +107,12 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'flex-end',
|
alignItems: 'flex-end',
|
||||||
height: 110, // Assure un espacement vertical régulier
|
height: 110,
|
||||||
},
|
},
|
||||||
priceContainer: {
|
priceContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'baseline',
|
alignItems: 'baseline',
|
||||||
},
|
},
|
||||||
price: {
|
price: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
@ -111,6 +126,5 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
stock: {
|
stock: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#22C55E',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
86
app/components/QuantityControl.tsx
Normal file
86
app/components/QuantityControl.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Text, TouchableOpacity, StyleSheet, Platform } from 'react-native';
|
||||||
|
import COLORS from "@/app/constants/colors";
|
||||||
|
import { Minus, Plus } from 'lucide-react-native';
|
||||||
|
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated';
|
||||||
|
import * as Haptics from 'expo-haptics';
|
||||||
|
|
||||||
|
interface QuantityControlProps {
|
||||||
|
quantity: number;
|
||||||
|
onIncrease: () => void;
|
||||||
|
onDecrease: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QuantityControl: React.FC<QuantityControlProps> = ({
|
||||||
|
quantity,
|
||||||
|
onIncrease,
|
||||||
|
onDecrease
|
||||||
|
}) => {
|
||||||
|
const animatedTextStyle = useAnimatedStyle(() => {
|
||||||
|
return {
|
||||||
|
opacity: 1,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleIncrease = () => {
|
||||||
|
onIncrease();
|
||||||
|
if (Platform.OS !== 'web') {
|
||||||
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDecrease = () => {
|
||||||
|
if (quantity > 0) {
|
||||||
|
onDecrease();
|
||||||
|
if (Platform.OS !== 'web') {
|
||||||
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.button, { opacity: quantity <= 0 ? 0.5 : 1 }]}
|
||||||
|
onPress={handleDecrease}
|
||||||
|
disabled={quantity <= 0}
|
||||||
|
>
|
||||||
|
<Minus size={16} color={COLORS.text} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<Animated.Text style={[styles.quantity, animatedTextStyle]}>
|
||||||
|
{quantity}
|
||||||
|
</Animated.Text>
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.button} onPress={handleIncrease}>
|
||||||
|
<Plus size={16} color={COLORS.text} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: 36,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
backgroundColor: COLORS.secondary,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 6,
|
||||||
|
},
|
||||||
|
quantity: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: COLORS.text,
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
minWidth: 40,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default QuantityControl;
|
21
app/constants/types.ts
Normal file
21
app/constants/types.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export interface Product {
|
||||||
|
id: string;
|
||||||
|
productName: string;
|
||||||
|
image: string;
|
||||||
|
description: string;
|
||||||
|
price: string;
|
||||||
|
inStock: boolean;
|
||||||
|
}
|
||||||
|
export interface CartItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
priceUnit: string;
|
||||||
|
quantity: number;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CartState {
|
||||||
|
items: CartItem[];
|
||||||
|
total: number;
|
||||||
|
}
|
59
app/context/cartContext.tsx
Normal file
59
app/context/cartContext.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// context/CartContext.tsx
|
||||||
|
import React, { createContext, useState, useContext, ReactNode } from 'react';
|
||||||
|
|
||||||
|
type CartItem = {
|
||||||
|
productName: string;
|
||||||
|
quantity: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CartState = {
|
||||||
|
items: CartItem[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CartContextType = {
|
||||||
|
cart: CartState;
|
||||||
|
addItem: (item: CartItem) => void;
|
||||||
|
clearCart: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CartContext = createContext<CartContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const CartProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [cart, setCart] = useState<CartState>({ items: [], total: 0 });
|
||||||
|
|
||||||
|
const addItem = (item: CartItem) => {
|
||||||
|
setCart(prev => {
|
||||||
|
const existing = prev.items.find(i => i.productName === item.productName);
|
||||||
|
let updatedItems;
|
||||||
|
if (existing) {
|
||||||
|
updatedItems = prev.items.map(i =>
|
||||||
|
i.productName === item.productName
|
||||||
|
? { ...i, quantity: i.quantity + item.quantity }
|
||||||
|
: i
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
updatedItems = [...prev.items, item];
|
||||||
|
}
|
||||||
|
const updatedTotal = updatedItems.reduce((sum, i) => sum + i.quantity, 0);
|
||||||
|
return { items: updatedItems, total: updatedTotal };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearCart = () => setCart({ items: [], total: 0 });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CartContext.Provider value={{ cart, addItem, clearCart }}>
|
||||||
|
{children}
|
||||||
|
</CartContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCart = () => {
|
||||||
|
const context = useContext(CartContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useCart must be used within a CartProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
@ -10,7 +10,6 @@ import COLORS from "@/app/constants/colors";
|
|||||||
|
|
||||||
|
|
||||||
const SignInScreen = () => {
|
const SignInScreen = () => {
|
||||||
const [step, setStep] = useState(1);
|
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
|
@ -1,46 +1,131 @@
|
|||||||
import { View, Text, StyleSheet, ScrollView,TouchableOpacity } from 'react-native';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { useRouter } from 'expo-router';
|
||||||
import { ArrowLeft } from 'lucide-react-native';
|
import { View, Text, Button, Alert, StyleSheet } from 'react-native';
|
||||||
import { router } from "expo-router";
|
import { getAuth } from 'firebase/auth';
|
||||||
|
import { collection, addDoc } from 'firebase/firestore'; // Make sure these are imported
|
||||||
|
import { db } from '@/firebase/config'; // Ensure this path is correct
|
||||||
|
import { useCart } from '@/app/context/cartContext';
|
||||||
|
|
||||||
|
const CartScreen = () => {
|
||||||
|
const { cart, clearCart } = useCart();
|
||||||
|
const auth = getAuth();
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Check if the user is logged in when the component mounts
|
||||||
|
useEffect(() => {
|
||||||
|
const user = auth.currentUser;
|
||||||
|
if (user) {
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
} else {
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
router.push('../screens/auth/SignInScreen'); // Redirect to sign-in screen if not logged in
|
||||||
|
}
|
||||||
|
}, [auth, router]);
|
||||||
|
|
||||||
|
const handleConfirmOrder = async () => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
Alert.alert('Erreur', 'Utilisateur non connecté.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cart.total <= 0) {
|
||||||
|
Alert.alert('Panier vide', 'Veuillez ajouter des produits à votre panier');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userId = auth.currentUser?.uid;
|
||||||
|
if (!userId) {
|
||||||
|
Alert.alert('Erreur', 'Utilisateur non connecté.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderData: any = {
|
||||||
|
userId,
|
||||||
|
status: 'En attente',
|
||||||
|
};
|
||||||
|
|
||||||
|
cart.items.forEach((item, index) => {
|
||||||
|
orderData[`item${index + 1}`] = [item.quantity, item.productName];
|
||||||
|
});
|
||||||
|
|
||||||
|
await addDoc(collection(db, 'orders'), orderData);
|
||||||
|
|
||||||
|
Alert.alert('Commande confirmée', 'Votre commande a été enregistrée.');
|
||||||
|
clearCart();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur de commande', error);
|
||||||
|
Alert.alert('Erreur', "Impossible d'enregistrer la commande.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Text>Chargement...</Text>; // Loading state while checking authentication
|
||||||
|
}
|
||||||
|
|
||||||
export default function CartScreen() {
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.header}>
|
<Text style={styles.title}>Votre Panier</Text>
|
||||||
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
|
|
||||||
<ArrowLeft size={24} color="#666" />
|
{cart.items.length === 0 ? (
|
||||||
</TouchableOpacity>
|
<Text style={styles.empty}>Votre panier est vide.</Text>
|
||||||
<Text style={styles.headerTitle}>Mon Panier</Text>
|
) : (
|
||||||
|
cart.items.map((item, index) => (
|
||||||
|
<View key={index} style={styles.itemContainer}>
|
||||||
|
<Text style={styles.itemText}>
|
||||||
|
{item.productName} - Quantité : {item.quantity}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cart.items.length > 0 && (
|
||||||
|
<View style={styles.buttonContainer}>
|
||||||
|
<Button title="Confirmer la commande" onPress={handleConfirmOrder} color="#8B4513" />
|
||||||
|
<View style={{ marginTop: 10 }}>
|
||||||
|
<Button title="Vider le panier" onPress={clearCart} color="#A52A2A" />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
)}
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
padding: 20,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#FFF',
|
||||||
|
flexGrow: 1,
|
||||||
},
|
},
|
||||||
header: {
|
title: {
|
||||||
height: 60,
|
fontSize: 22,
|
||||||
justifyContent: 'center',
|
fontWeight: 'bold',
|
||||||
alignItems: 'center',
|
marginBottom: 15,
|
||||||
position: 'relative',
|
textAlign: 'center',
|
||||||
backgroundColor: '#fff',
|
color: '#5A3E36',
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: '#eee',
|
|
||||||
marginTop:5,
|
|
||||||
},
|
},
|
||||||
backButton: {
|
empty: {
|
||||||
position: 'absolute',
|
fontSize: 16,
|
||||||
left: 20,
|
color: '#777',
|
||||||
top: '50%',
|
textAlign: 'center',
|
||||||
transform: [{ translateY: -12 }],
|
marginTop: 50,
|
||||||
},
|
},
|
||||||
headerTitle: {
|
itemContainer: {
|
||||||
fontSize: 20,
|
backgroundColor: '#F5F5DC',
|
||||||
fontWeight: '600',
|
padding: 15,
|
||||||
|
marginBottom: 10,
|
||||||
|
borderRadius: 10,
|
||||||
|
borderColor: '#DDD',
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
itemText: {
|
||||||
|
fontSize: 16,
|
||||||
color: '#333',
|
color: '#333',
|
||||||
},
|
},
|
||||||
});
|
buttonContainer: {
|
||||||
|
marginTop: 30,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default CartScreen;
|
||||||
|
@ -1,47 +1,33 @@
|
|||||||
import { View, Text, StyleSheet, ScrollView,TouchableOpacity } from 'react-native';
|
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { ArrowLeft } from 'lucide-react-native';
|
import { ArrowLeft } from 'lucide-react-native';
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import ProductCard from '../../components/ProductCard';
|
import ProductCard from '../../components/ProductCard';
|
||||||
import COLORS from '@/app/constants/colors';
|
import COLORS from '@/app/constants/colors';
|
||||||
|
import { collection, getDocs } from 'firebase/firestore';
|
||||||
const products = [
|
import { useEffect, useState } from 'react';
|
||||||
{
|
import { db } from '@/firebase/config';
|
||||||
id: '1',
|
import { Product } from '@/app/constants/types';
|
||||||
name: 'Café Brix Premium',
|
|
||||||
image: 'https://images.unsplash.com/photo-1559056199-641a0ac8b55e?w=200&h=300&fit=crop',
|
|
||||||
description: '50% robusta • 50% arabica',
|
|
||||||
price: 10,
|
|
||||||
inStock: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: 'Café Brix Gold',
|
|
||||||
image: 'https://images.unsplash.com/photo-1587734361993-0275d60eb3d0?w=200&h=300&fit=crop',
|
|
||||||
description: '30% robusta • 70% arabica',
|
|
||||||
price: 12,
|
|
||||||
inStock: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: 'Café Brix Prestige',
|
|
||||||
image: 'https://images.unsplash.com/photo-1559525839-b184a4d698c7?w=200&h=300&fit=crop',
|
|
||||||
description: '70% robusta • 30% arabica',
|
|
||||||
price: 11,
|
|
||||||
inStock: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
name: 'Café Brix Turc',
|
|
||||||
image: 'https://images.unsplash.com/photo-1562447457-579fc34967fb?w=200&h=300&fit=crop',
|
|
||||||
description: '50% robusta • 50% arabica',
|
|
||||||
price: 10,
|
|
||||||
inStock: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function GrainsScreen() {
|
export default function GrainsScreen() {
|
||||||
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchProducts = async () => {
|
||||||
|
try {
|
||||||
|
const querySnapshot = await getDocs(collection(db, 'products'));
|
||||||
|
const items: Product[] = querySnapshot.docs.map(doc => ({
|
||||||
|
id: doc.id,
|
||||||
|
...(doc.data() as Omit<Product, 'id'>),
|
||||||
|
}));
|
||||||
|
setProducts(items);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération des produits :", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchProducts();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
@ -49,7 +35,7 @@ export default function GrainsScreen() {
|
|||||||
<ArrowLeft size={24} color="#666" />
|
<ArrowLeft size={24} color="#666" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.headerTitle}>Grains de café</Text>
|
<Text style={styles.headerTitle}>Grains de café</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView style={styles.content}>
|
<ScrollView style={styles.content}>
|
||||||
{products.map((product) => (
|
{products.map((product) => (
|
||||||
@ -73,7 +59,7 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#eee',
|
borderBottomColor: '#eee',
|
||||||
marginTop:5,
|
marginTop: 5,
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@ -88,7 +74,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: 8,
|
padding: 28,
|
||||||
backgroundColor:COLORS.background_user ,
|
backgroundColor: COLORS.background_user,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
291
app/screens/user/ProductDetailsScreen.tsx
Normal file
291
app/screens/user/ProductDetailsScreen.tsx
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { View, Text, StyleSheet, Image, TouchableOpacity, ActivityIndicator,Alert } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||||
|
import { ChevronLeft, Minus, Plus } from 'lucide-react-native';
|
||||||
|
import { doc, getDoc } from 'firebase/firestore';
|
||||||
|
import { db } from '@/firebase/config';
|
||||||
|
import { useCart } from '@/app/context/cartContext';
|
||||||
|
import { Product } from '@/app/constants/types';
|
||||||
|
|
||||||
|
export default function ProductScreen() {
|
||||||
|
const { productId } = useLocalSearchParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const [product, setProduct] = useState<Product | null>(null);
|
||||||
|
const [quantity, setQuantity] = useState(1);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Cart items state
|
||||||
|
const [cartItems, setCartItems] = useState<{ productName: string; quantity: number }[]>([]);
|
||||||
|
|
||||||
|
const fetchProductDetails = async () => {
|
||||||
|
if (!productId) {
|
||||||
|
setError('Aucun ID de produit fourni.');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const productDoc = await getDoc(doc(db, 'products', productId as string));
|
||||||
|
if (productDoc.exists()) {
|
||||||
|
setProduct({
|
||||||
|
id: productDoc.id,
|
||||||
|
...(productDoc.data() as Omit<Product, 'id'>),
|
||||||
|
});
|
||||||
|
setQuantity(1);
|
||||||
|
} else {
|
||||||
|
setError('Produit non trouvé.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la récupération du produit:', error);
|
||||||
|
setError('Impossible de charger le produit. Veuillez réessayer.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProductDetails();
|
||||||
|
}, [productId]);
|
||||||
|
|
||||||
|
// Add item to cart
|
||||||
|
const { addItem } = useCart();
|
||||||
|
|
||||||
|
const handleAddToCart = () => {
|
||||||
|
if (!product) return;
|
||||||
|
|
||||||
|
addItem({
|
||||||
|
productName: product?.productName,
|
||||||
|
quantity: quantity,
|
||||||
|
});
|
||||||
|
|
||||||
|
Alert.alert('Ajouté', `${quantity} ${product.productName} ajouté au panier`);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity onPress={() => router.back()}>
|
||||||
|
<ChevronLeft size={24} color="#333" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.title}>{product?.productName || 'Détails du produit'}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
{loading ? (
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<ActivityIndicator size="large" color="#B07B4F" />
|
||||||
|
<Text style={styles.loadingText}>Chargement...</Text>
|
||||||
|
</View>
|
||||||
|
) : error ? (
|
||||||
|
<View style={styles.errorContainer}>
|
||||||
|
<Text style={styles.errorText}>{error}</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.retryButton}
|
||||||
|
onPress={() => {
|
||||||
|
setLoading(true);
|
||||||
|
fetchProductDetails();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={styles.retryButtonText}>Réessayer</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
) : !product ? (
|
||||||
|
<View style={styles.errorContainer}>
|
||||||
|
<Text style={styles.errorText}>Produit non trouvé.</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<View style={styles.content}>
|
||||||
|
<View style={styles.imageContainer}>
|
||||||
|
<Image
|
||||||
|
source={{ uri: product.image }}
|
||||||
|
style={styles.image}
|
||||||
|
resizeMode="cover"
|
||||||
|
onError={(e) => console.log('Image error:', e.nativeEvent.error)}
|
||||||
|
/>
|
||||||
|
{product.inStock && <Text style={styles.stock}>En Stock</Text>}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.details}>
|
||||||
|
<Text style={styles.name}>{product.productName}</Text>
|
||||||
|
<Text style={styles.blend}>{product.description}</Text>
|
||||||
|
|
||||||
|
<View style={styles.quantityContainer}>
|
||||||
|
<Text style={styles.quantityLabel}>Quantité</Text>
|
||||||
|
<View style={styles.quantityControls}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.quantityButton}
|
||||||
|
onPress={() => setQuantity(Math.max(1, quantity - 1))}
|
||||||
|
>
|
||||||
|
<Minus size={20} color="#666" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.quantity}>{quantity}</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.quantityButton}
|
||||||
|
onPress={() => setQuantity(quantity + 1)}
|
||||||
|
>
|
||||||
|
<Plus size={20} color="#666" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<TouchableOpacity style={styles.addButton} onPress={handleAddToCart}>
|
||||||
|
<Text style={styles.addButtonText}>Ajouter au panier</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#F8F9FA',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 20,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#eee',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#333',
|
||||||
|
marginLeft: 12,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
imageContainer: {
|
||||||
|
position: 'relative',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: 20,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
width: 200,
|
||||||
|
height: 300,
|
||||||
|
borderRadius: 12,
|
||||||
|
backgroundColor: '#eee', // Fallback while loading
|
||||||
|
},
|
||||||
|
stock: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 12,
|
||||||
|
right: 12,
|
||||||
|
backgroundColor: '#22C55E',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 12,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
details: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#333',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
blend: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#666',
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
quantityContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
quantityLabel: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#333',
|
||||||
|
},
|
||||||
|
quantityControls: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
quantityButton: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
backgroundColor: '#F1F3F5',
|
||||||
|
borderRadius: 20,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
quantity: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginHorizontal: 16,
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
padding: 20,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: '#eee',
|
||||||
|
},
|
||||||
|
addButton: {
|
||||||
|
backgroundColor: '#B07B4F',
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
addButtonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
loadingContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
marginTop: 10,
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#666',
|
||||||
|
},
|
||||||
|
errorContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#EF4444',
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
retryButton: {
|
||||||
|
backgroundColor: '#B07B4F',
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
retryButtonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
});
|
@ -1,7 +1,9 @@
|
|||||||
import { View, Text, StyleSheet, ScrollView, Image,Button } from 'react-native';
|
import { View, Text, StyleSheet, Image, } from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
import { TouchableOpacity } from 'react-native-gesture-handler';
|
import { TouchableOpacity } from 'react-native-gesture-handler';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
|
import { BellRing } from 'lucide-react-native';
|
||||||
|
import COLORS from '@/app/constants/colors';
|
||||||
|
|
||||||
|
|
||||||
const UserHomeScreen = () => {
|
const UserHomeScreen = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -13,6 +15,11 @@ const UserHomeScreen = () => {
|
|||||||
<Text style={styles.welcome}>Bienvenue,</Text>
|
<Text style={styles.welcome}>Bienvenue,</Text>
|
||||||
<Text style={styles.shopName}>Nom de Café</Text>
|
<Text style={styles.shopName}>Nom de Café</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<View style={styles.notification}>
|
||||||
|
<TouchableOpacity>
|
||||||
|
<BellRing size={24} color="#000" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.sectionTitle}>Nos Produits</Text>
|
<Text style={styles.sectionTitle}>Nos Produits</Text>
|
||||||
@ -23,14 +30,14 @@ const UserHomeScreen = () => {
|
|||||||
onPress={() => router.push('/screens/user/GrainsScreen')}>
|
onPress={() => router.push('/screens/user/GrainsScreen')}>
|
||||||
|
|
||||||
<Image
|
<Image
|
||||||
source={require('../../../assets/images/coffee_cup.jpg')} style={styles.categoryImage}
|
source={require('../../../assets/images/grains.jpg')} style={styles.categoryImage}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.categoryTitle}>Grains de café</Text>
|
<Text style={styles.categoryTitle}>Grains de café</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity style={styles.categoryCard}>
|
<TouchableOpacity style={styles.categoryCard}>
|
||||||
<Image
|
<Image
|
||||||
source={require('../../../assets/images/coffee_cup.jpg')} style={styles.categoryImage}
|
source={require('../../../assets/images/material.jpg')} style={styles.categoryImage}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.categoryTitle}>Matériels</Text>
|
<Text style={styles.categoryTitle}>Matériels</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@ -48,34 +55,50 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: 20,
|
padding: 15,
|
||||||
|
marginTop:26,
|
||||||
|
borderBottomWidth:0.2,
|
||||||
|
borderColor:'#999',
|
||||||
},
|
},
|
||||||
welcome: {
|
welcome: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: '#666',
|
color: '#666',
|
||||||
},
|
},
|
||||||
shopName: {
|
shopName: {
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: '#333',
|
|
||||||
},
|
|
||||||
avatar: {
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
borderRadius: 20,
|
|
||||||
},
|
|
||||||
sectionTitle: {
|
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: '600',
|
fontWeight: '700',
|
||||||
color: '#333',
|
color: '#333',
|
||||||
marginHorizontal: 20,
|
},
|
||||||
marginBottom: 16,
|
notification:{
|
||||||
|
backgroundColor:COLORS.secondary,
|
||||||
|
padding:8,
|
||||||
|
borderRadius:8,
|
||||||
|
borderColor:'#000',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 5,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.2,
|
||||||
|
shadowRadius: 10,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: '900',
|
||||||
|
color: '#666',
|
||||||
|
alignSelf:'center',
|
||||||
|
marginTop:10,
|
||||||
|
|
||||||
},
|
},
|
||||||
categories: {
|
categories: {
|
||||||
padding: 20,
|
flex: 1,
|
||||||
|
padding: 15,
|
||||||
|
justifyContent: 'space-evenly',
|
||||||
},
|
},
|
||||||
categoryCard: {
|
categoryCard: {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: COLORS.secondary,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@ -89,13 +112,17 @@ const styles = StyleSheet.create({
|
|||||||
elevation: 3,
|
elevation: 3,
|
||||||
},
|
},
|
||||||
categoryImage: {
|
categoryImage: {
|
||||||
width: '100%',
|
width: '80%',
|
||||||
|
alignSelf: 'center',
|
||||||
height: 160,
|
height: 160,
|
||||||
|
marginTop: 15,
|
||||||
|
borderRadius:16,
|
||||||
},
|
},
|
||||||
categoryTitle: {
|
categoryTitle: {
|
||||||
fontSize: 16,
|
fontSize: 20,
|
||||||
fontWeight: '600',
|
alignSelf:'center',
|
||||||
color: '#333',
|
fontWeight: '800',
|
||||||
|
color: '#000',
|
||||||
padding: 16,
|
padding: 16,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -43,7 +43,13 @@ export default function UserLayout() {
|
|||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="GrainsScreen"
|
name="GrainsScreen"
|
||||||
options={{
|
options={{
|
||||||
href: null, // 👈 not shown in bottom nav, but still navigable
|
href: null,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="ProductDetailsScreen"
|
||||||
|
options={{
|
||||||
|
href: null,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
22
app/utils/cartUtils.tsx
Normal file
22
app/utils/cartUtils.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { CartItem } from '@/app/constants/types';
|
||||||
|
|
||||||
|
export const calculateTotal = (items: CartItem[]): number => {
|
||||||
|
return items.reduce((total, item) => {
|
||||||
|
return total + item.price * item.quantity;
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateItemQuantity = (
|
||||||
|
items: CartItem[],
|
||||||
|
itemId: string,
|
||||||
|
newQuantity: number
|
||||||
|
): CartItem[] => {
|
||||||
|
// Don't allow negative quantities
|
||||||
|
if (newQuantity < 0) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.map((item) =>
|
||||||
|
item.id === itemId ? { ...item, quantity: newQuantity } : item
|
||||||
|
);
|
||||||
|
};
|
BIN
assets/fonts/Inter-VariableFont_opsz,wght.ttf
Normal file
BIN
assets/fonts/Inter-VariableFont_opsz,wght.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Sora-VariableFont_wght.ttf
Normal file
BIN
assets/fonts/Sora-VariableFont_wght.ttf
Normal file
Binary file not shown.
BIN
assets/images/grains.jpg
Normal file
BIN
assets/images/grains.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
BIN
assets/images/material.jpg
Normal file
BIN
assets/images/material.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 102 KiB |
Loading…
Reference in New Issue
Block a user