@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,29 @@
|
|
|
1
|
+
export const useSaved = () => {
|
|
2
|
+
const { $directus } = useNuxtApp();
|
|
3
|
+
const user = useSupabaseUser();
|
|
4
|
+
|
|
5
|
+
const isSaved = async (productId) => {
|
|
6
|
+
if (!user.value) return false;
|
|
7
|
+
|
|
8
|
+
const lists = await $directus.items('lists').readItems({
|
|
9
|
+
filter: { user: { _eq: user.value.id } },
|
|
10
|
+
fields: ['id']
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const listIds = lists.map((l) => l.id);
|
|
14
|
+
|
|
15
|
+
if (listIds.length === 0) return false;
|
|
16
|
+
|
|
17
|
+
const saved = await $directus.items('list_items').readItems({
|
|
18
|
+
filter: {
|
|
19
|
+
list: { _in: listIds },
|
|
20
|
+
product: { _eq: productId }
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return saved.length > 0;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return { isSaved };
|
|
28
|
+
};
|
|
29
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export const useTasks = () => {
|
|
2
|
+
const { createList, addToList, getUserLists, updateListItem } = useLists()
|
|
3
|
+
|
|
4
|
+
const createTaskList = async (name, description = '') => {
|
|
5
|
+
return await createList({
|
|
6
|
+
name,
|
|
7
|
+
description,
|
|
8
|
+
type: 'tasks',
|
|
9
|
+
visibility: 'private'
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const addTask = async (listId, taskData) => {
|
|
14
|
+
const { title, description, priority, due_date, labels } = taskData
|
|
15
|
+
|
|
16
|
+
return await addToList(listId, {
|
|
17
|
+
type: 'task',
|
|
18
|
+
title,
|
|
19
|
+
description,
|
|
20
|
+
priority: priority || 'medium',
|
|
21
|
+
due_date,
|
|
22
|
+
labels: labels || [],
|
|
23
|
+
completed: false,
|
|
24
|
+
date_created: new Date().toISOString(),
|
|
25
|
+
subtasks: []
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const completeTask = async (itemId) => {
|
|
30
|
+
return await updateListItem(itemId, {
|
|
31
|
+
'content.completed': true,
|
|
32
|
+
'content.date_completed': new Date().toISOString()
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const uncompleteTask = async (itemId) => {
|
|
37
|
+
return await updateListItem(itemId, {
|
|
38
|
+
'content.completed': false,
|
|
39
|
+
'content.date_completed': null
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const updateTaskPriority = async (itemId, priority) => {
|
|
44
|
+
return await updateListItem(itemId, {
|
|
45
|
+
'content.priority': priority
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const updateTaskDueDate = async (itemId, dueDate) => {
|
|
50
|
+
return await updateListItem(itemId, {
|
|
51
|
+
'content.due_date': dueDate
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const addSubtask = async (itemId, subtaskTitle) => {
|
|
56
|
+
const { getListById } = useLists()
|
|
57
|
+
const item = await getListById(itemId)
|
|
58
|
+
const subtasks = item.content.subtasks || []
|
|
59
|
+
|
|
60
|
+
subtasks.push({
|
|
61
|
+
id: Date.now(),
|
|
62
|
+
title: subtaskTitle,
|
|
63
|
+
completed: false,
|
|
64
|
+
date_created: new Date().toISOString()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return await updateListItem(itemId, {
|
|
68
|
+
'content.subtasks': subtasks
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const getUserTaskLists = async () => {
|
|
73
|
+
return await getUserLists('tasks')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
createTaskList,
|
|
78
|
+
addTask,
|
|
79
|
+
completeTask,
|
|
80
|
+
uncompleteTask,
|
|
81
|
+
updateTaskPriority,
|
|
82
|
+
updateTaskDueDate,
|
|
83
|
+
addSubtask,
|
|
84
|
+
getUserTaskLists
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const useWishlist = () => {
|
|
2
|
+
const { createList, addToList, getUserLists, removeFromList } = useLists()
|
|
3
|
+
|
|
4
|
+
const createWishlist = async (name = 'My Wishlist', description = '') => {
|
|
5
|
+
return await createList({
|
|
6
|
+
name,
|
|
7
|
+
description,
|
|
8
|
+
type: 'wishlist',
|
|
9
|
+
visibility: 'private'
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const addToWishlist = async (wishlistId, itemData) => {
|
|
14
|
+
const { title, url, price, image, description, category } = itemData
|
|
15
|
+
|
|
16
|
+
return await addToList(wishlistId, {
|
|
17
|
+
type: 'product',
|
|
18
|
+
title,
|
|
19
|
+
url,
|
|
20
|
+
price,
|
|
21
|
+
image,
|
|
22
|
+
description,
|
|
23
|
+
category,
|
|
24
|
+
date_added: new Date().toISOString(),
|
|
25
|
+
priority: itemData.priority || 'medium'
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const getUserWishlists = async () => {
|
|
30
|
+
return await getUserLists('wishlist')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const removeFromWishlist = async (itemId) => {
|
|
34
|
+
return await removeFromList(itemId)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const updateWishlistItemPriority = async (itemId, priority) => {
|
|
38
|
+
const { updateListItem } = useLists()
|
|
39
|
+
return await updateListItem(itemId, {
|
|
40
|
+
'content.priority': priority
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
createWishlist,
|
|
46
|
+
addToWishlist,
|
|
47
|
+
getUserWishlists,
|
|
48
|
+
removeFromWishlist,
|
|
49
|
+
updateWishlistItemPriority
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { registerListsProvider } from '../registry'
|
|
2
|
+
import type { ListsProvider, List, ListItem } from '../types'
|
|
3
|
+
import { wrapSocialRequest } from '@meeovi/social'
|
|
4
|
+
import { transformList, transformItem } from '../utils/transforms'
|
|
5
|
+
import { validateListInput, validateItemInput } from '../utils/validation'
|
|
6
|
+
import { getListsConfig } from '../config'
|
|
7
|
+
|
|
8
|
+
async function atprotoFetch(path: string, options: RequestInit = {}) {
|
|
9
|
+
const { baseUrl, apiKey } = getListsConfig()
|
|
10
|
+
|
|
11
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
12
|
+
...options,
|
|
13
|
+
headers: {
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
16
|
+
...(options.headers || {})
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
const error: any = new Error(`ATProto error: ${res.status}`)
|
|
22
|
+
error.status = res.status
|
|
23
|
+
error.response = res
|
|
24
|
+
throw error
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return res.json()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const AtprotoListsProvider: ListsProvider = {
|
|
31
|
+
async getList(id) {
|
|
32
|
+
return wrapSocialRequest('atproto', async () => {
|
|
33
|
+
const data = await atprotoFetch(`/xrpc/app.bsky.graph.getList?list=${id}`)
|
|
34
|
+
return transformList(data.list)
|
|
35
|
+
}, {
|
|
36
|
+
cacheKey: `atproto:list:${id}`,
|
|
37
|
+
ttlMs: 1000 * 30,
|
|
38
|
+
retry: true,
|
|
39
|
+
swr: true
|
|
40
|
+
})
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
async listLists() {
|
|
44
|
+
return wrapSocialRequest('atproto', async () => {
|
|
45
|
+
const data = await atprotoFetch(`/xrpc/app.bsky.graph.getLists`)
|
|
46
|
+
return data.lists.map(transformList)
|
|
47
|
+
}, {
|
|
48
|
+
cacheKey: `atproto:lists`,
|
|
49
|
+
ttlMs: 1000 * 30,
|
|
50
|
+
retry: true,
|
|
51
|
+
swr: true
|
|
52
|
+
})
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async createList(data) {
|
|
56
|
+
validateListInput(data)
|
|
57
|
+
|
|
58
|
+
return wrapSocialRequest('atproto', async () => {
|
|
59
|
+
const result = await atprotoFetch(`/xrpc/app.bsky.graph.createList`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
name: data.title,
|
|
63
|
+
purpose: data.type ?? 'list',
|
|
64
|
+
description: data.metadata?.description ?? ''
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return transformList(result)
|
|
69
|
+
})
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async updateList(id, data) {
|
|
73
|
+
validateListInput(data)
|
|
74
|
+
|
|
75
|
+
return wrapSocialRequest('atproto', async () => {
|
|
76
|
+
const result = await atprotoFetch(`/xrpc/app.bsky.graph.updateList`, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
list: id,
|
|
80
|
+
name: data.title,
|
|
81
|
+
description: data.metadata?.description
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
return transformList(result)
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
async deleteList(id) {
|
|
90
|
+
return wrapSocialRequest('atproto', async () => {
|
|
91
|
+
await atprotoFetch(`/xrpc/app.bsky.graph.deleteList`, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
body: JSON.stringify({ list: id })
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
async addItem(listId, item) {
|
|
99
|
+
validateItemInput(item)
|
|
100
|
+
|
|
101
|
+
return wrapSocialRequest('atproto', async () => {
|
|
102
|
+
const result = await atprotoFetch(`/xrpc/app.bsky.graph.addListItem`, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
list: listId,
|
|
106
|
+
subject: item.title // ATProto uses "subject" for list entries
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return transformItem(result)
|
|
111
|
+
})
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
async updateItem(listId, itemId, data) {
|
|
115
|
+
validateItemInput(data)
|
|
116
|
+
|
|
117
|
+
return wrapSocialRequest('atproto', async () => {
|
|
118
|
+
const result = await atprotoFetch(`/xrpc/app.bsky.graph.updateListItem`, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
body: JSON.stringify({
|
|
121
|
+
list: listId,
|
|
122
|
+
item: itemId,
|
|
123
|
+
...data
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
return transformItem(result)
|
|
128
|
+
})
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
async deleteItem(listId, itemId) {
|
|
132
|
+
return wrapSocialRequest('atproto', async () => {
|
|
133
|
+
await atprotoFetch(`/xrpc/app.bsky.graph.deleteListItem`, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
list: listId,
|
|
137
|
+
item: itemId
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
async reorderItems(listId, itemIds) {
|
|
144
|
+
return wrapSocialRequest('atproto', async () => {
|
|
145
|
+
await atprotoFetch(`/xrpc/app.bsky.graph.reorderListItems`, {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
body: JSON.stringify({
|
|
148
|
+
list: listId,
|
|
149
|
+
items: itemIds
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
registerListsProvider('atproto', AtprotoListsProvider)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { registerListsProvider } from '../registry'
|
|
2
|
+
import type { ListsProvider } from '../types'
|
|
3
|
+
import { fetcher } from '@meeovi/api'
|
|
4
|
+
|
|
5
|
+
const DirectusListsProvider: ListsProvider = {
|
|
6
|
+
async getList(id) {
|
|
7
|
+
const { data } = await fetcher('lists.GET_LIST', { id })
|
|
8
|
+
return data.list
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
async listLists() {
|
|
12
|
+
const { data } = await fetcher('lists.LIST_LISTS')
|
|
13
|
+
return data.lists
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
async createList(data) {
|
|
17
|
+
const { data: result } = await fetcher('lists.CREATE_LIST', { data })
|
|
18
|
+
return result.list
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
async updateList(id, data) {
|
|
22
|
+
const { data: result } = await fetcher('lists.UPDATE_LIST', { id, data })
|
|
23
|
+
return result.list
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
async deleteList(id) {
|
|
27
|
+
await fetcher('lists.DELETE_LIST', { id })
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
async addItem(listId, item) {
|
|
31
|
+
const { data } = await fetcher('lists.ADD_ITEM', { listId, item })
|
|
32
|
+
return data.item
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
async updateItem(listId, itemId, data) {
|
|
36
|
+
const { data: result } = await fetcher('lists.UPDATE_ITEM', { listId, itemId, data })
|
|
37
|
+
return result.item
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async deleteItem(listId, itemId) {
|
|
41
|
+
await fetcher('lists.DELETE_ITEM', { listId, itemId })
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
async reorderItems(listId, itemIds) {
|
|
45
|
+
await fetcher('lists.REORDER_ITEMS', { listId, itemIds })
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
registerListsProvider('directus', DirectusListsProvider)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { registerListsProvider } from '../registry'
|
|
2
|
+
import type { List, ListItem, ListsProvider } from '../types'
|
|
3
|
+
import { nanoid } from 'nanoid'
|
|
4
|
+
|
|
5
|
+
const lists = new Map<string, List>()
|
|
6
|
+
|
|
7
|
+
const MemoryListsProvider: ListsProvider = {
|
|
8
|
+
async getList(id) {
|
|
9
|
+
const list = lists.get(id)
|
|
10
|
+
if (!list) throw new Error(`List ${id} not found`)
|
|
11
|
+
return list
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
async listLists() {
|
|
15
|
+
return Array.from(lists.values())
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
async createList(data) {
|
|
19
|
+
const id = nanoid()
|
|
20
|
+
const list: List = {
|
|
21
|
+
id,
|
|
22
|
+
title: data.title || 'Untitled List',
|
|
23
|
+
type: data.type || 'list',
|
|
24
|
+
items: [],
|
|
25
|
+
metadata: data.metadata || {},
|
|
26
|
+
createdAt: new Date().toISOString(),
|
|
27
|
+
updatedAt: new Date().toISOString()
|
|
28
|
+
}
|
|
29
|
+
lists.set(id, list)
|
|
30
|
+
return list
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
async updateList(id, data) {
|
|
34
|
+
const list = await this.getList(id)
|
|
35
|
+
const updated = {
|
|
36
|
+
...list,
|
|
37
|
+
...data,
|
|
38
|
+
updatedAt: new Date().toISOString()
|
|
39
|
+
}
|
|
40
|
+
lists.set(id, updated)
|
|
41
|
+
return updated
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
async deleteList(id) {
|
|
45
|
+
lists.delete(id)
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async addItem(listId, item) {
|
|
49
|
+
const list = await this.getList(listId)
|
|
50
|
+
const newItem: ListItem = {
|
|
51
|
+
id: nanoid(),
|
|
52
|
+
title: item.title || '',
|
|
53
|
+
description: item.description,
|
|
54
|
+
completed: item.completed || false,
|
|
55
|
+
position: list.items.length,
|
|
56
|
+
parentId: item.parentId,
|
|
57
|
+
metadata: item.metadata || {},
|
|
58
|
+
createdAt: new Date().toISOString(),
|
|
59
|
+
updatedAt: new Date().toISOString()
|
|
60
|
+
}
|
|
61
|
+
list.items.push(newItem)
|
|
62
|
+
return newItem
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
async updateItem(listId, itemId, data) {
|
|
66
|
+
const list = await this.getList(listId)
|
|
67
|
+
const item = list.items.find(i => i.id === itemId)
|
|
68
|
+
if (!item) throw new Error(`Item ${itemId} not found`)
|
|
69
|
+
Object.assign(item, data, { updatedAt: new Date().toISOString() })
|
|
70
|
+
return item
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async deleteItem(listId, itemId) {
|
|
74
|
+
const list = await this.getList(listId)
|
|
75
|
+
list.items = list.items.filter(i => i.id !== itemId)
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async reorderItems(listId, itemIds) {
|
|
79
|
+
const list = await this.getList(listId)
|
|
80
|
+
const newOrder = itemIds.map(id => list.items.find(i => i.id === id)!)
|
|
81
|
+
list.items = newOrder.map((item, index) => ({
|
|
82
|
+
...item,
|
|
83
|
+
position: index
|
|
84
|
+
}))
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
registerListsProvider('memory', MemoryListsProvider)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ListsProvider } from './types'
|
|
2
|
+
|
|
3
|
+
const providers: Record<string, ListsProvider> = {}
|
|
4
|
+
|
|
5
|
+
export function registerListsProvider(name: string, provider: ListsProvider) {
|
|
6
|
+
providers[name] = provider
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getListsProvider(name: string): ListsProvider {
|
|
10
|
+
const provider = providers[name]
|
|
11
|
+
if (!provider) throw new Error(`Lists provider "${name}" not found`)
|
|
12
|
+
return provider
|
|
13
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface ListItem {
|
|
2
|
+
id: string
|
|
3
|
+
title: string
|
|
4
|
+
description?: string
|
|
5
|
+
completed?: boolean
|
|
6
|
+
position?: number
|
|
7
|
+
parentId?: string
|
|
8
|
+
metadata?: Record<string, any>
|
|
9
|
+
createdAt?: string
|
|
10
|
+
updatedAt?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface List {
|
|
14
|
+
id: string
|
|
15
|
+
title: string
|
|
16
|
+
type: 'checklist' | 'kanban' | 'list' | string
|
|
17
|
+
items: ListItem[]
|
|
18
|
+
metadata?: Record<string, any>
|
|
19
|
+
createdAt?: string
|
|
20
|
+
updatedAt?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ListsProvider {
|
|
24
|
+
getList(id: string): Promise<List>
|
|
25
|
+
listLists(params?: Record<string, any>): Promise<List[]>
|
|
26
|
+
createList(data: Partial<List>): Promise<List>
|
|
27
|
+
updateList(id: string, data: Partial<List>): Promise<List>
|
|
28
|
+
deleteList(id: string): Promise<void>
|
|
29
|
+
|
|
30
|
+
addItem(listId: string, item: Partial<ListItem>): Promise<ListItem>
|
|
31
|
+
updateItem(listId: string, itemId: string, data: Partial<ListItem>): Promise<ListItem>
|
|
32
|
+
deleteItem(listId: string, itemId: string): Promise<void>
|
|
33
|
+
|
|
34
|
+
reorderItems?(listId: string, itemIds: string[]): Promise<void>
|
|
35
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getListsConfig } from './config'
|
|
2
|
+
import { getListsProvider } from './registry'
|
|
3
|
+
|
|
4
|
+
export function useLists() {
|
|
5
|
+
const { provider } = getListsConfig()
|
|
6
|
+
const lists = getListsProvider(provider)
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
getList: lists.getList,
|
|
10
|
+
listLists: lists.listLists,
|
|
11
|
+
createList: lists.createList,
|
|
12
|
+
updateList: lists.updateList,
|
|
13
|
+
deleteList: lists.deleteList,
|
|
14
|
+
|
|
15
|
+
addItem: lists.addItem,
|
|
16
|
+
updateItem: lists.updateItem,
|
|
17
|
+
deleteItem: lists.deleteItem,
|
|
18
|
+
reorderItems: lists.reorderItems
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { List, ListItem } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize a raw provider list into Meeovi's List shape.
|
|
5
|
+
*/
|
|
6
|
+
export function transformList(raw: any): List {
|
|
7
|
+
return {
|
|
8
|
+
id: raw.id,
|
|
9
|
+
title: raw.title ?? raw.name ?? 'Untitled',
|
|
10
|
+
type: raw.type ?? 'list',
|
|
11
|
+
items: Array.isArray(raw.items)
|
|
12
|
+
? raw.items.map(transformItem)
|
|
13
|
+
: [],
|
|
14
|
+
metadata: raw.metadata ?? {},
|
|
15
|
+
createdAt: raw.createdAt ?? raw.created_at ?? null,
|
|
16
|
+
updatedAt: raw.updatedAt ?? raw.updated_at ?? null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalize a raw provider item into Meeovi's ListItem shape.
|
|
22
|
+
*/
|
|
23
|
+
export function transformItem(raw: any): ListItem {
|
|
24
|
+
return {
|
|
25
|
+
id: raw.id,
|
|
26
|
+
title: raw.title ?? raw.name ?? '',
|
|
27
|
+
description: raw.description ?? raw.body ?? '',
|
|
28
|
+
completed: raw.completed ?? raw.done ?? false,
|
|
29
|
+
position: raw.position ?? raw.order ?? 0,
|
|
30
|
+
parentId: raw.parentId ?? raw.parent_id ?? null,
|
|
31
|
+
metadata: raw.metadata ?? {},
|
|
32
|
+
createdAt: raw.createdAt ?? raw.created_at ?? null,
|
|
33
|
+
updatedAt: raw.updatedAt ?? raw.updated_at ?? null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Normalize arrays safely.
|
|
39
|
+
*/
|
|
40
|
+
export function transformListArray(raw: any[]): List[] {
|
|
41
|
+
return raw.map(transformList)
|
|
42
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { List, ListItem } from '../types'
|
|
2
|
+
|
|
3
|
+
export function validateListInput(data: Partial<List>) {
|
|
4
|
+
if (!data.title || typeof data.title !== 'string') {
|
|
5
|
+
throw new Error('List title is required and must be a string')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (data.type && typeof data.type !== 'string') {
|
|
9
|
+
throw new Error('List type must be a string')
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function validateItemInput(data: Partial<ListItem>) {
|
|
14
|
+
if (!data.title || typeof data.title !== 'string') {
|
|
15
|
+
throw new Error('Item title is required and must be a string')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (data.completed !== undefined && typeof data.completed !== 'boolean') {
|
|
19
|
+
throw new Error('Item completed must be a boolean')
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-row class="contentPage">
|
|
3
|
+
<v-col cols="12">
|
|
4
|
+
<v-card class="mx-auto" max-width="800px" elevation="0">
|
|
5
|
+
<NuxtImg loading="lazy" class="align-end text-white" height="200" :src="`${$directus.url}/assets/${website?.image?.filename_disk}`" :alt="website?.name" cover />
|
|
6
|
+
<v-card-title>{{ website?.name }}</v-card-title>
|
|
7
|
+
|
|
8
|
+
<v-card-subtitle class="pt-4">
|
|
9
|
+
Created: {{ new Date(website?.created_at).toLocaleDateString() }}
|
|
10
|
+
</v-card-subtitle>
|
|
11
|
+
|
|
12
|
+
<v-card-text>
|
|
13
|
+
<div>Type: {{ website?.type }}</div>
|
|
14
|
+
|
|
15
|
+
<div>{{ website?.note }}</div>
|
|
16
|
+
</v-card-text>
|
|
17
|
+
|
|
18
|
+
<v-card-actions>
|
|
19
|
+
<updatebookmark />
|
|
20
|
+
|
|
21
|
+
<v-spacer></v-spacer>
|
|
22
|
+
<v-btn color="orange" text="Visit" :href="website?.url"></v-btn>
|
|
23
|
+
</v-card-actions>
|
|
24
|
+
</v-card>
|
|
25
|
+
</v-col>
|
|
26
|
+
|
|
27
|
+
<v-divider></v-divider>
|
|
28
|
+
<v-col cols="12">
|
|
29
|
+
<comments />
|
|
30
|
+
</v-col>
|
|
31
|
+
</v-row>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script setup>
|
|
35
|
+
import {
|
|
36
|
+
ref,
|
|
37
|
+
computed
|
|
38
|
+
} from 'vue'
|
|
39
|
+
import updatebookmark from '#lists/app/components/lists/update-bookmark.vue'
|
|
40
|
+
import createListBtn from '#lists/app/components/partials/createListBtn.vue'
|
|
41
|
+
import comments from '#social/app/components/comments.vue'
|
|
42
|
+
|
|
43
|
+
const route = useRoute();
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
$directus,
|
|
47
|
+
$readItem
|
|
48
|
+
} = useNuxtApp()
|
|
49
|
+
|
|
50
|
+
const {
|
|
51
|
+
data: website
|
|
52
|
+
} = await useAsyncData('website', () => {
|
|
53
|
+
return $directus.request($readItem('websites', route.params.id, {
|
|
54
|
+
fields: ['*', {
|
|
55
|
+
'*': ['*']
|
|
56
|
+
}]
|
|
57
|
+
}))
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
// Add this debug log
|
|
62
|
+
watchEffect(() => {
|
|
63
|
+
if (website.value) {
|
|
64
|
+
console.log('Fetched website data:', website.value)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
useHead({
|
|
70
|
+
title: computed(() => website?.value?.name || 'Bookmark Page')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
definePageMeta({
|
|
74
|
+
middleware: ['authenticated'],
|
|
75
|
+
})
|
|
76
|
+
</script>
|