1+ import { Copy , Loader2 , MapIcon } from "lucide-react" ;
2+ import nzh from "nzh/hk" ;
13import { useDeferredValue , useEffect , useRef , useState } from "react" ;
24import { toast } from "sonner" ;
35import useSWR from "swr" ;
46import { Textarea } from "~/components/ui/textarea" ;
57import { cn } from "~/utils/cn.ts" ;
68import type { AddressToEnglishJson } from "~/utils/get-address-data-json" ;
7- import { translateAddressToEnglish } from "~/utils/translate-address-to-english" ;
9+ import {
10+ type TranslateAddressToEnglish ,
11+ translateAddressToEnglish ,
12+ } from "~/utils/translate-address-to-english" ;
13+ import { Button } from "./ui/button" ;
14+ import { ButtonGroup } from "./ui/button-group" ;
815
916export const AddressToEnglishInput = ( ) => {
1017 const inputRef = useRef < HTMLTextAreaElement > ( null ) ;
1118
1219 const [ value , setValue ] = useState ( "" ) ;
13- const [ result , setResult ] = useState < string > ( ) ;
14- const [ isValid , setIsValid ] = useState ( true ) ;
20+ const [ result , setResult ] = useState < TranslateAddressToEnglish [ ] > ( [ ] ) ;
1521
1622 const deferredValue = useDeferredValue ( value . trim ( ) ) ;
1723
@@ -30,40 +36,36 @@ export const AddressToEnglishInput = () => {
3036
3137 const address = searchParams . get ( "address" ) ;
3238
33- if ( address ) setValue ( address ) ;
39+ if ( address ) setValue ( address . replaceAll ( ";" , "\n" ) ) ;
3440 } catch {
3541 // ignore
3642 }
3743 } , [ ] ) ;
3844
3945 useEffect ( ( ) => {
40- async function translate ( ) {
41- // update url address param without reloading the page
42- const url = new URL ( location . href ) ;
46+ // update url address param without reloading the page
47+ const url = new URL ( location . href ) ;
4348
44- const searchParams = new URLSearchParams ( ) ;
49+ const searchParams = new URLSearchParams ( ) ;
4550
46- if ( deferredValue ) searchParams . set ( "address" , deferredValue ) ;
51+ const addresses = deferredValue
52+ . split ( "\n" )
53+ . map ( ( x ) => x . trim ( ) )
54+ . filter ( Boolean ) ;
4755
48- url . hash = searchParams . toString ( ) ;
56+ if ( deferredValue ) searchParams . set ( "address" , addresses . join ( ";" ) ) ;
4957
50- history . replaceState ( { } , "" , url . toString ( ) ) ;
58+ url . hash = searchParams . toString ( ) ;
5159
52- if ( ! addressData || ! deferredValue ) return ;
60+ history . replaceState ( { } , "" , url . toString ( ) ) ;
5361
54- const nzh = await import ( "nzh" ) . then ( ( module ) => module . default . hk ) ;
62+ if ( ! addressData || ! deferredValue ) return ;
5563
56- const { result, isValid } = translateAddressToEnglish (
57- addressData ,
58- deferredValue ,
59- nzh ,
60- ) ;
61-
62- setResult ( result ) ;
63- setIsValid ( isValid ) ;
64- }
65-
66- void translate ( ) ;
64+ setResult (
65+ addresses . map ( ( address ) =>
66+ translateAddressToEnglish ( addressData , address , nzh ) ,
67+ ) ,
68+ ) ;
6769 } , [ deferredValue , addressData ] ) ;
6870
6971 return (
@@ -75,12 +77,10 @@ export const AddressToEnglishInput = () => {
7577 crossOrigin = "anonymous"
7678 />
7779 < Textarea
80+ name = "address"
7881 autoFocus
7982 ref = { inputRef }
80- className = { cn (
81- "whitespace-pre-wrap inline-block text-start break-words break-all text-2xl bg-secondary/15 resize-none focus-visible:ring-0 !ring-offset-0 transition-colors" ,
82- ! isValid && "border-red-500" ,
83- ) }
83+ className = "whitespace-pre-wrap inline-block text-start break-words break-all text-2xl bg-secondary/15 resize-none focus-visible:ring-0 !ring-offset-0 transition-colors"
8484 value = { value }
8585 placeholder = "臺北市中正區重慶南路1段122號"
8686 onChange = { ( event ) => {
@@ -91,18 +91,37 @@ export const AddressToEnglishInput = () => {
9191 } }
9292 rows = { 3 }
9393 />
94- < Textarea
95- className = "whitespace-pre-wrap inline-block text-start break-words break-all text-lg border-none bg-secondary/50 resize-none focus-visible:ring-0 !ring-offset-0"
96- value = { isLoading ? "正在更新中華郵政資料..." : result }
97- readOnly
98- placeholder = "No. 122, Sec. 1, Chongqing S. Rd., Zhongzheng Dist., Taipei City 100, Taiwan (R.O.C.)"
99- rows = { 3 }
100- />
101- { result && (
102- < div className = "flex items-center gap-2 justify-center" >
103- < a
104- className = "text-blue-500 underline"
105- href = { location . href }
94+ { isLoading && (
95+ < span className = "text-muted-foreground flex justify-center items-center gap-2" >
96+ 正在載入中華郵政資料 < Loader2 className = "w-4 animate-spin" />
97+ </ span >
98+ ) }
99+ < div className = "flex flex-col gap-2" >
100+ { result . map ( ( result , index ) => (
101+ < TranslatedRow key = { `${ index } -${ result } ` } > { result } </ TranslatedRow >
102+ ) ) }
103+ </ div >
104+ </ div >
105+ ) ;
106+ } ;
107+
108+ function TranslatedRow ( { children } : { children : TranslateAddressToEnglish } ) {
109+ return (
110+ < div
111+ className = { cn (
112+ "px-3 py-2 rounded-md whitespace-pre-wrap text-start break-words break-all text-lg border-none bg-secondary/50" ,
113+ ! children . isValid && "border-destructive" ,
114+ ) }
115+ >
116+ < span className = "text-sm text-muted-foreground pb-1 block" >
117+ { children . original }
118+ </ span >
119+ < div >
120+ < span className = "text-pretty" > { children . result } </ span >
121+ < ButtonGroup className = "float-end inline-block py-1" >
122+ < Button
123+ variant = "outline"
124+ aria-label = "複製到剪貼簿"
106125 onClick = { async ( e ) => {
107126 e . preventDefault ( ) ;
108127 await navigator . clipboard
@@ -112,18 +131,21 @@ export const AddressToEnglishInput = () => {
112131 toast . success ( "已複製連結到剪貼簿" ) ;
113132 } }
114133 >
115- 複製連結
116- </ a >
117- < a
118- href = { `https://www.google.com/maps/search/${ encodeURIComponent ( result ) } ` }
119- target = "_blank"
120- rel = "noopener noreferrer"
121- className = "text-blue-500 underline"
122- >
123- 在 Google Maps 上查看
124- </ a >
125- </ div >
126- ) }
134+ < Copy />
135+ </ Button >
136+ < Button asChild variant = "outline" >
137+ < a
138+ target = "_blank"
139+ rel = "noopener noreferrer"
140+ href = { `https://www.google.com/maps/search/${ encodeURIComponent ( children . result ) } ` }
141+ title = "在 Google Map 查看"
142+ aria-label = "在 Google Map 查看"
143+ >
144+ < MapIcon />
145+ </ a >
146+ </ Button >
147+ </ ButtonGroup >
148+ </ div >
127149 </ div >
128150 ) ;
129- } ;
151+ }
0 commit comments