2626 </select >
2727 </div >
2828 <div class =" flex gap-2" >
29+ <input
30+ ref =" fileInput"
31+ type =" file"
32+ class =" hidden"
33+ accept =" .csv"
34+ @change =" handleFileChange"
35+ >
2936 <button
30- class =" flex-[3] sm:flex-none px-4 py-2 bg-ext-wf1 hover:bg-ext-wf3 text-white text-sm font-medium rounded-lg flex items-center justify-center gap-2 transition-colors"
31- @click =" exportSubscribers"
37+ class =" flex-[3] sm:flex-none px-4 py-2 bg-ext-wf1 hover:bg-ext-wf3 text-white text-sm font-medium rounded-lg flex items-center justify-center gap-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
38+ :disabled =" isImporting"
39+ @click =" importSubscribers"
3240 >
33- <BaseIcon name =" download" class =" w-4 h-4" />
34- <span class =" flex items-center whitespace-nowrap" >Export CSV</span >
41+ <BaseIcon v-if =" !isImporting" name =" upload" class =" w-4 h-4" />
42+ <div v-else class =" w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" ></div >
43+ <span class =" flex items-center whitespace-nowrap" >{{ isImporting ? 'Importing...' : 'Import CSV' }}</span >
3544 </button >
3645 </div >
3746 </div >
4756 @close =" closeSubscriberModal"
4857 @updated =" handleSubscriberUpdated"
4958 />
59+ <ImportResult
60+ :is-import-result-open =" isImportResultOpen"
61+ :import-result =" importResult"
62+ @close =" isImportResultOpen = false"
63+ />
5064 <div class =" p-4 sm:p-6 border-t border-slate-200 flex flex-col sm:flex-row justify-between items-center gap-4 text-sm text-slate-500" >
5165 <div class =" text-center sm:text-left" >
5266 Showing <span class =" font-medium text-slate-900" >{{ subscribers.length }}</span > of <span class =" font-medium text-slate-900" >{{ pagination.total }}</span > subscribers
@@ -76,6 +90,7 @@ import BaseIcon from '../base/BaseIcon.vue'
7690import SubscriberFilters from ' ./SubscriberFilters.vue'
7791import SubscriberTable from ' ./SubscriberTable.vue'
7892import SubscriberModal from ' ./SubscriberModal.vue'
93+ import ImportResult from ' ./ImportResult.vue'
7994import { inject , ref , onMounted , watch } from ' vue'
8095import { subscriberFilters } from ' ./subscriberFilters'
8196import { subscribersClient } from ' ../../api'
@@ -96,6 +111,14 @@ const searchQuery = ref('')
96111const searchColumn = ref (' email' )
97112const isModalOpen = ref (false )
98113const selectedSubscriberId = ref (null )
114+ const fileInput = ref (null )
115+ const isImporting = ref (false )
116+ const isImportResultOpen = ref (false )
117+ const importResult = ref ({
118+ imported: 0 ,
119+ skipped: 0 ,
120+ errors: []
121+ })
99122let searchTimeout = null
100123
101124const searchColumns = [
@@ -242,6 +265,33 @@ const handleFilterChange = (filterId) => {
242265 fetchSubscribers ()
243266}
244267
268+ const importSubscribers = () => {
269+ fileInput .value .click ()
270+ }
271+
272+ const handleFileChange = async (event ) => {
273+ const file = event .target .files [0 ]
274+ if (! file) return
275+
276+ isImporting .value = true
277+ try {
278+ const result = await subscribersClient .importSubscribers ({
279+ file,
280+ listId: 0 ,
281+ updateExisting: true
282+ })
283+ importResult .value = result
284+ isImportResultOpen .value = true
285+ fetchSubscribers ()
286+ } catch (error) {
287+ console .error (' Failed to import subscribers:' , error)
288+ alert (' Failed to import subscribers. Please check the file and try again.' )
289+ } finally {
290+ isImporting .value = false
291+ event .target .value = ' '
292+ }
293+ }
294+
245295const exportSubscribers = () => {
246296 const params = new URLSearchParams ()
247297 if (currentFilter .value && currentFilter .value !== ' all' ) {
0 commit comments