fixed login and signup page.
This commit is contained in:
@@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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)',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user