@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.
Files changed (47) hide show
  1. package/app/components/features/archived.vue +64 -0
  2. package/app/components/features/bookmarks.vue +64 -0
  3. package/app/components/features/lists.vue +61 -0
  4. package/app/components/features/starred.vue +64 -0
  5. package/app/components/lists/ListItemCard.vue +190 -0
  6. package/app/components/lists/add-bookmark.vue +52 -0
  7. package/app/components/lists/add-list-item.vue +88 -0
  8. package/app/components/lists/add-list.vue +57 -0
  9. package/app/components/lists/lists.vue +6 -0
  10. package/app/components/lists/listsettings.vue +145 -0
  11. package/app/components/lists/update-bookmark.vue +267 -0
  12. package/app/components/lists/update-list.vue +192 -0
  13. package/app/components/media/MediaPlayer.vue +302 -0
  14. package/app/components/partials/addtolist.vue +233 -0
  15. package/app/components/partials/createListBtn.vue +95 -0
  16. package/app/components/partials/listBtn.vue +35 -0
  17. package/app/components/related/list.vue +33 -0
  18. package/app/components/related/relatedlists.vue +43 -0
  19. package/app/components/tasks/TaskItem.vue +204 -0
  20. package/app/composables/bookmarks/createBookmark.js +30 -0
  21. package/app/composables/bookmarks/deleteBookmark.js +15 -0
  22. package/app/composables/bookmarks/updateBookmark.js +15 -0
  23. package/app/composables/config.ts +17 -0
  24. package/app/composables/content/uploadFiles.js +41 -0
  25. package/app/composables/globals/useDirectusForm.ts +1 -0
  26. package/app/composables/lists/createList.js +25 -0
  27. package/app/composables/lists/deleteList.js +14 -0
  28. package/app/composables/lists/updateList.js +20 -0
  29. package/app/composables/lists/useBookmarks.js +69 -0
  30. package/app/composables/lists/useLists.js +120 -0
  31. package/app/composables/lists/usePlaylist.js +64 -0
  32. package/app/composables/lists/useSaved.js +29 -0
  33. package/app/composables/lists/useTasks.js +86 -0
  34. package/app/composables/lists/useWishlist.js +51 -0
  35. package/app/composables/providers/atproto.ts +156 -0
  36. package/app/composables/providers/directus.ts +49 -0
  37. package/app/composables/providers/memory.ts +88 -0
  38. package/app/composables/registry.ts +13 -0
  39. package/app/composables/types.ts +35 -0
  40. package/app/composables/useLists.ts +20 -0
  41. package/app/composables/utils/transforms.ts +42 -0
  42. package/app/composables/utils/validation.ts +21 -0
  43. package/app/pages/lists/bookmark/[id].vue +76 -0
  44. package/app/pages/lists/index.vue +152 -0
  45. package/app/pages/lists/list/[...slug].vue +233 -0
  46. package/nuxt.config.ts +11 -0
  47. 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>