Compare commits
No commits in common. "a620921202544b3eb9f2e4275727ea3e627ab580" and "7ada05da6a8ffe42c5451ba29146c5d35e612371" have entirely different histories.
a620921202
...
7ada05da6a
@ -13,17 +13,12 @@ import { useUser, useAuth } from "@clerk/clerk-expo";
|
|||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { fitnessProfileApi } from "@/api/fitnessProfile";
|
import { fitnessProfileApi } from "@/api/fitnessProfile";
|
||||||
import { gymsApi, type Gym } from "@/api/gyms";
|
import { gymsApi, type Gym } from "@/api/gyms";
|
||||||
import { useTheme } from "../../contexts/ThemeContext";
|
|
||||||
import { Input } from "../../components/Input";
|
|
||||||
import { MinimalButton } from "../../components/MinimalButton";
|
|
||||||
import { MinimalCard } from "../../components/MinimalCard";
|
|
||||||
import log from "../../utils/logger";
|
import log from "../../utils/logger";
|
||||||
|
|
||||||
export default function OnboardingScreen() {
|
export default function OnboardingScreen() {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { getToken } = useAuth();
|
const { getToken } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { colors, typography } = useTheme();
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [gyms, setGyms] = useState<Gym[]>([]);
|
const [gyms, setGyms] = useState<Gym[]>([]);
|
||||||
const [gymsLoading, setGymsLoading] = useState<boolean>(false);
|
const [gymsLoading, setGymsLoading] = useState<boolean>(false);
|
||||||
@ -139,53 +134,24 @@ export default function OnboardingScreen() {
|
|||||||
const progress = calculateProgress();
|
const progress = calculateProgress();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView style={styles.container}>
|
||||||
style={[styles.container, { backgroundColor: colors.background }]}
|
<Text style={styles.title}>Set Up Your Fitness Profile</Text>
|
||||||
>
|
<Text style={styles.subtitle}>
|
||||||
<Text
|
|
||||||
style={[typography.h2, { color: colors.textPrimary }, styles.title]}
|
|
||||||
>
|
|
||||||
Set Up Your Fitness Profile
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
typography.body,
|
|
||||||
{ color: colors.textSecondary },
|
|
||||||
styles.subtitle,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
Help us personalize your fitness journey
|
Help us personalize your fitness journey
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Progress indicator */}
|
{/* Progress indicator */}
|
||||||
<View style={styles.progressContainer}>
|
<View style={styles.progressContainer}>
|
||||||
<View
|
<View style={styles.progressBarBackground}>
|
||||||
style={[
|
<View style={[styles.progressBarFill, { width: `${progress}%` }]} />
|
||||||
styles.progressBarBackground,
|
|
||||||
{ backgroundColor: colors.borderLight },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.progressBarFill,
|
|
||||||
{ width: `${progress}%`, backgroundColor: colors.primary },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
<Text
|
<Text style={styles.progressText}>{progress}% Complete</Text>
|
||||||
style={[
|
|
||||||
typography.caption,
|
|
||||||
{ color: colors.textTertiary },
|
|
||||||
styles.progressText,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{progress}% Complete
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.form}>
|
<View style={styles.form}>
|
||||||
<Input
|
<Text style={styles.label}>Height (cm)</Text>
|
||||||
label="Height (cm)"
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
value={fitnessProfile.height}
|
value={fitnessProfile.height}
|
||||||
onChangeText={(value) =>
|
onChangeText={(value) =>
|
||||||
setFitnessProfile({ ...fitnessProfile, height: value })
|
setFitnessProfile({ ...fitnessProfile, height: value })
|
||||||
@ -194,8 +160,9 @@ export default function OnboardingScreen() {
|
|||||||
placeholder="Enter height in cm"
|
placeholder="Enter height in cm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Text style={styles.label}>Weight (kg)</Text>
|
||||||
label="Weight (kg)"
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
value={fitnessProfile.weight}
|
value={fitnessProfile.weight}
|
||||||
onChangeText={(value) =>
|
onChangeText={(value) =>
|
||||||
setFitnessProfile({ ...fitnessProfile, weight: value })
|
setFitnessProfile({ ...fitnessProfile, weight: value })
|
||||||
@ -204,8 +171,9 @@ export default function OnboardingScreen() {
|
|||||||
placeholder="Enter weight in kg"
|
placeholder="Enter weight in kg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Text style={styles.label}>Age</Text>
|
||||||
label="Age"
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
value={fitnessProfile.age}
|
value={fitnessProfile.age}
|
||||||
onChangeText={(value) =>
|
onChangeText={(value) =>
|
||||||
setFitnessProfile({ ...fitnessProfile, age: value })
|
setFitnessProfile({ ...fitnessProfile, age: value })
|
||||||
@ -214,9 +182,9 @@ export default function OnboardingScreen() {
|
|||||||
placeholder="Enter your age"
|
placeholder="Enter your age"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Text style={styles.label}>Fitness Goals</Text>
|
||||||
label="Fitness Goals"
|
<TextInput
|
||||||
style={styles.textArea}
|
style={[styles.input, styles.textArea]}
|
||||||
value={fitnessProfile.goals}
|
value={fitnessProfile.goals}
|
||||||
onChangeText={(value) =>
|
onChangeText={(value) =>
|
||||||
setFitnessProfile({ ...fitnessProfile, goals: value })
|
setFitnessProfile({ ...fitnessProfile, goals: value })
|
||||||
@ -226,48 +194,33 @@ export default function OnboardingScreen() {
|
|||||||
placeholder="What are your fitness goals?"
|
placeholder="What are your fitness goals?"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text
|
<Text style={styles.label}>Fitness Level</Text>
|
||||||
style={[typography.h4, { color: colors.textPrimary }, styles.label]}
|
|
||||||
>
|
|
||||||
Fitness Level
|
|
||||||
</Text>
|
|
||||||
<View style={styles.buttonGroup}>
|
<View style={styles.buttonGroup}>
|
||||||
{["beginner", "intermediate", "advanced"].map((level) => (
|
{["beginner", "intermediate", "advanced"].map((level) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={level}
|
key={level}
|
||||||
style={[
|
style={[
|
||||||
styles.segmentButton,
|
styles.levelButton,
|
||||||
{
|
fitnessProfile.fitnessLevel === level && styles.selectedButton,
|
||||||
backgroundColor:
|
|
||||||
fitnessProfile.fitnessLevel === level
|
|
||||||
? colors.primary
|
|
||||||
: colors.surface,
|
|
||||||
borderColor: colors.border,
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
onPress={() => handleLevelSelect(level)}
|
onPress={() => handleLevelSelect(level)}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
typography.caption,
|
styles.levelButtonText,
|
||||||
{
|
fitnessProfile.fitnessLevel === level &&
|
||||||
color:
|
styles.selectedButtonText,
|
||||||
fitnessProfile.fitnessLevel === level
|
|
||||||
? colors.white
|
|
||||||
: colors.textSecondary,
|
|
||||||
textTransform: "capitalize",
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{level}
|
{level.charAt(0).toUpperCase() + level.slice(1)}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Input
|
<Text style={styles.label}>Medical Conditions</Text>
|
||||||
label="Medical Conditions"
|
<TextInput
|
||||||
style={styles.textArea}
|
style={[styles.input, styles.textArea]}
|
||||||
value={fitnessProfile.medicalConditions}
|
value={fitnessProfile.medicalConditions}
|
||||||
onChangeText={(value) =>
|
onChangeText={(value) =>
|
||||||
setFitnessProfile({ ...fitnessProfile, medicalConditions: value })
|
setFitnessProfile({ ...fitnessProfile, medicalConditions: value })
|
||||||
@ -277,9 +230,9 @@ export default function OnboardingScreen() {
|
|||||||
placeholder="Any medical conditions we should know about?"
|
placeholder="Any medical conditions we should know about?"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Text style={styles.label}>Dietary Restrictions</Text>
|
||||||
label="Dietary Restrictions"
|
<TextInput
|
||||||
style={styles.textArea}
|
style={[styles.input, styles.textArea]}
|
||||||
value={fitnessProfile.dietaryRestrictions}
|
value={fitnessProfile.dietaryRestrictions}
|
||||||
onChangeText={(value) =>
|
onChangeText={(value) =>
|
||||||
setFitnessProfile({
|
setFitnessProfile({
|
||||||
@ -292,47 +245,34 @@ export default function OnboardingScreen() {
|
|||||||
placeholder="Any dietary restrictions?"
|
placeholder="Any dietary restrictions?"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text
|
<Text style={styles.label}>Preferred Workout Time</Text>
|
||||||
style={[typography.h4, { color: colors.textPrimary }, styles.label]}
|
|
||||||
>
|
|
||||||
Preferred Workout Time
|
|
||||||
</Text>
|
|
||||||
<View style={styles.buttonGroup}>
|
<View style={styles.buttonGroup}>
|
||||||
{["morning", "afternoon", "evening"].map((time) => (
|
{["morning", "afternoon", "evening"].map((time) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={time}
|
key={time}
|
||||||
style={[
|
style={[
|
||||||
styles.segmentButton,
|
styles.timeButton,
|
||||||
{
|
fitnessProfile.preferredWorkoutTime === time &&
|
||||||
backgroundColor:
|
styles.selectedButton,
|
||||||
fitnessProfile.preferredWorkoutTime === time
|
|
||||||
? colors.primary
|
|
||||||
: colors.surface,
|
|
||||||
borderColor: colors.border,
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
onPress={() => handleTimeSelect(time)}
|
onPress={() => handleTimeSelect(time)}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
typography.caption,
|
styles.timeButtonText,
|
||||||
{
|
fitnessProfile.preferredWorkoutTime === time &&
|
||||||
color:
|
styles.selectedButtonText,
|
||||||
fitnessProfile.preferredWorkoutTime === time
|
|
||||||
? colors.white
|
|
||||||
: colors.textSecondary,
|
|
||||||
textTransform: "capitalize",
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{time}
|
{time.charAt(0).toUpperCase() + time.slice(1)}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Input
|
<Text style={styles.label}>Workouts per Week</Text>
|
||||||
label="Workouts per Week"
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
value={fitnessProfile.workoutFrequency}
|
value={fitnessProfile.workoutFrequency}
|
||||||
onChangeText={(value) =>
|
onChangeText={(value) =>
|
||||||
setFitnessProfile({ ...fitnessProfile, workoutFrequency: value })
|
setFitnessProfile({ ...fitnessProfile, workoutFrequency: value })
|
||||||
@ -341,11 +281,7 @@ export default function OnboardingScreen() {
|
|||||||
placeholder="Number of workouts per week"
|
placeholder="Number of workouts per week"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text
|
<Text style={styles.label}>Select a Gym</Text>
|
||||||
style={[typography.h4, { color: colors.textPrimary }, styles.label]}
|
|
||||||
>
|
|
||||||
Select a Gym
|
|
||||||
</Text>
|
|
||||||
{gymsLoading ? (
|
{gymsLoading ? (
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
) : (
|
) : (
|
||||||
@ -357,24 +293,15 @@ export default function OnboardingScreen() {
|
|||||||
<View style={{ flexDirection: "row" }}>
|
<View style={{ flexDirection: "row" }}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.segmentButton,
|
styles.levelButton,
|
||||||
{
|
selectedGymId === null && styles.selectedButton,
|
||||||
backgroundColor:
|
|
||||||
selectedGymId === null ? colors.primary : colors.surface,
|
|
||||||
borderColor: colors.border,
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
onPress={() => setSelectedGymId(null)}
|
onPress={() => setSelectedGymId(null)}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
typography.caption,
|
styles.levelButtonText,
|
||||||
{
|
selectedGymId === null && styles.selectedButtonText,
|
||||||
color:
|
|
||||||
selectedGymId === null
|
|
||||||
? colors.white
|
|
||||||
: colors.textSecondary,
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
Proceed without gym
|
Proceed without gym
|
||||||
@ -384,26 +311,15 @@ export default function OnboardingScreen() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={gym.id}
|
key={gym.id}
|
||||||
style={[
|
style={[
|
||||||
styles.segmentButton,
|
styles.levelButton,
|
||||||
{
|
selectedGymId === gym.id && styles.selectedButton,
|
||||||
backgroundColor:
|
|
||||||
selectedGymId === gym.id
|
|
||||||
? colors.primary
|
|
||||||
: colors.surface,
|
|
||||||
borderColor: colors.border,
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
onPress={() => setSelectedGymId(gym.id)}
|
onPress={() => setSelectedGymId(gym.id)}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
typography.caption,
|
styles.levelButtonText,
|
||||||
{
|
selectedGymId === gym.id && styles.selectedButtonText,
|
||||||
color:
|
|
||||||
selectedGymId === gym.id
|
|
||||||
? colors.white
|
|
||||||
: colors.textSecondary,
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{gym.name}
|
{gym.name}
|
||||||
@ -414,14 +330,17 @@ export default function OnboardingScreen() {
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<MinimalButton
|
<TouchableOpacity
|
||||||
title="Complete Setup"
|
style={styles.submitButton}
|
||||||
onPress={handleSubmit}
|
onPress={handleSubmit}
|
||||||
loading={isSubmitting}
|
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
fullWidth
|
>
|
||||||
size="lg"
|
{isSubmitting ? (
|
||||||
/>
|
<ActivityIndicator color="white" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.submitButtonText}>Complete Setup</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
@ -430,13 +349,18 @@ export default function OnboardingScreen() {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
backgroundColor: "#f5f5f5",
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: "bold",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
marginTop: 40,
|
marginTop: 40,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#666",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
marginBottom: 32,
|
marginBottom: 32,
|
||||||
},
|
},
|
||||||
@ -444,39 +368,91 @@ const styles = StyleSheet.create({
|
|||||||
padding: 20,
|
padding: 20,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
marginBottom: 8,
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#374151",
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#ddd",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 12,
|
||||||
|
marginBottom: 16,
|
||||||
|
backgroundColor: "white",
|
||||||
},
|
},
|
||||||
textArea: {
|
textArea: {
|
||||||
minHeight: 80,
|
height: 80,
|
||||||
textAlignVertical: "top",
|
textAlignVertical: "top",
|
||||||
marginBottom: 16,
|
|
||||||
},
|
},
|
||||||
buttonGroup: {
|
buttonGroup: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
flexWrap: "wrap",
|
justifyContent: "space-between",
|
||||||
gap: 8,
|
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
segmentButton: {
|
levelButton: {
|
||||||
minWidth: 100,
|
flex: 1,
|
||||||
|
backgroundColor: "#f3f4f6",
|
||||||
padding: 10,
|
padding: 10,
|
||||||
borderRadius: 10,
|
borderRadius: 8,
|
||||||
borderWidth: 1,
|
marginHorizontal: 4,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
|
timeButton: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "#f3f4f6",
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
marginHorizontal: 4,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
selectedButton: {
|
||||||
|
backgroundColor: "#3b82f6",
|
||||||
|
},
|
||||||
|
levelButtonText: {
|
||||||
|
color: "#374151",
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
timeButtonText: {
|
||||||
|
color: "#374151",
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
selectedButtonText: {
|
||||||
|
color: "white",
|
||||||
|
},
|
||||||
|
submitButton: {
|
||||||
|
backgroundColor: "#3b82f6",
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 24,
|
||||||
|
},
|
||||||
|
submitButtonText: {
|
||||||
|
color: "white",
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
progressContainer: {
|
progressContainer: {
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
progressBarBackground: {
|
progressBarBackground: {
|
||||||
height: 8,
|
height: 8,
|
||||||
|
backgroundColor: "#e5e7eb",
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
progressBarFill: {
|
progressBarFill: {
|
||||||
height: "100%",
|
height: "100%",
|
||||||
|
backgroundColor: "#3b82f6",
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
progressText: { textAlign: "center" },
|
progressText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#6b7280",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useRouter } from "expo-router";
|
|||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
|
TextInput,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
@ -12,9 +13,6 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { OAuthButtons } from "../../components/auth/OAuthButtons";
|
import { OAuthButtons } from "../../components/auth/OAuthButtons";
|
||||||
import { Input } from "../../components/Input";
|
|
||||||
import { MinimalButton } from "../../components/MinimalButton";
|
|
||||||
import { useTheme } from "../../contexts/ThemeContext";
|
|
||||||
import { getErrorMessage, getClerkErrorCode } from "../../utils/error-helpers";
|
import { getErrorMessage, getClerkErrorCode } from "../../utils/error-helpers";
|
||||||
import log from "../../utils/logger";
|
import log from "../../utils/logger";
|
||||||
|
|
||||||
@ -27,7 +25,6 @@ export default function SignInScreen() {
|
|||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const { colors, typography } = useTheme();
|
|
||||||
|
|
||||||
// Redirect if already signed in
|
// Redirect if already signed in
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -80,15 +77,8 @@ export default function SignInScreen() {
|
|||||||
if (isSignedIn) {
|
if (isSignedIn) {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, styles.centerContent]}>
|
<View style={[styles.container, styles.centerContent]}>
|
||||||
<ActivityIndicator size="large" color={colors.primary} />
|
<ActivityIndicator size="large" color="#2563eb" />
|
||||||
<Text
|
<Text style={styles.loadingText}>Redirecting...</Text>
|
||||||
style={[
|
|
||||||
typography.body,
|
|
||||||
{ color: colors.textSecondary, marginTop: 12 },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
Redirecting...
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -96,33 +86,19 @@ export default function SignInScreen() {
|
|||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
style={[styles.container, { backgroundColor: colors.background }]}
|
style={styles.container}
|
||||||
>
|
>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
>
|
>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<Text
|
<Text style={styles.title}>Welcome Back</Text>
|
||||||
style={[typography.h1, { color: colors.textPrimary }, styles.title]}
|
<Text style={styles.subtitle}>Sign in to continue to FitAI</Text>
|
||||||
>
|
|
||||||
Welcome Back
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
typography.body,
|
|
||||||
{ color: colors.textSecondary },
|
|
||||||
styles.subtitle,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
Sign in to continue to FitAI
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{error ? (
|
{error ? (
|
||||||
<View style={styles.errorContainer}>
|
<View style={styles.errorContainer}>
|
||||||
<Text style={[typography.caption, { color: colors.danger }]}>
|
<Text style={styles.errorText}>{error}</Text>
|
||||||
{error}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@ -136,11 +112,13 @@ export default function SignInScreen() {
|
|||||||
|
|
||||||
<View style={styles.form}>
|
<View style={styles.form}>
|
||||||
<View style={styles.inputContainer}>
|
<View style={styles.inputContainer}>
|
||||||
<Input
|
<Text style={styles.label}>Email</Text>
|
||||||
label="Email"
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
value={emailAddress}
|
value={emailAddress}
|
||||||
placeholder="Enter your email"
|
placeholder="Enter your email"
|
||||||
|
placeholderTextColor="#999"
|
||||||
onChangeText={setEmailAddress}
|
onChangeText={setEmailAddress}
|
||||||
keyboardType="email-address"
|
keyboardType="email-address"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
@ -149,10 +127,12 @@ export default function SignInScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.inputContainer}>
|
<View style={styles.inputContainer}>
|
||||||
<Input
|
<Text style={styles.label}>Password</Text>
|
||||||
label="Password"
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
value={password}
|
value={password}
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password"
|
||||||
|
placeholderTextColor="#999"
|
||||||
secureTextEntry={true}
|
secureTextEntry={true}
|
||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
autoComplete="password"
|
autoComplete="password"
|
||||||
@ -160,29 +140,26 @@ export default function SignInScreen() {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<MinimalButton
|
<TouchableOpacity
|
||||||
title="Sign In"
|
style={[styles.button, loading && styles.buttonDisabled]}
|
||||||
onPress={onSignInPress}
|
onPress={onSignInPress}
|
||||||
loading={loading}
|
|
||||||
disabled={loading || !emailAddress || !password}
|
disabled={loading || !emailAddress || !password}
|
||||||
fullWidth
|
>
|
||||||
size="lg"
|
{loading ? (
|
||||||
/>
|
<ActivityIndicator color="#fff" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.buttonText}>Sign In</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<Text style={[typography.caption, { color: colors.textSecondary }]}>
|
<Text style={styles.footerText}>Don't have an account? </Text>
|
||||||
Don't have an account?{" "}
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => router.push("/(auth)/sign-up")}
|
onPress={() => router.push("/(auth)/sign-up")}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<Text
|
<Text style={styles.linkText}>Sign Up</Text>
|
||||||
style={[typography.bodyEmphasis, { color: colors.primary }]}
|
|
||||||
>
|
|
||||||
Sign Up
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -194,11 +171,17 @@ export default function SignInScreen() {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
backgroundColor: "#f5f5f5",
|
||||||
},
|
},
|
||||||
centerContent: {
|
centerContent: {
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
|
loadingText: {
|
||||||
|
marginTop: 12,
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#666",
|
||||||
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
@ -210,18 +193,27 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: 40,
|
paddingVertical: 40,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: "#1a1a1a",
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#666",
|
||||||
marginBottom: 32,
|
marginBottom: 32,
|
||||||
},
|
},
|
||||||
errorContainer: {
|
errorContainer: {
|
||||||
backgroundColor: "rgba(255, 59, 59, 0.08)",
|
backgroundColor: "#fee",
|
||||||
padding: 12,
|
padding: 12,
|
||||||
borderRadius: 12,
|
borderRadius: 8,
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
borderLeftWidth: 4,
|
borderLeftWidth: 4,
|
||||||
borderLeftColor: "#FF3B3B",
|
borderLeftColor: "#f44",
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: "#c00",
|
||||||
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
@ -229,11 +221,51 @@ const styles = StyleSheet.create({
|
|||||||
inputContainer: {
|
inputContainer: {
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#333",
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#ddd",
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#1a1a1a",
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
backgroundColor: "#2563eb",
|
||||||
|
paddingVertical: 16,
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 8,
|
||||||
|
},
|
||||||
|
buttonDisabled: {
|
||||||
|
backgroundColor: "#93c5fd",
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
footer: {
|
footer: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
|
footerText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#666",
|
||||||
|
},
|
||||||
|
linkText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#2563eb",
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
dividerContainer: {
|
dividerContainer: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@ -242,11 +274,11 @@ const styles = StyleSheet.create({
|
|||||||
dividerLine: {
|
dividerLine: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
height: 1,
|
height: 1,
|
||||||
backgroundColor: "#E5E5EA",
|
backgroundColor: "#ddd",
|
||||||
},
|
},
|
||||||
dividerText: {
|
dividerText: {
|
||||||
marginHorizontal: 10,
|
marginHorizontal: 10,
|
||||||
color: "#8E8E93",
|
color: "#666",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useRouter } from "expo-router";
|
|||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
|
TextInput,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
@ -12,9 +13,6 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { OAuthButtons } from "../../components/auth/OAuthButtons";
|
import { OAuthButtons } from "../../components/auth/OAuthButtons";
|
||||||
import { Input } from "../../components/Input";
|
|
||||||
import { MinimalButton } from "../../components/MinimalButton";
|
|
||||||
import { useTheme } from "../../contexts/ThemeContext";
|
|
||||||
import { getErrorMessage, getClerkErrorCode } from "../../utils/error-helpers";
|
import { getErrorMessage, getClerkErrorCode } from "../../utils/error-helpers";
|
||||||
import log from "../../utils/logger";
|
import log from "../../utils/logger";
|
||||||
|
|
||||||
@ -31,7 +29,6 @@ export default function SignUpScreen() {
|
|||||||
const [code, setCode] = useState("");
|
const [code, setCode] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const { colors, typography } = useTheme();
|
|
||||||
|
|
||||||
// Redirect if already signed in
|
// Redirect if already signed in
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -104,15 +101,8 @@ export default function SignUpScreen() {
|
|||||||
if (isSignedIn) {
|
if (isSignedIn) {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, styles.centerContent]}>
|
<View style={[styles.container, styles.centerContent]}>
|
||||||
<ActivityIndicator size="large" color={colors.primary} />
|
<ActivityIndicator size="large" color="#2563eb" />
|
||||||
<Text
|
<Text style={styles.loadingText}>Redirecting...</Text>
|
||||||
style={[
|
|
||||||
typography.body,
|
|
||||||
{ color: colors.textSecondary, marginTop: 12 },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
Redirecting...
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -128,53 +118,42 @@ export default function SignUpScreen() {
|
|||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
>
|
>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<Text
|
<Text style={styles.title}>Verify Email</Text>
|
||||||
style={[
|
<Text style={styles.subtitle}>
|
||||||
typography.h1,
|
|
||||||
{ color: colors.textPrimary },
|
|
||||||
styles.title,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
Verify Email
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
typography.body,
|
|
||||||
{ color: colors.textSecondary },
|
|
||||||
styles.subtitle,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
Enter the verification code sent to {emailAddress}
|
Enter the verification code sent to {emailAddress}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{error ? (
|
{error ? (
|
||||||
<View style={styles.errorContainer}>
|
<View style={styles.errorContainer}>
|
||||||
<Text style={[typography.caption, { color: colors.danger }]}>
|
<Text style={styles.errorText}>{error}</Text>
|
||||||
{error}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<View style={styles.form}>
|
<View style={styles.form}>
|
||||||
<View style={styles.inputContainer}>
|
<View style={styles.inputContainer}>
|
||||||
<Input
|
<Text style={styles.label}>Verification Code</Text>
|
||||||
label="Verification Code"
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
value={code}
|
value={code}
|
||||||
placeholder="Enter verification code"
|
placeholder="Enter verification code"
|
||||||
|
placeholderTextColor="#999"
|
||||||
onChangeText={setCode}
|
onChangeText={setCode}
|
||||||
keyboardType="number-pad"
|
keyboardType="number-pad"
|
||||||
editable={!loading}
|
editable={!loading}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<MinimalButton
|
<TouchableOpacity
|
||||||
title="Verify Email"
|
style={[styles.button, loading && styles.buttonDisabled]}
|
||||||
onPress={onVerifyPress}
|
onPress={onVerifyPress}
|
||||||
loading={loading}
|
|
||||||
disabled={loading || !code}
|
disabled={loading || !code}
|
||||||
fullWidth
|
>
|
||||||
size="lg"
|
{loading ? (
|
||||||
/>
|
<ActivityIndicator color="#fff" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.buttonText}>Verify Email</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@ -185,33 +164,19 @@ export default function SignUpScreen() {
|
|||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
style={[styles.container, { backgroundColor: colors.background }]}
|
style={styles.container}
|
||||||
>
|
>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
>
|
>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<Text
|
<Text style={styles.title}>Create Account</Text>
|
||||||
style={[typography.h1, { color: colors.textPrimary }, styles.title]}
|
<Text style={styles.subtitle}>Sign up to get started with FitAI</Text>
|
||||||
>
|
|
||||||
Create Account
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
typography.body,
|
|
||||||
{ color: colors.textSecondary },
|
|
||||||
styles.subtitle,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
Sign up to get started with FitAI
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{error ? (
|
{error ? (
|
||||||
<View style={styles.errorContainer}>
|
<View style={styles.errorContainer}>
|
||||||
<Text style={[typography.caption, { color: colors.danger }]}>
|
<Text style={styles.errorText}>{error}</Text>
|
||||||
{error}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@ -225,10 +190,12 @@ export default function SignUpScreen() {
|
|||||||
|
|
||||||
<View style={styles.form}>
|
<View style={styles.form}>
|
||||||
<View style={styles.inputContainer}>
|
<View style={styles.inputContainer}>
|
||||||
<Input
|
<Text style={styles.label}>First Name</Text>
|
||||||
label="First Name"
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
value={firstName}
|
value={firstName}
|
||||||
placeholder="Enter your first name"
|
placeholder="Enter your first name"
|
||||||
|
placeholderTextColor="#999"
|
||||||
onChangeText={setFirstName}
|
onChangeText={setFirstName}
|
||||||
autoComplete="given-name"
|
autoComplete="given-name"
|
||||||
editable={!loading}
|
editable={!loading}
|
||||||
@ -236,10 +203,12 @@ export default function SignUpScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.inputContainer}>
|
<View style={styles.inputContainer}>
|
||||||
<Input
|
<Text style={styles.label}>Last Name</Text>
|
||||||
label="Last Name"
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
value={lastName}
|
value={lastName}
|
||||||
placeholder="Enter your last name"
|
placeholder="Enter your last name"
|
||||||
|
placeholderTextColor="#999"
|
||||||
onChangeText={setLastName}
|
onChangeText={setLastName}
|
||||||
autoComplete="family-name"
|
autoComplete="family-name"
|
||||||
editable={!loading}
|
editable={!loading}
|
||||||
@ -247,11 +216,13 @@ export default function SignUpScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.inputContainer}>
|
<View style={styles.inputContainer}>
|
||||||
<Input
|
<Text style={styles.label}>Email</Text>
|
||||||
label="Email"
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
value={emailAddress}
|
value={emailAddress}
|
||||||
placeholder="Enter your email"
|
placeholder="Enter your email"
|
||||||
|
placeholderTextColor="#999"
|
||||||
onChangeText={setEmailAddress}
|
onChangeText={setEmailAddress}
|
||||||
keyboardType="email-address"
|
keyboardType="email-address"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
@ -260,51 +231,42 @@ export default function SignUpScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.inputContainer}>
|
<View style={styles.inputContainer}>
|
||||||
<Input
|
<Text style={styles.label}>Password</Text>
|
||||||
label="Password"
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
value={password}
|
value={password}
|
||||||
placeholder="Create a password"
|
placeholder="Create a password"
|
||||||
|
placeholderTextColor="#999"
|
||||||
secureTextEntry={true}
|
secureTextEntry={true}
|
||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
autoComplete="password-new"
|
autoComplete="password-new"
|
||||||
editable={!loading}
|
editable={!loading}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text style={styles.hint}>Must be at least 8 characters</Text>
|
||||||
style={[
|
|
||||||
typography.caption,
|
|
||||||
{ color: colors.textTertiary },
|
|
||||||
styles.hint,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
Must be at least 8 characters
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<MinimalButton
|
<TouchableOpacity
|
||||||
title="Sign Up"
|
style={[styles.button, loading && styles.buttonDisabled]}
|
||||||
onPress={onSignUpPress}
|
onPress={onSignUpPress}
|
||||||
loading={loading}
|
|
||||||
disabled={
|
disabled={
|
||||||
loading || !emailAddress || !password || !firstName || !lastName
|
loading || !emailAddress || !password || !firstName || !lastName
|
||||||
}
|
}
|
||||||
fullWidth
|
>
|
||||||
size="lg"
|
{loading ? (
|
||||||
/>
|
<ActivityIndicator color="#fff" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.buttonText}>Sign Up</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<Text style={[typography.caption, { color: colors.textSecondary }]}>
|
<Text style={styles.footerText}>Already have an account? </Text>
|
||||||
Already have an account?{" "}
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => router.push("/(auth)/sign-in")}
|
onPress={() => router.push("/(auth)/sign-in")}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<Text
|
<Text style={styles.linkText}>Sign In</Text>
|
||||||
style={[typography.bodyEmphasis, { color: colors.primary }]}
|
|
||||||
>
|
|
||||||
Sign In
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -316,11 +278,17 @@ export default function SignUpScreen() {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
backgroundColor: "#f5f5f5",
|
||||||
},
|
},
|
||||||
centerContent: {
|
centerContent: {
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
|
loadingText: {
|
||||||
|
marginTop: 12,
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#666",
|
||||||
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
@ -332,18 +300,27 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: 40,
|
paddingVertical: 40,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: "#1a1a1a",
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#666",
|
||||||
marginBottom: 32,
|
marginBottom: 32,
|
||||||
},
|
},
|
||||||
errorContainer: {
|
errorContainer: {
|
||||||
backgroundColor: "rgba(255, 59, 59, 0.08)",
|
backgroundColor: "#fee",
|
||||||
padding: 12,
|
padding: 12,
|
||||||
borderRadius: 12,
|
borderRadius: 8,
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
borderLeftWidth: 4,
|
borderLeftWidth: 4,
|
||||||
borderLeftColor: "#FF3B3B",
|
borderLeftColor: "#f44",
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: "#c00",
|
||||||
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
@ -351,14 +328,56 @@ const styles = StyleSheet.create({
|
|||||||
inputContainer: {
|
inputContainer: {
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#333",
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#ddd",
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#1a1a1a",
|
||||||
|
},
|
||||||
hint: {
|
hint: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#999",
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
},
|
},
|
||||||
|
button: {
|
||||||
|
backgroundColor: "#2563eb",
|
||||||
|
paddingVertical: 16,
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 8,
|
||||||
|
},
|
||||||
|
buttonDisabled: {
|
||||||
|
backgroundColor: "#93c5fd",
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
footer: {
|
footer: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
|
footerText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#666",
|
||||||
|
},
|
||||||
|
linkText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#2563eb",
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
dividerContainer: {
|
dividerContainer: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@ -367,11 +386,11 @@ const styles = StyleSheet.create({
|
|||||||
dividerLine: {
|
dividerLine: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
height: 1,
|
height: 1,
|
||||||
backgroundColor: "#E5E5EA",
|
backgroundColor: "#ddd",
|
||||||
},
|
},
|
||||||
dividerText: {
|
dividerText: {
|
||||||
marginHorizontal: 10,
|
marginHorizontal: 10,
|
||||||
color: "#8E8E93",
|
color: "#666",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -80,7 +80,7 @@ export default function TabLayout() {
|
|||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="recommendations"
|
name="recommendations"
|
||||||
options={{
|
options={{
|
||||||
title: "Plans",
|
title: "AI",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
|
|||||||
@ -1,193 +1,269 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Alert,
|
TextInput,
|
||||||
ActivityIndicator,
|
Alert,
|
||||||
} from "react-native";
|
Platform,
|
||||||
import { Stack, useRouter } from "expo-router";
|
} from 'react-native';
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { useRouter, Stack } from 'expo-router';
|
||||||
import { useUser } from "@clerk/clerk-expo";
|
import { useUser } from '@clerk/clerk-expo';
|
||||||
import { useTheme } from "../contexts/ThemeContext";
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { MinimalCard } from "../components/MinimalCard";
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { Input } from "../components/Input";
|
import { theme } from '../styles/theme';
|
||||||
import { MinimalButton } from "../components/MinimalButton";
|
|
||||||
|
|
||||||
export default function PersonalDetailsScreen() {
|
export default function PersonalDetailsScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { colors, typography } = useTheme();
|
const [loading, setLoading] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
// Initialize with current user data
|
||||||
firstName: user?.firstName || "",
|
const [formData, setFormData] = useState({
|
||||||
lastName: user?.lastName || "",
|
firstName: user?.firstName || '',
|
||||||
email: user?.primaryEmailAddress?.emailAddress || "",
|
lastName: user?.lastName || '',
|
||||||
phone: user?.primaryPhoneNumber?.phoneNumber || "",
|
email: user?.primaryEmailAddress?.emailAddress || '',
|
||||||
});
|
phone: user?.primaryPhoneNumber?.phoneNumber || '',
|
||||||
|
});
|
||||||
|
|
||||||
const updateField = (field: "firstName" | "lastName", value: string) => {
|
const handleSave = async () => {
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setLoading(true);
|
||||||
};
|
try {
|
||||||
|
// Update user profile via Clerk
|
||||||
|
await user?.update({
|
||||||
|
firstName: formData.firstName,
|
||||||
|
lastName: formData.lastName,
|
||||||
|
});
|
||||||
|
|
||||||
const handleSave = async () => {
|
Alert.alert('Success', 'Personal details updated successfully', [
|
||||||
setLoading(true);
|
{ text: 'OK', onPress: () => router.back() },
|
||||||
try {
|
]);
|
||||||
await user?.update({
|
} catch (error) {
|
||||||
firstName: formData.firstName,
|
console.error('Error updating personal details:', error);
|
||||||
lastName: formData.lastName,
|
Alert.alert('Error', 'Failed to update personal details. Please try again.');
|
||||||
});
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Alert.alert("Success", "Personal details updated successfully", [
|
const updateField = (field: string, value: string) => {
|
||||||
{ text: "OK", onPress: () => router.back() },
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||||||
]);
|
};
|
||||||
} catch {
|
|
||||||
Alert.alert(
|
|
||||||
"Error",
|
|
||||||
"Failed to update personal details. Please try again.",
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen options={{ headerShown: false }} />
|
<Stack.Screen options={{ headerShown: false }} />
|
||||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
<View style={styles.container}>
|
||||||
<View
|
{/* Header */}
|
||||||
style={[styles.header, { borderBottomColor: colors.borderLight }]}
|
<LinearGradient
|
||||||
>
|
colors={theme.gradients.primary}
|
||||||
<TouchableOpacity
|
style={styles.header}
|
||||||
style={[
|
>
|
||||||
styles.backButton,
|
<TouchableOpacity
|
||||||
{ backgroundColor: colors.surfaceElevated },
|
style={styles.backButton}
|
||||||
]}
|
onPress={() => router.back()}
|
||||||
onPress={() => router.back()}
|
>
|
||||||
activeOpacity={0.8}
|
<Ionicons name="arrow-back" size={24} color="#fff" />
|
||||||
>
|
</TouchableOpacity>
|
||||||
<Ionicons name="arrow-back" size={20} color={colors.textPrimary} />
|
<Text style={styles.headerTitle}>Personal Details</Text>
|
||||||
</TouchableOpacity>
|
<View style={{ width: 40 }} />
|
||||||
<Text style={[typography.h3, { color: colors.textPrimary }]}>
|
</LinearGradient>
|
||||||
Personal Details
|
|
||||||
</Text>
|
|
||||||
<View style={styles.backButtonPlaceholder} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.content}
|
style={styles.content}
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<MinimalCard variant="elevated" style={styles.card}>
|
{/* First Name */}
|
||||||
<Input
|
<View style={styles.field}>
|
||||||
label="First Name"
|
<Text style={styles.label}>First Name *</Text>
|
||||||
value={formData.firstName}
|
<View style={styles.inputContainer}>
|
||||||
onChangeText={(value) => updateField("firstName", value)}
|
<Ionicons name="person-outline" size={20} color={theme.colors.gray400} style={styles.inputIcon} />
|
||||||
placeholder="Enter first name"
|
<TextInput
|
||||||
/>
|
style={styles.input}
|
||||||
|
value={formData.firstName}
|
||||||
|
onChangeText={(value) => updateField('firstName', value)}
|
||||||
|
placeholder="Enter first name"
|
||||||
|
placeholderTextColor={theme.colors.gray400}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<Input
|
{/* Last Name */}
|
||||||
label="Last Name"
|
<View style={styles.field}>
|
||||||
value={formData.lastName}
|
<Text style={styles.label}>Last Name *</Text>
|
||||||
onChangeText={(value) => updateField("lastName", value)}
|
<View style={styles.inputContainer}>
|
||||||
placeholder="Enter last name"
|
<Ionicons name="person-outline" size={20} color={theme.colors.gray400} style={styles.inputIcon} />
|
||||||
/>
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
value={formData.lastName}
|
||||||
|
onChangeText={(value) => updateField('lastName', value)}
|
||||||
|
placeholder="Enter last name"
|
||||||
|
placeholderTextColor={theme.colors.gray400}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={styles.readOnlyField}>
|
{/* Email (Read-only) */}
|
||||||
<Text style={[typography.h4, { color: colors.textPrimary }]}>
|
<View style={styles.field}>
|
||||||
Email
|
<Text style={styles.label}>Email</Text>
|
||||||
</Text>
|
<View style={[styles.inputContainer, styles.disabledInput]}>
|
||||||
<Text style={[typography.body, { color: colors.textSecondary }]}>
|
<Ionicons name="mail-outline" size={20} color={theme.colors.gray400} style={styles.inputIcon} />
|
||||||
{formData.email || "Not set"}
|
<TextInput
|
||||||
</Text>
|
style={[styles.input, styles.disabledText]}
|
||||||
<Text
|
value={formData.email}
|
||||||
style={[typography.caption, { color: colors.textTertiary }]}
|
editable={false}
|
||||||
>
|
placeholderTextColor={theme.colors.gray400}
|
||||||
Read-only in app settings
|
/>
|
||||||
</Text>
|
<Ionicons name="lock-closed-outline" size={16} color={theme.colors.gray400} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.helperText}>Email cannot be changed here</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Phone (Read-only for now) */}
|
||||||
|
<View style={styles.field}>
|
||||||
|
<Text style={styles.label}>Phone Number</Text>
|
||||||
|
<View style={[styles.inputContainer, styles.disabledInput]}>
|
||||||
|
<Ionicons name="call-outline" size={20} color={theme.colors.gray400} style={styles.inputIcon} />
|
||||||
|
<TextInput
|
||||||
|
style={[styles.input, styles.disabledText]}
|
||||||
|
value={formData.phone || 'Not set'}
|
||||||
|
editable={false}
|
||||||
|
placeholderTextColor={theme.colors.gray400}
|
||||||
|
/>
|
||||||
|
<Ionicons name="lock-closed-outline" size={16} color={theme.colors.gray400} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.helperText}>Phone number cannot be changed here</Text>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.saveButton, loading && styles.saveButtonDisabled]}
|
||||||
|
onPress={handleSave}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<LinearGradient
|
||||||
|
colors={theme.gradients.primary}
|
||||||
|
style={styles.saveButtonGradient}
|
||||||
|
>
|
||||||
|
<Ionicons name="checkmark-circle" size={20} color="#fff" />
|
||||||
|
<Text style={styles.saveButtonText}>
|
||||||
|
{loading ? 'Saving...' : 'Save Changes'}
|
||||||
|
</Text>
|
||||||
|
</LinearGradient>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</>
|
||||||
<View style={styles.readOnlyField}>
|
);
|
||||||
<Text style={[typography.h4, { color: colors.textPrimary }]}>
|
|
||||||
Phone Number
|
|
||||||
</Text>
|
|
||||||
<Text style={[typography.body, { color: colors.textSecondary }]}>
|
|
||||||
{formData.phone || "Not set"}
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
style={[typography.caption, { color: colors.textTertiary }]}
|
|
||||||
>
|
|
||||||
Read-only in app settings
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</MinimalCard>
|
|
||||||
|
|
||||||
<MinimalButton
|
|
||||||
title="Save Changes"
|
|
||||||
onPress={handleSave}
|
|
||||||
loading={loading}
|
|
||||||
disabled={loading}
|
|
||||||
fullWidth
|
|
||||||
size="lg"
|
|
||||||
style={{ marginTop: 16 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<View style={styles.loadingRow}>
|
|
||||||
<ActivityIndicator size="small" color={colors.primary} />
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
backgroundColor: theme.colors.background,
|
||||||
header: {
|
},
|
||||||
paddingTop: 56,
|
header: {
|
||||||
paddingBottom: 12,
|
flexDirection: 'row',
|
||||||
paddingHorizontal: 20,
|
alignItems: 'center',
|
||||||
borderBottomWidth: 1,
|
justifyContent: 'space-between',
|
||||||
flexDirection: "row",
|
paddingTop: Platform.OS === 'ios' ? 60 : 40,
|
||||||
alignItems: "center",
|
paddingBottom: 20,
|
||||||
justifyContent: "space-between",
|
paddingHorizontal: 20,
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
width: 36,
|
width: 40,
|
||||||
height: 36,
|
height: 40,
|
||||||
borderRadius: 10,
|
borderRadius: 20,
|
||||||
alignItems: "center",
|
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
},
|
alignItems: 'center',
|
||||||
backButtonPlaceholder: {
|
},
|
||||||
width: 36,
|
headerTitle: {
|
||||||
height: 36,
|
fontSize: theme.typography.fontSize['2xl'],
|
||||||
},
|
fontWeight: theme.typography.fontWeight.bold,
|
||||||
content: {
|
color: '#fff',
|
||||||
flex: 1,
|
},
|
||||||
},
|
content: {
|
||||||
scrollContent: {
|
flex: 1,
|
||||||
padding: 20,
|
},
|
||||||
paddingBottom: 80,
|
scrollContent: {
|
||||||
},
|
padding: 20,
|
||||||
card: {
|
paddingBottom: 100,
|
||||||
borderRadius: 16,
|
},
|
||||||
},
|
field: {
|
||||||
readOnlyField: {
|
marginBottom: 24,
|
||||||
marginTop: 8,
|
},
|
||||||
marginBottom: 12,
|
label: {
|
||||||
gap: 4,
|
fontSize: theme.typography.fontSize.sm,
|
||||||
},
|
fontWeight: theme.typography.fontWeight.semibold,
|
||||||
loadingRow: {
|
color: theme.colors.gray700,
|
||||||
marginTop: 16,
|
marginBottom: 8,
|
||||||
alignItems: "center",
|
},
|
||||||
},
|
inputContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: theme.borderRadius.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.colors.gray200,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
...theme.shadows.subtle,
|
||||||
|
},
|
||||||
|
inputIcon: {
|
||||||
|
marginRight: 12,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: 16,
|
||||||
|
fontSize: theme.typography.fontSize.base,
|
||||||
|
color: theme.colors.gray900,
|
||||||
|
},
|
||||||
|
disabledInput: {
|
||||||
|
backgroundColor: theme.colors.gray50,
|
||||||
|
},
|
||||||
|
disabledText: {
|
||||||
|
color: theme.colors.gray500,
|
||||||
|
},
|
||||||
|
helperText: {
|
||||||
|
fontSize: theme.typography.fontSize.xs,
|
||||||
|
color: theme.colors.gray500,
|
||||||
|
marginTop: 6,
|
||||||
|
marginLeft: 4,
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
padding: 20,
|
||||||
|
paddingBottom: Platform.OS === 'ios' ? 40 : 20,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: theme.colors.gray100,
|
||||||
|
...theme.shadows.medium,
|
||||||
|
},
|
||||||
|
saveButton: {
|
||||||
|
borderRadius: theme.borderRadius.lg,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
saveButtonGradient: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingVertical: 16,
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
saveButtonDisabled: {
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
saveButtonText: {
|
||||||
|
fontSize: theme.typography.fontSize.base,
|
||||||
|
fontWeight: theme.typography.fontWeight.bold,
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,12 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import { View, Text, TextInput, StyleSheet, TextInputProps } from "react-native";
|
||||||
View,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
StyleSheet,
|
|
||||||
TextInputProps,
|
|
||||||
} from "react-native";
|
|
||||||
import { useTheme } from "../contexts/ThemeContext";
|
|
||||||
|
|
||||||
interface InputProps extends TextInputProps {
|
interface InputProps extends TextInputProps {
|
||||||
label: string;
|
label: string;
|
||||||
@ -14,39 +7,15 @@ interface InputProps extends TextInputProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Input({ label, error, style, ...props }: InputProps) {
|
export function Input({ label, error, style, ...props }: InputProps) {
|
||||||
const { colors, typography } = useTheme();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text
|
<Text style={styles.label}>{label}</Text>
|
||||||
style={[typography.h4, { color: colors.textPrimary }, styles.label]}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[
|
style={[styles.input, error && styles.inputError, style]}
|
||||||
styles.input,
|
placeholderTextColor="#9ca3af"
|
||||||
{
|
|
||||||
backgroundColor: colors.surface,
|
|
||||||
borderColor: error ? colors.danger : colors.border,
|
|
||||||
color: colors.textPrimary,
|
|
||||||
},
|
|
||||||
style,
|
|
||||||
]}
|
|
||||||
placeholderTextColor={colors.textTertiary}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{error && (
|
{error && <Text style={styles.errorText}>{error}</Text>}
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
typography.caption,
|
|
||||||
styles.errorText,
|
|
||||||
{ color: colors.danger },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{error}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -56,16 +25,27 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#374151",
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
|
backgroundColor: "white",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderRadius: 12,
|
borderColor: "#e5e7eb",
|
||||||
|
borderRadius: 8,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 14,
|
paddingVertical: 12,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
color: "#1f2937",
|
||||||
|
},
|
||||||
|
inputError: {
|
||||||
|
borderColor: "#ef4444",
|
||||||
},
|
},
|
||||||
errorText: {
|
errorText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#ef4444",
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { View, Text, StyleSheet } from "react-native";
|
import { View, Text, StyleSheet } from "react-native";
|
||||||
import { Picker as RNPicker } from "@react-native-picker/picker";
|
import { Picker as RNPicker } from "@react-native-picker/picker";
|
||||||
import { useTheme } from "../contexts/ThemeContext";
|
|
||||||
|
|
||||||
interface PickerProps {
|
interface PickerProps {
|
||||||
label: string;
|
label: string;
|
||||||
@ -11,57 +10,23 @@ interface PickerProps {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Picker({
|
export function Picker({ label, value, onValueChange, items, error }: PickerProps) {
|
||||||
label,
|
|
||||||
value,
|
|
||||||
onValueChange,
|
|
||||||
items,
|
|
||||||
error,
|
|
||||||
}: PickerProps) {
|
|
||||||
const { colors, typography } = useTheme();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text
|
<Text style={styles.label}>{label}</Text>
|
||||||
style={[typography.h4, { color: colors.textPrimary }, styles.label]}
|
<View style={[styles.pickerWrapper, error && styles.pickerError]}>
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.pickerWrapper,
|
|
||||||
{
|
|
||||||
backgroundColor: colors.surface,
|
|
||||||
borderColor: error ? colors.danger : colors.border,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<RNPicker
|
<RNPicker
|
||||||
selectedValue={value}
|
selectedValue={value}
|
||||||
onValueChange={onValueChange}
|
onValueChange={onValueChange}
|
||||||
style={[styles.picker, { color: colors.textPrimary }]}
|
style={styles.picker}
|
||||||
>
|
>
|
||||||
<RNPicker.Item label="Select..." value="" />
|
<RNPicker.Item label="Select..." value="" />
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<RNPicker.Item
|
<RNPicker.Item key={item.value} label={item.label} value={item.value} />
|
||||||
key={item.value}
|
|
||||||
label={item.label}
|
|
||||||
value={item.value}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</RNPicker>
|
</RNPicker>
|
||||||
</View>
|
</View>
|
||||||
{error && (
|
{error && <Text style={styles.errorText}>{error}</Text>}
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
typography.caption,
|
|
||||||
styles.errorText,
|
|
||||||
{ color: colors.danger },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{error}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -71,17 +36,27 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#374151",
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
pickerWrapper: {
|
pickerWrapper: {
|
||||||
|
backgroundColor: "white",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderRadius: 12,
|
borderColor: "#e5e7eb",
|
||||||
|
borderRadius: 8,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
|
pickerError: {
|
||||||
|
borderColor: "#ef4444",
|
||||||
|
},
|
||||||
picker: {
|
picker: {
|
||||||
height: 50,
|
height: 50,
|
||||||
},
|
},
|
||||||
errorText: {
|
errorText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#ef4444",
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user