22 <component
33 :is =" dialogComponent"
44 v-model:open =" open"
5- :title =" t('items.entry_title')"
5+ :title =" phase === 'idle' ? t('items.entry_title') : ''"
6+ :dismissible =" phase === 'idle'"
67 >
78 <template #body >
8- <div class =" space-y-4" >
9- <UAlert
10- v-if =" isUrl "
11- color="neutral"
12- variant="soft"
13- :description =" t (' items.entry_url_note' )"
14- />
15- <UFormField :label =" t (' items.fields.title' )" >
16- <UInput
17- v-model =" input "
18- :placeholder =" t (' items.entry_placeholder' )"
19- autofocus
20- @keydown .enter =" proceed "
9+ <Transition
10+ mode="out-in"
11+ enter-active-class="animate-in fade-in-0 slide-in-from-bottom-2 duration-200 ease-out"
12+ leave-active-class="animate-out fade-out-0 slide-out-to-top-2 duration-150 ease-in"
13+ >
14+ <!-- idle -->
15+ <div v-if =" phase === 'idle'" key =" idle" class =" space-y-4" >
16+ <UFormField :label =" t (' items.fields.title' )" >
17+ <UInput
18+ v-model =" input "
19+ :placeholder =" t (' items.entry_placeholder' )"
20+ autofocus
21+ @keydown .enter =" proceed "
22+ />
23+ </UFormField >
24+ </div >
25+
26+ <!-- scanning -->
27+ <div v-else-if =" phase === 'scanning'" key =" scanning" class =" flex flex-col items-center py-6 text-center" >
28+ <UIcon
29+ name="i-heroicons-arrow-path"
30+ class="h-8 w-8 animate-spin text-black dark:text-white "
31+ />
32+ <div class =" mt-4 space-y-1" >
33+ <p class =" text-sm font-semibold text-black dark:text-white" >
34+ {{ t('items.scanning.title') }}
35+ </p >
36+ <p class =" max-w-[200px] truncate text-xs text-muted-gray" >
37+ {{ hostname }}
38+ </p >
39+ </div >
40+ </div >
41+
42+ <!-- success -->
43+ <div v-else-if =" phase === 'success'" key =" success" class =" flex flex-col items-center py-6 text-center" >
44+ <UIcon
45+ name="i-heroicons-check-circle"
46+ class="h-8 w-8 text-black dark:text-white "
47+ style =" animation : wp-spin-in 0.45s cubic-bezier (0.22 , 1 , 0.36 , 1 ) both "
48+ />
49+ <div class =" mt-4 space-y-1" >
50+ <p class =" text-sm font-semibold text-black dark:text-white" >
51+ {{ t('items.scanning.success') }}
52+ </p >
53+ </div >
54+ </div >
55+
56+ <!-- error -->
57+ <div v-else key =" error" class =" flex w-full flex-col items-center gap-4 py-6 text-center" >
58+ <UIcon
59+ name="i-heroicons-exclamation-circle"
60+ class="h-8 w-8 text-black dark:text-white "
61+ />
62+ <div class =" space-y-1" >
63+ <p class =" text-sm font-semibold text-black dark:text-white" >
64+ {{ t('items.scanning.error') }}
65+ </p >
66+ <p class =" text-xs text-muted-gray" >
67+ {{ t('items.scanning.error_body') }}
68+ </p >
69+ </div >
70+ <UButton
71+ class="w-full"
72+ color="neutral"
73+ :label =" t (' items.scanning.proceed' )"
74+ @click =" onErrorProceed "
2175 />
22- </UFormField >
23- </div >
76+ </div >
77+ </Transition >
2478 </template >
2579
26- <template #footer >
80+ <template v-if = " phase === ' idle ' " #footer >
2781 <UButton
2882 class="w-full"
2983 :disabled =" ! input .trim ()"
3690
3791<script setup lang="ts">
3892import { resolveComponent } from ' vue'
93+ import type { ParseUrlData } from ' ~/types/api'
94+
95+ const props = defineProps <{
96+ parseUrlFn? : (url : string ) => Promise <ParseUrlData >
97+ }>()
3998
4099const open = defineModel <boolean >(' open' , { default: false })
41100const emit = defineEmits <{
42- proceed: [payload : { title: string ; productUrl: string | null }]
101+ proceed: [payload : { title: string ; productUrl: string | null ; parsedData ? : ParseUrlData | null }]
43102}>()
44103
45104const { t } = useI18n ()
@@ -54,25 +113,68 @@ onMounted(() => {
54113
55114const dialogComponent = computed (() => (isMobile .value ? UDrawer : UModal ))
56115
116+ type Phase = ' idle' | ' scanning' | ' success' | ' error'
117+ const phase = ref <Phase >(' idle' )
57118const input = ref (' ' )
119+ const scannedData = ref <ParseUrlData | null >(null )
58120
59121const isUrl = computed (() => {
60122 const v = input .value .trim ()
61123 return v .startsWith (' http://' ) || v .startsWith (' https://' )
62124})
63125
64- function proceed() {
126+ const hostname = computed (() => {
127+ try {
128+ return new URL (input .value .trim ()).hostname .replace (' www.' , ' ' )
129+ } catch {
130+ return input .value .trim ()
131+ }
132+ })
133+
134+ async function proceed() {
65135 const v = input .value .trim ()
66136 if (! v ) return
67- emit (' proceed' , {
68- title: isUrl .value ? ' ' : v ,
69- productUrl: isUrl .value ? v : null ,
70- })
137+
138+ if (isUrl .value && props .parseUrlFn ) {
139+ phase .value = ' scanning'
140+ try {
141+ const data = await props .parseUrlFn (v )
142+ scannedData .value = data
143+ phase .value = ' success'
144+ setTimeout (() => {
145+ emit (' proceed' , { title: ' ' , productUrl: v , parsedData: data })
146+ reset ()
147+ }, 800 )
148+ } catch {
149+ phase .value = ' error'
150+ }
151+ } else {
152+ emit (' proceed' , {
153+ title: isUrl .value ? ' ' : v ,
154+ productUrl: isUrl .value ? v : null ,
155+ })
156+ reset ()
157+ }
158+ }
159+
160+ function onErrorProceed() {
161+ const v = input .value .trim ()
162+ emit (' proceed' , { title: ' ' , productUrl: v , parsedData: null })
163+ reset ()
164+ }
165+
166+ function reset() {
71167 input .value = ' '
72168 open .value = false
169+ phase .value = ' idle'
170+ scannedData .value = null
73171}
74172
75173watch (open , (val ) => {
76- if (! val ) input .value = ' '
174+ if (! val ) {
175+ input .value = ' '
176+ phase .value = ' idle'
177+ scannedData .value = null
178+ }
77179})
78180 </script >
0 commit comments