fixed login and signup page.

This commit is contained in:
glitchySid
2026-06-25 21:20:47 +05:30
parent 8ef007a8d1
commit e8972fa77b
11 changed files with 4298 additions and 421 deletions

View File

@@ -1,67 +1,91 @@
import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router';
import { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Image } from "expo-image";
import { useRouter } from "expo-router";
import { SymbolView } from "expo-symbols";
import { useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { BrandPressable } from '@/components/brand-pressable';
import { BrandTextInput } from '@/components/brand-text-input';
import { Brand, BrandFont } from '@/constants/theme';
import { BrandPressable } from "@/components/brand-pressable";
import { BrandTextInput } from "@/components/brand-text-input";
import { Brand, BrandFont } from "@/constants/theme";
import GoogleLogo from "@/assets/Logos/GoogleLogo.svg";
import LoginHero from "@/assets/images/LoginHero.png";
/**
* Frame `61-5` in the Figma ("Login"). The hero art in the Figma is replaced
* with a magenta gradient placeholder; the rest of the form — title, tabs,
* inputs, social row — is reproduced.
* Frames `252:281` (Log In) and `412:59` (Sign Up) in the Figma. The hero art
* in the Figma is replaced with a magenta gradient placeholder; the rest of
* the form — title, tabs, inputs, social row — is reproduced. Per the Figma,
* the social row has only Google and Phone buttons (no Apple / Facebook).
*/
export default function LoginScreen() {
const router = useRouter();
const insets = useSafeAreaInsets();
const [tab, setTab] = useState<'signup' | 'login'>('login');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [tab, setTab] = useState<"signup" | "login">("login");
const [email, setEmail] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
return (
<View style={[styles.root, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
<View style={styles.hero}>
<LinearGradient
colors={['#5a0a3a', '#3a0a25', Brand.bg]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={StyleSheet.absoluteFill}
/>
<HeroSilhouette />
</View>
<View
style={[
styles.root,
{ paddingTop: insets.top, paddingBottom: insets.bottom },
]}
>
<HeroSilhouette />
<View style={styles.body}>
<View style={styles.titleBlock}>
<Text style={styles.title}>Welcome back!</Text>
<Text style={styles.subtitle}>Log in to your account</Text>
</View>
<View style={styles.tabs}>
<Pressable onPress={() => setTab('signup')} style={styles.tab}>
<Pressable
onPress={() => setTab("signup")}
style={tab === "signup" ? styles.tabIndicator : styles.tab}
>
<Text
style={[
styles.tabLabel,
tab === 'signup' ? styles.tabLabelActive : styles.tabLabelInactive,
]}>
tab === "signup"
? styles.tabLabelActive
: styles.tabLabelInactive,
]}
>
Sign Up
</Text>
{tab === 'signup' ? <View style={styles.tabIndicator} /> : null}
{/*{tab === "signup" ? <View style={styles.tabIndicator} /> : null}*/}
</Pressable>
<Pressable onPress={() => setTab('login')} style={styles.tab}>
<Pressable
onPress={() => setTab("login")}
style={tab === "login" ? styles.tabIndicator : styles.tab}
>
<Text
style={[
styles.tabLabel,
tab === 'login' ? styles.tabLabelActive : styles.tabLabelInactive,
]}>
tab === "login"
? styles.tabLabelActive
: styles.tabLabelInactive,
]}
>
Log In
</Text>
{tab === 'login' ? <View style={styles.tabIndicator} /> : null}
{/*{tab === "login" ? <View style={styles.tabIndicator} /> : null}*/}
</Pressable>
</View>
<View style={styles.form}>
{tab === "signup" ? (
<Text style={styles.subtitle}>Username</Text>
) : null}
{tab === "signup" ? (
<BrandTextInput
placeholder="JohnDoe"
placeholderTextColor={Brand.textFaint}
autoCapitalize="none"
keyboardType="default"
value={username}
onChangeText={setUsername}
/>
) : null}
<Text style={styles.subtitle}>Email</Text>
<BrandTextInput
placeholder="Email"
placeholderTextColor={Brand.textFaint}
@@ -69,37 +93,37 @@ export default function LoginScreen() {
keyboardType="email-address"
value={email}
onChangeText={setEmail}
leadingIcon="envelope"
/>
<Text style={styles.subtitle}>Password</Text>
<BrandTextInput
placeholder="Password"
placeholderTextColor={Brand.textFaint}
secureToggle
value={password}
onChangeText={setPassword}
leadingIcon="lock"
/>
</View>
<Pressable style={styles.forgotRow}>
<Text style={styles.forgotText}>Forgot password?</Text>
</Pressable>
{tab === "login" ? (
<Pressable style={styles.forgotRow}>
<Text style={styles.forgotText}>Forgot password?</Text>
</Pressable>
) : null}
<BrandPressable
label={tab === 'login' ? 'Log In' : 'Create Account'}
onPress={() => router.push('/(auth)/select-games')}
label={tab === "login" ? "Log In" : "Create Account"}
onPress={() => router.push("/(auth)/select-games")}
/>
<View style={styles.dividerRow}>
<View style={styles.dividerLine} />
<Text style={styles.dividerText}>or continue with</Text>
<Text style={styles.dividerText}>Log In using</Text>
<View style={styles.dividerLine} />
</View>
<View style={styles.socialRow}>
<SocialButton label="Apple" />
<SocialButton label="Google" />
<SocialButton label="Facebook" />
<SocialButton kind="google" />
<SocialButton kind="phone" />
</View>
</View>
</View>
@@ -108,18 +132,34 @@ export default function LoginScreen() {
function HeroSilhouette() {
return (
<View style={silStyles.root}>
<View style={silStyles.ring} />
<View style={silStyles.disc} />
<View style={[silStyles.orb, silStyles.orbA]} />
<View style={[silStyles.orb, silStyles.orbB]} />
<View style={styles.hero}>
<Image source={LoginHero} style={styles.image} />
<Text style={styles.overlayText}>
Just one step away from finding your teammates
</Text>
</View>
);
}
function SocialButton({ label }: { label: string }) {
function SocialButton({ kind }: { kind: "google" | "phone" }) {
const label = kind === "google" ? "oogle" : "Phone";
return (
<Pressable accessibilityRole="button" style={({ pressed }) => [styles.socialButton, pressed && styles.socialPressed]}>
<Pressable
accessibilityRole="button"
style={({ pressed }) => [
styles.socialButton,
pressed && styles.socialPressed,
]}
>
{kind === "google" ? (
<Image source={GoogleLogo} style={styles.socialGoogle} />
) : (
<SymbolView
name={"phone" as Parameters<typeof SymbolView>[0]["name"]}
tintColor={Brand.text}
size={20}
/>
)}
<Text style={styles.socialLabel}>{label}</Text>
</Pressable>
);
@@ -132,9 +172,9 @@ const styles = StyleSheet.create({
},
hero: {
height: 220,
overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',
overflow: "hidden",
alignItems: "center",
justifyContent: "center",
},
body: {
flex: 1,
@@ -158,12 +198,19 @@ const styles = StyleSheet.create({
fontFamily: BrandFont.poppinsRegular,
},
tabs: {
flexDirection: 'row',
gap: 24,
paddingTop: 8,
flexDirection: "row",
alignItems: "center",
borderRadius: 8,
justifyContent: "center",
gap: 0,
backgroundColor: Brand.surface,
borderColor: Brand.border,
},
tab: {
paddingBottom: 8,
width: "50%",
alignItems: "center",
justifyContent: "center",
},
tabLabel: {
fontSize: 15,
@@ -177,20 +224,23 @@ const styles = StyleSheet.create({
color: Brand.textMuted,
},
tabIndicator: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 2,
marginVertical: 8,
alignSelf: "stretch",
marginHorizontal: "2%",
height: 52,
width: "46%",
borderRadius: 8,
alignItems: "center",
justifyContent: "center",
borderColor: Brand.border,
backgroundColor: Brand.accent,
borderRadius: 1,
},
form: {
gap: 12,
marginTop: 8,
},
forgotRow: {
alignSelf: 'flex-end',
alignSelf: "flex-end",
paddingVertical: 4,
},
forgotText: {
@@ -200,8 +250,8 @@ const styles = StyleSheet.create({
fontFamily: BrandFont.poppinsMedium,
},
dividerRow: {
flexDirection: 'row',
alignItems: 'center',
flexDirection: "row",
alignItems: "center",
gap: 12,
marginTop: 4,
},
@@ -217,67 +267,49 @@ const styles = StyleSheet.create({
fontFamily: BrandFont.poppinsRegular,
},
socialRow: {
flexDirection: 'row',
justifyContent: 'center',
gap: 16,
flexDirection: "row",
justifyContent: "center",
gap: 12,
marginTop: 4,
},
socialButton: {
width: 52,
flex: 1,
height: 52,
borderRadius: 26,
borderRadius: 8,
borderWidth: 1,
borderColor: Brand.border,
backgroundColor: Brand.surface,
alignItems: 'center',
justifyContent: 'center',
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 10,
},
socialPressed: {
opacity: 0.7,
},
socialLabel: {
color: Brand.text,
fontSize: 12,
lineHeight: 16,
fontSize: 14,
lineHeight: 18,
fontFamily: BrandFont.poppinsSemiBold,
},
});
const silStyles = StyleSheet.create({
root: {
width: 200,
height: 200,
alignItems: 'center',
justifyContent: 'center',
socialGoogle: {
width: 18,
height: 18,
},
ring: {
position: 'absolute',
width: 180,
height: 180,
borderRadius: 90,
borderWidth: 1.5,
borderColor: 'rgba(255,255,255,0.2)',
image: {
width: "100%",
height: "100%",
resizeMode: "cover",
},
disc: {
width: 110,
height: 110,
borderRadius: 55,
backgroundColor: 'rgba(255,255,255,0.06)',
},
orb: {
position: 'absolute',
width: 22,
height: 22,
borderRadius: 11,
backgroundColor: Brand.accent,
},
orbA: {
top: 30,
left: 50,
},
orbB: {
bottom: 40,
right: 40,
backgroundColor: '#f3c',
overlayText: {
fontSize: 26,
fontWeight: "bold",
fontFamily: "Poppins_600SemiBold",
position: "absolute",
left: 26,
top: "25%",
width: "50%",
color: "#fff",
},
});

View File

@@ -1,10 +1,9 @@
import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router';
import { StyleSheet, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useRouter } from "expo-router";
import { StyleSheet, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { BrandPressable } from '@/components/brand-pressable';
import { Brand, BrandFont } from '@/constants/theme';
import { BrandPressable } from "@/components/brand-pressable";
import { Brand, BrandFont } from "@/constants/theme";
/**
* Frame `52-54` in the Figma ("UI variation - 2"). The opening splash that
@@ -17,7 +16,12 @@ export default function OnboardingScreen() {
const insets = useSafeAreaInsets();
return (
<View style={[styles.root, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
<View
style={[
styles.root,
{ paddingTop: insets.top, paddingBottom: insets.bottom },
]}
>
<View style={styles.header}>
<View style={styles.logoMark}>
<Text style={styles.logoMarkText}>G</Text>
@@ -25,55 +29,29 @@ export default function OnboardingScreen() {
<Text style={styles.brandWordmark}>Gumble</Text>
</View>
<View style={styles.hero}>
<View style={styles.heroFrame}>
<LinearGradient
colors={['rgba(233,46,186,0.18)', 'rgba(255,255,255,0.02)']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={StyleSheet.absoluteFill}
/>
<GamePadIllustration />
</View>
</View>
<View style={styles.hero}></View>
<View style={styles.copy}>
<Text style={styles.title}>Gamble your games, gamble your skills</Text>
<Text style={styles.subtitle}>
Play mini-games, complete tasks, and earn rewards all powered by your favorite games.
<Text style={styles.title}>
Select games you play and find people with similar interests.
</Text>
</View>
<View style={styles.actions}>
<BrandPressable label="Sign Up" onPress={() => router.push('/(auth)/login')} />
<BrandPressable
label="Sign Up"
onPress={() => router.push("/(auth)/login")}
/>
<BrandPressable
label="Log In"
variant="outline"
onPress={() => router.push('/(auth)/login')}
onPress={() => router.push("/(auth)/login")}
/>
</View>
</View>
);
}
function GamePadIllustration() {
return (
<View style={illustStyles.root}>
<View style={illustStyles.pad}>
<View style={illustStyles.dpad}>
<View style={[illustStyles.dpadArm, illustStyles.dpadHorizontal]} />
<View style={[illustStyles.dpadArm, illustStyles.dpadVertical]} />
</View>
<View style={illustStyles.buttons}>
<View style={[illustStyles.button, { backgroundColor: '#e92eba' }]} />
<View style={[illustStyles.button, { backgroundColor: '#f3c' }]} />
</View>
<View style={illustStyles.stick} />
</View>
</View>
);
}
const styles = StyleSheet.create({
root: {
flex: 1,
@@ -81,8 +59,8 @@ const styles = StyleSheet.create({
paddingHorizontal: 24,
},
header: {
flexDirection: 'row',
alignItems: 'center',
flexDirection: "row",
alignItems: "center",
gap: 10,
paddingTop: 12,
},
@@ -91,8 +69,8 @@ const styles = StyleSheet.create({
height: 28,
borderRadius: 8,
backgroundColor: Brand.accent,
alignItems: 'center',
justifyContent: 'center',
alignItems: "center",
justifyContent: "center",
},
logoMarkText: {
color: Brand.text,
@@ -108,19 +86,19 @@ const styles = StyleSheet.create({
},
hero: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
justifyContent: "center",
alignItems: "center",
paddingVertical: 16,
},
heroFrame: {
width: '100%',
width: "100%",
aspectRatio: 1,
maxHeight: 320,
borderRadius: 16,
overflow: 'hidden',
overflow: "hidden",
backgroundColor: Brand.surfaceAlt,
alignItems: 'center',
justifyContent: 'center',
alignItems: "center",
justifyContent: "center",
},
copy: {
gap: 12,
@@ -131,14 +109,14 @@ const styles = StyleSheet.create({
fontSize: 26,
lineHeight: 32,
fontFamily: BrandFont.montserratBold,
textAlign: 'center',
textAlign: "center",
},
subtitle: {
color: Brand.textMuted,
fontSize: 14,
lineHeight: 20,
fontFamily: BrandFont.poppinsRegular,
textAlign: 'center',
textAlign: "center",
paddingHorizontal: 8,
},
actions: {
@@ -146,61 +124,3 @@ const styles = StyleSheet.create({
paddingBottom: 8,
},
});
const illustStyles = StyleSheet.create({
root: {
width: '70%',
aspectRatio: 1.2,
alignItems: 'center',
justifyContent: 'center',
},
pad: {
width: '100%',
height: '60%',
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 24,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.08)',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-around',
paddingHorizontal: 24,
},
dpad: {
width: 44,
height: 44,
alignItems: 'center',
justifyContent: 'center',
},
dpadArm: {
position: 'absolute',
backgroundColor: 'rgba(255,255,255,0.5)',
borderRadius: 3,
},
dpadHorizontal: {
width: 44,
height: 10,
},
dpadVertical: {
width: 10,
height: 44,
},
buttons: {
flexDirection: 'row',
gap: 12,
},
button: {
width: 18,
height: 18,
borderRadius: 9,
},
stick: {
position: 'absolute',
bottom: 14,
alignSelf: 'center',
width: 16,
height: 16,
borderRadius: 8,
backgroundColor: 'rgba(255,255,255,0.3)',
},
});

View File

@@ -1,117 +0,0 @@
import { useRouter } from 'expo-router';
import { useState } from 'react';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { BrandTextInput } from '@/components/brand-text-input';
import { GameTile } from '@/components/game-tile';
import { Brand, BrandFont } from '@/constants/theme';
import { useGameSearch } from '@/hooks/use-game-search';
/**
* Frames `24-101` (empty search + Most Popular), `29-3` ("Pubg|" + Most
* Popular), and `29-37` ("Pubg|" + Searched Results). All three are the same
* screen with different local search state; the section header swaps based on
* whether the query has results that aren't the popular set.
*/
export default function SearchGamesScreen() {
const router = useRouter();
const insets = useSafeAreaInsets();
const [query, setQuery] = useState('');
const results = useGameSearch(query);
const isEmpty = query.trim().length === 0;
const sectionTitle = isEmpty ? 'Most Popular' : 'Searched Results';
return (
<View style={[styles.root, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
<View style={styles.searchRow}>
<BrandTextInput
autoFocus
placeholder="Search games by name"
value={query}
onChangeText={setQuery}
leadingIcon="magnifyingglass"
returnKeyType="search"
autoCapitalize="none"
/>
</View>
<View style={styles.headerRow}>
<Text style={styles.sectionTitle}>{sectionTitle}</Text>
<Text onPress={() => router.back()} style={styles.cancel}>
Cancel
</Text>
</View>
<ScrollView
contentContainerStyle={styles.grid}
showsVerticalScrollIndicator={false}>
{results.length === 0 ? (
<Text style={styles.empty}>No games match {query}.</Text>
) : (
results.map((game) => (
<GameTile
key={game.id}
id={game.id}
name={game.name}
genre={game.genre}
gradient={game.gradient}
onPress={() => {
// Demo: a tap on a result just clears the query so the user
// can try another search. Real apps would add the game.
setQuery('');
}}
/>
))
)}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
root: {
flex: 1,
backgroundColor: Brand.bg,
paddingHorizontal: 24,
},
searchRow: {
paddingTop: 8,
paddingBottom: 12,
},
headerRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingBottom: 12,
},
sectionTitle: {
color: Brand.text,
fontSize: 18,
lineHeight: 24,
fontFamily: BrandFont.poppinsSemiBold,
},
cancel: {
color: Brand.textMuted,
fontSize: 13,
lineHeight: 18,
fontFamily: BrandFont.poppinsMedium,
padding: 4,
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
paddingBottom: 24,
rowGap: 16,
},
empty: {
color: Brand.textMuted,
fontSize: 14,
lineHeight: 20,
fontFamily: BrandFont.poppinsRegular,
paddingTop: 32,
textAlign: 'center',
alignSelf: 'center',
},
});

View File

@@ -4,20 +4,28 @@ import { ScrollView, StyleSheet, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { BrandPressable } from '@/components/brand-pressable';
import { BrandTextInput } from '@/components/brand-text-input';
import { GameTile } from '@/components/game-tile';
import { Brand, BrandFont } from '@/constants/theme';
import { GAMES } from '@/data/games';
import { filterGames } from '@/hooks/use-game-search';
/**
* Frames `20-21` (initial, nothing selected) and `24-46` (2 games selected).
* The two Figma frames are the same screen with different local state, so we
* model them as a single screen with a `Set<string>` of selected ids.
* Frame `20-21` in the Figma. A single screen that combines the "search" and
* "select" flows: the search input filters the grid below it, and the section
* header swaps between "Most Popular" (empty query) and "Searched Results"
* (non-empty query). Tapping a tile toggles its selection; the Continue
* button at the bottom is disabled until at least one game is selected.
*/
export default function SelectGamesScreen() {
const router = useRouter();
const insets = useSafeAreaInsets();
const [query, setQuery] = useState('');
const [selected, setSelected] = useState<Set<string>>(new Set());
const isEmpty = query.trim().length === 0;
const results = filterGames(query);
const sectionTitle = isEmpty ? 'Most Popular' : 'Searched Results';
const toggle = useCallback((id: string) => {
setSelected((prev) => {
const next = new Set(prev);
@@ -34,38 +42,45 @@ export default function SelectGamesScreen() {
<View style={[styles.root, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
<View style={styles.header}>
<Text style={styles.title}>Which games do you play?</Text>
<Text style={styles.subtitle}>
Pick the games you play the most to personalize your experience.
</Text>
<BrandTextInput
placeholder="Search for games..."
placeholderTextColor={Brand.textFaint}
value={query}
onChangeText={setQuery}
leadingIcon="magnifyingglass"
returnKeyType="search"
autoCapitalize="none"
/>
</View>
<Text style={styles.sectionTitle}>{sectionTitle}</Text>
<ScrollView
contentContainerStyle={styles.grid}
showsVerticalScrollIndicator={false}>
{GAMES.map((game) => (
<GameTile
key={game.id}
id={game.id}
name={game.name}
genre={game.genre}
gradient={game.gradient}
selected={selected.has(game.id)}
onPress={() => toggle(game.id)}
/>
))}
{results.length === 0 ? (
<Text style={styles.empty}>No games match {query}.</Text>
) : (
results.map((game) => (
<GameTile
key={game.id}
id={game.id}
name={game.name}
genre={game.genre}
gradient={game.gradient}
selected={selected.has(game.id)}
onPress={() => toggle(game.id)}
/>
))
)}
</ScrollView>
<View style={styles.footer}>
<BrandPressable
label={`Continue${selected.size > 0 ? ` (${selected.size})` : ''}`}
disabled={selected.size === 0}
onPress={() => router.push('/(auth)/search-games')}
onPress={() => router.push('/explore')}
/>
<Text
onPress={() => router.push('/(auth)/search-games')}
style={styles.link}>
Search for a game
</Text>
</View>
</View>
);
@@ -78,9 +93,9 @@ const styles = StyleSheet.create({
paddingHorizontal: 24,
},
header: {
paddingTop: 16,
paddingTop: 32,
paddingBottom: 16,
gap: 6,
gap: 20,
},
title: {
color: Brand.text,
@@ -88,29 +103,31 @@ const styles = StyleSheet.create({
lineHeight: 28,
fontFamily: BrandFont.montserratBold,
},
subtitle: {
color: Brand.textMuted,
fontSize: 13,
lineHeight: 18,
fontFamily: BrandFont.poppinsRegular,
sectionTitle: {
color: Brand.text,
fontSize: 15,
lineHeight: 20,
fontFamily: BrandFont.poppinsSemiBold,
paddingTop: 4,
paddingBottom: 8,
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
paddingVertical: 8,
paddingBottom: 16,
rowGap: 16,
},
footer: {
paddingTop: 16,
gap: 8,
alignItems: 'center',
paddingTop: 12,
},
link: {
color: Brand.accent,
fontSize: 13,
lineHeight: 18,
fontFamily: BrandFont.poppinsMedium,
paddingVertical: 6,
empty: {
color: Brand.textMuted,
fontSize: 14,
lineHeight: 20,
fontFamily: BrandFont.poppinsRegular,
paddingTop: 32,
textAlign: 'center',
alignSelf: 'center',
},
});

View File

@@ -1,9 +1,15 @@
import { LinearGradient } from 'expo-linear-gradient';
import { Pressable, StyleSheet, Text, type StyleProp, type ViewStyle } from 'react-native';
import { LinearGradient } from "expo-linear-gradient";
import {
Pressable,
StyleSheet,
Text,
type StyleProp,
type ViewStyle,
} from "react-native";
import { Brand, BrandFont } from '@/constants/theme';
import { Brand, BrandFont } from "@/constants/theme";
type Variant = 'solid' | 'outline';
type Variant = "solid" | "outline";
export type BrandPressableProps = {
label: string;
@@ -22,11 +28,11 @@ export type BrandPressableProps = {
export function BrandPressable({
label,
onPress,
variant = 'solid',
variant = "solid",
disabled = false,
style,
}: BrandPressableProps) {
if (variant === 'outline') {
if (variant === "outline") {
return (
<Pressable
accessibilityRole="button"
@@ -39,7 +45,8 @@ export function BrandPressable({
disabled && styles.disabled,
pressed && styles.pressed,
style,
]}>
]}
>
<Text style={[styles.label, styles.outlineLabel]}>{label}</Text>
</Pressable>
);
@@ -56,12 +63,14 @@ export function BrandPressable({
disabled && styles.disabled,
pressed && styles.pressed,
style,
]}>
]}
>
<LinearGradient
colors={[...Brand.gradient]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.gradient}>
style={styles.gradient}
>
<Text style={[styles.label, styles.solidLabel]}>{label}</Text>
</LinearGradient>
</Pressable>
@@ -71,17 +80,17 @@ export function BrandPressable({
const styles = StyleSheet.create({
base: {
height: 55,
borderRadius: 8,
overflow: 'hidden',
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'stretch',
borderRadius: 28,
overflow: "hidden",
justifyContent: "center",
alignItems: "center",
alignSelf: "stretch",
},
gradient: {
flex: 1,
width: '100%',
justifyContent: 'center',
alignItems: 'center',
width: "100%",
justifyContent: "center",
alignItems: "center",
},
label: {
fontSize: 16,
@@ -92,7 +101,7 @@ const styles = StyleSheet.create({
color: Brand.text,
},
outline: {
backgroundColor: 'transparent',
backgroundColor: "transparent",
borderWidth: 1,
borderColor: Brand.border,
},

View File

@@ -1,16 +1,31 @@
import { SymbolView } from 'expo-symbols';
import { useState } from 'react';
import { SymbolView, type SymbolViewProps } from "expo-symbols";
import { useState } from "react";
import {
Pressable,
StyleSheet,
TextInput,
View,
type TextInputProps,
} from 'react-native';
} from "react-native";
import { Brand, BrandFont } from '@/constants/theme';
import { Brand, BrandFont } from "@/constants/theme";
export type BrandTextInputProps = Omit<TextInputProps, 'style'> & {
// Cross-platform symbol names so the icon renders on iOS, Android, and web.
// SF Symbols are iOS-only; Material symbols cover Android and web.
const ICON_NAME: Record<
"leading" | "eye" | "eyeSlash",
SymbolViewProps["name"]
> = {
leading: { ios: "envelope", android: "mail", web: "mail" },
eye: { ios: "eye", android: "visibility", web: "visibility" },
eyeSlash: {
ios: "eye.slash",
android: "visibility_off",
web: "visibility_off",
},
};
export type BrandTextInputProps = Omit<TextInputProps, "style"> & {
/** SF Symbol name to render on the leading edge. */
leadingIcon?: string;
/** When true, renders a show/hide eye toggle on the right edge. */
@@ -38,7 +53,7 @@ export function BrandTextInput({
<View style={styles.inputRow}>
{leadingIcon ? (
<SymbolView
name={leadingIcon as Parameters<typeof SymbolView>[0]['name']}
name={ICON_NAME.leading}
tintColor={Brand.textMuted}
size={18}
style={styles.leadingIcon}
@@ -54,17 +69,14 @@ export function BrandTextInput({
{secureToggle ? (
<Pressable
accessibilityRole="button"
accessibilityLabel={hidden ? 'Show password' : 'Hide password'}
accessibilityLabel={hidden ? "Show password" : "Hide password"}
hitSlop={8}
onPress={() => setHidden((v) => !v)}
style={styles.trailingIcon}>
style={styles.trailingIcon}
>
<SymbolView
name={
(hidden ? 'eye' : 'eye.slash') as Parameters<
typeof SymbolView
>[0]['name']
}
tintColor={Brand.textMuted}
name={hidden ? ICON_NAME.eye : ICON_NAME.eyeSlash}
tintColor={Brand.text}
size={18}
/>
</Pressable>
@@ -77,22 +89,24 @@ export function BrandTextInput({
const styles = StyleSheet.create({
wrapper: {
alignSelf: 'stretch',
alignSelf: "stretch",
},
inputRow: {
height: 55,
height: 45,
borderRadius: 10,
backgroundColor: Brand.inputBg,
borderWidth: 1,
borderColor: Brand.border,
paddingHorizontal: 16,
flexDirection: 'row',
alignItems: 'center',
flexDirection: "row",
alignItems: "center",
},
leadingIcon: {
marginRight: 12,
},
trailingIcon: {
height: 24,
width: 24,
marginLeft: 8,
padding: 4,
},
@@ -100,7 +114,6 @@ const styles = StyleSheet.create({
flex: 1,
color: Brand.text,
fontSize: 15,
lineHeight: 20,
fontFamily: BrandFont.poppinsRegular,
padding: 0,
},