newcandies 0.1.19 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newcandies",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "description": "Scaffold Expo Router + Uniwind React Native apps with layered templates.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,15 @@
1
+ import { ReactNode } from "react";
2
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
+ import { useTanStackQueryDevTools } from "@rozenite/tanstack-query-plugin";
4
+
5
+ const queryClient = new QueryClient();
6
+
7
+ const APIProvider = ({ children }: { children: ReactNode }) => {
8
+ useTanStackQueryDevTools(queryClient);
9
+
10
+ return (
11
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
12
+ );
13
+ };
14
+
15
+ export default APIProvider;
@@ -0,0 +1,8 @@
1
+ import axios from 'axios';
2
+
3
+ console.log(process.env.EXPO_PUBLIC_API_URL, 'logging env');
4
+
5
+ export const client = axios.create({
6
+ baseURL: process.env.EXPO_PUBLIC_API_URL,
7
+ timeout: 60000,
8
+ });
@@ -0,0 +1,18 @@
1
+ import { client } from "../client";
2
+
3
+ export const getPlants = async () => {
4
+ const response = await client.get("/plants/");
5
+ return response.data;
6
+ };
7
+
8
+ export const listPlants = async (page: number = 1, pageSize: number = 20) => {
9
+ const response = await client.get("/plants/", {
10
+ params: { page, limit:pageSize },
11
+ });
12
+ return response.data;
13
+ };
14
+
15
+ export const getPlantById = async (id: string) => {
16
+ const response = await client.get(`/plants/${id}`);
17
+ return response.data.data;
18
+ };
@@ -0,0 +1,46 @@
1
+ import { useInfiniteQuery } from "@tanstack/react-query";
2
+ import { listPlants } from ".";
3
+
4
+ interface UsePlantsOptions {
5
+ pageSize?: number;
6
+ }
7
+
8
+ export const usePlants = (options: UsePlantsOptions = {}) => {
9
+ const { pageSize = 10 } = options;
10
+
11
+ const {
12
+ data,
13
+ fetchNextPage,
14
+ hasNextPage,
15
+ isFetchingNextPage,
16
+ refetch,
17
+ isRefetching,
18
+ isLoading,
19
+ status,
20
+ error,
21
+ } = useInfiniteQuery({
22
+ queryKey: ["plants-list", pageSize],
23
+ queryFn: ({ pageParam = 1 }) => listPlants(pageParam, pageSize),
24
+ initialPageParam: 1,
25
+ getNextPageParam: (lastPage: PlantsResponse, _, lastPageParam) => {
26
+ if (lastPage.metadata.currentPage >= lastPage.metadata.lastPage) {
27
+ return undefined;
28
+ }
29
+ return lastPageParam + 1;
30
+ },
31
+ });
32
+
33
+ const plants: Plant[] = data?.pages.flatMap((page) => page.plants) ?? [];
34
+
35
+ return {
36
+ plants,
37
+ status,
38
+ error,
39
+ isLoading,
40
+ refetch,
41
+ isRefetching,
42
+ isFetchingNextPage,
43
+ hasNextPage,
44
+ fetchNextPage,
45
+ };
46
+ };
@@ -0,0 +1,52 @@
1
+ import IonIcons from "@expo/vector-icons/Ionicons";
2
+ import { Tabs } from "expo-router";
3
+ import { useCSSVariable } from "uniwind";
4
+
5
+ function TabBarIcon(props: {
6
+ name: React.ComponentProps<typeof IonIcons>["name"];
7
+ color: string;
8
+ }) {
9
+ return <IonIcons size={28} style={{ marginBottom: -4 }} {...props} />;
10
+ }
11
+
12
+ export default function TabLayout() {
13
+ const backgroundColor = useCSSVariable("--color-muted") as string;
14
+ const primaryColor = useCSSVariable("--color-primary") as string;
15
+ const tabBarActiveTintColor = useCSSVariable(
16
+ "--color-muted-foreground"
17
+ ) as string;
18
+
19
+ return (
20
+ <Tabs
21
+ screenOptions={{
22
+ headerShown: false,
23
+ tabBarActiveTintColor: primaryColor,
24
+ tabBarInactiveTintColor: tabBarActiveTintColor,
25
+ tabBarStyle: {
26
+ backgroundColor,
27
+ },
28
+ }}
29
+ >
30
+ <Tabs.Screen
31
+ name="index"
32
+ options={{
33
+ title: "Home",
34
+ headerShown: false,
35
+ tabBarShowLabel: false,
36
+ tabBarIcon: ({ color }) => (
37
+ <TabBarIcon name="partly-sunny" color={color} />
38
+ ),
39
+ }}
40
+ />
41
+ <Tabs.Screen
42
+ name="explore"
43
+ options={{
44
+ title: "Explore",
45
+ headerShown: false,
46
+ tabBarShowLabel: false,
47
+ tabBarIcon: ({ color }) => <TabBarIcon name="grid" color={color} />,
48
+ }}
49
+ />
50
+ </Tabs>
51
+ );
52
+ }
@@ -0,0 +1,84 @@
1
+ import { Ionicons } from "@expo/vector-icons";
2
+ import { FlashList } from "@shopify/flash-list";
3
+ import { useRouter } from "expo-router";
4
+ import { ActivityIndicator, RefreshControl, View } from "react-native";
5
+ import { usePlants } from "~/api/plants/usePlants";
6
+ import ItemSeparator from "~/components/screens/shared/item-separator";
7
+ import PlantCard from "~/components/screens/shared/plant-card";
8
+ import QueryState from "~/components/screens/shared/query-state";
9
+ import { Screen } from "~/components/ui/screen";
10
+ import Text from "~/components/ui/text";
11
+
12
+ export default function Two() {
13
+ const router = useRouter();
14
+
15
+ const {
16
+ plants,
17
+ status,
18
+ error,
19
+ fetchNextPage,
20
+ hasNextPage,
21
+ isFetchingNextPage,
22
+ refetch,
23
+ isRefetching,
24
+ } = usePlants({ pageSize: 10 });
25
+
26
+ const renderItem = ({ item }: { item: Plant }) => (
27
+ <PlantCard
28
+ id={item.id}
29
+ name={item.name}
30
+ scientificName={item.scientificName}
31
+ coverImg={item.coverImg}
32
+ maintenance={item.maintenance}
33
+ onPress={() => router.push(`/plant/${item.id}`)}
34
+ />
35
+ );
36
+
37
+ const onEndReached = () => {
38
+ if (hasNextPage && !isFetchingNextPage) {
39
+ fetchNextPage();
40
+ }
41
+ };
42
+
43
+ if (status === "error") {
44
+ return (
45
+ <QueryState
46
+ isError={true}
47
+ error={
48
+ error instanceof Error ? error : new Error("Failed to load plants")
49
+ }
50
+ />
51
+ );
52
+ }
53
+
54
+ return (
55
+ <Screen>
56
+ <View className="px-6 pt-5 pb-4 flex-row items-center gap-3">
57
+ <Ionicons name="sunny" size={32} color="#234823" />
58
+ <Text variant="display" className="text-primary">
59
+ Plants
60
+ </Text>
61
+ </View>
62
+ <FlashList
63
+ data={plants}
64
+ renderItem={renderItem}
65
+ keyExtractor={(item: Plant) => item.id}
66
+ className="px-4"
67
+ showsVerticalScrollIndicator={false}
68
+ ItemSeparatorComponent={ItemSeparator}
69
+ onEndReached={onEndReached}
70
+ onEndReachedThreshold={0.5}
71
+ ListFooterComponent={
72
+ isFetchingNextPage ? (
73
+ <View className="py-4 items-center">
74
+ <ActivityIndicator size="small" />
75
+ </View>
76
+ ) : null
77
+ }
78
+ refreshControl={
79
+ <RefreshControl onRefresh={refetch} refreshing={isRefetching} />
80
+ }
81
+ />
82
+ </Screen>
83
+ );
84
+ }
@@ -0,0 +1,46 @@
1
+ import { FlashList, ListRenderItem } from "@shopify/flash-list";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { useRouter } from "expo-router";
4
+ import { getPlants } from "~/api/plants";
5
+ import { HomeHeader } from "~/components/screens/home/home-header";
6
+ import ItemSeparator from "~/components/screens/shared/item-separator";
7
+ import PlantCard from "~/components/screens/shared/plant-card";
8
+ import QueryState from "~/components/screens/shared/query-state";
9
+ import { Screen } from "~/components/ui/screen";
10
+
11
+ export default function Index() {
12
+ const router = useRouter();
13
+ const { data, isPending, isError, error } = useQuery<PlantsResponse>({
14
+ queryKey: ["plants"],
15
+ queryFn: getPlants,
16
+ });
17
+
18
+ const renderItem = ({ item }: { item: Plant }) => (
19
+ <PlantCard
20
+ id={item.id}
21
+ name={item.name}
22
+ scientificName={item.scientificName}
23
+ coverImg={item.coverImg}
24
+ maintenance={item.maintenance}
25
+ onPress={() => router.push(`/plant/${item.id}`)}
26
+ />
27
+ );
28
+
29
+ if (isPending || isError) {
30
+ return <QueryState isPending={isPending} isError={isError} error={error} />;
31
+ }
32
+
33
+ return (
34
+ <Screen>
35
+ <HomeHeader />
36
+ <FlashList
37
+ data={data?.plants || []}
38
+ keyExtractor={(item: Plant) => item.id}
39
+ ItemSeparatorComponent={ItemSeparator}
40
+ showsVerticalScrollIndicator={false}
41
+ renderItem={renderItem}
42
+ className="px-4"
43
+ />
44
+ </Screen>
45
+ );
46
+ }
@@ -0,0 +1,32 @@
1
+ import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
2
+ import { Stack } from "expo-router";
3
+ import { GestureHandlerRootView } from "react-native-gesture-handler";
4
+ import { KeyboardProvider } from "react-native-keyboard-controller";
5
+ import "react-native-reanimated";
6
+ import { Toaster } from "sonner-native";
7
+ import APIProvider from "~/api/api-provider";
8
+ import "../../global.css";
9
+ import { StyleSheet } from "react-native";
10
+
11
+ export default function RootLayout() {
12
+ return (
13
+ <GestureHandlerRootView style={styles.container}>
14
+ <KeyboardProvider>
15
+ <APIProvider>
16
+ <BottomSheetModalProvider>
17
+ <Stack>
18
+ <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
19
+ </Stack>
20
+ <Toaster />
21
+ </BottomSheetModalProvider>
22
+ </APIProvider>
23
+ </KeyboardProvider>
24
+ </GestureHandlerRootView>
25
+ );
26
+ }
27
+
28
+ const styles = StyleSheet.create({
29
+ container: {
30
+ flex: 1,
31
+ },
32
+ });
@@ -0,0 +1,92 @@
1
+ import { Ionicons } from "@expo/vector-icons";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { Stack, useLocalSearchParams, useRouter } from "expo-router";
4
+ import { Image, Pressable, ScrollView, View } from "react-native";
5
+ import { getPlantById } from "~/api/plants";
6
+ import QueryState from "~/components/screens/shared/query-state";
7
+ import { Screen } from "~/components/ui/screen";
8
+ import { Squircle } from "~/components/ui/squircle";
9
+ import Text from "~/components/ui/text";
10
+
11
+ export default function PlantDetails() {
12
+ const { id } = useLocalSearchParams<{ id: string }>();
13
+ const router = useRouter();
14
+
15
+ const {
16
+ data: plant,
17
+ isPending,
18
+ isError,
19
+ error,
20
+ } = useQuery<Plant>({
21
+ queryKey: ["plant", id],
22
+ queryFn: () => getPlantById(id!),
23
+ enabled: !!id,
24
+ });
25
+
26
+ if (isPending || isError || !plant) {
27
+ return (
28
+ <QueryState
29
+ isPending={isPending}
30
+ isError={isError || !plant}
31
+ error={error}
32
+ errorMessage="Error"
33
+ />
34
+ );
35
+ }
36
+
37
+ return (
38
+ <>
39
+ <Stack.Screen options={{ headerShown: false }} />
40
+ <Screen className="flex-1 relative">
41
+ <ScrollView className="flex-1">
42
+ <View className="bg-background">
43
+ <Image
44
+ source={{ uri: plant.coverImg }}
45
+ className="w-full h-96"
46
+ resizeMode="cover"
47
+ />
48
+ <Pressable
49
+ onPress={() => router.back()}
50
+ className="absolute top-4 left-4 w-10 h-10 rounded-full bg-white justify-center items-center"
51
+ >
52
+ <Ionicons name="chevron-back" size={24} color={"black"} />
53
+ </Pressable>
54
+ </View>
55
+
56
+ <View className="p-6 bg-primary-foreground">
57
+ <Text variant="display">{plant.name}</Text>
58
+ <Text variant="subtitle" className="text-primary mb-4">
59
+ {plant.scientificName}
60
+ </Text>
61
+
62
+ <Text variant="title" className="text-primary mb-6">
63
+ ${(plant.price / 100).toFixed(2)}
64
+ </Text>
65
+
66
+ <View className="mb-6">
67
+ <Text variant="subtitle" className="mb-2">
68
+ About
69
+ </Text>
70
+ <Text className="leading-6">{plant.description}</Text>
71
+ </View>
72
+
73
+ {plant.specialCare && (
74
+ <View className="mb-6">
75
+ <Text variant="subtitle" className="mb-3">
76
+ Special Care
77
+ </Text>
78
+ <Squircle
79
+ cornerSmoothing={1}
80
+ className="bg-white p-6"
81
+ style={{ borderRadius: 16 }}
82
+ >
83
+ <Text className="leading-6">{plant.specialCare}</Text>
84
+ </Squircle>
85
+ </View>
86
+ )}
87
+ </View>
88
+ </ScrollView>
89
+ </Screen>
90
+ </>
91
+ );
92
+ }
@@ -0,0 +1,38 @@
1
+ import { Image, View } from "react-native";
2
+ import Text from "~/components/ui/text";
3
+
4
+ const getDate = () => {
5
+ const date = new Date();
6
+ return {
7
+ weekday: date.toLocaleString("en-US", { weekday: "long" }),
8
+ };
9
+ };
10
+
11
+ export const HomeHeader = () => {
12
+ const { weekday } = getDate();
13
+
14
+ return (
15
+ <View className="px-6 pt-2">
16
+ <View className="flex-row items-center justify-between mb-6">
17
+ <View className="flex-row items-center gap-3">
18
+ <Image
19
+ source={require("assets/images/sproutsies-logo.png")}
20
+ style={{ width: 24, height: 24 }}
21
+ resizeMode="contain"
22
+ />
23
+ <Image
24
+ source={require("assets/images/sproutsy-main-logo.png")}
25
+ style={{ height: 24, width: 124 }}
26
+ resizeMode="contain"
27
+ />
28
+ </View>
29
+ <View className="items-end">
30
+ <Text variant="caption-primary" className="text-muted-foreground">
31
+ Happy
32
+ </Text>
33
+ <Text variant="subtitle-primary">{weekday}</Text>
34
+ </View>
35
+ </View>
36
+ </View>
37
+ );
38
+ };
@@ -0,0 +1,7 @@
1
+ import { View } from "react-native";
2
+
3
+ const ItemSeparator = () => {
4
+ return <View className="h-4" />;
5
+ };
6
+
7
+ export default ItemSeparator;
@@ -0,0 +1,70 @@
1
+ import { Ionicons } from "@expo/vector-icons";
2
+ import { Image, Pressable, View } from "react-native";
3
+ import { Squircle } from "~/components/ui/squircle";
4
+ import Text from "~/components/ui/text";
5
+
6
+ type PlantCardProps = {
7
+ id: string;
8
+ name: string;
9
+ scientificName: string;
10
+ coverImg: string;
11
+ maintenance?: string;
12
+ onPress?: () => void;
13
+ };
14
+
15
+ const PlantCard = ({
16
+ name,
17
+ scientificName,
18
+ coverImg,
19
+ maintenance,
20
+ onPress,
21
+ }: PlantCardProps) => {
22
+ return (
23
+ <Pressable onPress={onPress}>
24
+ <Squircle cornerSmoothing={1} className="overflow-hidden rounded-3xl">
25
+ <View className="bg-background">
26
+ <Image
27
+ source={{ uri: coverImg }}
28
+ className="h-[356] w-full"
29
+ resizeMode="cover"
30
+ />
31
+
32
+ <View className="flex-1 bg-card p-4 justify-end">
33
+ <Text variant="subtitle" className="text-card-foreground mb-3">
34
+ {name}
35
+ </Text>
36
+ <View className="flex-row justify-between items-center gap-2">
37
+ <View className="flex-1 flex-row items-center gap-2">
38
+ <View className="w-5 h-5 bg-overlay items-center justify-center rounded-full">
39
+ <Ionicons name="flask" size={12} color="white" />
40
+ </View>
41
+ <View className="flex-1">
42
+ <Text variant="caption">Scientific</Text>
43
+ <Text variant="caption" numberOfLines={1}>
44
+ {scientificName}
45
+ </Text>
46
+ </View>
47
+ </View>
48
+ {maintenance && (
49
+ <Squircle
50
+ cornerSmoothing={1}
51
+ className="bg-overlay-muted px-3 py-1 flex-row items-center gap-2 rounded-xl"
52
+ >
53
+ <Ionicons name="shield-checkmark" size={12} color="white" />
54
+ <View>
55
+ <Text variant="caption">Care</Text>
56
+ <Text className="text-card-foreground" variant="caption-primary">
57
+ {maintenance.toLowerCase()}
58
+ </Text>
59
+ </View>
60
+ </Squircle>
61
+ )}
62
+ </View>
63
+ </View>
64
+ </View>
65
+ </Squircle>
66
+ </Pressable>
67
+ );
68
+ };
69
+
70
+ export default PlantCard;
@@ -0,0 +1,50 @@
1
+ import { View, ActivityIndicator } from "react-native";
2
+ import { Stack } from "expo-router";
3
+ import { Screen } from "~/components/ui/screen";
4
+ import Text from "~/components/ui/text";
5
+
6
+ interface QueryStateProps {
7
+ isPending?: boolean;
8
+ isError?: boolean;
9
+ error?: Error | null;
10
+ errorMessage?: string;
11
+ }
12
+
13
+ const QueryState = ({
14
+ isPending,
15
+ isError,
16
+ error,
17
+ errorMessage = "Something went wrong",
18
+ }: QueryStateProps) => {
19
+ if (isPending) {
20
+ return (
21
+ <>
22
+ <Stack.Screen options={{ headerShown: false }} />
23
+ <Screen>
24
+ <View className="flex-1 items-center justify-center">
25
+ <ActivityIndicator size="large" />
26
+ </View>
27
+ </Screen>
28
+ </>
29
+ );
30
+ }
31
+
32
+ if (isError) {
33
+ return (
34
+ <>
35
+ <Stack.Screen options={{ headerShown: false }} />
36
+ <Screen>
37
+ <View className="flex-1 items-center justify-center">
38
+ <Text className="text-lg font-medium text-red-500">
39
+ {error?.message || errorMessage}
40
+ </Text>
41
+ </View>
42
+ </Screen>
43
+ </>
44
+ );
45
+ }
46
+
47
+ return null;
48
+ };
49
+
50
+ export default QueryState;
@@ -0,0 +1,17 @@
1
+ import { withUniwind } from "uniwind";
2
+ import { SafeAreaView as RNSafeAreaView } from "react-native-safe-area-context";
3
+ import { ReactNode } from "react";
4
+
5
+ const SafeAreaView = withUniwind(RNSafeAreaView);
6
+
7
+ interface ScreenProps {
8
+ children: ReactNode;
9
+ className?: string;
10
+ }
11
+
12
+ export const Screen = ({
13
+ children,
14
+ className = "flex-1 bg-background",
15
+ }: ScreenProps) => {
16
+ return <SafeAreaView className={className}>{children}</SafeAreaView>;
17
+ };
@@ -0,0 +1,4 @@
1
+ import { withUniwind } from "uniwind";
2
+ import SquircleView from "react-native-fast-squircle";
3
+
4
+ export const Squircle = withUniwind(SquircleView);
@@ -0,0 +1,54 @@
1
+ import { Text as RNText, TextProps } from "react-native";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ type TypographyVariant =
5
+ | "title"
6
+ | "subtitle"
7
+ | "body"
8
+ | "caption"
9
+ | "button"
10
+ | "display"
11
+ | "caption-primary"
12
+ | "body-primary"
13
+ | "subtitle-primary"
14
+ | "subtitle-secondary"
15
+ | "body-secondary";
16
+
17
+ interface TextComponentProps extends TextProps {
18
+ className?: string;
19
+ variant?: TypographyVariant;
20
+ }
21
+
22
+ const variantStyles: Record<TypographyVariant, string> = {
23
+ title: "text-2xl font-bold",
24
+ subtitle: "text-xl font-semibold",
25
+ "subtitle-primary": "text-xl font-semibold text-primary",
26
+ "subtitle-secondary": "text-xl font-semibold text-secondary",
27
+ body: "text-base",
28
+ "body-primary": "text-base text-primary",
29
+ "body-secondary": "text-base text-secondary",
30
+ caption: "text-sm font-medium text-white",
31
+ "caption-primary": "text-sm text-primary font-medium",
32
+ button: "text-xl font-semibold text-white text-center",
33
+ display: "text-3xl font-bold",
34
+ };
35
+
36
+ const Text = ({
37
+ variant = "body",
38
+ children,
39
+ className,
40
+ ...props
41
+ }: TextComponentProps) => {
42
+ const textStyle = twMerge(
43
+ "text-foreground",
44
+ variantStyles[variant],
45
+ className
46
+ );
47
+ return (
48
+ <RNText className={textStyle} {...props}>
49
+ {children}
50
+ </RNText>
51
+ );
52
+ };
53
+
54
+ export default Text;