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
+103
View File
@@ -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);
+91
View File
@@ -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
View File
@@ -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,
},
});
+167
View File
@@ -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,
},
});