Initial commit
Generated by create-expo-app 3.2.0.
This commit is contained in:
+103
@@ -0,0 +1,103 @@
|
||||
import CardButton from "@/components/CartButton";
|
||||
import { useReactQueryDevTools } from "@dev-plugins/react-query";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import {
|
||||
router,
|
||||
Stack,
|
||||
useNavigationContainerRef,
|
||||
useRouter,
|
||||
} from "expo-router";
|
||||
import { useMMKVDevTools } from "@dev-plugins/react-native-mmkv";
|
||||
import { storage } from "@/store/mmkv";
|
||||
import * as Sentry from "@sentry/react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { TouchableOpacity } from "react-native";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { NavigationContainerRef } from "@react-navigation/native";
|
||||
|
||||
const navigationIntegration = Sentry.reactNavigationIntegration({
|
||||
enableTimeToInitialDisplay: true, // Only in native builds, not in Expo Go.
|
||||
});
|
||||
|
||||
Sentry.init({
|
||||
dsn: "https://8af0004917f7c8cfb06b2beac6fd3a66@o4507486642765824.ingest.de.sentry.io/4508981409808464",
|
||||
attachScreenshot: true,
|
||||
debug: false,
|
||||
tracesSampleRate: 1.0, // Adjust this value in production
|
||||
_experiments: {
|
||||
profilesSampleRate: 1.0, // Only during debugging, change to lower value in production
|
||||
replaysSessionSampleRate: 1.0, // Only during debugging, change to lower value in production
|
||||
replaysOnErrorSampleRate: 1,
|
||||
},
|
||||
integrations: [
|
||||
Sentry.mobileReplayIntegration({
|
||||
maskAllText: false,
|
||||
maskAllImages: true,
|
||||
maskAllVectors: false,
|
||||
}),
|
||||
Sentry.spotlightIntegration(),
|
||||
navigationIntegration,
|
||||
],
|
||||
// uncomment the line below to enable Spotlight (https://spotlightjs.com)
|
||||
// spotlight: __DEV__,
|
||||
});
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const RootLayout = () => {
|
||||
useReactQueryDevTools(queryClient);
|
||||
useMMKVDevTools({
|
||||
storage,
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const navigationRef = useNavigationContainerRef();
|
||||
|
||||
useEffect(() => {
|
||||
navigationIntegration.registerNavigationContainer(navigationRef);
|
||||
}, [navigationRef]);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<GestureHandlerRootView>
|
||||
<Stack>
|
||||
<Stack.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: "Products",
|
||||
headerShadowVisible: false,
|
||||
headerSearchBarOptions: {
|
||||
placeholder: "Search products",
|
||||
hideWhenScrolling: false,
|
||||
hideNavigationBar: false,
|
||||
},
|
||||
headerRight: () => <CardButton />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen name="product/[id]" options={{ title: "Product" }} />
|
||||
<Stack.Screen
|
||||
name="cart"
|
||||
options={{
|
||||
title: "Cart",
|
||||
presentation: "modal",
|
||||
headerLeft: () => (
|
||||
<TouchableOpacity onPress={() => router.back()}>
|
||||
<Ionicons name="arrow-back" size={20} color="black" />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</GestureHandlerRootView>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sentry.wrap(RootLayout);
|
||||
@@ -0,0 +1,91 @@
|
||||
import CartItem from "@/components/CartItem";
|
||||
import useCartStore from "@/store/cartStore";
|
||||
import { COLORS } from "@/utils/colors";
|
||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||
import { useRouter } from "expo-router";
|
||||
import {
|
||||
Alert,
|
||||
FlatList,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
const Page = () => {
|
||||
const { products, total, clearCart } = useCartStore();
|
||||
const { bottom } = useSafeAreaInsets();
|
||||
const router = useRouter();
|
||||
|
||||
const handleCheckout = () => {
|
||||
if (products.length === 0) {
|
||||
Alert.alert("Add some products to your cart first!");
|
||||
return;
|
||||
}
|
||||
clearCart();
|
||||
Alert.alert("Checkout successful!");
|
||||
router.dismiss();
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{products.length === 0 && (
|
||||
<Text style={styles.emptyText}>No products in the cart</Text>
|
||||
)}
|
||||
<FlatList
|
||||
data={products}
|
||||
contentContainerStyle={{ gap: 10 }}
|
||||
renderItem={({ item }) => <CartItem item={item} />}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
ListHeaderComponent={() => (
|
||||
<>
|
||||
{products.length && (
|
||||
<Text style={styles.totalText}>Total: ${total.toFixed(2)}</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.addToCartButton,
|
||||
{ paddingBottom: Platform.OS === "ios" ? bottom : 20 },
|
||||
]}
|
||||
onPress={handleCheckout}
|
||||
>
|
||||
<Ionicons name="checkmark" size={20} color="white" />
|
||||
<Text style={styles.addToCartText}>Checkout</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default Page;
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
emptyText: {
|
||||
textAlign: "center",
|
||||
},
|
||||
totalText: {
|
||||
fontSize: 20,
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
padding: 10,
|
||||
},
|
||||
addToCartButton: {
|
||||
backgroundColor: COLORS.primary,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
padding: 16,
|
||||
},
|
||||
addToCartText: {
|
||||
color: "white",
|
||||
fontWeight: "600",
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import ProductCard from "@/components/ProductCart";
|
||||
import { fetchProducts, getCategories, Product } from "@/utils/api";
|
||||
import { COLORS } from "@/utils/colors";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Stack } from "expo-router";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import {
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
View,
|
||||
Text,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import { useHeaderHeight } from "@react-navigation/elements";
|
||||
import { ProductShimmerGrid } from "@/components/ProductListShimmer";
|
||||
|
||||
export default function Index() {
|
||||
const [selectedCategory, setSelectedCategory] = React.useState("all");
|
||||
const [searchQuery, setSearchQuery] = React.useState("");
|
||||
const headerHeight = useHeaderHeight();
|
||||
|
||||
const {
|
||||
data: products,
|
||||
refetch,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
} = useQuery({
|
||||
queryKey: ["products"],
|
||||
queryFn: fetchProducts,
|
||||
});
|
||||
|
||||
const { data: categories = [] } = useQuery({
|
||||
queryKey: ["categories"],
|
||||
queryFn: getCategories,
|
||||
});
|
||||
|
||||
const renderProduct = useCallback(
|
||||
({ item }: { item: Product }) => <ProductCard product={item} />,
|
||||
[]
|
||||
);
|
||||
|
||||
const allCategories = ["all", ...categories];
|
||||
|
||||
const filteredProducts = useMemo(() => {
|
||||
return products?.filter((product) => {
|
||||
if (selectedCategory !== "all") {
|
||||
return product.category === selectedCategory;
|
||||
}
|
||||
return product.title.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
});
|
||||
}, [products, selectedCategory, searchQuery]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ marginTop: Platform.select({ ios: headerHeight, android: 0 }) },
|
||||
]}
|
||||
>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
headerSearchBarOptions: {
|
||||
onChangeText: (e) => setSearchQuery(e.nativeEvent.text),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<View style={styles.categoryContainer}>
|
||||
<ScrollView
|
||||
style={styles.categoryScrollView}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
>
|
||||
{allCategories.map((category) => (
|
||||
<Pressable
|
||||
key={category}
|
||||
onPress={() => setSelectedCategory(category)}
|
||||
style={[
|
||||
styles.categoryButton,
|
||||
selectedCategory === category && styles.selectedCategoryButton,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.categoryText,
|
||||
selectedCategory === category && styles.selectedCategoryText,
|
||||
]}
|
||||
>
|
||||
{category.charAt(0).toUpperCase() + category.slice(1)}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{isLoading ? (
|
||||
<ProductShimmerGrid />
|
||||
) : (
|
||||
<FlashList
|
||||
data={filteredProducts}
|
||||
renderItem={renderProduct}
|
||||
estimatedItemSize={200}
|
||||
numColumns={2}
|
||||
contentContainerStyle={{ padding: 8 }}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
onRefresh={refetch}
|
||||
refreshing={isRefetching}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
categoryContainer: {
|
||||
height: 60,
|
||||
zIndex: 1,
|
||||
boxShadow: "0 0 10px rgba(0, 0, 0, 0.1)",
|
||||
},
|
||||
categoryScrollView: {
|
||||
padding: 12,
|
||||
},
|
||||
categoryButton: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
marginHorizontal: 4,
|
||||
backgroundColor: COLORS.lightGray,
|
||||
alignSelf: "center",
|
||||
},
|
||||
categoryText: {
|
||||
fontSize: 14,
|
||||
color: COLORS.white,
|
||||
},
|
||||
selectedCategoryButton: {
|
||||
backgroundColor: COLORS.primary,
|
||||
},
|
||||
selectedCategoryText: {
|
||||
color: COLORS.white,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
import { ProductDetailsShimmer } from "@/components/ProductDetailsShimmer";
|
||||
import { fetchProduct } from "@/utils/api";
|
||||
import { COLORS } from "@/utils/colors";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Image } from "expo-image";
|
||||
import { Stack, useLocalSearchParams } from "expo-router";
|
||||
import React from "react";
|
||||
|
||||
import useCartStore from "@/store/cartStore";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import {
|
||||
Platform,
|
||||
ScrollView,
|
||||
Share,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
const Page = () => {
|
||||
const { id } = useLocalSearchParams();
|
||||
const { bottom } = useSafeAreaInsets();
|
||||
|
||||
const { addProduct } = useCartStore();
|
||||
|
||||
const { data: product, isLoading } = useQuery({
|
||||
queryKey: ["product", id],
|
||||
queryFn: () => fetchProduct(Number(id)),
|
||||
});
|
||||
|
||||
const onShare = async () => {
|
||||
const url = `mnkyshop://product/${product?.id}`;
|
||||
if (Platform.OS === "ios") {
|
||||
await Share.share({
|
||||
url,
|
||||
message: `Check out this product on Galaxies Shop: ${url}`,
|
||||
});
|
||||
} else {
|
||||
await Share.share({
|
||||
message: `Check out this product on Galaxies Shop: ${url}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <ProductDetailsShimmer />;
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
return <Text>Product not found</Text>;
|
||||
}
|
||||
|
||||
const handleAddToCart = () => {
|
||||
addProduct(product);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: product?.title,
|
||||
headerRight: () => (
|
||||
<TouchableOpacity onPress={onShare}>
|
||||
<Ionicons name="share-outline" size={24} color={COLORS.primary} />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<ScrollView>
|
||||
<Image
|
||||
source={{ uri: product.image }}
|
||||
style={styles.image}
|
||||
contentFit="contain"
|
||||
/>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>{product.title}</Text>
|
||||
<Text style={styles.price}>${product.price}</Text>
|
||||
<Text style={styles.category}>{product.category}</Text>
|
||||
<Text style={styles.description}>{product.description}</Text>
|
||||
<View style={styles.ratingContainer}>
|
||||
<Text style={styles.rating}>★ {product.rating.rate}</Text>
|
||||
<Text style={styles.ratingCount}>
|
||||
({product.rating.count} reviews)
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.addToCartButton,
|
||||
{ paddingBottom: Platform.OS === "ios" ? bottom : 20 },
|
||||
]}
|
||||
onPress={handleAddToCart}
|
||||
>
|
||||
<Ionicons name="cart" size={20} color="white" />
|
||||
<Text style={styles.addToCartText}>Add to Cart</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
image: {
|
||||
width: "100%",
|
||||
height: 300,
|
||||
backgroundColor: "#f9f9f9",
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
gap: 12,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: "600",
|
||||
},
|
||||
price: {
|
||||
fontSize: 20,
|
||||
fontWeight: "700",
|
||||
color: COLORS.primary,
|
||||
},
|
||||
category: {
|
||||
fontSize: 16,
|
||||
color: "#666",
|
||||
textTransform: "capitalize",
|
||||
},
|
||||
description: {
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
color: "#333",
|
||||
},
|
||||
ratingContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
},
|
||||
rating: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
color: "#FFB800",
|
||||
},
|
||||
ratingCount: {
|
||||
fontSize: 14,
|
||||
color: "#666",
|
||||
},
|
||||
addToCartButton: {
|
||||
backgroundColor: COLORS.primary,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
padding: 16,
|
||||
},
|
||||
addToCartText: {
|
||||
color: "white",
|
||||
fontWeight: "600",
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user