Initial commit
Generated by create-expo-app 3.2.0.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { StyleSheet, Text, View, TouchableOpacity } from "react-native";
|
||||
import React from "react";
|
||||
import useCartStore from "@/store/cartStore";
|
||||
import { COLORS } from "@/utils/colors";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Link } from "expo-router";
|
||||
|
||||
const CardButton = () => {
|
||||
const { count } = useCartStore();
|
||||
return (
|
||||
<Link href="/cart" asChild>
|
||||
<TouchableOpacity onPress={() => {}}>
|
||||
{count > 0 && (
|
||||
<View style={styles.countContainer}>
|
||||
<Text style={styles.countText}>{count}</Text>
|
||||
</View>
|
||||
)}
|
||||
<Ionicons name="cart" size={28} color="black" />
|
||||
</TouchableOpacity>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardButton;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
countContainer: {
|
||||
position: "absolute",
|
||||
right: -10,
|
||||
bottom: -5,
|
||||
backgroundColor: COLORS.primary,
|
||||
borderRadius: 10,
|
||||
zIndex: 1,
|
||||
width: 20,
|
||||
height: 20,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
countText: {
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
color: COLORS.white,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,189 @@
|
||||
import { Product } from "@/utils/api";
|
||||
import { Image } from "expo-image";
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
||||
import useCartStore from "@/store/cartStore";
|
||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||
import { COLORS } from "@/utils/colors";
|
||||
import { useRef } from "react";
|
||||
import Reanimated, {
|
||||
SharedValue,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withSequence,
|
||||
withSpring,
|
||||
Easing,
|
||||
withTiming,
|
||||
} from "react-native-reanimated";
|
||||
|
||||
import ReanimatedSwipeable, {
|
||||
SwipeableMethods,
|
||||
} from "react-native-gesture-handler/ReanimatedSwipeable";
|
||||
|
||||
interface CartItemProps {
|
||||
item: Product & { quantity: number };
|
||||
}
|
||||
|
||||
const LeftActions = (
|
||||
progress: SharedValue<number>,
|
||||
dragX: SharedValue<number>,
|
||||
onShouldDelete: () => void
|
||||
) => {
|
||||
const styleAnimation = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [{ translateX: dragX.value - 100 }],
|
||||
};
|
||||
});
|
||||
return (
|
||||
<Reanimated.View style={styleAnimation}>
|
||||
<TouchableOpacity style={styles.leftAction} onPress={onShouldDelete}>
|
||||
<Ionicons name="trash" size={24} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
</Reanimated.View>
|
||||
);
|
||||
};
|
||||
|
||||
const CartItem = ({ item }: CartItemProps) => {
|
||||
const { addProduct, reduceProduct } = useCartStore();
|
||||
const reanimatedRef = useRef<SwipeableMethods>(null);
|
||||
const opacityAnim = useSharedValue(1);
|
||||
const scaleAnim = useSharedValue(1);
|
||||
const heightAnim = useSharedValue(80);
|
||||
|
||||
const handleQuantityChanged = (type: "increment" | "decrement") => {
|
||||
if (type === "increment") {
|
||||
addProduct(item);
|
||||
} else {
|
||||
reduceProduct(item);
|
||||
}
|
||||
|
||||
scaleAnim.value = withSequence(
|
||||
withSpring(1.2, { damping: 2, stiffness: 80 }),
|
||||
withSpring(1, { damping: 2, stiffness: 80 })
|
||||
);
|
||||
};
|
||||
|
||||
const onShouldDelete = async () => {
|
||||
opacityAnim.value = withTiming(0, {
|
||||
duration: 300,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
});
|
||||
|
||||
heightAnim.value = withTiming(0, {
|
||||
duration: 300,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
reanimatedRef.current?.close();
|
||||
for (let i = 0; i < item.quantity; i++) {
|
||||
reduceProduct(item);
|
||||
}
|
||||
};
|
||||
|
||||
const quantityAnimatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [{ scale: scaleAnim.value }],
|
||||
};
|
||||
});
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: opacityAnim.value,
|
||||
height: heightAnim.value,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Reanimated.View style={animatedStyle}>
|
||||
<ReanimatedSwipeable
|
||||
ref={reanimatedRef}
|
||||
leftThreshold={50}
|
||||
friction={2}
|
||||
containerStyle={styles.swipeable}
|
||||
renderLeftActions={(progress, dragX) =>
|
||||
LeftActions(progress, dragX, onShouldDelete)
|
||||
}
|
||||
>
|
||||
<View style={styles.cartItemContainer}>
|
||||
<Image source={{ uri: item.image }} style={styles.image} />
|
||||
<View style={styles.itemContainer}>
|
||||
<Text style={styles.cartItemName} numberOfLines={2}>
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text>Price: ${item.price}</Text>
|
||||
</View>
|
||||
<View style={styles.quantityContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => handleQuantityChanged("decrement")}
|
||||
style={styles.quantityButton}
|
||||
>
|
||||
<Ionicons name="remove" size={24} color="black" />
|
||||
</TouchableOpacity>
|
||||
<Reanimated.Text
|
||||
style={[styles.cartItemQuantity, quantityAnimatedStyle]}
|
||||
>
|
||||
{item.quantity}
|
||||
</Reanimated.Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => handleQuantityChanged("increment")}
|
||||
style={styles.quantityButton}
|
||||
>
|
||||
<Ionicons name="add" size={24} color="black" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ReanimatedSwipeable>
|
||||
</Reanimated.View>
|
||||
);
|
||||
};
|
||||
export default CartItem;
|
||||
const styles = StyleSheet.create({
|
||||
cartItemContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 20,
|
||||
backgroundColor: "#fff",
|
||||
height: 80,
|
||||
},
|
||||
image: {
|
||||
width: 50,
|
||||
height: "100%",
|
||||
borderRadius: 10,
|
||||
resizeMode: "contain",
|
||||
},
|
||||
itemContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
cartItemName: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
},
|
||||
quantityContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
},
|
||||
quantityButton: {
|
||||
padding: 10,
|
||||
},
|
||||
cartItemQuantity: {
|
||||
fontWeight: "bold",
|
||||
backgroundColor: COLORS.primary,
|
||||
fontSize: 16,
|
||||
padding: 5,
|
||||
width: 30,
|
||||
color: "white",
|
||||
textAlign: "center",
|
||||
},
|
||||
swipeable: {
|
||||
height: 80,
|
||||
},
|
||||
leftAction: {
|
||||
backgroundColor: "red",
|
||||
width: 100,
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Product } from "@/utils/api";
|
||||
import { COLORS } from "@/utils/colors";
|
||||
import { useRouter } from "expo-router";
|
||||
import React from "react";
|
||||
import { Pressable, StyleSheet, Text, Image, View } from "react-native";
|
||||
|
||||
interface ProductCardProps {
|
||||
product: Product;
|
||||
}
|
||||
|
||||
const ProductCard = ({ product }: ProductCardProps) => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.productCard}
|
||||
onPress={() => router.push(`/product/${product.id}`)}
|
||||
>
|
||||
<Image source={{ uri: product.image }} style={styles.image} />
|
||||
<View style={styles.productInfo}>
|
||||
<Text style={styles.title} numberOfLines={2}>
|
||||
{product.title}
|
||||
</Text>
|
||||
<Text style={styles.price}>{product.price}</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductCard;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
productCard: {
|
||||
flex: 1,
|
||||
margin: 0,
|
||||
gap: 8,
|
||||
padding: 12,
|
||||
borderRadius: 16,
|
||||
backgroundColor: "#fff",
|
||||
boxShadow: "0 0 10px rgba(0, 0, 0, 0.1)",
|
||||
},
|
||||
image: {
|
||||
width: "100%",
|
||||
height: 150,
|
||||
borderRadius: 12,
|
||||
},
|
||||
productInfo: {
|
||||
gap: 4,
|
||||
},
|
||||
title: {
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
},
|
||||
price: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
color: COLORS.primary,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { View, StyleSheet } from "react-native";
|
||||
import { createShimmerPlaceholder } from "react-native-shimmer-placeholder";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
|
||||
const ShimmerPlaceholder = createShimmerPlaceholder(LinearGradient);
|
||||
|
||||
export function ProductDetailsShimmer() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ShimmerPlaceholder
|
||||
style={styles.image}
|
||||
shimmerColors={["#ebebeb", "#ddd", "#ebebeb"]}
|
||||
/>
|
||||
<View style={styles.content}>
|
||||
<ShimmerPlaceholder
|
||||
style={styles.title}
|
||||
shimmerColors={["#ebebeb", "#ddd", "#ebebeb"]}
|
||||
/>
|
||||
<ShimmerPlaceholder
|
||||
style={styles.price}
|
||||
shimmerColors={["#ebebeb", "#ddd", "#ebebeb"]}
|
||||
/>
|
||||
<ShimmerPlaceholder
|
||||
style={styles.category}
|
||||
shimmerColors={["#ebebeb", "#ddd", "#ebebeb"]}
|
||||
/>
|
||||
<ShimmerPlaceholder
|
||||
style={styles.description}
|
||||
shimmerColors={["#ebebeb", "#ddd", "#ebebeb"]}
|
||||
/>
|
||||
<ShimmerPlaceholder
|
||||
style={styles.rating}
|
||||
shimmerColors={["#ebebeb", "#ddd", "#ebebeb"]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
image: {
|
||||
width: "100%",
|
||||
height: 400,
|
||||
backgroundColor: "#f9f9f9",
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
gap: 12,
|
||||
},
|
||||
title: {
|
||||
height: 28,
|
||||
width: "70%",
|
||||
borderRadius: 4,
|
||||
},
|
||||
price: {
|
||||
height: 24,
|
||||
width: "20%",
|
||||
borderRadius: 4,
|
||||
},
|
||||
category: {
|
||||
height: 20,
|
||||
width: "40%",
|
||||
borderRadius: 4,
|
||||
},
|
||||
description: {
|
||||
height: 80,
|
||||
width: "100%",
|
||||
borderRadius: 4,
|
||||
},
|
||||
rating: {
|
||||
height: 20,
|
||||
width: "30%",
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { View, StyleSheet, Dimensions } from "react-native";
|
||||
import React from "react";
|
||||
import { createShimmerPlaceholder } from "react-native-shimmer-placeholder";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
|
||||
const Placeholder = createShimmerPlaceholder(LinearGradient);
|
||||
|
||||
const { width } = Dimensions.get("window");
|
||||
const CARD_WIDTH = width * 0.43;
|
||||
|
||||
const ProductShimmer = () => {
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
{/* Image placeholder */}
|
||||
<Placeholder
|
||||
style={styles.image}
|
||||
shimmerColors={["#ebebeb", "#ddd", "#ebebeb"]}
|
||||
/>
|
||||
|
||||
{/* Content container */}
|
||||
<View style={styles.content}>
|
||||
{/* Title placeholder */}
|
||||
<Placeholder
|
||||
style={styles.title}
|
||||
shimmerColors={["#ebebeb", "#ddd", "#ebebeb"]}
|
||||
/>
|
||||
|
||||
{/* Rating container placeholder */}
|
||||
<View style={styles.ratingContainer}>
|
||||
<Placeholder
|
||||
style={styles.rating}
|
||||
shimmerColors={["#ebebeb", "#ddd", "#ebebeb"]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProductShimmerGrid = () => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{[...Array(6)].map((_, index) => (
|
||||
<ProductShimmer key={index} />
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: 8,
|
||||
justifyContent: "center",
|
||||
},
|
||||
card: {
|
||||
width: CARD_WIDTH,
|
||||
backgroundColor: "white",
|
||||
borderRadius: 12,
|
||||
margin: 8,
|
||||
boxShadow: "0px 2px 4px rgba(0, 0, 0, 0.1)",
|
||||
},
|
||||
image: {
|
||||
width: "100%",
|
||||
height: CARD_WIDTH, // Square image
|
||||
borderTopLeftRadius: 12,
|
||||
borderTopRightRadius: 12,
|
||||
},
|
||||
content: {
|
||||
padding: 12,
|
||||
gap: 8,
|
||||
},
|
||||
title: {
|
||||
height: 20,
|
||||
width: "85%",
|
||||
borderRadius: 4,
|
||||
},
|
||||
ratingContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
rating: {
|
||||
height: 16,
|
||||
width: "30%",
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user