Initial commit

Generated by create-expo-app 3.2.0.
This commit is contained in:
Dennis Hundertmark
2025-03-10 19:49:23 +01:00
commit b24544f115
30 changed files with 17173 additions and 0 deletions
+44
View File
@@ -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,
},
});
+189
View File
@@ -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",
},
});
+58
View File
@@ -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,
},
});
+79
View File
@@ -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,
},
});
+88
View File
@@ -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,
},
});