From 0d2fef18781b6212796603af45b4a3949b4b3373 Mon Sep 17 00:00:00 2001 From: bagasbanuna Date: Thu, 26 Mar 2026 11:27:59 +0800 Subject: [PATCH] Feat: add reusable PhoneInput component without flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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'; 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/settings.json | 7 + components/PhoneInput/PhoneInputCustom.tsx | 259 +++++++++++++++++++++ components/index.ts | 4 + constants/countries.ts | 89 +++++++ 4 files changed, 359 insertions(+) create mode 100644 .qwen/settings.json create mode 100644 components/PhoneInput/PhoneInputCustom.tsx create mode 100644 constants/countries.ts diff --git a/.qwen/settings.json b/.qwen/settings.json new file mode 100644 index 0000000..322d785 --- /dev/null +++ b/.qwen/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(git add *)" + ] + } +} \ No newline at end of file diff --git a/components/PhoneInput/PhoneInputCustom.tsx b/components/PhoneInput/PhoneInputCustom.tsx new file mode 100644 index 0000000..f385646 --- /dev/null +++ b/components/PhoneInput/PhoneInputCustom.tsx @@ -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 */} + + setCountryPickerVisible(true)} + disabled={disabled} + activeOpacity={0.7} + > + +{selectedCountry.callingCode} + + + + + + + + + {/* Country Picker Modal */} + setCountryPickerVisible(false)} + > + + + + Pilih Negara + setCountryPickerVisible(false)}> + + + + + + + + {filteredCountries.map((country) => ( + handleSelectCountry(country)} + activeOpacity={0.7} + > + + {country.name} + +{country.callingCode} + + {selectedCountry.code === country.code && ( + + )} + + ))} + + + + + + ); +} + +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", + }, +}); diff --git a/components/index.ts b/components/index.ts index 27e7c1f..d94d80e 100644 --- a/components/index.ts +++ b/components/index.ts @@ -49,6 +49,8 @@ import MapCustom from "./Map/MapCustom"; import CenterCustom from "./Center/CenterCustom"; // Clickable import ClickableCustom from "./Clickable/ClickableCustom"; +// PhoneInput +import PhoneInputCustom from "./PhoneInput/PhoneInputCustom"; // Scroll import ScrollableCustom from "./Scroll/ScrollCustom"; // ShareComponent @@ -95,6 +97,8 @@ export { CheckboxGroup, // Clickable ClickableCustom, + // PhoneInput + PhoneInputCustom, // Container CircleContainer, // Divider diff --git a/constants/countries.ts b/constants/countries.ts new file mode 100644 index 0000000..f0b3d30 --- /dev/null +++ b/constants/countries.ts @@ -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) + ); +}