feat: implement mobile reader

- implement mobile epub reader
This commit is contained in:
2026-06-20 13:33:44 -05:00
parent 84c0497f21
commit 22ded87250
41 changed files with 3184 additions and 1111 deletions
+34 -112
View File
@@ -1,10 +1,8 @@
import { useState, type ReactNode } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
ActivityIndicator,
KeyboardAvoidingView,
@@ -12,40 +10,39 @@ import {
ScrollView,
} from "react-native";
import { useAuth } from "../context/AuthContext";
import {
isValidEmail,
validatePasswordStrength,
} from "@cloud-reader/shared";
import { isStrongPassword } from "@cloud-reader/shared";
import { authStyles } from "./authStyles";
import { AuthFormError } from "./AuthFormError";
import { requireValidEmail } from "./authValidation";
export default function RegisterScreen({
navigation,
}: {
navigation: any;
}): ReactNode {
const { state, register, clearError } = useAuth();
const { register } = useAuth();
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
const handleRegister = async () => {
clearError();
setError(null);
if (!username.trim()) {
Alert.alert("Validation Error", "Please enter a username.");
return;
}
if (!email.trim()) {
Alert.alert("Validation Error", "Please enter your email.");
if (!requireValidEmail(email)) {
return;
}
if (!isValidEmail(email.trim())) {
Alert.alert("Validation Error", "Please enter a valid email address.");
return;
}
const passwordError = validatePasswordStrength(password);
if (passwordError) {
Alert.alert("Validation Error", passwordError);
if (!isStrongPassword(password)) {
Alert.alert(
"Validation Error",
"Password must be at least 8 characters and include uppercase, lowercase, and a number.",
);
return;
}
if (password !== confirmPassword) {
@@ -53,30 +50,29 @@ export default function RegisterScreen({
return;
}
setSubmitting(true);
try {
await register(email.trim(), password, username.trim());
await register(email.trim(), username.trim(), password);
} catch {
// Error handled in context
setError("Could not create the account. Try a different email.");
} finally {
setSubmitting(false);
}
};
return (
<KeyboardAvoidingView
style={styles.container}
style={authStyles.container}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<ScrollView contentContainerStyle={styles.inner}>
<Text style={styles.title}>Create Account</Text>
<Text style={styles.subtitle}>Join Cloud Reader</Text>
<ScrollView contentContainerStyle={authStyles.innerScroll}>
<Text style={authStyles.titleCompact}>Create Account</Text>
<Text style={authStyles.subtitle}>Join Cloud Reader</Text>
{state.error && (
<View style={styles.errorBox}>
<Text style={styles.errorText}>{state.error}</Text>
</View>
)}
{error ? <AuthFormError message={error} /> : null}
<TextInput
style={styles.input}
style={authStyles.input}
placeholder="Username"
placeholderTextColor="#666"
value={username}
@@ -86,7 +82,7 @@ export default function RegisterScreen({
/>
<TextInput
style={styles.input}
style={authStyles.input}
placeholder="Email"
placeholderTextColor="#666"
value={email}
@@ -97,7 +93,7 @@ export default function RegisterScreen({
/>
<TextInput
style={styles.input}
style={authStyles.input}
placeholder="Password"
placeholderTextColor="#666"
value={password}
@@ -106,7 +102,7 @@ export default function RegisterScreen({
/>
<TextInput
style={styles.input}
style={authStyles.input}
placeholder="Confirm Password"
placeholderTextColor="#666"
value={confirmPassword}
@@ -115,98 +111,24 @@ export default function RegisterScreen({
/>
<TouchableOpacity
style={[styles.button, state.isLoading && styles.buttonDisabled]}
style={[authStyles.button, submitting && authStyles.buttonDisabled]}
onPress={handleRegister}
disabled={state.isLoading}
disabled={submitting}
>
{state.isLoading ? (
{submitting ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>Create Account</Text>
<Text style={authStyles.buttonText}>Create Account</Text>
)}
</TouchableOpacity>
<TouchableOpacity onPress={() => navigation.goBack()}>
<Text style={styles.linkText}>
<Text style={authStyles.linkText}>
Already have an account?{" "}
<Text style={styles.linkBold}>Sign In</Text>
<Text style={authStyles.linkBold}>Sign In</Text>
</Text>
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#0f0f23",
},
inner: {
flexGrow: 1,
justifyContent: "center",
paddingHorizontal: 24,
paddingVertical: 48,
},
title: {
fontSize: 28,
fontWeight: "bold",
color: "#fff",
textAlign: "center",
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: "#888",
textAlign: "center",
marginBottom: 32,
},
input: {
backgroundColor: "#1a1a2e",
borderRadius: 8,
padding: 16,
fontSize: 16,
color: "#fff",
marginBottom: 12,
borderWidth: 1,
borderColor: "#333",
},
button: {
backgroundColor: "#4f8ef7",
borderRadius: 8,
padding: 16,
alignItems: "center",
marginTop: 8,
marginBottom: 24,
},
buttonDisabled: {
opacity: 0.6,
},
buttonText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
errorBox: {
backgroundColor: "rgba(255, 69, 58, 0.15)",
borderRadius: 8,
padding: 12,
marginBottom: 16,
borderWidth: 1,
borderColor: "rgba(255, 69, 58, 0.3)",
},
errorText: {
color: "#ff453a",
fontSize: 14,
textAlign: "center",
},
linkText: {
color: "#888",
textAlign: "center",
fontSize: 14,
},
linkBold: {
color: "#4f8ef7",
fontWeight: "600",
},
});