@meeovi/layer-lists 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/features/archived.vue +64 -0
- package/app/components/features/bookmarks.vue +64 -0
- package/app/components/features/lists.vue +61 -0
- package/app/components/features/starred.vue +64 -0
- package/app/components/lists/ListItemCard.vue +190 -0
- package/app/components/lists/add-bookmark.vue +52 -0
- package/app/components/lists/add-list-item.vue +88 -0
- package/app/components/lists/add-list.vue +57 -0
- package/app/components/lists/lists.vue +6 -0
- package/app/components/lists/listsettings.vue +145 -0
- package/app/components/lists/update-bookmark.vue +267 -0
- package/app/components/lists/update-list.vue +192 -0
- package/app/components/media/MediaPlayer.vue +302 -0
- package/app/components/partials/addtolist.vue +233 -0
- package/app/components/partials/createListBtn.vue +95 -0
- package/app/components/partials/listBtn.vue +35 -0
- package/app/components/related/list.vue +33 -0
- package/app/components/related/relatedlists.vue +43 -0
- package/app/components/tasks/TaskItem.vue +204 -0
- package/app/composables/bookmarks/createBookmark.js +30 -0
- package/app/composables/bookmarks/deleteBookmark.js +15 -0
- package/app/composables/bookmarks/updateBookmark.js +15 -0
- package/app/composables/config.ts +17 -0
- package/app/composables/content/uploadFiles.js +41 -0
- package/app/composables/globals/useDirectusForm.ts +1 -0
- package/app/composables/lists/createList.js +25 -0
- package/app/composables/lists/deleteList.js +14 -0
- package/app/composables/lists/updateList.js +20 -0
- package/app/composables/lists/useBookmarks.js +69 -0
- package/app/composables/lists/useLists.js +120 -0
- package/app/composables/lists/usePlaylist.js +64 -0
- package/app/composables/lists/useSaved.js +29 -0
- package/app/composables/lists/useTasks.js +86 -0
- package/app/composables/lists/useWishlist.js +51 -0
- package/app/composables/providers/atproto.ts +156 -0
- package/app/composables/providers/directus.ts +49 -0
- package/app/composables/providers/memory.ts +88 -0
- package/app/composables/registry.ts +13 -0
- package/app/composables/types.ts +35 -0
- package/app/composables/useLists.ts +20 -0
- package/app/composables/utils/transforms.ts +42 -0
- package/app/composables/utils/validation.ts +21 -0
- package/app/pages/lists/bookmark/[id].vue +76 -0
- package/app/pages/lists/index.vue +152 -0
- package/app/pages/lists/list/[...slug].vue +233 -0
- package/nuxt.config.ts +11 -0
- package/package.json +26 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card class="media-player">
|
|
3
|
+
<div v-if="currentMedia">
|
|
4
|
+
<!-- Video Player -->
|
|
5
|
+
<video
|
|
6
|
+
v-if="currentMedia.media_type === 'video'"
|
|
7
|
+
ref="videoPlayer"
|
|
8
|
+
:src="currentMedia.url"
|
|
9
|
+
:poster="currentMedia.thumbnail"
|
|
10
|
+
controls
|
|
11
|
+
@loadedmetadata="onMediaLoaded"
|
|
12
|
+
@timeupdate="onTimeUpdate"
|
|
13
|
+
@ended="onMediaEnded"
|
|
14
|
+
class="w-100"
|
|
15
|
+
/>
|
|
16
|
+
|
|
17
|
+
<!-- Audio Player -->
|
|
18
|
+
<div v-else-if="currentMedia.media_type === 'audio'" class="audio-player">
|
|
19
|
+
<div class="d-flex align-center pa-4">
|
|
20
|
+
<v-img
|
|
21
|
+
:src="currentMedia.thumbnail || '/default-audio.png'"
|
|
22
|
+
width="80"
|
|
23
|
+
height="80"
|
|
24
|
+
class="rounded me-4"
|
|
25
|
+
/>
|
|
26
|
+
<div class="grow">
|
|
27
|
+
<h3 class="text-h6">{{ currentMedia.title }}</h3>
|
|
28
|
+
<p class="text-body-2 text-medium-emphasis">
|
|
29
|
+
{{ currentMedia.metadata?.artist || 'Unknown Artist' }}
|
|
30
|
+
</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<audio
|
|
35
|
+
ref="audioPlayer"
|
|
36
|
+
:src="currentMedia.url"
|
|
37
|
+
@loadedmetadata="onMediaLoaded"
|
|
38
|
+
@timeupdate="onTimeUpdate"
|
|
39
|
+
@ended="onMediaEnded"
|
|
40
|
+
style="display: none;"
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Controls -->
|
|
46
|
+
<v-card-actions class="px-4 py-2">
|
|
47
|
+
<v-btn
|
|
48
|
+
icon
|
|
49
|
+
@click="previousTrack"
|
|
50
|
+
:disabled="currentIndex === 0"
|
|
51
|
+
>
|
|
52
|
+
<v-icon>mdi-skip-previous</v-icon>
|
|
53
|
+
</v-btn>
|
|
54
|
+
|
|
55
|
+
<v-btn
|
|
56
|
+
icon
|
|
57
|
+
@click="togglePlayPause"
|
|
58
|
+
color="primary"
|
|
59
|
+
size="large"
|
|
60
|
+
>
|
|
61
|
+
<v-icon>{{ isPlaying ? 'mdi-pause' : 'mdi-play' }}</v-icon>
|
|
62
|
+
</v-btn>
|
|
63
|
+
|
|
64
|
+
<v-btn
|
|
65
|
+
icon
|
|
66
|
+
@click="nextTrack"
|
|
67
|
+
:disabled="currentIndex === playlist.length - 1"
|
|
68
|
+
>
|
|
69
|
+
<v-icon>mdi-skip-next</v-icon>
|
|
70
|
+
</v-btn>
|
|
71
|
+
|
|
72
|
+
<v-spacer />
|
|
73
|
+
|
|
74
|
+
<span class="text-caption">{{ formatTime(currentTime) }}</span>
|
|
75
|
+
<v-slider
|
|
76
|
+
v-model="currentTime"
|
|
77
|
+
:max="duration"
|
|
78
|
+
@update:model-value="seekTo"
|
|
79
|
+
class="mx-4"
|
|
80
|
+
style="min-width: 200px;"
|
|
81
|
+
hide-details
|
|
82
|
+
/>
|
|
83
|
+
<span class="text-caption">{{ formatTime(duration) }}</span>
|
|
84
|
+
|
|
85
|
+
<v-spacer />
|
|
86
|
+
|
|
87
|
+
<v-btn
|
|
88
|
+
icon
|
|
89
|
+
@click="toggleShuffle"
|
|
90
|
+
:color="shuffle ? 'primary' : 'default'"
|
|
91
|
+
>
|
|
92
|
+
<v-icon>mdi-shuffle</v-icon>
|
|
93
|
+
</v-btn>
|
|
94
|
+
|
|
95
|
+
<v-btn
|
|
96
|
+
icon
|
|
97
|
+
@click="toggleRepeat"
|
|
98
|
+
:color="repeat !== 'none' ? 'primary' : 'default'"
|
|
99
|
+
>
|
|
100
|
+
<v-icon>{{ repeatIcon }}</v-icon>
|
|
101
|
+
</v-btn>
|
|
102
|
+
|
|
103
|
+
<v-btn icon @click="showPlaylist = !showPlaylist">
|
|
104
|
+
<v-icon>mdi-playlist-music</v-icon>
|
|
105
|
+
</v-btn>
|
|
106
|
+
</v-card-actions>
|
|
107
|
+
|
|
108
|
+
<!-- Playlist -->
|
|
109
|
+
<v-expand-transition>
|
|
110
|
+
<div v-show="showPlaylist">
|
|
111
|
+
<v-divider />
|
|
112
|
+
<v-list class="playlist-list" max-height="300" style="overflow-y: auto;">
|
|
113
|
+
<v-list-item
|
|
114
|
+
v-for="(item, index) in playlist"
|
|
115
|
+
:key="item.id"
|
|
116
|
+
@click="playTrack(index)"
|
|
117
|
+
:active="index === currentIndex"
|
|
118
|
+
>
|
|
119
|
+
<template v-slot:prepend>
|
|
120
|
+
<v-img
|
|
121
|
+
:src="item.thumbnail || '/default-audio.png'"
|
|
122
|
+
width="40"
|
|
123
|
+
height="40"
|
|
124
|
+
class="rounded me-3"
|
|
125
|
+
/>
|
|
126
|
+
</template>
|
|
127
|
+
|
|
128
|
+
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
|
129
|
+
<v-list-item-subtitle>
|
|
130
|
+
{{ item.metadata?.artist || 'Unknown Artist' }}
|
|
131
|
+
</v-list-item-subtitle>
|
|
132
|
+
|
|
133
|
+
<template v-slot:append>
|
|
134
|
+
<span class="text-caption">{{ formatTime(item.duration) }}</span>
|
|
135
|
+
</template>
|
|
136
|
+
</v-list-item>
|
|
137
|
+
</v-list>
|
|
138
|
+
</div>
|
|
139
|
+
</v-expand-transition>
|
|
140
|
+
</v-card>
|
|
141
|
+
</template>
|
|
142
|
+
|
|
143
|
+
<script setup>
|
|
144
|
+
const props = defineProps({
|
|
145
|
+
playlist: {
|
|
146
|
+
type: Array,
|
|
147
|
+
required: true
|
|
148
|
+
},
|
|
149
|
+
autoplay: {
|
|
150
|
+
type: Boolean,
|
|
151
|
+
default: false
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const currentIndex = ref(0)
|
|
156
|
+
const isPlaying = ref(false)
|
|
157
|
+
const currentTime = ref(0)
|
|
158
|
+
const duration = ref(0)
|
|
159
|
+
const shuffle = ref(false)
|
|
160
|
+
const repeat = ref('none') // 'none', 'one', 'all'
|
|
161
|
+
const showPlaylist = ref(false)
|
|
162
|
+
|
|
163
|
+
const videoPlayer = ref(null)
|
|
164
|
+
const audioPlayer = ref(null)
|
|
165
|
+
|
|
166
|
+
const currentMedia = computed(() => {
|
|
167
|
+
return props.playlist[currentIndex.value] || null
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const currentPlayer = computed(() => {
|
|
171
|
+
return currentMedia.value?.media_type === 'video' ? videoPlayer.value : audioPlayer.value
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const repeatIcon = computed(() => {
|
|
175
|
+
switch (repeat.value) {
|
|
176
|
+
case 'one': return 'mdi-repeat-once'
|
|
177
|
+
case 'all': return 'mdi-repeat'
|
|
178
|
+
default: return 'mdi-repeat-off'
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const togglePlayPause = () => {
|
|
183
|
+
if (!currentPlayer.value) return
|
|
184
|
+
|
|
185
|
+
if (isPlaying.value) {
|
|
186
|
+
currentPlayer.value.pause()
|
|
187
|
+
} else {
|
|
188
|
+
currentPlayer.value.play()
|
|
189
|
+
}
|
|
190
|
+
isPlaying.value = !isPlaying.value
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const playTrack = (index) => {
|
|
194
|
+
currentIndex.value = index
|
|
195
|
+
nextTick(() => {
|
|
196
|
+
if (currentPlayer.value) {
|
|
197
|
+
currentPlayer.value.play()
|
|
198
|
+
isPlaying.value = true
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const nextTrack = () => {
|
|
204
|
+
if (shuffle.value) {
|
|
205
|
+
currentIndex.value = Math.floor(Math.random() * props.playlist.length)
|
|
206
|
+
} else if (currentIndex.value < props.playlist.length - 1) {
|
|
207
|
+
currentIndex.value++
|
|
208
|
+
} else if (repeat.value === 'all') {
|
|
209
|
+
currentIndex.value = 0
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
nextTick(() => {
|
|
213
|
+
if (currentPlayer.value && isPlaying.value) {
|
|
214
|
+
currentPlayer.value.play()
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const previousTrack = () => {
|
|
220
|
+
if (currentIndex.value > 0) {
|
|
221
|
+
currentIndex.value--
|
|
222
|
+
} else if (repeat.value === 'all') {
|
|
223
|
+
currentIndex.value = props.playlist.length - 1
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
nextTick(() => {
|
|
227
|
+
if (currentPlayer.value && isPlaying.value) {
|
|
228
|
+
currentPlayer.value.play()
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const seekTo = (time) => {
|
|
234
|
+
if (currentPlayer.value) {
|
|
235
|
+
currentPlayer.value.currentTime = time
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const toggleShuffle = () => {
|
|
240
|
+
shuffle.value = !shuffle.value
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const toggleRepeat = () => {
|
|
244
|
+
const modes = ['none', 'all', 'one']
|
|
245
|
+
const currentIndex = modes.indexOf(repeat.value)
|
|
246
|
+
repeat.value = modes[(currentIndex + 1) % modes.length]
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const onMediaLoaded = () => {
|
|
250
|
+
if (currentPlayer.value) {
|
|
251
|
+
duration.value = currentPlayer.value.duration || 0
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const onTimeUpdate = () => {
|
|
256
|
+
if (currentPlayer.value) {
|
|
257
|
+
currentTime.value = currentPlayer.value.currentTime || 0
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const onMediaEnded = () => {
|
|
262
|
+
isPlaying.value = false
|
|
263
|
+
|
|
264
|
+
if (repeat.value === 'one') {
|
|
265
|
+
currentPlayer.value.currentTime = 0
|
|
266
|
+
currentPlayer.value.play()
|
|
267
|
+
isPlaying.value = true
|
|
268
|
+
} else {
|
|
269
|
+
nextTrack()
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const formatTime = (seconds) => {
|
|
274
|
+
if (!seconds || isNaN(seconds)) return '0:00'
|
|
275
|
+
const mins = Math.floor(seconds / 60)
|
|
276
|
+
const secs = Math.floor(seconds % 60)
|
|
277
|
+
return `${mins}:${secs.toString().padStart(2, '0')}`
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
onMounted(() => {
|
|
281
|
+
if (props.autoplay && currentMedia.value) {
|
|
282
|
+
nextTick(() => {
|
|
283
|
+
togglePlayPause()
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
</script>
|
|
288
|
+
|
|
289
|
+
<style scoped>
|
|
290
|
+
.media-player {
|
|
291
|
+
max-width: 100%;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.audio-player {
|
|
295
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
296
|
+
color: white;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.playlist-list {
|
|
300
|
+
background-color: rgba(0, 0, 0, 0.02);
|
|
301
|
+
}
|
|
302
|
+
</style>
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="text-center">
|
|
3
|
+
<v-dialog v-model="dialog" max-width="600">
|
|
4
|
+
<template v-slot:activator="{ props: activatorProps }">
|
|
5
|
+
<v-btn prepend-icon="fas:fa fa-plus" text="Add to List" color="primary" size="large"
|
|
6
|
+
v-bind="activatorProps"></v-btn>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<v-card>
|
|
10
|
+
<v-tabs v-model="tab" bg-color="primary">
|
|
11
|
+
<v-tab value="one">Add to List</v-tab>
|
|
12
|
+
<v-tab value="two">Create List</v-tab>
|
|
13
|
+
</v-tabs>
|
|
14
|
+
|
|
15
|
+
<v-card-text>
|
|
16
|
+
<v-tabs-window v-model="tab">
|
|
17
|
+
<v-tabs-window-item value="one">
|
|
18
|
+
<v-card>
|
|
19
|
+
<v-card-title class="text-h6">
|
|
20
|
+
Add this Product to your List
|
|
21
|
+
</v-card-title>
|
|
22
|
+
<v-card-text>
|
|
23
|
+
<v-row dense>
|
|
24
|
+
<v-col cols="12" sm="6">
|
|
25
|
+
<v-autocomplete
|
|
26
|
+
v-model="selectedLists"
|
|
27
|
+
:items="lists"
|
|
28
|
+
item-title="name"
|
|
29
|
+
item-value="id"
|
|
30
|
+
label="Choose a List"
|
|
31
|
+
auto-select-first
|
|
32
|
+
multiple
|
|
33
|
+
chips
|
|
34
|
+
:loading="loading"
|
|
35
|
+
:disabled="loading"
|
|
36
|
+
/>
|
|
37
|
+
</v-col>
|
|
38
|
+
</v-row>
|
|
39
|
+
|
|
40
|
+
<div v-if="productPreview" class="mt-4">
|
|
41
|
+
<v-img :src="productPreview.image" height="100" cover class="rounded" />
|
|
42
|
+
<div class="mt-2 text-subtitle-1">{{ productPreview.name }}</div>
|
|
43
|
+
<div class="text-caption text-grey">{{ productPreview.sku }}</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<v-alert
|
|
47
|
+
v-if="error"
|
|
48
|
+
type="error"
|
|
49
|
+
class="mt-4"
|
|
50
|
+
>
|
|
51
|
+
{{ error }}
|
|
52
|
+
</v-alert>
|
|
53
|
+
</v-card-text>
|
|
54
|
+
|
|
55
|
+
<v-divider></v-divider>
|
|
56
|
+
|
|
57
|
+
<v-card-actions>
|
|
58
|
+
<v-spacer></v-spacer>
|
|
59
|
+
<v-btn text="Close" variant="plain" @click="closeDialog"></v-btn>
|
|
60
|
+
<v-btn
|
|
61
|
+
color="primary"
|
|
62
|
+
text="Save"
|
|
63
|
+
variant="tonal"
|
|
64
|
+
@click="saveToLists"
|
|
65
|
+
:loading="loading"
|
|
66
|
+
:disabled="loading || selectedLists.length === 0"
|
|
67
|
+
>
|
|
68
|
+
Save
|
|
69
|
+
</v-btn>
|
|
70
|
+
</v-card-actions>
|
|
71
|
+
</v-card>
|
|
72
|
+
</v-tabs-window-item>
|
|
73
|
+
|
|
74
|
+
<v-tabs-window-item value="two">
|
|
75
|
+
<createlist @list-created="handleListCreated" />
|
|
76
|
+
</v-tabs-window-item>
|
|
77
|
+
</v-tabs-window>
|
|
78
|
+
</v-card-text>
|
|
79
|
+
</v-card>
|
|
80
|
+
</v-dialog>
|
|
81
|
+
</div>
|
|
82
|
+
</template>
|
|
83
|
+
|
|
84
|
+
<script setup>
|
|
85
|
+
import { ref, computed, onMounted } from 'vue';
|
|
86
|
+
|
|
87
|
+
// Define props with Magento product structure
|
|
88
|
+
const props = defineProps({
|
|
89
|
+
product: {
|
|
90
|
+
type: Object,
|
|
91
|
+
required: true,
|
|
92
|
+
validator: (value) => {
|
|
93
|
+
return value && value.uid && value.name && value.sku;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const emit = defineEmits(['item-added']);
|
|
99
|
+
|
|
100
|
+
const { $directus } = useNuxtApp();
|
|
101
|
+
const user = useSupabaseAuth();
|
|
102
|
+
|
|
103
|
+
const dialog = ref(false);
|
|
104
|
+
const tab = ref('one');
|
|
105
|
+
const lists = ref([]);
|
|
106
|
+
const selectedLists = ref([]);
|
|
107
|
+
const loading = ref(false);
|
|
108
|
+
const error = ref(null);
|
|
109
|
+
|
|
110
|
+
// Fetch user's lists from Directus
|
|
111
|
+
const fetchLists = async () => {
|
|
112
|
+
if (!user.value) return;
|
|
113
|
+
|
|
114
|
+
loading.value = true;
|
|
115
|
+
error.value = null;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const response = await $directus.items('lists').readItems({
|
|
119
|
+
filter: {
|
|
120
|
+
user: {
|
|
121
|
+
_eq: user.value.id
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
fields: ['id', 'name', 'description']
|
|
125
|
+
});
|
|
126
|
+
lists.value = response.data;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.error('Error fetching lists:', err);
|
|
129
|
+
error.value = 'Failed to load your lists. Please try again.';
|
|
130
|
+
} finally {
|
|
131
|
+
loading.value = false;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
onMounted(fetchLists);
|
|
136
|
+
|
|
137
|
+
// Save product to selected lists
|
|
138
|
+
const saveToLists = async () => {
|
|
139
|
+
if (!props.product?.uid || selectedLists.value.length === 0) return;
|
|
140
|
+
|
|
141
|
+
loading.value = true;
|
|
142
|
+
error.value = null;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
// Check if product already exists in any of the selected lists
|
|
146
|
+
const existingItems = await Promise.all(
|
|
147
|
+
selectedLists.value.map(listId =>
|
|
148
|
+
isProductInList(listId, props.product.uid)
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Filter out lists where the product already exists
|
|
153
|
+
const newLists = selectedLists.value.filter((_, index) => !existingItems[index]);
|
|
154
|
+
|
|
155
|
+
if (newLists.length === 0) {
|
|
156
|
+
error.value = 'This product is already in all selected lists.';
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Create list items for each selected list
|
|
161
|
+
const promises = newLists.map(listId => {
|
|
162
|
+
return $directus.items('list_items').createItem({
|
|
163
|
+
list: listId,
|
|
164
|
+
magento_product_uid: props.product.uid,
|
|
165
|
+
magento_product_sku: props.product.sku,
|
|
166
|
+
magento_product_name: props.product.name,
|
|
167
|
+
magento_product_image: props.product.image?.url || null,
|
|
168
|
+
date_added: new Date().toISOString()
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await Promise.all(promises);
|
|
173
|
+
|
|
174
|
+
// Emit event for parent component
|
|
175
|
+
emit('item-added', {
|
|
176
|
+
product: props.product,
|
|
177
|
+
lists: newLists
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
closeDialog();
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error('Error saving to lists:', err);
|
|
183
|
+
error.value = 'Failed to add product to lists. Please try again.';
|
|
184
|
+
} finally {
|
|
185
|
+
loading.value = false;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const closeDialog = () => {
|
|
190
|
+
dialog.value = false;
|
|
191
|
+
selectedLists.value = [];
|
|
192
|
+
error.value = null;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Handle newly created list
|
|
196
|
+
const handleListCreated = (newList) => {
|
|
197
|
+
lists.value.push(newList);
|
|
198
|
+
selectedLists.value = [newList.id];
|
|
199
|
+
tab.value = 'one';
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Computed property for product preview
|
|
203
|
+
const productPreview = computed(() => {
|
|
204
|
+
if (!props.product) return null;
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
image: props.product.image?.url || '',
|
|
208
|
+
name: props.product.name,
|
|
209
|
+
sku: props.product.sku
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const isProductInList = async (listId, productUid) => {
|
|
214
|
+
try {
|
|
215
|
+
const response = await $directus.items('list_items').readItems({
|
|
216
|
+
filter: {
|
|
217
|
+
list: { _eq: listId },
|
|
218
|
+
magento_product_uid: { _eq: productUid }
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
return response.data.length > 0;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error('Error checking product in list:', error);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
</script>
|
|
228
|
+
|
|
229
|
+
<style scoped>
|
|
230
|
+
.v-card {
|
|
231
|
+
border-radius: 8px;
|
|
232
|
+
}
|
|
233
|
+
</style>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<v-dialog v-model="dialogOpen" justify="center">
|
|
4
|
+
<template v-slot:activator="{ props }">
|
|
5
|
+
<v-btn size="m" variant="tertiary" v-bind="props">
|
|
6
|
+
<SfIconFavorite size="m" />
|
|
7
|
+
Add to list
|
|
8
|
+
</v-btn>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<v-card max-width="500px">
|
|
12
|
+
<v-tabs v-model="tab" bg-color="info">
|
|
13
|
+
<v-tab value="one">Your Lists</v-tab>
|
|
14
|
+
<!--<v-tab value="two">Create a List</v-tab>
|
|
15
|
+
<v-tab value="three">Item Three</v-tab>-->
|
|
16
|
+
</v-tabs>
|
|
17
|
+
|
|
18
|
+
<v-card-text>
|
|
19
|
+
<v-tabs-window v-model="tab">
|
|
20
|
+
<v-tabs-window-item value="one">
|
|
21
|
+
<v-row>
|
|
22
|
+
<v-col cols="12" v-for="list in lists" :key="list.id">
|
|
23
|
+
<strong>
|
|
24
|
+
<p style="text-align: center;">Save</p>
|
|
25
|
+
</strong>
|
|
26
|
+
<v-list lines="two">
|
|
27
|
+
<v-list-item :title="list?.name" :subtitle="list?.type"
|
|
28
|
+
:prepend-avatar="`${$directus.url}/assets/${list?.image?.filename_disk}`"
|
|
29
|
+
@click="saveProductToList(list.id)" style="cursor: pointer;" :disabled="loading">
|
|
30
|
+
<template v-slot:append>
|
|
31
|
+
<v-progress-circular v-if="loading" indeterminate size="24"></v-progress-circular>
|
|
32
|
+
<v-icon v-else icon="fas:fa fa-plus"></v-icon>
|
|
33
|
+
</template>
|
|
34
|
+
</v-list-item>
|
|
35
|
+
</v-list>
|
|
36
|
+
</v-col>
|
|
37
|
+
</v-row>
|
|
38
|
+
|
|
39
|
+
<createlist />
|
|
40
|
+
</v-tabs-window-item>
|
|
41
|
+
|
|
42
|
+
<v-tabs-window-item value="two">
|
|
43
|
+
</v-tabs-window-item>
|
|
44
|
+
|
|
45
|
+
<v-tabs-window-item value="three">
|
|
46
|
+
Three
|
|
47
|
+
</v-tabs-window-item>
|
|
48
|
+
</v-tabs-window>
|
|
49
|
+
</v-card-text>
|
|
50
|
+
</v-card>
|
|
51
|
+
</v-dialog>
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<script setup>
|
|
56
|
+
import {
|
|
57
|
+
ref,
|
|
58
|
+
onMounted
|
|
59
|
+
} from 'vue'
|
|
60
|
+
import {
|
|
61
|
+
|
|
62
|
+
SfIconFavorite
|
|
63
|
+
} from '@storefront-ui/vue'
|
|
64
|
+
import list from '../features/lists.vue'
|
|
65
|
+
import createlist from '../lists/add-list.vue'
|
|
66
|
+
|
|
67
|
+
const loading = ref(false)
|
|
68
|
+
|
|
69
|
+
const route = useRoute();
|
|
70
|
+
const dialogOpen = ref(false);
|
|
71
|
+
const tab = ref(null);
|
|
72
|
+
const {
|
|
73
|
+
$directus,
|
|
74
|
+
$readItems,
|
|
75
|
+
$createItem
|
|
76
|
+
} = useNuxtApp()
|
|
77
|
+
|
|
78
|
+
const {
|
|
79
|
+
data: lists
|
|
80
|
+
} = await useAsyncData('lists', () => {
|
|
81
|
+
return $directus.request($readItems('lists', {
|
|
82
|
+
filter: {
|
|
83
|
+
status: {
|
|
84
|
+
_eq: 'Public'
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}))
|
|
88
|
+
})
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<style scoped>
|
|
92
|
+
.v-dialog {
|
|
93
|
+
border-radius: 8px;
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<v-dialog max-width="500">
|
|
4
|
+
<template v-slot:activator="{ props: activatorProps }">
|
|
5
|
+
<v-btn v-bind="activatorProps" prepend-icon="fas:fa fa-plus" color="white" text="Create a List"
|
|
6
|
+
variant="text"></v-btn>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<template v-slot:default="{ isActive }">
|
|
10
|
+
<div v-if="target.type === 'product'">
|
|
11
|
+
<addtolist :target="productId.id" />
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div v-if="target.type === 'post'">
|
|
15
|
+
<addtolist :target="postId.id" />
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
</v-dialog>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script setup>
|
|
23
|
+
import addtolist from '../lists/add-list.vue'
|
|
24
|
+
|
|
25
|
+
defineProps({
|
|
26
|
+
productId: {
|
|
27
|
+
type: String,
|
|
28
|
+
required: true
|
|
29
|
+
},
|
|
30
|
+
postId: {
|
|
31
|
+
type: String,
|
|
32
|
+
required: true
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
</script>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="item-wrapper card_1">
|
|
4
|
+
<div class="item-content">
|
|
5
|
+
<h4 class="item-title_1 mbr-fonts-style display-7">
|
|
6
|
+
<strong>{{ list?.name }}</strong>
|
|
7
|
+
</h4>
|
|
8
|
+
<p class="item-text_1 mbr-fonts-style display-4" v-dompurify-html="list?.description"></p>
|
|
9
|
+
<div class="mbr-section-btn item-footer">
|
|
10
|
+
<NuxtLink :to="`/lists/list/${list?.slug}`" class="btn item-btn btn-info-outline display-4">
|
|
11
|
+
View
|
|
12
|
+
</NuxtLink>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="item-img" v-if="list?.image?.filename_disk">
|
|
16
|
+
<img :src="`${$directus.url}assets/${list?.image?.filename_disk}`" :alt="list?.name" >
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="item-img" v-else>
|
|
20
|
+
<img src="images/image10.jpg" :alt="list?.name" />
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup>
|
|
27
|
+
const props = defineProps({
|
|
28
|
+
list: {
|
|
29
|
+
type: Object,
|
|
30
|
+
required: true,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
</script>
|