newcandies 0.1.28 → 0.1.30
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 +1 -1
- package/templates/app-briefs/expo-router/flourish/src/app/_layout.tsx +1 -2
- package/templates/app-briefs/expo-router/flourish-auth/README.md +63 -110
- package/templates/app-briefs/expo-router/flourish-auth/app.json +1 -2
- package/templates/app-briefs/expo-router/flourish-auth/babel.config.js +0 -1
- package/templates/app-briefs/expo-router/flourish-auth/eas.json +0 -1
- package/templates/app-briefs/expo-router/flourish-auth/global.css +0 -1
- package/templates/app-briefs/expo-router/flourish-auth/metro.config.js +0 -1
- package/templates/app-briefs/expo-router/flourish-auth/package.json +1 -3
- package/templates/app-briefs/expo-router/flourish-auth/src/app/(tabs)/_layout.tsx +16 -13
- package/templates/app-briefs/expo-router/flourish-auth/src/app/(tabs)/explore.tsx +69 -11
- package/templates/app-briefs/expo-router/flourish-auth/src/app/(tabs)/index.tsx +46 -51
- package/templates/app-briefs/expo-router/flourish-auth/src/app/(tabs)/profile.tsx +99 -17
- package/templates/app-briefs/expo-router/flourish-auth/src/app/(tabs)/search.tsx +15 -14
- package/templates/app-briefs/expo-router/flourish-auth/src/app/_layout.tsx +5 -20
- package/templates/app-briefs/expo-router/flourish-auth/src/components/ui/screen.tsx +0 -1
- package/templates/app-briefs/expo-router/flourish-auth/src/components/ui/squircle.tsx +0 -1
- package/templates/app-briefs/expo-router/flourish-auth/src/components/ui/text.tsx +0 -1
- package/templates/app-briefs/expo-router/flourish-auth/src/lib/constants/index.ts +47 -0
- package/templates/app-briefs/expo-router/flourish-auth/src/lib/utils/index.ts +0 -1
- package/templates/app-briefs/expo-router/flourish-auth/src/types/plant.d.ts +0 -1
- package/templates/app-briefs/expo-router/flourish-auth/tsconfig.json +0 -1
- package/templates/app-briefs/expo-router/flourish-auth/src/app/(auth)/_layout.tsx +0 -14
- package/templates/app-briefs/expo-router/flourish-auth/src/app/(auth)/sign-in.tsx +0 -25
- package/templates/app-briefs/expo-router/flourish-auth/src/app/(auth)/sign-up.tsx +0 -26
- package/templates/app-briefs/expo-router/flourish-auth/src/app/(auth)/welcome.tsx +0 -22
- package/templates/app-briefs/expo-router/flourish-auth/src/app/plant/[id].tsx +0 -48
- package/templates/app-briefs/expo-router/flourish-auth/src/lib/stores/README.md +0 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Flourish Auth - Expo Router
|
|
1
|
+
# Flourish Auth - Expo Router Authentication App Brief
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A beautiful plant care app built with React Native, Expo, and Expo Router for learning authentication flows.
|
|
4
4
|
|
|
5
5
|
## Getting Started
|
|
6
6
|
|
|
@@ -19,135 +19,88 @@ bun start
|
|
|
19
19
|
```
|
|
20
20
|
src/
|
|
21
21
|
├── app/
|
|
22
|
-
│ ├── _layout.tsx # Root layout
|
|
23
|
-
│ ├── (
|
|
24
|
-
│ │ ├── _layout.tsx #
|
|
25
|
-
│ │ ├──
|
|
26
|
-
│ │ ├──
|
|
27
|
-
│ │
|
|
28
|
-
│
|
|
29
|
-
|
|
30
|
-
│
|
|
31
|
-
│
|
|
32
|
-
│
|
|
33
|
-
│ │ └── profile.tsx # TODO: Add logout button
|
|
34
|
-
│ └── plant/
|
|
35
|
-
│ └── [id].tsx # TODO
|
|
36
|
-
├── components/ui/ # Complete
|
|
22
|
+
│ ├── _layout.tsx # Root layout with Stack
|
|
23
|
+
│ ├── (tabs)/ # Tab group
|
|
24
|
+
│ │ ├── _layout.tsx # Tab navigator config
|
|
25
|
+
│ │ ├── index.tsx # Home tab
|
|
26
|
+
│ │ ├── explore.tsx # Explore tab
|
|
27
|
+
│ │ ├── search.tsx # Search tab
|
|
28
|
+
│ │ └── profile.tsx # Profile tab
|
|
29
|
+
├── components/ui/
|
|
30
|
+
│ ├── screen.tsx
|
|
31
|
+
│ ├── squircle.tsx
|
|
32
|
+
│ └── text.tsx
|
|
37
33
|
├── lib/
|
|
38
|
-
│ ├── constants/
|
|
39
|
-
│
|
|
40
|
-
│ │ └── README.md # TODO: Create auth.ts store
|
|
41
|
-
│ └── utils/ # Complete
|
|
34
|
+
│ ├── constants/index.ts # Plant data
|
|
35
|
+
│ └── utils/index.ts # Date helpers
|
|
42
36
|
└── types/
|
|
37
|
+
└── plant.d.ts
|
|
43
38
|
```
|
|
44
39
|
|
|
45
|
-
## Your
|
|
40
|
+
## Your Task
|
|
46
41
|
|
|
47
|
-
|
|
42
|
+
This app brief teaches **Expo Router Authentication Flows**. The base app has tab navigation already set up. Your task is to add authentication.
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
import { create } from 'zustand';
|
|
44
|
+
### Screens to Build
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const useAuth = create<AuthState>((set) => ({
|
|
59
|
-
isLoggedIn: false,
|
|
60
|
-
login: () => set({ isLoggedIn: true }),
|
|
61
|
-
logout: () => set({ isLoggedIn: false }),
|
|
62
|
-
}));
|
|
63
|
-
|
|
64
|
-
export default useAuth;
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### 2. Implement Protected Routes (`src/app/_layout.tsx`)
|
|
46
|
+
1. **`(auth)/welcome.tsx`** - Welcome/landing screen
|
|
47
|
+
- App branding and logo
|
|
48
|
+
- "Sign In" and "Sign Up" buttons
|
|
49
|
+
- Navigate to respective auth screens
|
|
68
50
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
<Stack screenOptions={{ headerShown: false }}>
|
|
77
|
-
{/* Show auth screens when NOT logged in */}
|
|
78
|
-
<Stack.Protected guard={!isLoggedIn}>
|
|
79
|
-
<Stack.Screen name="(auth)" />
|
|
80
|
-
</Stack.Protected>
|
|
81
|
-
|
|
82
|
-
{/* Show app screens when logged in */}
|
|
83
|
-
<Stack.Protected guard={isLoggedIn}>
|
|
84
|
-
<Stack.Screen name="(tabs)" />
|
|
85
|
-
<Stack.Screen name="plant/[id]" />
|
|
86
|
-
</Stack.Protected>
|
|
87
|
-
</Stack>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
```
|
|
51
|
+
2. **`(auth)/sign-in.tsx`** - Sign in form
|
|
52
|
+
- Email and password inputs
|
|
53
|
+
- Sign in button
|
|
54
|
+
- Link to sign up page
|
|
55
|
+
- Back button to welcome
|
|
91
56
|
|
|
92
|
-
|
|
57
|
+
3. **`(auth)/sign-up.tsx`** - Sign up form
|
|
58
|
+
- Name, email, and password inputs
|
|
59
|
+
- Sign up button
|
|
60
|
+
- Link to sign in page
|
|
61
|
+
- Back button to welcome
|
|
93
62
|
|
|
94
|
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
- "Sign In" link → sign-in
|
|
63
|
+
4. **`(auth)/_layout.tsx`** - Auth group layout
|
|
64
|
+
- Stack navigator for auth screens
|
|
65
|
+
- Configure screen options
|
|
98
66
|
|
|
99
|
-
|
|
100
|
-
- Email & password inputs
|
|
101
|
-
- Sign In button → calls `login()`
|
|
102
|
-
- Link to sign-up
|
|
67
|
+
### Authentication Concepts
|
|
103
68
|
|
|
104
|
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
69
|
+
You'll need to implement:
|
|
70
|
+
- Protected routes (redirect unauthenticated users)
|
|
71
|
+
- Auth state management (using Zustand or Context)
|
|
72
|
+
- Conditional navigation based on auth status
|
|
73
|
+
- Route groups for organization: `(auth)` and `(tabs)`
|
|
108
74
|
|
|
109
|
-
###
|
|
75
|
+
### Navigation Flow
|
|
110
76
|
|
|
111
|
-
```tsx
|
|
112
|
-
import useAuth from "~/lib/stores/auth";
|
|
113
|
-
|
|
114
|
-
export default function Profile() {
|
|
115
|
-
const { logout } = useAuth();
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
// ... profile content ...
|
|
119
|
-
<Pressable onPress={logout}>
|
|
120
|
-
<Text>Sign Out</Text>
|
|
121
|
-
</Pressable>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
77
|
```
|
|
78
|
+
Unauthenticated:
|
|
79
|
+
welcome -> sign-in -> (tabs)
|
|
80
|
+
welcome -> sign-up -> (tabs)
|
|
125
81
|
|
|
126
|
-
|
|
82
|
+
Authenticated:
|
|
83
|
+
(tabs) directly
|
|
84
|
+
```
|
|
127
85
|
|
|
128
|
-
###
|
|
129
|
-
- `(auth)` - Screens for unauthenticated users
|
|
130
|
-
- `(tabs)` - Main app for authenticated users
|
|
86
|
+
### Protecting Routes
|
|
131
87
|
|
|
132
|
-
|
|
133
|
-
Conditionally shows screens based on a guard boolean:
|
|
88
|
+
In `_layout.tsx`, check auth state and redirect:
|
|
134
89
|
```tsx
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
90
|
+
// Redirect to welcome if not authenticated
|
|
91
|
+
if (!isAuthenticated && !isAuthRoute) {
|
|
92
|
+
return <Redirect href="/welcome" />;
|
|
93
|
+
}
|
|
138
94
|
```
|
|
139
95
|
|
|
140
|
-
|
|
141
|
-
1. User opens app → sees welcome (not logged in)
|
|
142
|
-
2. User signs in → `login()` sets `isLoggedIn: true`
|
|
143
|
-
3. Protected Routes automatically show `(tabs)`
|
|
144
|
-
4. User taps Sign Out → `logout()` → back to `(auth)`
|
|
96
|
+
## Design System
|
|
145
97
|
|
|
146
|
-
|
|
98
|
+
- 🌿 Green nature theme (`#234823` primary)
|
|
99
|
+
- ☀️ Golden accent (`#ffce47`)
|
|
100
|
+
- 🟢 Squircle cards with smooth corners
|
|
147
101
|
|
|
148
|
-
## Available
|
|
102
|
+
## Available Data
|
|
149
103
|
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
-
-
|
|
153
|
-
- Home screen fully implemented
|
|
104
|
+
- `customPlants` - Array of 5 plants with full details
|
|
105
|
+
- `getWeekDays(date)` - Helper for calendar display
|
|
106
|
+
- `Plant` interface - TypeScript type for plants
|
|
@@ -42,8 +42,7 @@
|
|
|
42
42
|
"sonner-native": "latest",
|
|
43
43
|
"tailwind-merge": "latest",
|
|
44
44
|
"tailwindcss": "^4.1.16",
|
|
45
|
-
"uniwind": "^1.0.0"
|
|
46
|
-
"zustand": "^5.0.9"
|
|
45
|
+
"uniwind": "^1.0.0"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
48
|
"@rozenite/metro": "^1.0.0-alpha.16",
|
|
@@ -52,4 +51,3 @@
|
|
|
52
51
|
"typescript": "~5.9.2"
|
|
53
52
|
}
|
|
54
53
|
}
|
|
55
|
-
|
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
import IonIcons from "@expo/vector-icons/Ionicons";
|
|
2
2
|
import { Tabs } from "expo-router";
|
|
3
|
+
import React from "react";
|
|
3
4
|
import { useCSSVariable } from "uniwind";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
type TabBarIconType = {
|
|
6
7
|
name: React.ComponentProps<typeof IonIcons>["name"];
|
|
7
8
|
color: string;
|
|
8
|
-
}
|
|
9
|
-
return <IonIcons size={28} style={{ marginBottom: -4 }} {...props} />;
|
|
10
|
-
}
|
|
9
|
+
};
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const TabBarIcon = (props: TabBarIconType) => {
|
|
12
|
+
return <IonIcons size={28} {...props} />;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const TabsLayout = () => {
|
|
14
16
|
const primaryColor = useCSSVariable("--color-primary") as string;
|
|
15
|
-
const
|
|
17
|
+
const tabBarInActiveTintColor = useCSSVariable(
|
|
16
18
|
"--color-muted-foreground"
|
|
17
19
|
) as string;
|
|
18
20
|
|
|
21
|
+
const backgroundColor = useCSSVariable("--color-muted") as string;
|
|
22
|
+
|
|
19
23
|
return (
|
|
20
24
|
<Tabs
|
|
21
25
|
screenOptions={{
|
|
22
26
|
headerShown: false,
|
|
27
|
+
|
|
23
28
|
tabBarActiveTintColor: primaryColor,
|
|
24
|
-
tabBarInactiveTintColor:
|
|
29
|
+
tabBarInactiveTintColor: tabBarInActiveTintColor,
|
|
25
30
|
tabBarStyle: {
|
|
26
31
|
backgroundColor,
|
|
32
|
+
paddingTop: 16,
|
|
27
33
|
},
|
|
28
34
|
}}
|
|
29
35
|
>
|
|
@@ -31,7 +37,6 @@ export default function TabLayout() {
|
|
|
31
37
|
name="index"
|
|
32
38
|
options={{
|
|
33
39
|
title: "Home",
|
|
34
|
-
headerShown: false,
|
|
35
40
|
tabBarShowLabel: false,
|
|
36
41
|
tabBarIcon: ({ color }) => (
|
|
37
42
|
<TabBarIcon name="partly-sunny" color={color} />
|
|
@@ -42,7 +47,6 @@ export default function TabLayout() {
|
|
|
42
47
|
name="explore"
|
|
43
48
|
options={{
|
|
44
49
|
title: "Explore",
|
|
45
|
-
headerShown: false,
|
|
46
50
|
tabBarShowLabel: false,
|
|
47
51
|
tabBarIcon: ({ color }) => <TabBarIcon name="grid" color={color} />,
|
|
48
52
|
}}
|
|
@@ -51,7 +55,6 @@ export default function TabLayout() {
|
|
|
51
55
|
name="search"
|
|
52
56
|
options={{
|
|
53
57
|
title: "Search",
|
|
54
|
-
headerShown: false,
|
|
55
58
|
tabBarShowLabel: false,
|
|
56
59
|
tabBarIcon: ({ color }) => <TabBarIcon name="search" color={color} />,
|
|
57
60
|
}}
|
|
@@ -60,12 +63,12 @@ export default function TabLayout() {
|
|
|
60
63
|
name="profile"
|
|
61
64
|
options={{
|
|
62
65
|
title: "Profile",
|
|
63
|
-
headerShown: false,
|
|
64
66
|
tabBarShowLabel: false,
|
|
65
67
|
tabBarIcon: ({ color }) => <TabBarIcon name="person" color={color} />,
|
|
66
68
|
}}
|
|
67
69
|
/>
|
|
68
70
|
</Tabs>
|
|
69
71
|
);
|
|
70
|
-
}
|
|
72
|
+
};
|
|
71
73
|
|
|
74
|
+
export default TabsLayout;
|
|
@@ -1,24 +1,82 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Ionicons from "@expo/vector-icons/Ionicons";
|
|
2
|
+
import { FlatList, Image, Pressable, ScrollView, View } from "react-native";
|
|
3
|
+
import { twMerge } from "tailwind-merge";
|
|
2
4
|
import { Screen } from "~/components/ui/screen";
|
|
5
|
+
import { Squircle } from "~/components/ui/squircle";
|
|
3
6
|
import Text from "~/components/ui/text";
|
|
7
|
+
import { categories, customPlants } from "~/lib/constants";
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
type ExploreCardProps = {
|
|
10
|
+
item: Plant;
|
|
11
|
+
};
|
|
12
|
+
const ExploreCard = ({ item }: ExploreCardProps) => {
|
|
13
|
+
return (
|
|
14
|
+
<Pressable className="mx-5">
|
|
15
|
+
<Squircle
|
|
16
|
+
cornerSmoothing={1}
|
|
17
|
+
className="overflow-hidden rounded-3xl bg-primary"
|
|
18
|
+
>
|
|
19
|
+
<Image
|
|
20
|
+
source={{ uri: item.coverImg }}
|
|
21
|
+
className="w-full h-48"
|
|
22
|
+
resizeMode="cover"
|
|
23
|
+
/>
|
|
24
|
+
<View className="p-5">
|
|
25
|
+
<Text variant="subtitle-secondary">{item.locationType}</Text>
|
|
26
|
+
<Text variant="caption">{item.name}</Text>
|
|
27
|
+
</View>
|
|
28
|
+
</Squircle>
|
|
29
|
+
</Pressable>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
10
32
|
|
|
33
|
+
const ListHeader = () => {
|
|
11
34
|
return (
|
|
12
|
-
|
|
35
|
+
<>
|
|
13
36
|
<View className="px-5 pt-4 pb-4">
|
|
14
37
|
<Text variant="display" className="text-primary">
|
|
15
38
|
Explore
|
|
16
39
|
</Text>
|
|
17
|
-
<Text className="text-muted-foreground mt-2">
|
|
18
|
-
Build this screen to browse plants by category
|
|
19
|
-
</Text>
|
|
20
40
|
</View>
|
|
41
|
+
<ScrollView
|
|
42
|
+
horizontal
|
|
43
|
+
showsHorizontalScrollIndicator={false}
|
|
44
|
+
contentContainerClassName="px-5 gap-2 pb-4"
|
|
45
|
+
>
|
|
46
|
+
{categories.map((category) => (
|
|
47
|
+
<Pressable key={category.id}>
|
|
48
|
+
<Squircle
|
|
49
|
+
cornerSmoothing={1}
|
|
50
|
+
className={twMerge(
|
|
51
|
+
"flex-row bg-primary items-center gap-2 px-3 py-2 rounded-xl",
|
|
52
|
+
category.bgColor
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
<Ionicons
|
|
56
|
+
name={category.icon as any}
|
|
57
|
+
size={16}
|
|
58
|
+
color={category.bgColor}
|
|
59
|
+
/>
|
|
60
|
+
<Text variant="caption-primary">{category.label}</Text>
|
|
61
|
+
</Squircle>
|
|
62
|
+
</Pressable>
|
|
63
|
+
))}
|
|
64
|
+
</ScrollView>
|
|
65
|
+
</>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const Explore = () => {
|
|
70
|
+
return (
|
|
71
|
+
<Screen>
|
|
72
|
+
<FlatList
|
|
73
|
+
data={customPlants}
|
|
74
|
+
renderItem={({ item }) => <ExploreCard item={item} />}
|
|
75
|
+
contentContainerClassName="pb-8 gap-5 pt-2"
|
|
76
|
+
ListHeaderComponent={ListHeader}
|
|
77
|
+
/>
|
|
21
78
|
</Screen>
|
|
22
79
|
);
|
|
23
|
-
}
|
|
80
|
+
};
|
|
24
81
|
|
|
82
|
+
export default Explore;
|
|
@@ -1,51 +1,71 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { format } from "date-fns";
|
|
2
|
+
import { router } from "expo-router";
|
|
3
|
+
import { FlatList, Image, Pressable, View } from "react-native";
|
|
2
4
|
import { Screen } from "~/components/ui/screen";
|
|
3
|
-
import Text from "~/components/ui/text";
|
|
4
5
|
import { Squircle } from "~/components/ui/squircle";
|
|
6
|
+
import Text from "~/components/ui/text";
|
|
5
7
|
import { customPlants } from "~/lib/constants";
|
|
6
|
-
import { useRouter } from "expo-router";
|
|
7
|
-
import { format } from "date-fns";
|
|
8
8
|
import { getWeekDays } from "~/lib/utils";
|
|
9
9
|
|
|
10
|
+
type PlantCardProps = {
|
|
11
|
+
item: Plant;
|
|
12
|
+
onPress?: () => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const PlantCard = ({ item, onPress }: PlantCardProps) => {
|
|
16
|
+
return (
|
|
17
|
+
<Pressable onPress={() => router.push(`/`)} className="flex-1">
|
|
18
|
+
<Squircle
|
|
19
|
+
className="bg-primary overflow-hidden rounded-3xl p-2"
|
|
20
|
+
cornerSmoothing={1}
|
|
21
|
+
>
|
|
22
|
+
<Squircle className="overflow-hidden rounded-2xl w-full aspect-square">
|
|
23
|
+
<Image
|
|
24
|
+
source={{ uri: item.coverImg }}
|
|
25
|
+
className="w-full h-full aspect-square"
|
|
26
|
+
/>
|
|
27
|
+
</Squircle>
|
|
28
|
+
<View className="px-1 py-2">
|
|
29
|
+
<Text variant="body-secondary">{item.name}</Text>
|
|
30
|
+
</View>
|
|
31
|
+
</Squircle>
|
|
32
|
+
</Pressable>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
10
36
|
const now = new Date();
|
|
11
37
|
const weekDays = getWeekDays(now);
|
|
12
38
|
|
|
13
39
|
const HomeHeader = () => {
|
|
14
40
|
return (
|
|
15
|
-
<View className="px-5
|
|
41
|
+
<View className="px-5 py-4">
|
|
16
42
|
<View className="flex-row justify-between items-start mb-5">
|
|
17
43
|
<View className="flex-row items-center gap-2">
|
|
18
|
-
<Text variant="display"
|
|
19
|
-
<View className="w-
|
|
44
|
+
<Text variant="display">{format(now, "EEE")}</Text>
|
|
45
|
+
<View className="w-4 h-4 rounded-full bg-accent"></View>
|
|
20
46
|
</View>
|
|
21
|
-
|
|
22
47
|
<View className="items-end">
|
|
23
48
|
<Text variant="subtitle">{format(now, "MMMM d")}</Text>
|
|
24
|
-
<Text
|
|
49
|
+
<Text>{format(now, "yyyy")}</Text>
|
|
25
50
|
</View>
|
|
26
51
|
</View>
|
|
27
|
-
|
|
28
52
|
<View className="flex-row justify-between">
|
|
29
53
|
{weekDays.map((day, index) => (
|
|
30
54
|
<View key={index} className="items-center">
|
|
31
55
|
{day.isToday ? (
|
|
32
56
|
<Squircle
|
|
33
|
-
className="bg-primary
|
|
57
|
+
className="bg-primary p-2 items-center rounded-xl"
|
|
34
58
|
cornerSmoothing={1}
|
|
35
59
|
>
|
|
36
|
-
<Text
|
|
37
|
-
|
|
38
|
-
</Text>
|
|
39
|
-
<Text className="text-xs font-medium text-secondary mt-1">
|
|
40
|
-
{day.day}
|
|
41
|
-
</Text>
|
|
60
|
+
<Text variant="subtitle-secondary">{day.date}</Text>
|
|
61
|
+
<Text variant="caption">{day.day}</Text>
|
|
42
62
|
</Squircle>
|
|
43
63
|
) : (
|
|
44
|
-
<View className="
|
|
45
|
-
<Text className="text-
|
|
64
|
+
<View className="p-2 items-center">
|
|
65
|
+
<Text variant="subtitle" className="text-primary/40">
|
|
46
66
|
{day.date}
|
|
47
67
|
</Text>
|
|
48
|
-
<Text className="text-
|
|
68
|
+
<Text variant="caption" className="text-primary/30">
|
|
49
69
|
{day.day}
|
|
50
70
|
</Text>
|
|
51
71
|
</View>
|
|
@@ -57,46 +77,21 @@ const HomeHeader = () => {
|
|
|
57
77
|
);
|
|
58
78
|
};
|
|
59
79
|
|
|
60
|
-
const
|
|
61
|
-
<Pressable onPress={onPress} className="flex-1">
|
|
62
|
-
<Squircle className="bg-primary overflow-hidden rounded-3xl p-2" cornerSmoothing={1}>
|
|
63
|
-
<Squircle className="overflow-hidden rounded-2xl w-full aspect-square" cornerSmoothing={1}>
|
|
64
|
-
<Image source={{ uri: item.coverImg }} className="w-full h-full" resizeMode="cover" />
|
|
65
|
-
</Squircle>
|
|
66
|
-
<View className="px-1 py-2">
|
|
67
|
-
<Text variant="body-secondary" className="text-primary-foreground">{item.name}</Text>
|
|
68
|
-
</View>
|
|
69
|
-
</Squircle>
|
|
70
|
-
</Pressable>
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
const ListHeader = () => (
|
|
74
|
-
<>
|
|
75
|
-
<HomeHeader />
|
|
76
|
-
<View className="px-5 pb-4">
|
|
77
|
-
<Text variant="subtitle" className="text-primary">Our Plants</Text>
|
|
78
|
-
</View>
|
|
79
|
-
</>
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
export default function Index() {
|
|
83
|
-
const router = useRouter();
|
|
84
|
-
|
|
80
|
+
const Home = () => {
|
|
85
81
|
return (
|
|
86
82
|
<Screen>
|
|
87
83
|
<FlatList
|
|
88
84
|
data={customPlants}
|
|
85
|
+
renderItem={({ item }) => <PlantCard item={item} />}
|
|
89
86
|
numColumns={2}
|
|
87
|
+
ListHeaderComponent={HomeHeader}
|
|
90
88
|
keyExtractor={(item) => item.id}
|
|
91
|
-
renderItem={({ item }) => (
|
|
92
|
-
<PlantCard item={item} onPress={() => router.push(`/plant/${item.id}`)} />
|
|
93
|
-
)}
|
|
94
|
-
ListHeaderComponent={ListHeader}
|
|
95
|
-
columnWrapperClassName="gap-4 px-5"
|
|
96
89
|
contentContainerClassName="gap-4 pb-8"
|
|
90
|
+
columnWrapperClassName="gap-4 px-5"
|
|
97
91
|
showsVerticalScrollIndicator={false}
|
|
98
92
|
/>
|
|
99
93
|
</Screen>
|
|
100
94
|
);
|
|
101
|
-
}
|
|
95
|
+
};
|
|
102
96
|
|
|
97
|
+
export default Home;
|
|
@@ -1,27 +1,109 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Ionicons from "@expo/vector-icons/Ionicons";
|
|
2
|
+
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
|
|
3
|
+
import { Image, Pressable, ScrollView, View } from "react-native";
|
|
4
|
+
import { useCSSVariable } from "uniwind";
|
|
2
5
|
import { Screen } from "~/components/ui/screen";
|
|
6
|
+
import { Squircle } from "~/components/ui/squircle";
|
|
3
7
|
import Text from "~/components/ui/text";
|
|
8
|
+
import { userData } from "~/lib/constants";
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// - Show plant stats (plants owned, waterings)
|
|
11
|
-
// - Add "Currently..." status card
|
|
12
|
-
// - Add Edit Profile button
|
|
13
|
-
// - Add Sign Out button that calls logout()
|
|
10
|
+
const Profile = () => {
|
|
11
|
+
const [primaryColor, secondaryColor] = useCSSVariable([
|
|
12
|
+
"--color-primary",
|
|
13
|
+
"--color-secondary",
|
|
14
|
+
]);
|
|
14
15
|
|
|
15
16
|
return (
|
|
16
17
|
<Screen>
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
<ScrollView
|
|
19
|
+
className="flex-1"
|
|
20
|
+
contentContainerClassName="pb-8"
|
|
21
|
+
showsVerticalScrollIndicator={false}
|
|
22
|
+
>
|
|
23
|
+
<View className="items-center mt-8 px-8">
|
|
24
|
+
<View className="relative w-36">
|
|
25
|
+
<Squircle className="absolute -bottom-1.5 -left-1.5 w-full aspect-3/4 bg-primary rounded-3xl -rotate-3" />
|
|
26
|
+
<Squircle
|
|
27
|
+
className="w-full aspect-3/4
|
|
28
|
+
overflow-hidden rounded-3xl z-10
|
|
29
|
+
"
|
|
30
|
+
>
|
|
31
|
+
<Image
|
|
32
|
+
source={{ uri: userData.avatar }}
|
|
33
|
+
className="w-full h-full"
|
|
34
|
+
resizeMode="cover"
|
|
35
|
+
/>
|
|
36
|
+
</Squircle>
|
|
37
|
+
</View>
|
|
38
|
+
</View>
|
|
39
|
+
<Text variant="title" className="text-primary text-center my-4">
|
|
40
|
+
{userData.username}
|
|
23
41
|
</Text>
|
|
42
|
+
|
|
43
|
+
<View className="px-6 mt-8 pb-4">
|
|
44
|
+
<View className="relative">
|
|
45
|
+
<View className="absolute -top-3 left-8 flex-row items-end gap-1 z-10">
|
|
46
|
+
<View className="size-4 rounded-full bg-primary" />
|
|
47
|
+
<View className="size-5 rounded-full bg-primary" />
|
|
48
|
+
</View>
|
|
49
|
+
<Squircle
|
|
50
|
+
className="absolute top-1 left-0 right-0 -bottom-2 bg-accent
|
|
51
|
+
rounded-3xl"
|
|
52
|
+
/>
|
|
53
|
+
|
|
54
|
+
<Squircle className="bg-primary p-5 rounded-3xl z-10">
|
|
55
|
+
<View className="flex-row items-center gap-4">
|
|
56
|
+
<View className="w-12 h-12 rounded-full bg-secondary/20 items-center justify-center">
|
|
57
|
+
<Ionicons
|
|
58
|
+
name="chatbubble-outline"
|
|
59
|
+
size={26}
|
|
60
|
+
color={"white"}
|
|
61
|
+
/>
|
|
62
|
+
</View>
|
|
63
|
+
<View className="flex-1">
|
|
64
|
+
<Text className="text-secondary/80">
|
|
65
|
+
{userData.plantLoveLabel}
|
|
66
|
+
</Text>
|
|
67
|
+
<Text className="text-primary-foreground">
|
|
68
|
+
{userData.plantLoveValue}
|
|
69
|
+
</Text>
|
|
70
|
+
</View>
|
|
71
|
+
</View>
|
|
72
|
+
</Squircle>
|
|
73
|
+
</View>
|
|
74
|
+
</View>
|
|
75
|
+
|
|
76
|
+
<View className="px-6 mt-6 gap-5">
|
|
77
|
+
<View className="flex-row items-center gap-4">
|
|
78
|
+
<MaterialCommunityIcons
|
|
79
|
+
name="flower-tulip"
|
|
80
|
+
size={28}
|
|
81
|
+
color={primaryColor as string}
|
|
82
|
+
/>
|
|
83
|
+
<Text variant="title" className="text-primary">
|
|
84
|
+
{userData.plantsOwned} Plants
|
|
85
|
+
</Text>
|
|
86
|
+
</View>
|
|
87
|
+
<View className="flex-row items-center gap-4">
|
|
88
|
+
<MaterialCommunityIcons
|
|
89
|
+
name="water"
|
|
90
|
+
size={28}
|
|
91
|
+
color={primaryColor as string}
|
|
92
|
+
/>
|
|
93
|
+
<Text variant="title" className="text-primary">
|
|
94
|
+
{userData.plantsWatered} Watered
|
|
95
|
+
</Text>
|
|
96
|
+
</View>
|
|
97
|
+
</View>
|
|
98
|
+
</ScrollView>
|
|
99
|
+
|
|
100
|
+
<View className="px-6 pb-6 pt-4">
|
|
101
|
+
<Pressable className="flex-row items-center justify-center gap-3 bg-primary py-4 px-6 rounded-full">
|
|
102
|
+
<Text variant="button">Edit Profile</Text>
|
|
103
|
+
</Pressable>
|
|
24
104
|
</View>
|
|
25
105
|
</Screen>
|
|
26
106
|
);
|
|
27
|
-
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export default Profile;
|
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Ionicons from "@expo/vector-icons/Ionicons";
|
|
2
|
+
import { TextInput, View } from "react-native";
|
|
3
|
+
import { useCSSVariable } from "uniwind";
|
|
2
4
|
import { Screen } from "~/components/ui/screen";
|
|
3
|
-
import Text from "~/components/ui/text";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// - Add a search input with icon
|
|
8
|
-
// - Filter plants by name as user types
|
|
9
|
-
// - Display matching results
|
|
6
|
+
const Search = () => {
|
|
7
|
+
const [mutedColor] = useCSSVariable(["--color-muted-foreground"]);
|
|
10
8
|
|
|
11
9
|
return (
|
|
12
10
|
<Screen>
|
|
13
11
|
<View className="px-5 pt-4">
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
<View className="flex-row items-center bg-muted/50 rounded-xl px-4 py-3 gap-3">
|
|
13
|
+
<Ionicons name="search" size={20} />
|
|
14
|
+
<TextInput
|
|
15
|
+
placeholder="Search plants..."
|
|
16
|
+
placeholderTextColor={mutedColor as string}
|
|
17
|
+
className="flex-1 text-base text-foreground"
|
|
18
|
+
/>
|
|
19
|
+
</View>
|
|
20
20
|
</View>
|
|
21
21
|
</Screen>
|
|
22
22
|
);
|
|
23
|
-
}
|
|
23
|
+
};
|
|
24
24
|
|
|
25
|
+
export default Search;
|
|
@@ -8,31 +8,16 @@ import "../../global.css";
|
|
|
8
8
|
import { StyleSheet } from "react-native";
|
|
9
9
|
|
|
10
10
|
export default function RootLayout() {
|
|
11
|
-
// TODO: Import useAuth from ~/lib/stores/auth
|
|
12
|
-
// TODO: Get isLoggedIn from the auth store
|
|
13
|
-
|
|
14
11
|
return (
|
|
15
12
|
<GestureHandlerRootView style={styles.container}>
|
|
16
13
|
<KeyboardProvider>
|
|
17
14
|
<BottomSheetModalProvider>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<Stack.Protected guard={!isLoggedIn}>
|
|
24
|
-
<Stack.Screen name="(auth)" />
|
|
25
|
-
</Stack.Protected>
|
|
26
|
-
|
|
27
|
-
<Stack.Protected guard={isLoggedIn}>
|
|
28
|
-
<Stack.Screen name="(tabs)" />
|
|
29
|
-
<Stack.Screen name="plant/[id]" />
|
|
30
|
-
</Stack.Protected>
|
|
31
|
-
*/}
|
|
32
|
-
<Stack screenOptions={{ headerShown: false }}>
|
|
33
|
-
<Stack.Screen name="(auth)" />
|
|
15
|
+
<Stack
|
|
16
|
+
screenOptions={{
|
|
17
|
+
headerShown: false,
|
|
18
|
+
}}
|
|
19
|
+
>
|
|
34
20
|
<Stack.Screen name="(tabs)" />
|
|
35
|
-
<Stack.Screen name="plant/[id]" />
|
|
36
21
|
</Stack>
|
|
37
22
|
<Toaster />
|
|
38
23
|
</BottomSheetModalProvider>
|
|
@@ -146,3 +146,50 @@ export const customPlants: Plant[] = [
|
|
|
146
146
|
},
|
|
147
147
|
];
|
|
148
148
|
|
|
149
|
+
export const categories = [
|
|
150
|
+
{
|
|
151
|
+
id: "indoor",
|
|
152
|
+
label: "Indoor",
|
|
153
|
+
icon: "home-outline",
|
|
154
|
+
bgColor: "bg-primary/15",
|
|
155
|
+
iconColor: "#234823",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "outdoor",
|
|
159
|
+
label: "Outdoor",
|
|
160
|
+
icon: "sunny-outline",
|
|
161
|
+
bgColor: "bg-accent/40",
|
|
162
|
+
iconColor: "#234823",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "low-water",
|
|
166
|
+
label: "Low Water",
|
|
167
|
+
icon: "water-outline",
|
|
168
|
+
bgColor: "bg-muted",
|
|
169
|
+
iconColor: "#234823",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "pet-safe",
|
|
173
|
+
label: "Pet Safe",
|
|
174
|
+
icon: "paw-outline",
|
|
175
|
+
bgColor: "bg-secondary",
|
|
176
|
+
iconColor: "#234823",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: "beginner",
|
|
180
|
+
label: "Beginner",
|
|
181
|
+
icon: "leaf-outline",
|
|
182
|
+
bgColor: "bg-primary/10",
|
|
183
|
+
iconColor: "#234823",
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
export const userData = {
|
|
188
|
+
username: "Olive",
|
|
189
|
+
level: 5,
|
|
190
|
+
avatar: customPlants[3].coverImg,
|
|
191
|
+
plantLoveLabel: "Currently",
|
|
192
|
+
plantLoveValue: "Talking to my plants",
|
|
193
|
+
plantsOwned: 12,
|
|
194
|
+
plantsWatered: 47,
|
|
195
|
+
};
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { Stack } from "expo-router";
|
|
2
|
-
|
|
3
|
-
export default function AuthLayout() {
|
|
4
|
-
// This is the auth stack navigator
|
|
5
|
-
// It wraps the welcome, sign-in, and sign-up screens
|
|
6
|
-
|
|
7
|
-
return (
|
|
8
|
-
<Stack screenOptions={{ headerShown: false }}>
|
|
9
|
-
<Stack.Screen name="welcome" />
|
|
10
|
-
<Stack.Screen name="sign-in" />
|
|
11
|
-
<Stack.Screen name="sign-up" />
|
|
12
|
-
</Stack>
|
|
13
|
-
);
|
|
14
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { View } from "react-native";
|
|
2
|
-
import { Screen } from "~/components/ui/screen";
|
|
3
|
-
import Text from "~/components/ui/text";
|
|
4
|
-
|
|
5
|
-
export default function SignIn() {
|
|
6
|
-
// TODO: Build the sign-in screen
|
|
7
|
-
// - Add back button to go back
|
|
8
|
-
// - Add email input field
|
|
9
|
-
// - Add password input field
|
|
10
|
-
// - Add "Sign In" button that calls login() from auth store
|
|
11
|
-
// - Add link to sign-up screen
|
|
12
|
-
|
|
13
|
-
return (
|
|
14
|
-
<Screen>
|
|
15
|
-
<View className="flex-1 px-6 justify-center">
|
|
16
|
-
<Text variant="display" className="text-primary">
|
|
17
|
-
Sign In
|
|
18
|
-
</Text>
|
|
19
|
-
<Text className="text-muted-foreground mt-2">
|
|
20
|
-
Build this screen with email/password form
|
|
21
|
-
</Text>
|
|
22
|
-
</View>
|
|
23
|
-
</Screen>
|
|
24
|
-
);
|
|
25
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { View } from "react-native";
|
|
2
|
-
import { Screen } from "~/components/ui/screen";
|
|
3
|
-
import Text from "~/components/ui/text";
|
|
4
|
-
|
|
5
|
-
export default function SignUp() {
|
|
6
|
-
// TODO: Build the sign-up screen
|
|
7
|
-
// - Add back button to go back
|
|
8
|
-
// - Add name input field
|
|
9
|
-
// - Add email input field
|
|
10
|
-
// - Add password input field
|
|
11
|
-
// - Add "Create Account" button that calls login() from auth store
|
|
12
|
-
// - Add link to sign-in screen
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<Screen>
|
|
16
|
-
<View className="flex-1 px-6 justify-center">
|
|
17
|
-
<Text variant="display" className="text-primary">
|
|
18
|
-
Sign Up
|
|
19
|
-
</Text>
|
|
20
|
-
<Text className="text-muted-foreground mt-2">
|
|
21
|
-
Build this screen with registration form
|
|
22
|
-
</Text>
|
|
23
|
-
</View>
|
|
24
|
-
</Screen>
|
|
25
|
-
);
|
|
26
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { View } from "react-native";
|
|
2
|
-
import { Screen } from "~/components/ui/screen";
|
|
3
|
-
import Text from "~/components/ui/text";
|
|
4
|
-
|
|
5
|
-
export default function Welcome() {
|
|
6
|
-
// TODO: Build the welcome/landing screen
|
|
7
|
-
// - Display app logo/icon (use MaterialCommunityIcons "flower")
|
|
8
|
-
// - Show app name "Flourish" and tagline
|
|
9
|
-
// - Add "Get Started" button → navigates to sign-up
|
|
10
|
-
// - Add "Already have an account? Sign In" link → navigates to sign-in
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
<Screen className="justify-center items-center">
|
|
14
|
-
<Text variant="display" className="text-primary">
|
|
15
|
-
Welcome
|
|
16
|
-
</Text>
|
|
17
|
-
<Text className="text-muted-foreground mt-2">
|
|
18
|
-
Build this screen with login/signup buttons
|
|
19
|
-
</Text>
|
|
20
|
-
</Screen>
|
|
21
|
-
);
|
|
22
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { View } from "react-native";
|
|
2
|
-
import { Stack, useLocalSearchParams } from "expo-router";
|
|
3
|
-
import { Screen } from "~/components/ui/screen";
|
|
4
|
-
import Text from "~/components/ui/text";
|
|
5
|
-
import { customPlants } from "~/lib/constants";
|
|
6
|
-
|
|
7
|
-
export default function PlantDetails() {
|
|
8
|
-
const { id } = useLocalSearchParams<{ id: string }>();
|
|
9
|
-
|
|
10
|
-
const plant = customPlants.find((p) => p.id === id);
|
|
11
|
-
|
|
12
|
-
if (!plant) {
|
|
13
|
-
return (
|
|
14
|
-
<>
|
|
15
|
-
<Stack.Screen options={{ headerShown: false }} />
|
|
16
|
-
<Screen className="flex-1 items-center justify-center bg-background">
|
|
17
|
-
<Text>Plant not found</Text>
|
|
18
|
-
</Screen>
|
|
19
|
-
</>
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// TODO: Implement the Plant Details screen
|
|
24
|
-
// - Add back button navigation
|
|
25
|
-
// - Display plant image in a stacked card design
|
|
26
|
-
// - Show plant name and scientific name
|
|
27
|
-
// - Add quick info pills (water needs, light needs)
|
|
28
|
-
// - Display "About this plant" card with description
|
|
29
|
-
// - Show care details (water frequency, growth rate, maintenance)
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<>
|
|
33
|
-
<Stack.Screen options={{ headerShown: false }} />
|
|
34
|
-
<Screen className="flex-1 bg-background">
|
|
35
|
-
<View className="px-5 pt-4">
|
|
36
|
-
<Text variant="display">{plant.name}</Text>
|
|
37
|
-
<Text className="text-muted-foreground mt-1">
|
|
38
|
-
{plant.scientificName}
|
|
39
|
-
</Text>
|
|
40
|
-
<Text className="text-muted-foreground mt-4">
|
|
41
|
-
Build this screen to show plant details
|
|
42
|
-
</Text>
|
|
43
|
-
</View>
|
|
44
|
-
</Screen>
|
|
45
|
-
</>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# Stores
|
|
2
|
-
|
|
3
|
-
This folder is where you'll create your Zustand stores for authentication.
|
|
4
|
-
|
|
5
|
-
## Your Task
|
|
6
|
-
|
|
7
|
-
Create an `auth.ts` store here to manage authentication state.
|
|
8
|
-
|
|
9
|
-
The store should have:
|
|
10
|
-
- `isLoggedIn` - boolean state
|
|
11
|
-
- `login()` - function to set logged in
|
|
12
|
-
- `logout()` - function to set logged out
|
|
13
|
-
|
|
14
|
-
## Example Structure
|
|
15
|
-
|
|
16
|
-
```typescript
|
|
17
|
-
import { create } from 'zustand';
|
|
18
|
-
|
|
19
|
-
interface AuthState {
|
|
20
|
-
isLoggedIn: boolean;
|
|
21
|
-
login: () => void;
|
|
22
|
-
logout: () => void;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const useAuth = create<AuthState>((set) => ({
|
|
26
|
-
// Your implementation here
|
|
27
|
-
}));
|
|
28
|
-
|
|
29
|
-
export default useAuth;
|
|
30
|
-
```
|
|
31
|
-
|