x
This commit is contained in:
1 parent
bce5c48b59
commit
2098548140
4 files changed
+67715
-85
No files matched your search
@@ -0,0 +1,158 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
export interface AddressData {
|
||||
subdistrict: string;
|
||||
district: string;
|
||||
province: string;
|
||||
zipcode: string;
|
||||
}
|
||||
|
||||
interface ThaiAddressInputProps {
|
||||
value: AddressData;
|
||||
onChange: (val: AddressData) => void;
|
||||
}
|
||||
|
||||
interface ThaiAddressDB {
|
||||
district: string;
|
||||
amphoe: string;
|
||||
province: string;
|
||||
zipcode: number;
|
||||
}
|
||||
|
||||
export function ThaiAddressInput({ value, onChange }: ThaiAddressInputProps) {
|
||||
const [db, setDb] = useState<ThaiAddressDB[]>([]);
|
||||
const [results, setResults] = useState<ThaiAddressDB[]>([]);
|
||||
const [focusedField, setFocusedField] = useState<keyof AddressData | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/data/thai_addresses.json')
|
||||
.then(res => res.json())
|
||||
.then(data => setDb(data))
|
||||
.catch(err => console.error('Failed to load thai addresses:', err));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setResults([]);
|
||||
setFocusedField(null);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleSearch = (text: string, field: keyof AddressData) => {
|
||||
onChange({ ...value, [field]: text });
|
||||
|
||||
if (text.length < 2) {
|
||||
setResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const searchStr = text.toLowerCase();
|
||||
const filtered = db.filter(item =>
|
||||
item.district.toLowerCase().includes(searchStr) ||
|
||||
item.amphoe.toLowerCase().includes(searchStr) ||
|
||||
item.province.toLowerCase().includes(searchStr) ||
|
||||
item.zipcode.toString().includes(searchStr)
|
||||
).slice(0, 20); // max 20 results
|
||||
|
||||
setResults(filtered);
|
||||
};
|
||||
|
||||
const handleSelect = (item: ThaiAddressDB) => {
|
||||
onChange({
|
||||
...value,
|
||||
subdistrict: item.district,
|
||||
district: item.amphoe,
|
||||
province: item.province,
|
||||
zipcode: item.zipcode.toString(),
|
||||
});
|
||||
setResults([]);
|
||||
setFocusedField(null);
|
||||
};
|
||||
|
||||
const renderDropdown = (field: keyof AddressData) => {
|
||||
if (focusedField !== field || results.length === 0) return null;
|
||||
|
||||
return (
|
||||
<ul className="absolute z-50 w-full bg-white border border-spa-stone/20 rounded-lg mt-1 max-h-60 overflow-y-auto shadow-xl">
|
||||
{results.map((item, idx) => (
|
||||
<li
|
||||
key={`${item.district}-${item.amphoe}-${item.province}-${item.zipcode}-${idx}`}
|
||||
className="p-3 hover:bg-spa-sage/10 cursor-pointer text-xs text-pop-black border-b border-spa-stone/10 last:border-0"
|
||||
onMouseDown={(e) => {
|
||||
// use onMouseDown to prevent onBlur from firing before onClick
|
||||
e.preventDefault();
|
||||
handleSelect(item);
|
||||
}}
|
||||
>
|
||||
ต.{item.district} อ.{item.amphoe} จ.{item.province} {item.zipcode}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="grid grid-cols-2 md:grid-cols-4 gap-4 w-full">
|
||||
<div className="space-y-1.5 relative">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">แขวง / ตำบล</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={value.subdistrict}
|
||||
onChange={e => handleSearch(e.target.value, 'subdistrict')}
|
||||
onFocus={() => { setFocusedField('subdistrict'); handleSearch(value.subdistrict, 'subdistrict'); }}
|
||||
onBlur={() => setTimeout(() => { if (focusedField === 'subdistrict') setFocusedField(null); }, 200)}
|
||||
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all"
|
||||
/>
|
||||
{renderDropdown('subdistrict')}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 relative">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">เขต / อำเภอ</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={value.district}
|
||||
onChange={e => handleSearch(e.target.value, 'district')}
|
||||
onFocus={() => { setFocusedField('district'); handleSearch(value.district, 'district'); }}
|
||||
onBlur={() => setTimeout(() => { if (focusedField === 'district') setFocusedField(null); }, 200)}
|
||||
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all"
|
||||
/>
|
||||
{renderDropdown('district')}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 relative">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">จังหวัด</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={value.province}
|
||||
onChange={e => handleSearch(e.target.value, 'province')}
|
||||
onFocus={() => { setFocusedField('province'); handleSearch(value.province, 'province'); }}
|
||||
onBlur={() => setTimeout(() => { if (focusedField === 'province') setFocusedField(null); }, 200)}
|
||||
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all"
|
||||
/>
|
||||
{renderDropdown('province')}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 relative">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">รหัสไปรษณีย์</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={value.zipcode}
|
||||
onChange={e => handleSearch(e.target.value, 'zipcode')}
|
||||
onFocus={() => { setFocusedField('zipcode'); handleSearch(value.zipcode, 'zipcode'); }}
|
||||
onBlur={() => setTimeout(() => { if (focusedField === 'zipcode') setFocusedField(null); }, 200)}
|
||||
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all"
|
||||
/>
|
||||
{renderDropdown('zipcode')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user