Feat: add reusable PhoneInput component without flags
New Components: - PhoneInputCustom: Reusable phone input without emoji flags - constants/countries.ts: Country data with calling codes only Features: ✅ NO emoji flags - only country name + calling code (+62, +65, etc) ✅ Clean, professional UI ✅ Modal country picker with search ✅ 15 countries supported ✅ Helper functions: getCountryByCallingCode, getCountryByCode, searchCountries ✅ Fully typed with TypeScript ✅ Reusable across the app ✅ Maximum compatibility (no emoji rendering issues) UI Design: - Phone Input: [+62 ⌄ | 812-3456-7890] - Country Picker: Modal with search - Display: Country name + calling code only Usage: import { PhoneInputCustom } from '@/components'; import { DEFAULT_COUNTRY } from '@/constants/countries'; <PhoneInputCustom value={phoneNumber} onChangePhoneNumber={setPhoneNumber} selectedCountry={selectedCountry} onChangeCountry={setSelectedCountry} /> Benefits: ✅ Works on ALL iOS versions (no emoji issues) ✅ Consistent across all platforms ✅ Faster render (no emoji/image loading) ✅ Cleaner code structure ✅ Easy to maintain Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
7
.qwen/settings.json
Normal file
7
.qwen/settings.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(git add *)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
259
components/PhoneInput/PhoneInputCustom.tsx
Normal file
259
components/PhoneInput/PhoneInputCustom.tsx
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import {
|
||||||
|
COUNTRIES,
|
||||||
|
DEFAULT_COUNTRY,
|
||||||
|
searchCountries,
|
||||||
|
type CountryData,
|
||||||
|
} from "@/constants/countries";
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
|
interface PhoneInputProps {
|
||||||
|
value: string;
|
||||||
|
onChangePhoneNumber: (phone: string) => void;
|
||||||
|
selectedCountry?: CountryData;
|
||||||
|
onChangeCountry: (country: CountryData) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PhoneInputCustom({
|
||||||
|
value,
|
||||||
|
onChangePhoneNumber,
|
||||||
|
selectedCountry = DEFAULT_COUNTRY,
|
||||||
|
onChangeCountry,
|
||||||
|
placeholder = "Masukkan nomor",
|
||||||
|
disabled = false,
|
||||||
|
}: PhoneInputProps) {
|
||||||
|
const [countryPickerVisible, setCountryPickerVisible] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
const filteredCountries = searchCountries(searchQuery);
|
||||||
|
|
||||||
|
const handleSelectCountry = (country: CountryData) => {
|
||||||
|
onChangeCountry(country);
|
||||||
|
setCountryPickerVisible(false);
|
||||||
|
setSearchQuery("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePhoneChange = (text: string) => {
|
||||||
|
// Only allow numbers and spaces
|
||||||
|
const cleaned = text.replace(/[^\d\s]/g, "");
|
||||||
|
onChangePhoneNumber(cleaned);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Phone Input Field */}
|
||||||
|
<View style={styles.container}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.countryPickerButton}
|
||||||
|
onPress={() => setCountryPickerVisible(true)}
|
||||||
|
disabled={disabled}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={styles.countryCodeText}>+{selectedCountry.callingCode}</Text>
|
||||||
|
<Text style={styles.dropdownIcon}>⌄</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.divider} />
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
style={[styles.phoneInput, disabled && styles.disabledInput]}
|
||||||
|
placeholder={placeholder}
|
||||||
|
placeholderTextColor={MainColor.placeholder}
|
||||||
|
value={value}
|
||||||
|
onChangeText={handlePhoneChange}
|
||||||
|
keyboardType="phone-pad"
|
||||||
|
returnKeyType="done"
|
||||||
|
autoComplete="tel"
|
||||||
|
importantForAutofill="yes"
|
||||||
|
editable={!disabled}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Country Picker Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={countryPickerVisible}
|
||||||
|
transparent
|
||||||
|
animationType="slide"
|
||||||
|
onRequestClose={() => setCountryPickerVisible(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<View style={styles.modalContent}>
|
||||||
|
<View style={styles.modalHeader}>
|
||||||
|
<Text style={styles.modalTitle}>Pilih Negara</Text>
|
||||||
|
<TouchableOpacity onPress={() => setCountryPickerVisible(false)}>
|
||||||
|
<Text style={styles.modalClose}>✕</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
style={styles.searchInput}
|
||||||
|
placeholder="Cari negara atau kode..."
|
||||||
|
placeholderTextColor={MainColor.placeholder}
|
||||||
|
value={searchQuery}
|
||||||
|
onChangeText={setSearchQuery}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollView style={styles.countryList}>
|
||||||
|
{filteredCountries.map((country) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={country.code}
|
||||||
|
style={[
|
||||||
|
styles.countryItem,
|
||||||
|
selectedCountry.code === country.code &&
|
||||||
|
styles.countryItemSelected,
|
||||||
|
]}
|
||||||
|
onPress={() => handleSelectCountry(country)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<View style={styles.countryInfo}>
|
||||||
|
<Text style={styles.countryName}>{country.name}</Text>
|
||||||
|
<Text style={styles.countryCode}>+{country.callingCode}</Text>
|
||||||
|
</View>
|
||||||
|
{selectedCountry.code === country.code && (
|
||||||
|
<Text style={styles.checkmark}>✓</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
// Container
|
||||||
|
container: {
|
||||||
|
flexDirection: "row",
|
||||||
|
backgroundColor: MainColor.white,
|
||||||
|
borderRadius: 8,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: MainColor.white_gray,
|
||||||
|
marginBottom: 16,
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
// Country Picker Button
|
||||||
|
countryPickerButton: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 14,
|
||||||
|
backgroundColor: MainColor.text_input,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: MainColor.white_gray,
|
||||||
|
},
|
||||||
|
countryCodeText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: MainColor.black,
|
||||||
|
fontWeight: "600",
|
||||||
|
},
|
||||||
|
dropdownIcon: {
|
||||||
|
fontSize: 18,
|
||||||
|
color: MainColor.placeholder,
|
||||||
|
marginLeft: 4,
|
||||||
|
},
|
||||||
|
// Divider
|
||||||
|
divider: {
|
||||||
|
width: 1,
|
||||||
|
backgroundColor: MainColor.white_gray,
|
||||||
|
},
|
||||||
|
// Phone Input
|
||||||
|
phoneInput: {
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: 14,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
fontSize: 16,
|
||||||
|
color: MainColor.black,
|
||||||
|
},
|
||||||
|
disabledInput: {
|
||||||
|
backgroundColor: MainColor.text_input,
|
||||||
|
color: MainColor.placeholder,
|
||||||
|
},
|
||||||
|
// Modal
|
||||||
|
modalOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
},
|
||||||
|
modalContent: {
|
||||||
|
backgroundColor: MainColor.white,
|
||||||
|
borderTopLeftRadius: 20,
|
||||||
|
borderTopRightRadius: 20,
|
||||||
|
maxHeight: "80%",
|
||||||
|
paddingBottom: 34,
|
||||||
|
},
|
||||||
|
modalHeader: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: 20,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: MainColor.white_gray,
|
||||||
|
},
|
||||||
|
modalTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: MainColor.black,
|
||||||
|
},
|
||||||
|
modalClose: {
|
||||||
|
fontSize: 24,
|
||||||
|
color: MainColor.placeholder,
|
||||||
|
padding: 5,
|
||||||
|
},
|
||||||
|
// Search Input
|
||||||
|
searchInput: {
|
||||||
|
backgroundColor: MainColor.text_input,
|
||||||
|
margin: 16,
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
fontSize: 16,
|
||||||
|
color: MainColor.black,
|
||||||
|
},
|
||||||
|
// Country List
|
||||||
|
countryList: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
},
|
||||||
|
countryItem: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: MainColor.white_gray,
|
||||||
|
},
|
||||||
|
countryItemSelected: {
|
||||||
|
backgroundColor: MainColor.soft_darkblue + "15",
|
||||||
|
},
|
||||||
|
countryInfo: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
countryName: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: MainColor.black,
|
||||||
|
fontWeight: "500",
|
||||||
|
},
|
||||||
|
countryCode: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: MainColor.placeholder,
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
checkmark: {
|
||||||
|
fontSize: 20,
|
||||||
|
color: MainColor.green,
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -49,6 +49,8 @@ import MapCustom from "./Map/MapCustom";
|
|||||||
import CenterCustom from "./Center/CenterCustom";
|
import CenterCustom from "./Center/CenterCustom";
|
||||||
// Clickable
|
// Clickable
|
||||||
import ClickableCustom from "./Clickable/ClickableCustom";
|
import ClickableCustom from "./Clickable/ClickableCustom";
|
||||||
|
// PhoneInput
|
||||||
|
import PhoneInputCustom from "./PhoneInput/PhoneInputCustom";
|
||||||
// Scroll
|
// Scroll
|
||||||
import ScrollableCustom from "./Scroll/ScrollCustom";
|
import ScrollableCustom from "./Scroll/ScrollCustom";
|
||||||
// ShareComponent
|
// ShareComponent
|
||||||
@@ -95,6 +97,8 @@ export {
|
|||||||
CheckboxGroup,
|
CheckboxGroup,
|
||||||
// Clickable
|
// Clickable
|
||||||
ClickableCustom,
|
ClickableCustom,
|
||||||
|
// PhoneInput
|
||||||
|
PhoneInputCustom,
|
||||||
// Container
|
// Container
|
||||||
CircleContainer,
|
CircleContainer,
|
||||||
// Divider
|
// Divider
|
||||||
|
|||||||
89
constants/countries.ts
Normal file
89
constants/countries.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { type CountryCode } from "libphonenumber-js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Country data for phone number input
|
||||||
|
* Contains only country name and calling code (NO flags for maximum compatibility)
|
||||||
|
*/
|
||||||
|
export interface CountryData {
|
||||||
|
code: CountryCode;
|
||||||
|
name: string;
|
||||||
|
callingCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of supported countries for phone number input
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* This list includes major countries across different regions.
|
||||||
|
* Countries are ordered by likelihood of use (Indonesia first as default).
|
||||||
|
*
|
||||||
|
* @note
|
||||||
|
* NO emoji flags used - only text-based country name and calling code
|
||||||
|
* This ensures maximum compatibility across all platforms and iOS versions
|
||||||
|
*/
|
||||||
|
export const COUNTRIES: CountryData[] = [
|
||||||
|
// Asia Pacific (Primary markets)
|
||||||
|
{ code: "ID", name: "Indonesia", callingCode: "62" },
|
||||||
|
{ code: "SG", name: "Singapore", callingCode: "65" },
|
||||||
|
{ code: "MY", name: "Malaysia", callingCode: "60" },
|
||||||
|
{ code: "AU", name: "Australia", callingCode: "61" },
|
||||||
|
|
||||||
|
// Asia (Other)
|
||||||
|
{ code: "CN", name: "China", callingCode: "86" },
|
||||||
|
{ code: "JP", name: "Japan", callingCode: "81" },
|
||||||
|
{ code: "KR", name: "South Korea", callingCode: "82" },
|
||||||
|
{ code: "IN", name: "India", callingCode: "91" },
|
||||||
|
|
||||||
|
// Middle East
|
||||||
|
{ code: "AE", name: "United Arab Emirates", callingCode: "971" },
|
||||||
|
{ code: "SA", name: "Saudi Arabia", callingCode: "966" },
|
||||||
|
|
||||||
|
// Europe
|
||||||
|
{ code: "GB", name: "United Kingdom", callingCode: "44" },
|
||||||
|
{ code: "DE", name: "Germany", callingCode: "49" },
|
||||||
|
{ code: "FR", name: "France", callingCode: "33" },
|
||||||
|
{ code: "NL", name: "Netherlands", callingCode: "31" },
|
||||||
|
|
||||||
|
// Americas
|
||||||
|
{ code: "US", name: "United States", callingCode: "1" },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default country for phone number input
|
||||||
|
* Used when no country is selected (Indonesia by default)
|
||||||
|
*/
|
||||||
|
export const DEFAULT_COUNTRY: CountryData = COUNTRIES[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get country by calling code
|
||||||
|
* @param callingCode - The calling code to search for (e.g., "62", "1")
|
||||||
|
* @returns The matching country data or undefined if not found
|
||||||
|
*/
|
||||||
|
export function getCountryByCallingCode(callingCode: string): CountryData | undefined {
|
||||||
|
return COUNTRIES.find((country) => country.callingCode === callingCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get country by country code (ISO 3166-1 alpha-2)
|
||||||
|
* @param code - The country code to search for (e.g., "ID", "US")
|
||||||
|
* @returns The matching country data or undefined if not found
|
||||||
|
*/
|
||||||
|
export function getCountryByCode(code: CountryCode): CountryData | undefined {
|
||||||
|
return COUNTRIES.find((country) => country.code === code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search countries by name or calling code
|
||||||
|
* @param query - The search query (case-insensitive)
|
||||||
|
* @returns Array of matching countries
|
||||||
|
*/
|
||||||
|
export function searchCountries(query: string): CountryData[] {
|
||||||
|
const normalizedQuery = query.toLowerCase().trim();
|
||||||
|
|
||||||
|
return COUNTRIES.filter(
|
||||||
|
(country) =>
|
||||||
|
country.name.toLowerCase().includes(normalizedQuery) ||
|
||||||
|
country.code.toLowerCase().includes(normalizedQuery) ||
|
||||||
|
country.callingCode.includes(normalizedQuery)
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user