feat: 회원가입 화면 생성
This commit is contained in:
@@ -1,12 +1,17 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Text, type TextProps, type TextStyle } from "react-native";
|
import {
|
||||||
|
Text,
|
||||||
|
type StyleProp,
|
||||||
|
type TextProps,
|
||||||
|
type TextStyle,
|
||||||
|
} from "react-native";
|
||||||
import { Theme } from "../../theme/theme";
|
import { Theme } from "../../theme/theme";
|
||||||
|
|
||||||
type Variant = "logo" | "title" | "body" | "muted" | "error";
|
type Variant = "logo" | "title" | "body" | "muted" | "error";
|
||||||
|
|
||||||
type Props = TextProps & {
|
type Props = TextProps & {
|
||||||
variant?: Variant;
|
variant?: Variant;
|
||||||
style?: TextStyle | TextStyle[];
|
style?: StyleProp<TextStyle>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AppText({ variant = "body", style, ...props }: Props) {
|
export default function AppText({ variant = "body", style, ...props }: Props) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Pressable, type PressableProps, StyleSheet, View } from "react-native";
|
import { Pressable, type PressableProps, StyleSheet } from "react-native";
|
||||||
import { Theme } from "../../theme/theme";
|
import { Theme } from "../../theme/theme";
|
||||||
import AppText from "./AppText";
|
import AppText from "./AppText";
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ export default function Button({
|
|||||||
title,
|
title,
|
||||||
variant = "primary",
|
variant = "primary",
|
||||||
style,
|
style,
|
||||||
|
disabled,
|
||||||
...props
|
...props
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const isPrimary = variant === "primary";
|
const isPrimary = variant === "primary";
|
||||||
@@ -19,42 +20,42 @@ export default function Button({
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
{...props}
|
{...props}
|
||||||
|
disabled={disabled}
|
||||||
style={({ pressed }) => [
|
style={({ pressed }) => [
|
||||||
styles.base,
|
styles.base,
|
||||||
isPrimary ? styles.primary : styles.secondary,
|
isPrimary ? styles.primary : styles.secondary,
|
||||||
pressed &&
|
|
||||||
|
// pressed 스타일(비활성화면 적용 안 함)
|
||||||
|
!disabled &&
|
||||||
|
pressed &&
|
||||||
(isPrimary ? styles.primaryPressed : styles.secondaryPressed),
|
(isPrimary ? styles.primaryPressed : styles.secondaryPressed),
|
||||||
|
|
||||||
|
// disabled 스타일
|
||||||
|
disabled && styles.disabled,
|
||||||
|
|
||||||
typeof style === "function" ? style({ pressed }) : style,
|
typeof style === "function" ? style({ pressed }) : style,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<View>
|
<AppText
|
||||||
<AppText
|
style={[
|
||||||
style={{
|
styles.text,
|
||||||
textAlign: "center",
|
isPrimary ? styles.textPrimary : styles.textSecondary,
|
||||||
fontWeight: Theme.FontWeight.bold,
|
disabled && styles.textDisabled,
|
||||||
color: isPrimary ? Theme.Colors.text : Theme.Colors.text,
|
]}
|
||||||
}}
|
>
|
||||||
>
|
{title}
|
||||||
{title}
|
</AppText>
|
||||||
</AppText>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
base: {
|
base: {
|
||||||
height: 48,
|
paddingVertical: Theme.Spacing.md,
|
||||||
paddingVertical: 0,
|
|
||||||
paddingHorizontal: Theme.Spacing.lg,
|
|
||||||
borderRadius: Theme.Radius.md,
|
borderRadius: Theme.Radius.md,
|
||||||
alignItems: "center", // 가로 가운데
|
alignItems: "center",
|
||||||
justifyContent: "center", // 세로 가운데
|
justifyContent: "center",
|
||||||
},
|
minHeight: 48,
|
||||||
text: {
|
|
||||||
textAlign: "center",
|
|
||||||
fontWeight: Theme.FontWeight.bold,
|
|
||||||
lineHeight: 20,
|
|
||||||
},
|
},
|
||||||
primary: {
|
primary: {
|
||||||
backgroundColor: Theme.Colors.primary,
|
backgroundColor: Theme.Colors.primary,
|
||||||
@@ -70,4 +71,22 @@ const styles = StyleSheet.create({
|
|||||||
secondaryPressed: {
|
secondaryPressed: {
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
disabled: {
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
|
||||||
|
text: {
|
||||||
|
textAlign: "center",
|
||||||
|
fontWeight: Theme.FontWeight.bold,
|
||||||
|
},
|
||||||
|
textPrimary: {
|
||||||
|
color: Theme.Colors.text,
|
||||||
|
},
|
||||||
|
textSecondary: {
|
||||||
|
color: Theme.Colors.text,
|
||||||
|
},
|
||||||
|
textDisabled: {
|
||||||
|
color: Theme.Colors.mutedText,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import LoginScreen from "../screens/LoginScreen";
|
import LoginScreen from "../screens/LoginScreen";
|
||||||
|
import SignUpScreen from "../screens/SignUpScreen";
|
||||||
|
|
||||||
export type AuthStackParamList = {
|
export type AuthStackParamList = {
|
||||||
Login: undefined;
|
Login: undefined;
|
||||||
|
SignUp: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<AuthStackParamList>();
|
const Stack = createNativeStackNavigator<AuthStackParamList>();
|
||||||
@@ -12,6 +14,10 @@ export default function AuthStack() {
|
|||||||
return (
|
return (
|
||||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||||
<Stack.Screen name="Login" component={LoginScreen} />
|
<Stack.Screen name="Login" component={LoginScreen} />
|
||||||
|
|
||||||
|
<Stack.Group screenOptions={{ presentation: "modal" }}>
|
||||||
|
<Stack.Screen name="SignUp" component={SignUpScreen} />
|
||||||
|
</Stack.Group>
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import { Button, StyleSheet, View } from "react-native";
|
import { Button, StyleSheet, View } from "react-native";
|
||||||
|
import { useAuth } from "../store/auth";
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
|
const { signOut } = useAuth();
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Button
|
<Button
|
||||||
@@ -12,6 +14,7 @@ export default function HomeScreen() {
|
|||||||
console.log(entries);
|
console.log(entries);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Button title="logout" onPress={() => signOut()}></Button>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { useNavigation } from "@react-navigation/native";
|
||||||
|
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Pressable, StyleSheet, View } from "react-native";
|
import { Pressable, StyleSheet, View } from "react-native";
|
||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
import AppText from "../components/ui/AppText";
|
import AppText from "../components/ui/AppText";
|
||||||
import Button from "../components/ui/Button";
|
import Button from "../components/ui/Button";
|
||||||
import Input from "../components/ui/Input";
|
import Input from "../components/ui/Input";
|
||||||
|
import { AuthStackParamList } from "../navigation/AuthStack";
|
||||||
import { useAuth } from "../store/auth";
|
import { useAuth } from "../store/auth";
|
||||||
import { Theme } from "../theme/theme";
|
import { Theme } from "../theme/theme";
|
||||||
|
|
||||||
@@ -12,6 +15,8 @@ export default function LoginScreen() {
|
|||||||
const [loginId, setLoginId] = useState("");
|
const [loginId, setLoginId] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const canSubmit = loginId.trim().length > 0 && password.length > 0;
|
const canSubmit = loginId.trim().length > 0 && password.length > 0;
|
||||||
|
const navigation =
|
||||||
|
useNavigation<NativeStackNavigationProp<AuthStackParamList>>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safe}>
|
<SafeAreaView style={styles.safe}>
|
||||||
@@ -60,7 +65,7 @@ export default function LoginScreen() {
|
|||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
console.log("feawf");
|
navigation.navigate("SignUp");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AppText variant="muted" style={styles.signupLine}>
|
<AppText variant="muted" style={styles.signupLine}>
|
||||||
|
|||||||
121
src/screens/SignUpScreen.tsx
Normal file
121
src/screens/SignUpScreen.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { useNavigation } from "@react-navigation/native";
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { StyleSheet, View } from "react-native";
|
||||||
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
import AppText from "../components/ui/AppText";
|
||||||
|
import Button from "../components/ui/Button";
|
||||||
|
import Input from "../components/ui/Input";
|
||||||
|
import { Theme } from "../theme/theme";
|
||||||
|
|
||||||
|
export default function SignUpScreen() {
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
const [loginId, setLoginId] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [passwordConfirm, setPasswordConfirm] = useState("");
|
||||||
|
|
||||||
|
const { canSubmit, errorText } = useMemo(() => {
|
||||||
|
const idOk = loginId.trim().length >= 4;
|
||||||
|
const pwOk = password.length >= 8;
|
||||||
|
const match = password.length > 0 && password === passwordConfirm;
|
||||||
|
|
||||||
|
const can = idOk && pwOk && match;
|
||||||
|
|
||||||
|
let err = "";
|
||||||
|
if (loginId.length > 0 && !idOk)
|
||||||
|
err = "Username must be at least 4 characters.";
|
||||||
|
else if (password.length > 0 && !pwOk)
|
||||||
|
err = "Password must be at least 8 characters.";
|
||||||
|
else if (passwordConfirm.length > 0 && !match)
|
||||||
|
err = "Passwords do not match.";
|
||||||
|
|
||||||
|
return { canSubmit: can, errorText: err };
|
||||||
|
}, [loginId, password, passwordConfirm]);
|
||||||
|
|
||||||
|
const onSignUp = async () => {
|
||||||
|
navigation.goBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.safe}>
|
||||||
|
<View style={styles.container}>
|
||||||
|
<AppText variant="title">Create your account</AppText>
|
||||||
|
<AppText variant="muted">Start listening in minutes</AppText>
|
||||||
|
|
||||||
|
<View style={{ height: Theme.Spacing.lg }} />
|
||||||
|
|
||||||
|
<Input
|
||||||
|
placeholder="Username"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
textContentType="username"
|
||||||
|
value={loginId}
|
||||||
|
onChangeText={setLoginId}
|
||||||
|
returnKeyType="next"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={{ height: Theme.Spacing.sm }} />
|
||||||
|
|
||||||
|
<Input
|
||||||
|
placeholder="Password"
|
||||||
|
secureTextEntry
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
textContentType="newPassword"
|
||||||
|
value={password}
|
||||||
|
onChangeText={setPassword}
|
||||||
|
returnKeyType="next"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={{ height: Theme.Spacing.sm }} />
|
||||||
|
|
||||||
|
<Input
|
||||||
|
placeholder="Confirm password"
|
||||||
|
secureTextEntry
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
textContentType="newPassword"
|
||||||
|
value={passwordConfirm}
|
||||||
|
onChangeText={setPasswordConfirm}
|
||||||
|
returnKeyType="done"
|
||||||
|
onSubmitEditing={() => {
|
||||||
|
if (canSubmit) onSignUp();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{errorText ? (
|
||||||
|
<AppText variant="muted" style={styles.error}>
|
||||||
|
{errorText}
|
||||||
|
</AppText>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<View style={{ height: Theme.Spacing.lg }} />
|
||||||
|
|
||||||
|
<Button title="Sign up" disabled={!canSubmit} onPress={onSignUp} />
|
||||||
|
<View style={{ height: Theme.Spacing.sm }} />
|
||||||
|
<Button
|
||||||
|
title="Back"
|
||||||
|
variant="secondary"
|
||||||
|
onPress={() => navigation.goBack()}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
safe: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: Theme.Colors.bg,
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
padding: Theme.Spacing.xl,
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: Theme.Spacing.sm,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
textAlign: "center",
|
||||||
|
color: Theme.Colors.mutedText,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user