newcandies 0.1.18 → 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/dist/index.js +1 -6
- package/package.json +1 -1
- package/templates/app-briefs/react-query/sproutsy/src/api/api-provider.tsx +15 -0
- package/templates/app-briefs/react-query/sproutsy/src/api/client.ts +8 -0
- package/templates/app-briefs/react-query/sproutsy/src/api/plants/index.ts +18 -0
- package/templates/app-briefs/react-query/sproutsy/src/api/plants/usePlants.ts +46 -0
- package/templates/app-briefs/react-query/sproutsy/src/app/(tabs)/_layout.tsx +52 -0
- package/templates/app-briefs/react-query/sproutsy/src/app/(tabs)/explore.tsx +84 -0
- package/templates/app-briefs/react-query/sproutsy/src/app/(tabs)/index.tsx +46 -0
- package/templates/app-briefs/react-query/sproutsy/src/app/_layout.tsx +32 -0
- package/templates/app-briefs/react-query/sproutsy/src/app/plant/[id].tsx +92 -0
- package/templates/app-briefs/react-query/sproutsy/src/components/screens/home/home-header.tsx +38 -0
- package/templates/app-briefs/react-query/sproutsy/src/components/screens/shared/item-separator.tsx +7 -0
- package/templates/app-briefs/react-query/sproutsy/src/components/screens/shared/plant-card.tsx +70 -0
- package/templates/app-briefs/react-query/sproutsy/src/components/screens/shared/query-state.tsx +50 -0
- package/templates/app-briefs/react-query/sproutsy/src/components/ui/screen.tsx +17 -0
- package/templates/app-briefs/react-query/sproutsy/src/components/ui/squircle.tsx +4 -0
- package/templates/app-briefs/react-query/sproutsy/src/components/ui/text.tsx +54 -0
package/dist/index.js
CHANGED
|
@@ -142,12 +142,7 @@ async function main() {
|
|
|
142
142
|
const tmplPkg = await fs.readJSON(templatePkgPath);
|
|
143
143
|
const isFullPackageJson = tmplPkg.scripts || tmplPkg.main || tmplPkg.version;
|
|
144
144
|
if (isFullPackageJson) {
|
|
145
|
-
|
|
146
|
-
...tmplPkg,
|
|
147
|
-
dependencies: { ...basePkg.dependencies, ...tmplPkg.dependencies },
|
|
148
|
-
devDependencies: { ...basePkg.devDependencies, ...tmplPkg.devDependencies }
|
|
149
|
-
};
|
|
150
|
-
await fs.writeJSON(path.join(dest, "package.json"), finalPkg, { spaces: 2 });
|
|
145
|
+
await fs.writeJSON(path.join(dest, "package.json"), tmplPkg, { spaces: 2 });
|
|
151
146
|
} else {
|
|
152
147
|
basePkg.dependencies = { ...basePkg.dependencies, ...tmplPkg.dependencies };
|
|
153
148
|
basePkg.devDependencies = { ...basePkg.devDependencies, ...tmplPkg.devDependencies };
|
package/package.json
CHANGED
|
@@ -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,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
|
+
};
|
package/templates/app-briefs/react-query/sproutsy/src/components/screens/shared/plant-card.tsx
ADDED
|
@@ -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;
|
package/templates/app-briefs/react-query/sproutsy/src/components/screens/shared/query-state.tsx
ADDED
|
@@ -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,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;
|