feat: 로그인 화면 및 공통 속성 생성
This commit is contained in:
27
src/components/ui/AppText.tsx
Normal file
27
src/components/ui/AppText.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
import { Text, type TextProps, type TextStyle } from "react-native";
|
||||
import { Theme } from "../../theme/theme";
|
||||
|
||||
type Variant = "logo" | "title" | "body" | "muted" | "error";
|
||||
|
||||
type Props = TextProps & {
|
||||
variant?: Variant;
|
||||
style?: TextStyle | TextStyle[];
|
||||
};
|
||||
|
||||
export default function AppText({ variant = "body", style, ...props }: Props) {
|
||||
const base: TextStyle = {
|
||||
color: Theme.Colors.text,
|
||||
fontSize: Theme.FontSize.md,
|
||||
};
|
||||
|
||||
const variants: Record<Variant, TextStyle> = {
|
||||
logo: { fontSize: Theme.FontSize.xxl, fontWeight: Theme.FontWeight.bold },
|
||||
title: { fontSize: Theme.FontSize.xl, fontWeight: Theme.FontWeight.bold },
|
||||
body: { fontSize: Theme.FontSize.md },
|
||||
muted: { color: Theme.Colors.mutedText, fontSize: Theme.FontSize.sm },
|
||||
error: { color: Theme.Colors.danger, fontSize: Theme.FontSize.sm },
|
||||
};
|
||||
|
||||
return <Text {...props} style={[base, variants[variant], style]} />;
|
||||
}
|
||||
73
src/components/ui/Button.tsx
Normal file
73
src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from "react";
|
||||
import { Pressable, type PressableProps, StyleSheet, View } from "react-native";
|
||||
import { Theme } from "../../theme/theme";
|
||||
import AppText from "./AppText";
|
||||
|
||||
type Props = PressableProps & {
|
||||
title: string;
|
||||
variant?: "primary" | "secondary";
|
||||
};
|
||||
|
||||
export default function Button({
|
||||
title,
|
||||
variant = "primary",
|
||||
style,
|
||||
...props
|
||||
}: Props) {
|
||||
const isPrimary = variant === "primary";
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
{...props}
|
||||
style={({ pressed }) => [
|
||||
styles.base,
|
||||
isPrimary ? styles.primary : styles.secondary,
|
||||
pressed &&
|
||||
(isPrimary ? styles.primaryPressed : styles.secondaryPressed),
|
||||
typeof style === "function" ? style({ pressed }) : style,
|
||||
]}
|
||||
>
|
||||
<View>
|
||||
<AppText
|
||||
style={{
|
||||
textAlign: "center",
|
||||
fontWeight: Theme.FontWeight.bold,
|
||||
color: isPrimary ? Theme.Colors.text : Theme.Colors.text,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</AppText>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
height: 48,
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: Theme.Spacing.lg,
|
||||
borderRadius: Theme.Radius.md,
|
||||
alignItems: "center", // 가로 가운데
|
||||
justifyContent: "center", // 세로 가운데
|
||||
},
|
||||
text: {
|
||||
textAlign: "center",
|
||||
fontWeight: Theme.FontWeight.bold,
|
||||
lineHeight: 20,
|
||||
},
|
||||
primary: {
|
||||
backgroundColor: Theme.Colors.primary,
|
||||
},
|
||||
primaryPressed: {
|
||||
backgroundColor: Theme.Colors.primaryPressed,
|
||||
},
|
||||
secondary: {
|
||||
backgroundColor: Theme.Colors.surface,
|
||||
borderWidth: 1,
|
||||
borderColor: Theme.Colors.border,
|
||||
},
|
||||
secondaryPressed: {
|
||||
opacity: 0.9,
|
||||
},
|
||||
});
|
||||
32
src/components/ui/Input.tsx
Normal file
32
src/components/ui/Input.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, TextInput, type TextInputProps, View } from "react-native";
|
||||
import { Theme } from "../../theme/theme";
|
||||
|
||||
type Props = TextInputProps;
|
||||
|
||||
export default function Input(props: Props) {
|
||||
return (
|
||||
<View style={styles.wrap}>
|
||||
<TextInput
|
||||
{...props}
|
||||
placeholderTextColor={Theme.Colors.placeholder}
|
||||
style={[styles.input, props.style]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrap: {
|
||||
borderWidth: 1,
|
||||
borderColor: Theme.Colors.border,
|
||||
borderRadius: Theme.Radius.md,
|
||||
backgroundColor: Theme.Colors.inputBg,
|
||||
},
|
||||
input: {
|
||||
paddingHorizontal: Theme.Spacing.md,
|
||||
paddingVertical: Theme.Spacing.md,
|
||||
color: Theme.Colors.text,
|
||||
fontSize: Theme.FontSize.md,
|
||||
},
|
||||
});
|
||||
17
src/navigation/AppStack.tsx
Normal file
17
src/navigation/AppStack.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import React from "react";
|
||||
import HomeScreen from "../screens/HomeScreen";
|
||||
|
||||
export type AppStackParamList = {
|
||||
Home: undefined;
|
||||
};
|
||||
|
||||
const Stack = createNativeStackNavigator<AppStackParamList>();
|
||||
|
||||
export default function AppStack() {
|
||||
return (
|
||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="Home" component={HomeScreen} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
10
src/navigation/AuthGate.tsx
Normal file
10
src/navigation/AuthGate.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import AppStack from "./AppStack";
|
||||
import AuthStack from "./AuthStack";
|
||||
|
||||
export default function AuthGate() {
|
||||
// const { isAuthed } = useAuth();
|
||||
const isAuthed = true;
|
||||
// return <AuthStack />;
|
||||
return isAuthed ? <AppStack /> : <AuthStack />;
|
||||
}
|
||||
17
src/navigation/AuthStack.tsx
Normal file
17
src/navigation/AuthStack.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||
import React from "react";
|
||||
import LoginScreen from "../screens/LoginScreen";
|
||||
|
||||
export type AuthStackParamList = {
|
||||
Login: undefined;
|
||||
};
|
||||
|
||||
const Stack = createNativeStackNavigator<AuthStackParamList>();
|
||||
|
||||
export default function AuthStack() {
|
||||
return (
|
||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="Login" component={LoginScreen} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
18
src/screens/HomeScreen.tsx
Normal file
18
src/screens/HomeScreen.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
|
||||
export default function HomeScreen() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>feawfewa</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "red",
|
||||
alignContent: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
});
|
||||
74
src/screens/LoginScreen.tsx
Normal file
74
src/screens/LoginScreen.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from "react";
|
||||
import { Pressable, 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 LoginScreen() {
|
||||
return (
|
||||
<SafeAreaView style={styles.safe}>
|
||||
<View style={styles.container}>
|
||||
<AppText variant="title">Your personal growth podcast</AppText>
|
||||
<AppText variant="muted">Sign in to continue</AppText>
|
||||
|
||||
<View style={{ height: Theme.Spacing.lg }} />
|
||||
|
||||
<Input
|
||||
placeholder="Username"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
textContentType="username"
|
||||
/>
|
||||
|
||||
<View style={{ height: Theme.Spacing.sm }} />
|
||||
|
||||
<Input
|
||||
placeholder="Password"
|
||||
secureTextEntry
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
textContentType="password"
|
||||
/>
|
||||
|
||||
<View style={{ height: Theme.Spacing.lg }} />
|
||||
|
||||
<Button title="Continue" onPress={() => {}} />
|
||||
|
||||
<View style={{ height: Theme.Spacing.md }} />
|
||||
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
console.log("feawf");
|
||||
}}
|
||||
>
|
||||
<AppText variant="muted" style={styles.signupLine}>
|
||||
Don't have an account?{" "}
|
||||
<AppText style={styles.signupLink}>Sign up</AppText>
|
||||
</AppText>
|
||||
</Pressable>
|
||||
</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,
|
||||
},
|
||||
signupLine: {
|
||||
textAlign: "center",
|
||||
},
|
||||
signupLink: {
|
||||
color: Theme.Colors.primary,
|
||||
fontWeight: Theme.FontWeight.bold,
|
||||
},
|
||||
});
|
||||
31
src/store/auth.tsx
Normal file
31
src/store/auth.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React, { createContext, useContext, useMemo, useState } from "react";
|
||||
|
||||
export type AuthContextValue = {
|
||||
isAuthed: boolean;
|
||||
login: () => void;
|
||||
logout: () => void;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [isAuthed, setIsAuthed] = useState<boolean>(false);
|
||||
console.log("isAuthed: " + isAuthed);
|
||||
|
||||
const value = useMemo<AuthContextValue>(
|
||||
() => ({
|
||||
isAuthed,
|
||||
login: () => setIsAuthed(true),
|
||||
logout: () => setIsAuthed(false),
|
||||
}),
|
||||
[isAuthed]
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth(): AuthContextValue {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
|
||||
return ctx;
|
||||
}
|
||||
22
src/theme/colors.ts
Normal file
22
src/theme/colors.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export const Colors = {
|
||||
// base
|
||||
bg: "#0B1220",
|
||||
surface: "#111827",
|
||||
card: "#0F172A",
|
||||
|
||||
// text
|
||||
text: "#FFFFFF",
|
||||
mutedText: "#9CA3AF",
|
||||
placeholder: "#6B7280",
|
||||
|
||||
// brand
|
||||
primary: "#4F46E5",
|
||||
primaryPressed: "#4338CA",
|
||||
|
||||
// status
|
||||
danger: "#EF4444",
|
||||
|
||||
// lines
|
||||
border: "#22304A",
|
||||
inputBg: "#0B1220",
|
||||
} as const;
|
||||
6
src/theme/radius.ts
Normal file
6
src/theme/radius.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const Radius = {
|
||||
sm: 10,
|
||||
md: 12,
|
||||
lg: 16,
|
||||
xl: 20,
|
||||
} as const;
|
||||
8
src/theme/spacing.ts
Normal file
8
src/theme/spacing.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const Spacing = {
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 12,
|
||||
lg: 16,
|
||||
xl: 20,
|
||||
"2xl": 24,
|
||||
} as const;
|
||||
12
src/theme/theme.ts
Normal file
12
src/theme/theme.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Colors } from "./colors";
|
||||
import { Radius } from "./radius";
|
||||
import { Spacing } from "./spacing";
|
||||
import { FontSize, FontWeight } from "./typography";
|
||||
|
||||
export const Theme = {
|
||||
Colors,
|
||||
Spacing,
|
||||
Radius,
|
||||
FontSize,
|
||||
FontWeight,
|
||||
} as const;
|
||||
14
src/theme/typography.ts
Normal file
14
src/theme/typography.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const FontSize = {
|
||||
sm: 13,
|
||||
md: 15,
|
||||
lg: 18,
|
||||
xl: 22,
|
||||
xxl: 26,
|
||||
} as const;
|
||||
|
||||
export const FontWeight = {
|
||||
regular: "400",
|
||||
medium: "500",
|
||||
semibold: "600",
|
||||
bold: "700",
|
||||
} as const;
|
||||
Reference in New Issue
Block a user