fixed login and signup page.
This commit is contained in:
6
assets/Logos/GoogleLogo.svg
Normal file
6
assets/Logos/GoogleLogo.svg
Normal 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
BIN
assets/images/LoginHero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
10
eslint.config.js
Normal file
10
eslint.config.js
Normal 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
3985
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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": {
|
||||||
|
|||||||
@@ -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',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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)',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -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 { 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',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user