create-croissant 0.1.47 → 0.1.48
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/template/apps/platform/src/routes/api/auth/$.ts +1 -1
- package/template/apps/platform/src/routes/api/rpc.$.ts +2 -2
- package/template/package.json +2 -7
- package/template/tsconfig.json +1 -2
- package/template/apps/mobile/.vscode/extensions.json +0 -1
- package/template/apps/mobile/.vscode/settings.json +0 -7
- package/template/apps/mobile/README.md +0 -50
- package/template/apps/mobile/app/(tabs)/_layout.tsx +0 -43
- package/template/apps/mobile/app/(tabs)/account.tsx +0 -147
- package/template/apps/mobile/app/(tabs)/explore.tsx +0 -345
- package/template/apps/mobile/app/(tabs)/index.tsx +0 -112
- package/template/apps/mobile/app/_layout.tsx +0 -43
- package/template/apps/mobile/app/index.tsx +0 -129
- package/template/apps/mobile/app/login.tsx +0 -135
- package/template/apps/mobile/app/signup.tsx +0 -144
- package/template/apps/mobile/app.json +0 -56
- package/template/apps/mobile/assets/images/android-icon-background.png +0 -0
- package/template/apps/mobile/assets/images/android-icon-foreground.png +0 -0
- package/template/apps/mobile/assets/images/android-icon-monochrome.png +0 -0
- package/template/apps/mobile/assets/images/favicon.png +0 -0
- package/template/apps/mobile/assets/images/icon.png +0 -0
- package/template/apps/mobile/assets/images/partial-react-logo.png +0 -0
- package/template/apps/mobile/assets/images/react-logo.png +0 -0
- package/template/apps/mobile/assets/images/react-logo@2x.png +0 -0
- package/template/apps/mobile/assets/images/react-logo@3x.png +0 -0
- package/template/apps/mobile/assets/images/splash-icon.png +0 -0
- package/template/apps/mobile/components/external-link.tsx +0 -25
- package/template/apps/mobile/components/haptic-tab.tsx +0 -18
- package/template/apps/mobile/components/hello-wave.tsx +0 -20
- package/template/apps/mobile/components/parallax-scroll-view.tsx +0 -81
- package/template/apps/mobile/components/themed-text.tsx +0 -60
- package/template/apps/mobile/components/themed-view.tsx +0 -14
- package/template/apps/mobile/components/ui/button.tsx +0 -86
- package/template/apps/mobile/components/ui/collapsible.tsx +0 -46
- package/template/apps/mobile/components/ui/icon-symbol.ios.tsx +0 -32
- package/template/apps/mobile/components/ui/icon-symbol.tsx +0 -41
- package/template/apps/mobile/components/ui/input.tsx +0 -56
- package/template/apps/mobile/constants/theme.ts +0 -53
- package/template/apps/mobile/hooks/use-color-scheme.ts +0 -1
- package/template/apps/mobile/hooks/use-color-scheme.web.ts +0 -21
- package/template/apps/mobile/hooks/use-theme-color.ts +0 -21
- package/template/apps/mobile/lib/auth-client.ts +0 -14
- package/template/apps/mobile/lib/orpc.ts +0 -28
- package/template/apps/mobile/package.json +0 -57
- package/template/apps/mobile/scripts/reset-project.js +0 -112
- package/template/apps/mobile/tsconfig.json +0 -13
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ import { auth } from "@workspace/auth/lib/auth";
|
|
|
2
2
|
import { createFileRoute } from "@tanstack/react-router";
|
|
3
3
|
|
|
4
4
|
const CORS_HEADERS = {
|
|
5
|
-
"Access-Control-Allow-Origin": "
|
|
5
|
+
"Access-Control-Allow-Origin": "*",
|
|
6
6
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
7
7
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
8
8
|
"Access-Control-Allow-Credentials": "true",
|
|
@@ -28,7 +28,7 @@ export const Route = createFileRoute("/api/rpc/$")({
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
if (response) {
|
|
31
|
-
response.headers.set("Access-Control-Allow-Origin", "
|
|
31
|
+
response.headers.set("Access-Control-Allow-Origin", "*");
|
|
32
32
|
response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
33
33
|
response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
34
34
|
response.headers.set("Access-Control-Allow-Credentials", "true");
|
|
@@ -40,7 +40,7 @@ export const Route = createFileRoute("/api/rpc/$")({
|
|
|
40
40
|
return new Response(null, {
|
|
41
41
|
status: 204,
|
|
42
42
|
headers: {
|
|
43
|
-
"Access-Control-Allow-Origin": "
|
|
43
|
+
"Access-Control-Allow-Origin": "*",
|
|
44
44
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
45
45
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
46
46
|
"Access-Control-Allow-Credentials": "true",
|
package/template/package.json
CHANGED
|
@@ -13,9 +13,6 @@
|
|
|
13
13
|
"quality": "turbo quality",
|
|
14
14
|
"quality:fix": "turbo quality:fix",
|
|
15
15
|
"typecheck": "turbo typecheck",
|
|
16
|
-
"dev:mobile": "turbo run dev --filter=mobile",
|
|
17
|
-
"dev:ios": "turbo run dev --filter=mobile -- --ios",
|
|
18
|
-
"dev:android": "turbo run dev --filter=mobile -- --android",
|
|
19
16
|
"ci": "pnpm run lint && pnpm run typecheck && pnpm run build",
|
|
20
17
|
"db:up": "docker compose up -d",
|
|
21
18
|
"db:down": "docker compose down",
|
|
@@ -25,8 +22,7 @@
|
|
|
25
22
|
},
|
|
26
23
|
"dependencies": {
|
|
27
24
|
"react": "19.2.5",
|
|
28
|
-
"react-dom": "19.2.5"
|
|
29
|
-
"react-native": "0.83.6"
|
|
25
|
+
"react-dom": "19.2.5"
|
|
30
26
|
},
|
|
31
27
|
"devDependencies": {
|
|
32
28
|
"@better-auth/core": "^1.6.9",
|
|
@@ -46,8 +42,7 @@
|
|
|
46
42
|
"@noble/ciphers": "2.2.0",
|
|
47
43
|
"drizzle-orm": "^0.45.2",
|
|
48
44
|
"react": "19.2.5",
|
|
49
|
-
"react-dom": "19.2.5"
|
|
50
|
-
"react-native": "0.83.6"
|
|
45
|
+
"react-dom": "19.2.5"
|
|
51
46
|
}
|
|
52
47
|
},
|
|
53
48
|
"engines": {
|
package/template/tsconfig.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{ "recommendations": ["expo.vscode-expo-tools"] }
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# Welcome to your Expo app 👋
|
|
2
|
-
|
|
3
|
-
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
|
|
4
|
-
|
|
5
|
-
## Get started
|
|
6
|
-
|
|
7
|
-
1. Install dependencies
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
pnpm install
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
2. Start the app
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
pnpm dlx expo start
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
In the output, you'll find options to open the app in a
|
|
20
|
-
|
|
21
|
-
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
|
|
22
|
-
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
|
|
23
|
-
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
|
|
24
|
-
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
|
|
25
|
-
|
|
26
|
-
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
|
|
27
|
-
|
|
28
|
-
## Get a fresh project
|
|
29
|
-
|
|
30
|
-
When you're ready, run:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
pnpm run reset-project
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
|
|
37
|
-
|
|
38
|
-
## Learn more
|
|
39
|
-
|
|
40
|
-
To learn more about developing your project with Expo, look at the following resources:
|
|
41
|
-
|
|
42
|
-
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
|
|
43
|
-
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
|
|
44
|
-
|
|
45
|
-
## Join the community
|
|
46
|
-
|
|
47
|
-
Join our community of developers creating universal apps.
|
|
48
|
-
|
|
49
|
-
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
|
|
50
|
-
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Tabs } from "expo-router";
|
|
3
|
-
import { Ionicons } from "@expo/vector-icons";
|
|
4
|
-
|
|
5
|
-
export default function TabLayout() {
|
|
6
|
-
return (
|
|
7
|
-
<Tabs
|
|
8
|
-
screenOptions={{
|
|
9
|
-
tabBarActiveTintColor: "#000",
|
|
10
|
-
tabBarInactiveTintColor: "#888",
|
|
11
|
-
headerShown: true,
|
|
12
|
-
}}
|
|
13
|
-
>
|
|
14
|
-
<Tabs.Screen
|
|
15
|
-
name="index"
|
|
16
|
-
options={{
|
|
17
|
-
title: "Dashboard",
|
|
18
|
-
tabBarIcon: ({ color, size }) => (
|
|
19
|
-
<Ionicons name="home" size={size} color={color} />
|
|
20
|
-
),
|
|
21
|
-
}}
|
|
22
|
-
/>
|
|
23
|
-
<Tabs.Screen
|
|
24
|
-
name="explore"
|
|
25
|
-
options={{
|
|
26
|
-
title: "Explore",
|
|
27
|
-
tabBarIcon: ({ color, size }) => (
|
|
28
|
-
<Ionicons name="planet" size={size} color={color} />
|
|
29
|
-
),
|
|
30
|
-
}}
|
|
31
|
-
/>
|
|
32
|
-
<Tabs.Screen
|
|
33
|
-
name="account"
|
|
34
|
-
options={{
|
|
35
|
-
title: "Account",
|
|
36
|
-
tabBarIcon: ({ color, size }) => (
|
|
37
|
-
<Ionicons name="person" size={size} color={color} />
|
|
38
|
-
),
|
|
39
|
-
}}
|
|
40
|
-
/>
|
|
41
|
-
</Tabs>
|
|
42
|
-
);
|
|
43
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from "react";
|
|
2
|
-
import { View, Text, StyleSheet, ScrollView, Alert, ActivityIndicator } from "react-native";
|
|
3
|
-
import { useRouter } from "expo-router";
|
|
4
|
-
import { authClient } from "@/lib/auth-client";
|
|
5
|
-
import { Button } from "@/components/ui/button";
|
|
6
|
-
import { Input } from "@/components/ui/input";
|
|
7
|
-
|
|
8
|
-
export default function AccountScreen() {
|
|
9
|
-
const router = useRouter();
|
|
10
|
-
const { data: session, isPending } = authClient.useSession();
|
|
11
|
-
const [updating, setUpdating] = useState(false);
|
|
12
|
-
|
|
13
|
-
// Form states
|
|
14
|
-
const [name, setName] = useState("");
|
|
15
|
-
const [email, setEmail] = useState("");
|
|
16
|
-
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
if (!isPending && !session) {
|
|
19
|
-
router.replace("/login");
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (session) {
|
|
24
|
-
setName(session.user.name);
|
|
25
|
-
setEmail(session.user.email);
|
|
26
|
-
}
|
|
27
|
-
}, [session, isPending, router]);
|
|
28
|
-
|
|
29
|
-
const handleUpdateProfile = async () => {
|
|
30
|
-
if (!name) {
|
|
31
|
-
Alert.alert("Error", "Name is required");
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
setUpdating(true);
|
|
36
|
-
try {
|
|
37
|
-
const { error } = await authClient.updateUser({
|
|
38
|
-
name,
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
if (error) {
|
|
42
|
-
Alert.alert("Error", error.message || "Failed to update profile");
|
|
43
|
-
} else {
|
|
44
|
-
Alert.alert("Success", "Profile updated successfully");
|
|
45
|
-
}
|
|
46
|
-
} catch (err) {
|
|
47
|
-
Alert.alert("Error", "An unexpected error occurred");
|
|
48
|
-
} finally {
|
|
49
|
-
setUpdating(false);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
if (isPending) {
|
|
54
|
-
return (
|
|
55
|
-
<View style={styles.center}>
|
|
56
|
-
<ActivityIndicator size="large" color="#000" />
|
|
57
|
-
</View>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
|
63
|
-
<Text style={styles.title}>Account Settings</Text>
|
|
64
|
-
|
|
65
|
-
<View style={styles.section}>
|
|
66
|
-
<Text style={styles.sectionTitle}>Profile Information</Text>
|
|
67
|
-
<View style={styles.form}>
|
|
68
|
-
<Input
|
|
69
|
-
label="Name"
|
|
70
|
-
value={name}
|
|
71
|
-
onChangeText={setName}
|
|
72
|
-
placeholder="Your name"
|
|
73
|
-
/>
|
|
74
|
-
<Input
|
|
75
|
-
label="Email"
|
|
76
|
-
value={email}
|
|
77
|
-
editable={false}
|
|
78
|
-
style={styles.disabledInput}
|
|
79
|
-
/>
|
|
80
|
-
<Button
|
|
81
|
-
onPress={handleUpdateProfile}
|
|
82
|
-
loading={updating}
|
|
83
|
-
style={styles.button}
|
|
84
|
-
>
|
|
85
|
-
Update Profile
|
|
86
|
-
</Button>
|
|
87
|
-
</View>
|
|
88
|
-
</View>
|
|
89
|
-
|
|
90
|
-
<View style={styles.section}>
|
|
91
|
-
<Text style={styles.sectionTitle}>Security</Text>
|
|
92
|
-
<Text style={styles.infoText}>
|
|
93
|
-
Password management and other security settings are currently available on the web platform.
|
|
94
|
-
</Text>
|
|
95
|
-
</View>
|
|
96
|
-
</ScrollView>
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const styles = StyleSheet.create({
|
|
101
|
-
container: {
|
|
102
|
-
flex: 1,
|
|
103
|
-
backgroundColor: "#fff",
|
|
104
|
-
},
|
|
105
|
-
content: {
|
|
106
|
-
padding: 24,
|
|
107
|
-
},
|
|
108
|
-
center: {
|
|
109
|
-
flex: 1,
|
|
110
|
-
justifyContent: "center",
|
|
111
|
-
alignItems: "center",
|
|
112
|
-
},
|
|
113
|
-
title: {
|
|
114
|
-
fontSize: 28,
|
|
115
|
-
fontWeight: "bold",
|
|
116
|
-
marginBottom: 24,
|
|
117
|
-
},
|
|
118
|
-
section: {
|
|
119
|
-
marginBottom: 32,
|
|
120
|
-
},
|
|
121
|
-
sectionTitle: {
|
|
122
|
-
fontSize: 18,
|
|
123
|
-
fontWeight: "600",
|
|
124
|
-
marginBottom: 16,
|
|
125
|
-
color: "#333",
|
|
126
|
-
},
|
|
127
|
-
form: {
|
|
128
|
-
gap: 16,
|
|
129
|
-
},
|
|
130
|
-
disabledInput: {
|
|
131
|
-
backgroundColor: "#f5f5f5",
|
|
132
|
-
color: "#888",
|
|
133
|
-
},
|
|
134
|
-
button: {
|
|
135
|
-
marginTop: 8,
|
|
136
|
-
},
|
|
137
|
-
infoText: {
|
|
138
|
-
fontSize: 14,
|
|
139
|
-
color: "#666",
|
|
140
|
-
lineHeight: 20,
|
|
141
|
-
backgroundColor: "#f9f9f9",
|
|
142
|
-
padding: 16,
|
|
143
|
-
borderRadius: 8,
|
|
144
|
-
borderWidth: 1,
|
|
145
|
-
borderColor: "#eee",
|
|
146
|
-
},
|
|
147
|
-
});
|
|
@@ -1,345 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import {
|
|
3
|
-
Alert,
|
|
4
|
-
FlatList,
|
|
5
|
-
Modal,
|
|
6
|
-
StyleSheet,
|
|
7
|
-
Text,
|
|
8
|
-
View,
|
|
9
|
-
ActivityIndicator,
|
|
10
|
-
ScrollView,
|
|
11
|
-
} from "react-native";
|
|
12
|
-
import { useQueryClient } from "@tanstack/react-query";
|
|
13
|
-
import { useForm } from "@tanstack/react-form";
|
|
14
|
-
import { z } from "zod";
|
|
15
|
-
import { Button } from "@/components/ui/button";
|
|
16
|
-
import { Input } from "@/components/ui/input";
|
|
17
|
-
import { usePlanets, useCreatePlanet, useUpdatePlanet, useDeletePlanet } from "@workspace/orpc/react";
|
|
18
|
-
|
|
19
|
-
const planetSchema = z.object({
|
|
20
|
-
name: z.string().min(1, "Name is required"),
|
|
21
|
-
description: z.string(),
|
|
22
|
-
distance: z.string().refine((val) => !isNaN(parseFloat(val)), {
|
|
23
|
-
message: "Must be a number",
|
|
24
|
-
}),
|
|
25
|
-
diameter: z.string().refine((val) => !isNaN(parseFloat(val)), {
|
|
26
|
-
message: "Must be a number",
|
|
27
|
-
}),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
export default function ExploreScreen() {
|
|
31
|
-
const [modalVisible, setModalVisible] = useState(false);
|
|
32
|
-
const [editingId, setEditingId] = useState<number | null>(null);
|
|
33
|
-
|
|
34
|
-
const { data: planets = [], isLoading } = usePlanets();
|
|
35
|
-
|
|
36
|
-
const form = useForm({
|
|
37
|
-
defaultValues: {
|
|
38
|
-
name: "",
|
|
39
|
-
description: "",
|
|
40
|
-
distance: "0",
|
|
41
|
-
diameter: "0",
|
|
42
|
-
},
|
|
43
|
-
validators: {
|
|
44
|
-
onChange: planetSchema,
|
|
45
|
-
},
|
|
46
|
-
onSubmit: async ({ value }) => {
|
|
47
|
-
const payload = {
|
|
48
|
-
name: value.name,
|
|
49
|
-
description: value.description || undefined,
|
|
50
|
-
distanceFromSun: parseFloat(value.distance) || 0,
|
|
51
|
-
diameter: parseFloat(value.diameter) || 0,
|
|
52
|
-
hasRings: false,
|
|
53
|
-
};
|
|
54
|
-
console.log('connard')
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
if (editingId) {
|
|
58
|
-
await updateMutation.mutateAsync({ id: editingId, ...payload });
|
|
59
|
-
} else {
|
|
60
|
-
await createMutation.mutateAsync(payload);
|
|
61
|
-
}
|
|
62
|
-
closeModal();
|
|
63
|
-
} catch (err) {
|
|
64
|
-
// Error handled in mutation callbacks
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const resetForm = () => {
|
|
70
|
-
form.reset();
|
|
71
|
-
setEditingId(null);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const createMutation = useCreatePlanet({
|
|
75
|
-
onSuccess: () => {
|
|
76
|
-
Alert.alert("Success", "Planet added successfully");
|
|
77
|
-
},
|
|
78
|
-
onError: (err) => {
|
|
79
|
-
Alert.alert("Error", err.message || "Failed to add planet");
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const updateMutation = useUpdatePlanet({
|
|
84
|
-
onSuccess: () => {
|
|
85
|
-
Alert.alert("Success", "Planet updated successfully");
|
|
86
|
-
},
|
|
87
|
-
onError: (err) => {
|
|
88
|
-
Alert.alert("Error", err.message || "Failed to update planet");
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const deleteMutation = useDeletePlanet({
|
|
93
|
-
onSuccess: () => {
|
|
94
|
-
Alert.alert("Success", "Planet deleted successfully");
|
|
95
|
-
},
|
|
96
|
-
onError: (err) => {
|
|
97
|
-
Alert.alert("Error", err.message || "Failed to delete planet");
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const handleEdit = (planet: any) => {
|
|
102
|
-
setEditingId(planet.id);
|
|
103
|
-
form.setFieldValue("name", planet.name);
|
|
104
|
-
form.setFieldValue("description", planet.description || "");
|
|
105
|
-
form.setFieldValue("distance", planet.distanceFromSun.toString());
|
|
106
|
-
form.setFieldValue("diameter", planet.diameter.toString());
|
|
107
|
-
setModalVisible(true);
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const handleDelete = (id: number) => {
|
|
111
|
-
deleteMutation.mutateAsync({id})
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const closeModal = () => {
|
|
115
|
-
setModalVisible(false);
|
|
116
|
-
resetForm();
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
if (isLoading) {
|
|
120
|
-
return (
|
|
121
|
-
<View style={styles.center}>
|
|
122
|
-
<ActivityIndicator size="large" color="#000" />
|
|
123
|
-
</View>
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return (
|
|
128
|
-
<View style={styles.container}>
|
|
129
|
-
<FlatList
|
|
130
|
-
data={planets}
|
|
131
|
-
keyExtractor={(item) => item.id.toString()}
|
|
132
|
-
contentContainerStyle={styles.listContent}
|
|
133
|
-
renderItem={({ item }) => (
|
|
134
|
-
<View style={styles.planetCard}>
|
|
135
|
-
<View style={styles.planetInfo}>
|
|
136
|
-
<Text style={styles.planetName}>{item.name}</Text>
|
|
137
|
-
<Text style={styles.planetDesc}>{item.description}</Text>
|
|
138
|
-
<Text style={styles.planetDetails}>
|
|
139
|
-
Distance: {item.distanceFromSun} AU • Diameter: {item.diameter} km
|
|
140
|
-
</Text>
|
|
141
|
-
</View>
|
|
142
|
-
<View style={styles.actions}>
|
|
143
|
-
<Button
|
|
144
|
-
variant="outline"
|
|
145
|
-
onPress={() => handleEdit(item)}
|
|
146
|
-
style={styles.actionBtn}
|
|
147
|
-
>
|
|
148
|
-
Edit
|
|
149
|
-
</Button>
|
|
150
|
-
<Button
|
|
151
|
-
variant="destructive"
|
|
152
|
-
onPress={() => handleDelete(item.id)}
|
|
153
|
-
style={styles.actionBtn}
|
|
154
|
-
>
|
|
155
|
-
Delete
|
|
156
|
-
</Button>
|
|
157
|
-
</View>
|
|
158
|
-
</View>
|
|
159
|
-
)}
|
|
160
|
-
ListEmptyComponent={
|
|
161
|
-
<Text style={styles.emptyText}>No planets found. Add one!</Text>
|
|
162
|
-
}
|
|
163
|
-
/>
|
|
164
|
-
|
|
165
|
-
<Button
|
|
166
|
-
onPress={() => setModalVisible(true)}
|
|
167
|
-
style={styles.fab}
|
|
168
|
-
>
|
|
169
|
-
Add Planet
|
|
170
|
-
</Button>
|
|
171
|
-
|
|
172
|
-
<Modal
|
|
173
|
-
visible={modalVisible}
|
|
174
|
-
animationType="slide"
|
|
175
|
-
onRequestClose={closeModal}
|
|
176
|
-
>
|
|
177
|
-
<View style={styles.modalContainer}>
|
|
178
|
-
<ScrollView contentContainerStyle={styles.modalContent}>
|
|
179
|
-
<Text style={styles.modalTitle}>
|
|
180
|
-
{editingId ? "Edit Planet" : "Add New Planet"}
|
|
181
|
-
</Text>
|
|
182
|
-
|
|
183
|
-
<form.Field name="name">
|
|
184
|
-
{(field: any) => (
|
|
185
|
-
<Input
|
|
186
|
-
label="Name"
|
|
187
|
-
value={field.state.value}
|
|
188
|
-
onChangeText={field.handleChange}
|
|
189
|
-
placeholder="Earth"
|
|
190
|
-
error={field.state.meta.errors?.[0]?.toString()}
|
|
191
|
-
/>
|
|
192
|
-
)}
|
|
193
|
-
</form.Field>
|
|
194
|
-
|
|
195
|
-
<form.Field name="description">
|
|
196
|
-
{(field: any) => (
|
|
197
|
-
<Input
|
|
198
|
-
label="Description"
|
|
199
|
-
value={field.state.value}
|
|
200
|
-
onChangeText={field.handleChange}
|
|
201
|
-
placeholder="The blue planet"
|
|
202
|
-
/>
|
|
203
|
-
)}
|
|
204
|
-
</form.Field>
|
|
205
|
-
|
|
206
|
-
<form.Field name="distance">
|
|
207
|
-
{(field: any) => (
|
|
208
|
-
<Input
|
|
209
|
-
label="Distance from Sun (AU)"
|
|
210
|
-
value={field.state.value}
|
|
211
|
-
onChangeText={field.handleChange}
|
|
212
|
-
keyboardType="numeric"
|
|
213
|
-
error={field.state.meta.errors?.[0]?.toString()}
|
|
214
|
-
/>
|
|
215
|
-
)}
|
|
216
|
-
</form.Field>
|
|
217
|
-
|
|
218
|
-
<form.Field name="diameter">
|
|
219
|
-
{(field: any) => (
|
|
220
|
-
<Input
|
|
221
|
-
label="Diameter (km)"
|
|
222
|
-
value={field.state.value}
|
|
223
|
-
onChangeText={field.handleChange}
|
|
224
|
-
keyboardType="numeric"
|
|
225
|
-
error={field.state.meta.errors?.[0]?.toString()}
|
|
226
|
-
/>
|
|
227
|
-
)}
|
|
228
|
-
</form.Field>
|
|
229
|
-
|
|
230
|
-
<View style={styles.modalActions}>
|
|
231
|
-
<Button
|
|
232
|
-
variant="outline"
|
|
233
|
-
onPress={closeModal}
|
|
234
|
-
style={styles.modalBtn}
|
|
235
|
-
>
|
|
236
|
-
Cancel
|
|
237
|
-
</Button>
|
|
238
|
-
<Button
|
|
239
|
-
onPress={form.handleSubmit}
|
|
240
|
-
loading={createMutation.isPending || updateMutation.isPending}
|
|
241
|
-
style={styles.modalBtn}
|
|
242
|
-
>
|
|
243
|
-
{editingId ? "Update" : "Create"}
|
|
244
|
-
</Button>
|
|
245
|
-
</View>
|
|
246
|
-
</ScrollView>
|
|
247
|
-
</View>
|
|
248
|
-
</Modal>
|
|
249
|
-
</View>
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const styles = StyleSheet.create({
|
|
254
|
-
container: {
|
|
255
|
-
flex: 1,
|
|
256
|
-
backgroundColor: "#fff",
|
|
257
|
-
},
|
|
258
|
-
center: {
|
|
259
|
-
flex: 1,
|
|
260
|
-
justifyContent: "center",
|
|
261
|
-
alignItems: "center",
|
|
262
|
-
},
|
|
263
|
-
listContent: {
|
|
264
|
-
padding: 16,
|
|
265
|
-
paddingBottom: 100,
|
|
266
|
-
},
|
|
267
|
-
planetCard: {
|
|
268
|
-
padding: 16,
|
|
269
|
-
borderRadius: 12,
|
|
270
|
-
borderWidth: 1,
|
|
271
|
-
borderColor: "#eee",
|
|
272
|
-
marginBottom: 16,
|
|
273
|
-
backgroundColor: "#fff",
|
|
274
|
-
shadowColor: "#000",
|
|
275
|
-
shadowOffset: { width: 0, height: 2 },
|
|
276
|
-
shadowOpacity: 0.05,
|
|
277
|
-
shadowRadius: 4,
|
|
278
|
-
elevation: 2,
|
|
279
|
-
},
|
|
280
|
-
planetInfo: {
|
|
281
|
-
marginBottom: 16,
|
|
282
|
-
},
|
|
283
|
-
planetName: {
|
|
284
|
-
fontSize: 18,
|
|
285
|
-
fontWeight: "bold",
|
|
286
|
-
marginBottom: 4,
|
|
287
|
-
},
|
|
288
|
-
planetDesc: {
|
|
289
|
-
fontSize: 14,
|
|
290
|
-
color: "#666",
|
|
291
|
-
marginBottom: 8,
|
|
292
|
-
},
|
|
293
|
-
planetDetails: {
|
|
294
|
-
fontSize: 12,
|
|
295
|
-
color: "#999",
|
|
296
|
-
},
|
|
297
|
-
actions: {
|
|
298
|
-
flexDirection: "row",
|
|
299
|
-
gap: 8,
|
|
300
|
-
},
|
|
301
|
-
actionBtn: {
|
|
302
|
-
flex: 1,
|
|
303
|
-
height: 36,
|
|
304
|
-
},
|
|
305
|
-
emptyText: {
|
|
306
|
-
textAlign: "center",
|
|
307
|
-
marginTop: 40,
|
|
308
|
-
color: "#999",
|
|
309
|
-
fontStyle: "italic",
|
|
310
|
-
},
|
|
311
|
-
fab: {
|
|
312
|
-
position: "absolute",
|
|
313
|
-
bottom: 24,
|
|
314
|
-
left: 24,
|
|
315
|
-
right: 24,
|
|
316
|
-
height: 56,
|
|
317
|
-
borderRadius: 28,
|
|
318
|
-
shadowColor: "#000",
|
|
319
|
-
shadowOffset: { width: 0, height: 4 },
|
|
320
|
-
shadowOpacity: 0.2,
|
|
321
|
-
shadowRadius: 8,
|
|
322
|
-
elevation: 5,
|
|
323
|
-
},
|
|
324
|
-
modalContainer: {
|
|
325
|
-
flex: 1,
|
|
326
|
-
backgroundColor: "#fff",
|
|
327
|
-
},
|
|
328
|
-
modalContent: {
|
|
329
|
-
padding: 24,
|
|
330
|
-
paddingTop: 60,
|
|
331
|
-
},
|
|
332
|
-
modalTitle: {
|
|
333
|
-
fontSize: 24,
|
|
334
|
-
fontWeight: "bold",
|
|
335
|
-
marginBottom: 32,
|
|
336
|
-
},
|
|
337
|
-
modalActions: {
|
|
338
|
-
flexDirection: "row",
|
|
339
|
-
gap: 12,
|
|
340
|
-
marginTop: 32,
|
|
341
|
-
},
|
|
342
|
-
modalBtn: {
|
|
343
|
-
flex: 1,
|
|
344
|
-
},
|
|
345
|
-
});
|