summaryrefslogtreecommitdiff
path: root/app/(auth)
diff options
context:
space:
mode:
authorJustZvan <justzvan@justzvan.xyz>2026-02-06 13:22:33 +0100
committerJustZvan <justzvan@justzvan.xyz>2026-02-06 13:22:33 +0100
commit7eb8ccae48b0cc18a9dcaa9c3626a02df8e6d919 (patch)
tree57b7dd06ac9aa7053c671d916f7183e3b4fa9410 /app/(auth)
feat: initial commit!
Diffstat (limited to 'app/(auth)')
-rw-r--r--app/(auth)/_layout.tsx29
-rw-r--r--app/(auth)/signin.tsx165
-rw-r--r--app/(auth)/signup.tsx185
-rw-r--r--app/(auth)/welcome.tsx64
4 files changed, 443 insertions, 0 deletions
diff --git a/app/(auth)/_layout.tsx b/app/(auth)/_layout.tsx
new file mode 100644
index 0000000..e18b629
--- /dev/null
+++ b/app/(auth)/_layout.tsx
@@ -0,0 +1,29 @@
+import { Redirect, Stack } from "expo-router";
+import { useAuth } from "../../lib/auth";
+import { colors } from "../../lib/theme";
+import { LoadingScreen } from "../../lib/ui";
+
+export default function AuthLayout() {
+ const { isLoading, isAuthenticated } = useAuth();
+
+ if (isLoading) {
+ return <LoadingScreen />;
+ }
+
+ if (isAuthenticated) {
+ return <Redirect href="/(tabs)" />;
+ }
+
+ return (
+ <Stack
+ screenOptions={{
+ headerShown: false,
+ contentStyle: { backgroundColor: colors.background },
+ }}
+ >
+ <Stack.Screen name="welcome" />
+ <Stack.Screen name="signin" />
+ <Stack.Screen name="signup" />
+ </Stack>
+ );
+}
diff --git a/app/(auth)/signin.tsx b/app/(auth)/signin.tsx
new file mode 100644
index 0000000..1eac63b
--- /dev/null
+++ b/app/(auth)/signin.tsx
@@ -0,0 +1,165 @@
+import { Ionicons } from "@expo/vector-icons";
+import { router } from "expo-router";
+import { useState } from "react";
+import {
+ KeyboardAvoidingView,
+ Platform,
+ ScrollView,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
+import { useAuth } from "../../lib/auth";
+import { t } from "../../lib/locales";
+import { colors } from "../../lib/theme";
+import { Button, H1, Muted, TextInput } from "../../lib/ui";
+
+export default function SignIn() {
+ const { signIn } = useAuth();
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState("");
+ const [emailError, setEmailError] = useState("");
+ const [passwordError, setPasswordError] = useState("");
+
+ const validateForm = () => {
+ let valid = true;
+ setEmailError("");
+ setPasswordError("");
+
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(email)) {
+ setEmailError(t("invalidEmail"));
+ valid = false;
+ }
+
+ if (!password) {
+ setPasswordError(t("passwordRequired"));
+ valid = false;
+ }
+
+ return valid;
+ };
+
+ const handleSignIn = async () => {
+ if (!validateForm()) return;
+
+ setLoading(true);
+ setError("");
+
+ try {
+ const result = await signIn(email, password);
+ if (!result.success) {
+ setError(result.reason || t("signInError"));
+ }
+ } catch (e) {
+ setError(t("signInError"));
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+ <SafeAreaView style={styles.container}>
+ <KeyboardAvoidingView
+ style={styles.keyboardView}
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
+ >
+ <ScrollView
+ contentContainerStyle={styles.scrollContent}
+ keyboardShouldPersistTaps="handled"
+ >
+ <TouchableOpacity
+ style={styles.backButton}
+ onPress={() => router.back()}
+ >
+ <Ionicons name="arrow-back" size={24} color={colors.onBackground} />
+ </TouchableOpacity>
+
+ <View style={styles.header}>
+ <H1>{t("signIn")}</H1>
+ </View>
+
+ <View style={styles.form}>
+ <TextInput
+ label={t("email")}
+ value={email}
+ onChangeText={setEmail}
+ placeholder={t("emailPlaceholder")}
+ keyboardType="email-address"
+ autoCapitalize="none"
+ error={emailError}
+ />
+
+ <TextInput
+ label={t("password")}
+ value={password}
+ onChangeText={setPassword}
+ placeholder={t("passwordPlaceholder")}
+ secureTextEntry
+ error={passwordError}
+ />
+
+ {error ? <Text style={styles.error}>{error}</Text> : null}
+
+ <Button
+ title={t("signIn")}
+ onPress={handleSignIn}
+ loading={loading}
+ disabled={loading}
+ />
+ </View>
+
+ <View style={styles.footer}>
+ <Muted>{t("dontHaveAccount")}</Muted>
+ <TouchableOpacity onPress={() => router.replace("/(auth)/signup")}>
+ <Text style={styles.link}>{t("signUp")}</Text>
+ </TouchableOpacity>
+ </View>
+ </ScrollView>
+ </KeyboardAvoidingView>
+ </SafeAreaView>
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ keyboardView: {
+ flex: 1,
+ },
+ scrollContent: {
+ flexGrow: 1,
+ padding: 24,
+ },
+ backButton: {
+ marginBottom: 16,
+ },
+ header: {
+ marginBottom: 32,
+ },
+ form: {
+ gap: 20,
+ },
+ error: {
+ color: colors.primary,
+ fontSize: 14,
+ textAlign: "center",
+ },
+ footer: {
+ flexDirection: "row",
+ justifyContent: "center",
+ alignItems: "center",
+ gap: 8,
+ marginTop: 32,
+ },
+ link: {
+ color: colors.primary,
+ fontWeight: "700",
+ },
+});
diff --git a/app/(auth)/signup.tsx b/app/(auth)/signup.tsx
new file mode 100644
index 0000000..127ea9f
--- /dev/null
+++ b/app/(auth)/signup.tsx
@@ -0,0 +1,185 @@
+import { Ionicons } from "@expo/vector-icons";
+import { router } from "expo-router";
+import { useState } from "react";
+import {
+ KeyboardAvoidingView,
+ Platform,
+ ScrollView,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
+import { useAuth } from "../../lib/auth";
+import { t } from "../../lib/locales";
+import { colors } from "../../lib/theme";
+import { Button, H1, Muted, TextInput } from "../../lib/ui";
+
+export default function SignUp() {
+ const { signUp } = useAuth();
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState("");
+ const [emailError, setEmailError] = useState("");
+ const [passwordError, setPasswordError] = useState("");
+ const [confirmPasswordError, setConfirmPasswordError] = useState("");
+
+ const validateForm = () => {
+ let valid = true;
+ setEmailError("");
+ setPasswordError("");
+ setConfirmPasswordError("");
+
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(email)) {
+ setEmailError(t("invalidEmail"));
+ valid = false;
+ }
+
+ if (!password) {
+ setPasswordError(t("passwordRequired"));
+ valid = false;
+ } else if (password.length < 8) {
+ setPasswordError(t("passwordTooShort"));
+ valid = false;
+ }
+
+ if (password !== confirmPassword) {
+ setConfirmPasswordError(t("passwordsDoNotMatch"));
+ valid = false;
+ }
+
+ return valid;
+ };
+
+ const handleSignUp = async () => {
+ if (!validateForm()) return;
+
+ setLoading(true);
+ setError("");
+
+ try {
+ const result = await signUp(email, password);
+ if (!result.success) {
+ setError(result.reason || t("signUpError"));
+ }
+ } catch (e) {
+ setError(t("signUpError"));
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+ <SafeAreaView style={styles.container}>
+ <KeyboardAvoidingView
+ style={styles.keyboardView}
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
+ >
+ <ScrollView
+ contentContainerStyle={styles.scrollContent}
+ keyboardShouldPersistTaps="handled"
+ >
+ <TouchableOpacity
+ style={styles.backButton}
+ onPress={() => router.back()}
+ >
+ <Ionicons name="arrow-back" size={24} color={colors.onBackground} />
+ </TouchableOpacity>
+
+ <View style={styles.header}>
+ <H1>{t("createAccount")}</H1>
+ </View>
+
+ <View style={styles.form}>
+ <TextInput
+ label={t("email")}
+ value={email}
+ onChangeText={setEmail}
+ placeholder={t("emailPlaceholder")}
+ keyboardType="email-address"
+ autoCapitalize="none"
+ error={emailError}
+ />
+
+ <TextInput
+ label={t("password")}
+ value={password}
+ onChangeText={setPassword}
+ placeholder={t("passwordPlaceholder")}
+ secureTextEntry
+ error={passwordError}
+ />
+
+ <TextInput
+ label={t("confirmPassword")}
+ value={confirmPassword}
+ onChangeText={setConfirmPassword}
+ placeholder={t("confirmPasswordPlaceholder")}
+ secureTextEntry
+ error={confirmPasswordError}
+ />
+
+ {error ? <Text style={styles.error}>{error}</Text> : null}
+
+ <Button
+ title={t("signUp")}
+ onPress={handleSignUp}
+ loading={loading}
+ disabled={loading}
+ />
+ </View>
+
+ <View style={styles.footer}>
+ <Muted>{t("alreadyHaveAccount")}</Muted>
+ <TouchableOpacity onPress={() => router.replace("/(auth)/signin")}>
+ <Text style={styles.link}>{t("signIn")}</Text>
+ </TouchableOpacity>
+ </View>
+ </ScrollView>
+ </KeyboardAvoidingView>
+ </SafeAreaView>
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ keyboardView: {
+ flex: 1,
+ },
+ scrollContent: {
+ flexGrow: 1,
+ padding: 24,
+ },
+ backButton: {
+ marginBottom: 16,
+ },
+ header: {
+ marginBottom: 32,
+ },
+ form: {
+ gap: 20,
+ },
+ error: {
+ color: colors.primary,
+ fontSize: 14,
+ textAlign: "center",
+ },
+ footer: {
+ flexDirection: "row",
+ justifyContent: "center",
+ alignItems: "center",
+ gap: 8,
+ marginTop: 32,
+ },
+ link: {
+ color: colors.primary,
+ fontWeight: "700",
+ },
+});
diff --git a/app/(auth)/welcome.tsx b/app/(auth)/welcome.tsx
new file mode 100644
index 0000000..9062c12
--- /dev/null
+++ b/app/(auth)/welcome.tsx
@@ -0,0 +1,64 @@
+import { router } from "expo-router";
+import { Image, StyleSheet, View } from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
+import { t } from "../../lib/locales";
+import { colors } from "../../lib/theme";
+import { Button, H1, Muted } from "../../lib/ui";
+
+import dogLogo from "../../assets/images/dog-logo.png";
+
+export default function Welcome() {
+ return (
+ <SafeAreaView style={styles.container}>
+ <View style={styles.content}>
+ <View style={styles.hero}>
+ <View style={styles.iconContainer}>
+ <Image source={dogLogo} style={{ width: 100, height: 100 }} />
+ </View>
+ <H1>{t("welcomeTitle")}</H1>
+ <Muted style={styles.subtitle}>{t("welcomeSubtitle")}</Muted>
+ </View>
+
+ <View style={styles.actions}>
+ <Button
+ title={t("getStarted")}
+ onPress={() => router.push("/(auth)/signup")}
+ />
+ <Button
+ title={t("signIn")}
+ variant="secondary"
+ onPress={() => router.push("/(auth)/signin")}
+ />
+ </View>
+ </View>
+ </SafeAreaView>
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ content: {
+ flex: 1,
+ padding: 24,
+ justifyContent: "space-between",
+ },
+ hero: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ gap: 16,
+ },
+ iconContainer: {
+ marginBottom: 24,
+ },
+ subtitle: {
+ textAlign: "center",
+ paddingHorizontal: 20,
+ },
+ actions: {
+ gap: 12,
+ },
+});