Skip to content

Commit

Permalink
Add Yup validation
Browse files Browse the repository at this point in the history
  • Loading branch information
ProchaLu committed Nov 18, 2024
1 parent a224e19 commit efb789f
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 3 deletions.
45 changes: 45 additions & 0 deletions app/(auth)/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,23 @@ import {
TextInput,
View,
} from 'react-native';
import * as yup from 'yup';
import { colors } from '../../constants/colors';
import { validateToFieldErrors } from '../../util/validateToFieldErrors';
import type { UserResponseBodyGet } from '../api/user+api';
import type { LoginResponseBodyPost } from './api/login+api';

const loginSchema = yup.object({
username: yup
.string()
.min(3, 'Username must be at least 3 characters')
.required('Username is required'),
password: yup
.string()
.min(3, 'Password must be at least 3 characters')
.required('Password is required'),
});

const styles = StyleSheet.create({
container: {
flex: 1,
Expand Down Expand Up @@ -59,6 +72,14 @@ const styles = StyleSheet.create({
alignItems: 'center',
gap: 4,
},
inputError: {
borderColor: 'red',
},
errorText: {
color: 'red',
fontSize: 14,
marginBottom: 8,
},
button: {
marginTop: 30,
backgroundColor: colors.text,
Expand All @@ -82,6 +103,7 @@ const styles = StyleSheet.create({
export default function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const [focusedInput, setFocusedInput] = useState<string | undefined>();

const { returnTo } = useLocalSearchParams<{ returnTo: string }>();
Expand Down Expand Up @@ -118,24 +140,33 @@ export default function Login() {
style={[
styles.input,
focusedInput === 'username' && styles.inputFocused,
fieldErrors.username && styles.inputError,
]}
value={username}
onChangeText={setUsername}
onFocus={() => setFocusedInput('username')}
onBlur={() => setFocusedInput(undefined)}
/>
{fieldErrors.username && (
<Text style={styles.errorText}>{fieldErrors.username}</Text>
)}

<Text style={styles.label}>Password</Text>
<TextInput
style={[
styles.input,
focusedInput === 'password' && styles.inputFocused,
fieldErrors.password && styles.inputError,
]}
secureTextEntry
value={password}
onChangeText={setPassword}
onFocus={() => setFocusedInput('password')}
onBlur={() => setFocusedInput(undefined)}
/>
{fieldErrors.password && (
<Text style={styles.errorText}>{fieldErrors.password}</Text>
)}
<View style={styles.promptTextContainer}>
<Text style={{ color: colors.text }}>Don't have an account?</Text>
<Link href="/register" style={{ color: colors.text }}>
Expand All @@ -146,6 +177,20 @@ export default function Login() {
<Pressable
style={({ pressed }) => [styles.button, { opacity: pressed ? 0.5 : 1 }]}
onPress={async () => {
const validationResult = await validateToFieldErrors(loginSchema, {
username,
password,
});

if ('fieldErrors' in validationResult) {
const errors = Object.fromEntries(validationResult.fieldErrors);
setFieldErrors(errors);
return;
}

// Clear errors if validation passes
setFieldErrors({});

const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password, attending: false }),
Expand Down
47 changes: 47 additions & 0 deletions app/(auth)/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,22 @@ import {
TextInput,
View,
} from 'react-native';
import * as yup from 'yup';
import { colors } from '../../constants/colors';
import { validateToFieldErrors } from '../../util/validateToFieldErrors';
import type { RegisterResponseBodyPost } from './api/register+api';

const registerSchema = yup.object({
username: yup
.string()
.min(3, 'Username must be at least 3 characters')
.required('Username is required'),
password: yup
.string()
.min(3, 'Password must be at least 3 characters')
.required('Password is required'),
});

const styles = StyleSheet.create({
container: {
flex: 1,
Expand Down Expand Up @@ -46,6 +59,14 @@ const styles = StyleSheet.create({
inputFocused: {
borderColor: colors.white,
},
inputError: {
borderColor: 'red',
},
errorText: {
color: 'red',
fontSize: 14,
marginBottom: 8,
},
promptTextContainer: {
flexDirection: 'row',
justifyContent: 'center',
Expand Down Expand Up @@ -75,6 +96,7 @@ const styles = StyleSheet.create({
export default function Register() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const [focusedInput, setFocusedInput] = useState<string | undefined>();

useFocusEffect(
Expand Down Expand Up @@ -103,34 +125,59 @@ export default function Register() {
style={[
styles.input,
focusedInput === 'username' && styles.inputFocused,
fieldErrors.username && styles.inputError,
]}
value={username}
onChangeText={setUsername}
onFocus={() => setFocusedInput('username')}
onBlur={() => setFocusedInput(undefined)}
/>
{fieldErrors.username && (
<Text style={styles.errorText}>{fieldErrors.username}</Text>
)}

<Text style={styles.label}>Password</Text>
<TextInput
style={[
styles.input,
focusedInput === 'password' && styles.inputFocused,
fieldErrors.password && styles.inputError,
]}
secureTextEntry
value={password}
onChangeText={setPassword}
onFocus={() => setFocusedInput('password')}
onBlur={() => setFocusedInput(undefined)}
/>
{fieldErrors.password && (
<Text style={styles.errorText}>{fieldErrors.password}</Text>
)}

<View style={styles.promptTextContainer}>
<Text style={{ color: colors.text }}>Already have an account?</Text>
<Link href="/(auth)/login" style={{ color: colors.text }}>
Login
</Link>
</View>
</View>

<Pressable
style={({ pressed }) => [styles.button, { opacity: pressed ? 0.5 : 1 }]}
onPress={async () => {
const validationResult = await validateToFieldErrors(registerSchema, {
username,
password,
});

if ('fieldErrors' in validationResult) {
const errors = Object.fromEntries(validationResult.fieldErrors);
setFieldErrors(errors);
return;
}

// Clear errors if validation passes
setFieldErrors({});

const response = await fetch('/api/register', {
method: 'POST',
body: JSON.stringify({ username, password, attending: false }),
Expand Down
46 changes: 46 additions & 0 deletions app/guests/newGuest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,22 @@ import {
TextInput,
View,
} from 'react-native';
import * as yup from 'yup';
import { colors } from '../../constants/colors';
import { validateToFieldErrors } from '../../util/validateToFieldErrors';
import type { GuestsResponseBodyPost } from '../api/guests/index+api';

const guestSchema = yup.object({
firstName: yup
.string()
.min(1, 'First name is required')
.max(30, 'First name must be less than 30 characters'),
lastName: yup
.string()
.min(1, 'Last name is required')
.max(30, 'Last name must be less than 30 characters'),
});

const styles = StyleSheet.create({
container: {
flex: 1,
Expand Down Expand Up @@ -46,6 +59,14 @@ const styles = StyleSheet.create({
inputFocused: {
borderColor: colors.white,
},
inputError: {
borderColor: 'red',
},
errorText: {
color: 'red',
fontSize: 14,
marginBottom: 8,
},
button: {
marginTop: 30,
backgroundColor: colors.text,
Expand All @@ -69,6 +90,7 @@ const styles = StyleSheet.create({
export default function NewGuest() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const [focusedInput, setFocusedInput] = useState<string | undefined>();

return (
Expand All @@ -79,27 +101,51 @@ export default function NewGuest() {
style={[
styles.input,
focusedInput === 'firstName' && styles.inputFocused,
fieldErrors.firstName && styles.inputError,
]}
value={firstName}
onChangeText={setFirstName}
onFocus={() => setFocusedInput('firstName')}
onBlur={() => setFocusedInput(undefined)}
/>
{fieldErrors.firstName && (
<Text style={styles.errorText}>{fieldErrors.firstName}</Text>
)}

<Text style={styles.label}>Last Name</Text>
<TextInput
style={[
styles.input,
focusedInput === 'lastName' && styles.inputFocused,
fieldErrors.lastName && styles.inputError,
]}
value={lastName}
onChangeText={setLastName}
onFocus={() => setFocusedInput('lastName')}
onBlur={() => setFocusedInput(undefined)}
/>
{fieldErrors.lastName && (
<Text style={styles.errorText}>{fieldErrors.lastName}</Text>
)}
</View>

<Pressable
style={({ pressed }) => [styles.button, { opacity: pressed ? 0.5 : 1 }]}
onPress={async () => {
const validationResult = await validateToFieldErrors(guestSchema, {
firstName,
lastName,
});

if ('fieldErrors' in validationResult) {
const errors = Object.fromEntries(validationResult.fieldErrors);
setFieldErrors(errors);
return;
}

// Clear errors if validation passes
setFieldErrors({});

const response = await fetch('/api/guests', {
method: 'POST',
body: JSON.stringify({ firstName, lastName, attending: false }),
Expand Down
Loading

0 comments on commit efb789f

Please sign in to comment.