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

@@ -0,0 +1,6 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.99992 4.90906V7.2327H9.229C9.08721 7.97997 8.6617 8.61273 8.02353 9.03817L9.97079 10.5491C11.1053 9.50186 11.7599 7.96366 11.7599 6.13639C11.7599 5.71094 11.7217 5.30181 11.6508 4.90912L5.99992 4.90906Z" fill="#4285F4"/>
<path d="M2.63734 7.14203L2.19816 7.47822L0.643588 8.68911C1.63086 10.6473 3.65435 12 5.99979 12C7.61975 12 8.97792 11.4655 9.97067 10.5491L8.0234 9.03821C7.48885 9.39821 6.80703 9.61641 5.99979 9.61641C4.4398 9.61641 3.11438 8.56369 2.6398 7.14549L2.63734 7.14203Z" fill="#34A853"/>
<path d="M0.643591 3.31091C0.234521 4.11815 0 5.02908 0 5.99997C0 6.97086 0.234521 7.88179 0.643591 8.68903C0.643591 8.69445 2.63999 7.13994 2.63999 7.13994C2.51999 6.77994 2.44906 6.39814 2.44906 5.99991C2.44906 5.60167 2.51999 5.21988 2.63999 4.85988L0.643591 3.31091Z" fill="#FBBC05"/>
<path d="M5.99991 2.38909C6.88356 2.38909 7.669 2.69454 8.29628 3.28364L10.0144 1.56548C8.97262 0.594583 7.61994 0 5.99991 0C3.65447 0 1.63086 1.34727 0.643588 3.31092L2.63993 4.86001C3.11444 3.44181 4.43992 2.38909 5.99991 2.38909Z" fill="#EA4335"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/images/LoginHero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

10
eslint.config.js Normal file
View File

@@ -0,0 +1,10 @@
// https://docs.expo.dev/guides/using-eslint/
const { defineConfig } = require('eslint/config');
const expoConfig = require("eslint-config-expo/flat");
module.exports = defineConfig([
expoConfig,
{
ignores: ["dist/*"],
}
]);

3985
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -32,6 +32,8 @@
}, },
"devDependencies": { "devDependencies": {
"@types/react": "~19.2.2", "@types/react": "~19.2.2",
"eslint": "^9.0.0",
"eslint-config-expo": "~56.0.4",
"typescript": "~6.0.3" "typescript": "~6.0.3"
}, },
"scripts": { "scripts": {

View File

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

View File

@@ -1,10 +1,9 @@
import { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from "expo-router";
import { useRouter } from 'expo-router'; import { StyleSheet, Text, View } from "react-native";
import { StyleSheet, Text, View } from 'react-native'; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { BrandPressable } from '@/components/brand-pressable'; import { BrandPressable } from "@/components/brand-pressable";
import { Brand, BrandFont } from '@/constants/theme'; import { Brand, BrandFont } from "@/constants/theme";
/** /**
* Frame `52-54` in the Figma ("UI variation - 2"). The opening splash that * Frame `52-54` in the Figma ("UI variation - 2"). The opening splash that
@@ -17,7 +16,12 @@ export default function OnboardingScreen() {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
return ( 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.header}>
<View style={styles.logoMark}> <View style={styles.logoMark}>
<Text style={styles.logoMarkText}>G</Text> <Text style={styles.logoMarkText}>G</Text>
@@ -25,55 +29,29 @@ export default function OnboardingScreen() {
<Text style={styles.brandWordmark}>Gumble</Text> <Text style={styles.brandWordmark}>Gumble</Text>
</View> </View>
<View style={styles.hero}> <View style={styles.hero}></View>
<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.copy}> <View style={styles.copy}>
<Text style={styles.title}>Gamble your games, gamble your skills</Text> <Text style={styles.title}>
<Text style={styles.subtitle}> Select games you play and find people with similar interests.
Play mini-games, complete tasks, and earn rewards all powered by your favorite games.
</Text> </Text>
</View> </View>
<View style={styles.actions}> <View style={styles.actions}>
<BrandPressable label="Sign Up" onPress={() => router.push('/(auth)/login')} /> <BrandPressable
label="Sign Up"
onPress={() => router.push("/(auth)/login")}
/>
<BrandPressable <BrandPressable
label="Log In" label="Log In"
variant="outline" variant="outline"
onPress={() => router.push('/(auth)/login')} onPress={() => router.push("/(auth)/login")}
/> />
</View> </View>
</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({ const styles = StyleSheet.create({
root: { root: {
flex: 1, flex: 1,
@@ -81,8 +59,8 @@ const styles = StyleSheet.create({
paddingHorizontal: 24, paddingHorizontal: 24,
}, },
header: { header: {
flexDirection: 'row', flexDirection: "row",
alignItems: 'center', alignItems: "center",
gap: 10, gap: 10,
paddingTop: 12, paddingTop: 12,
}, },
@@ -91,8 +69,8 @@ const styles = StyleSheet.create({
height: 28, height: 28,
borderRadius: 8, borderRadius: 8,
backgroundColor: Brand.accent, backgroundColor: Brand.accent,
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
}, },
logoMarkText: { logoMarkText: {
color: Brand.text, color: Brand.text,
@@ -108,19 +86,19 @@ const styles = StyleSheet.create({
}, },
hero: { hero: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: "center",
alignItems: 'center', alignItems: "center",
paddingVertical: 16, paddingVertical: 16,
}, },
heroFrame: { heroFrame: {
width: '100%', width: "100%",
aspectRatio: 1, aspectRatio: 1,
maxHeight: 320, maxHeight: 320,
borderRadius: 16, borderRadius: 16,
overflow: 'hidden', overflow: "hidden",
backgroundColor: Brand.surfaceAlt, backgroundColor: Brand.surfaceAlt,
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
}, },
copy: { copy: {
gap: 12, gap: 12,
@@ -131,14 +109,14 @@ const styles = StyleSheet.create({
fontSize: 26, fontSize: 26,
lineHeight: 32, lineHeight: 32,
fontFamily: BrandFont.montserratBold, fontFamily: BrandFont.montserratBold,
textAlign: 'center', textAlign: "center",
}, },
subtitle: { subtitle: {
color: Brand.textMuted, color: Brand.textMuted,
fontSize: 14, fontSize: 14,
lineHeight: 20, lineHeight: 20,
fontFamily: BrandFont.poppinsRegular, fontFamily: BrandFont.poppinsRegular,
textAlign: 'center', textAlign: "center",
paddingHorizontal: 8, paddingHorizontal: 8,
}, },
actions: { actions: {
@@ -146,61 +124,3 @@ const styles = StyleSheet.create({
paddingBottom: 8, 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 { useSafeAreaInsets } from 'react-native-safe-area-context';
import { BrandPressable } from '@/components/brand-pressable'; import { BrandPressable } from '@/components/brand-pressable';
import { BrandTextInput } from '@/components/brand-text-input';
import { GameTile } from '@/components/game-tile'; import { GameTile } from '@/components/game-tile';
import { Brand, BrandFont } from '@/constants/theme'; 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). * Frame `20-21` in the Figma. A single screen that combines the "search" and
* The two Figma frames are the same screen with different local state, so we * "select" flows: the search input filters the grid below it, and the section
* model them as a single screen with a `Set<string>` of selected ids. * 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() { export default function SelectGamesScreen() {
const router = useRouter(); const router = useRouter();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const [query, setQuery] = useState('');
const [selected, setSelected] = useState<Set<string>>(new Set()); 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) => { const toggle = useCallback((id: string) => {
setSelected((prev) => { setSelected((prev) => {
const next = new Set(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.root, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
<View style={styles.header}> <View style={styles.header}>
<Text style={styles.title}>Which games do you play?</Text> <Text style={styles.title}>Which games do you play?</Text>
<Text style={styles.subtitle}> <BrandTextInput
Pick the games you play the most to personalize your experience. placeholder="Search for games..."
</Text> placeholderTextColor={Brand.textFaint}
value={query}
onChangeText={setQuery}
leadingIcon="magnifyingglass"
returnKeyType="search"
autoCapitalize="none"
/>
</View> </View>
<Text style={styles.sectionTitle}>{sectionTitle}</Text>
<ScrollView <ScrollView
contentContainerStyle={styles.grid} contentContainerStyle={styles.grid}
showsVerticalScrollIndicator={false}> showsVerticalScrollIndicator={false}>
{GAMES.map((game) => ( {results.length === 0 ? (
<GameTile <Text style={styles.empty}>No games match {query}.</Text>
key={game.id} ) : (
id={game.id} results.map((game) => (
name={game.name} <GameTile
genre={game.genre} key={game.id}
gradient={game.gradient} id={game.id}
selected={selected.has(game.id)} name={game.name}
onPress={() => toggle(game.id)} genre={game.genre}
/> gradient={game.gradient}
))} selected={selected.has(game.id)}
onPress={() => toggle(game.id)}
/>
))
)}
</ScrollView> </ScrollView>
<View style={styles.footer}> <View style={styles.footer}>
<BrandPressable <BrandPressable
label={`Continue${selected.size > 0 ? ` (${selected.size})` : ''}`} label={`Continue${selected.size > 0 ? ` (${selected.size})` : ''}`}
disabled={selected.size === 0} 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>
</View> </View>
); );
@@ -78,9 +93,9 @@ const styles = StyleSheet.create({
paddingHorizontal: 24, paddingHorizontal: 24,
}, },
header: { header: {
paddingTop: 16, paddingTop: 32,
paddingBottom: 16, paddingBottom: 16,
gap: 6, gap: 20,
}, },
title: { title: {
color: Brand.text, color: Brand.text,
@@ -88,29 +103,31 @@ const styles = StyleSheet.create({
lineHeight: 28, lineHeight: 28,
fontFamily: BrandFont.montserratBold, fontFamily: BrandFont.montserratBold,
}, },
subtitle: { sectionTitle: {
color: Brand.textMuted, color: Brand.text,
fontSize: 13, fontSize: 15,
lineHeight: 18, lineHeight: 20,
fontFamily: BrandFont.poppinsRegular, fontFamily: BrandFont.poppinsSemiBold,
paddingTop: 4,
paddingBottom: 8,
}, },
grid: { grid: {
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', flexWrap: 'wrap',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingVertical: 8, paddingBottom: 16,
rowGap: 16, rowGap: 16,
}, },
footer: { footer: {
paddingTop: 16, paddingTop: 12,
gap: 8,
alignItems: 'center',
}, },
link: { empty: {
color: Brand.accent, color: Brand.textMuted,
fontSize: 13, fontSize: 14,
lineHeight: 18, lineHeight: 20,
fontFamily: BrandFont.poppinsMedium, fontFamily: BrandFont.poppinsRegular,
paddingVertical: 6, paddingTop: 32,
textAlign: 'center',
alignSelf: 'center',
}, },
}); });

View File

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

View File

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