@finema/finework-layer 2.0.15 → 2.0.16

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/CHANGELOG.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.16](https://gitlab.finema.co/finema/finework/finework-frontend-layer/compare/2.0.15...2.0.16) (2026-07-31)
4
+
3
5
  ## [2.0.15](https://gitlab.finema.co/finema/finework/finework-frontend-layer/compare/2.0.14...2.0.15) (2026-07-27)
4
6
 
5
7
  ## [2.0.14](https://gitlab.finema.co/finema/finework/finework-frontend-layer/compare/2.0.13...2.0.14) (2026-07-22)
@@ -0,0 +1,245 @@
1
+ <template>
2
+ <Popover
3
+ v-model:open="isOpen"
4
+ :content="{
5
+ align: 'end',
6
+ side: 'bottom',
7
+ sideOffset: 8,
8
+ }"
9
+ >
10
+ <Icon
11
+ name="ph:star"
12
+ class="size-6 cursor-pointer text-gray-500"
13
+ title="Add bookmark"
14
+ />
15
+
16
+ <template #content>
17
+ <Card class="w-[360px]">
18
+ <div class="mb-3 flex items-center justify-between">
19
+ <h3 class="text-base font-semibold">
20
+ Add bookmark
21
+ </h3>
22
+ <button
23
+ type="button"
24
+ class="flex size-7 cursor-pointer items-center justify-center rounded-full text-gray-400 hover:bg-gray-100"
25
+ @click="isOpen = false"
26
+ >
27
+ <Icon
28
+ name="ph:x"
29
+ class="size-4"
30
+ />
31
+ </button>
32
+ </div>
33
+
34
+ <div class="flex gap-3">
35
+ <div class="flex size-16 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-gray-100">
36
+ <Loader :loading="preview.status.value.isLoading">
37
+ <img
38
+ v-if="thumbnailUrl"
39
+ :src="thumbnailUrl"
40
+ class="size-16 object-cover"
41
+ />
42
+ <Icon
43
+ v-else
44
+ name="ph:link"
45
+ class="size-6 text-gray-400"
46
+ />
47
+ </Loader>
48
+ </div>
49
+
50
+ <div class="flex flex-1 flex-col gap-2">
51
+ <Input
52
+ v-model="url"
53
+ placeholder="วางลิงก์ที่ต้องการ bookmark"
54
+ size="lg"
55
+ @blur="onFetchPreview"
56
+ @keyup.enter="onFetchPreview"
57
+ />
58
+ <Input
59
+ v-model="name"
60
+ placeholder="Name"
61
+ size="lg"
62
+ />
63
+ <div
64
+ v-if="!isCreatingFolder"
65
+ class="flex gap-2"
66
+ >
67
+ <Select
68
+ v-if="folderOptions.length > 0"
69
+ v-model="folderId"
70
+ :items="folderOptions"
71
+ placeholder="Folder"
72
+ size="lg"
73
+ class="flex-1"
74
+ />
75
+ <Button
76
+ icon="ph:folder-plus"
77
+ variant="outline"
78
+ color="neutral"
79
+ size="lg"
80
+ :class="{ 'flex-1 justify-center': folderOptions.length === 0 }"
81
+ :label="folderOptions.length === 0 ? 'New folder' : undefined"
82
+ title="New folder"
83
+ @click="isCreatingFolder = true"
84
+ />
85
+ </div>
86
+ <div
87
+ v-else
88
+ class="flex gap-2"
89
+ >
90
+ <Input
91
+ v-model="newFolderName"
92
+ placeholder="New folder name"
93
+ size="lg"
94
+ autofocus
95
+ @keyup.enter="onCreateFolder"
96
+ />
97
+ <Button
98
+ icon="ph:check"
99
+ size="lg"
100
+ :loading="folders.add.status.isLoading"
101
+ :disabled="!newFolderName"
102
+ @click="onCreateFolder"
103
+ />
104
+ <Button
105
+ icon="ph:x"
106
+ size="lg"
107
+ variant="outline"
108
+ color="neutral"
109
+ @click="isCreatingFolder = false; newFolderName = ''"
110
+ />
111
+ </div>
112
+ </div>
113
+ </div>
114
+
115
+ <div class="mt-4 flex justify-end gap-2">
116
+ <Button
117
+ v-if="isBookmarked"
118
+ variant="outline"
119
+ color="neutral"
120
+ :loading="bookmarks.delete.status.isLoading"
121
+ @click="onRemove"
122
+ >
123
+ Remove
124
+ </Button>
125
+ <Button
126
+ :loading="bookmarks.add.status.isLoading || bookmarks.update.status.isLoading"
127
+ :disabled="!url || !name"
128
+ @click="onSave"
129
+ >
130
+ Done
131
+ </Button>
132
+ </div>
133
+ </Card>
134
+ </template>
135
+ </Popover>
136
+ </template>
137
+
138
+ <script lang="ts" setup>
139
+ import { useBookmarkFoldersLoader, useBookmarkPreviewLoader, useBookmarksLoader } from '#layer/app/loaders/bookmarks/bookmarks'
140
+
141
+ const isOpen = ref(false)
142
+ const url = ref('')
143
+ const name = ref('')
144
+ const folderId = ref<string | undefined>(undefined)
145
+ const thumbnailUrl = ref<string | null>(null)
146
+ const isCreatingFolder = ref(false)
147
+ const newFolderName = ref('')
148
+
149
+ const bookmarks = useBookmarksLoader()
150
+ const folders = useBookmarkFoldersLoader()
151
+ const preview = useBookmarkPreviewLoader()
152
+
153
+ const existing = computed(() => bookmarks.fetch.items.find((item) => item.url === url.value) || null)
154
+ const isBookmarked = computed(() => !!existing.value)
155
+
156
+ const folderOptions = computed(() => folders.fetch.items.map((folder) => ({
157
+ label: folder.name,
158
+ value: folder.id,
159
+ })))
160
+
161
+ const defaultFolderId = computed(() => folders.fetch.items[0]?.id)
162
+
163
+ onMounted(() => {
164
+ bookmarks.fetchAll()
165
+ folders.fetchAll()
166
+ })
167
+
168
+ watch(isOpen, (open) => {
169
+ if (!open) return
170
+
171
+ isCreatingFolder.value = false
172
+ newFolderName.value = ''
173
+ url.value = ''
174
+ name.value = ''
175
+ thumbnailUrl.value = null
176
+ folderId.value = defaultFolderId.value
177
+ })
178
+
179
+ const onFetchPreview = () => {
180
+ if (!url.value) return
181
+
182
+ if (existing.value) {
183
+ name.value = existing.value.name
184
+ folderId.value = existing.value.folder_id ?? defaultFolderId.value
185
+ thumbnailUrl.value = existing.value.favicon_url || existing.value.image_url
186
+
187
+ return
188
+ }
189
+
190
+ preview.run({
191
+ params: {
192
+ url: url.value,
193
+ },
194
+ }).then(() => {
195
+ if (!preview.data.value) return
196
+ if (!name.value) name.value = preview.data.value.name
197
+ thumbnailUrl.value = preview.data.value.favicon_url || preview.data.value.image_url || null
198
+ })
199
+ }
200
+
201
+ const onCreateFolder = async () => {
202
+ if (!newFolderName.value) return
203
+
204
+ await folders.addRun({
205
+ data: {
206
+ name: newFolderName.value,
207
+ },
208
+ })
209
+
210
+ if (folders.add.item) {
211
+ folderId.value = folders.add.item.id
212
+ folders.fetchAll()
213
+ }
214
+
215
+ newFolderName.value = ''
216
+ isCreatingFolder.value = false
217
+ }
218
+
219
+ const onSave = async () => {
220
+ if (existing.value) {
221
+ await bookmarks.updateRun(existing.value.id, {
222
+ data: {
223
+ name: name.value,
224
+ folder_id: folderId.value,
225
+ },
226
+ })
227
+ } else {
228
+ await bookmarks.addRun({
229
+ data: {
230
+ url: url.value,
231
+ name: name.value,
232
+ folder_id: folderId.value,
233
+ },
234
+ })
235
+ }
236
+
237
+ isOpen.value = false
238
+ }
239
+
240
+ const onRemove = async () => {
241
+ if (!existing.value) return
242
+ await bookmarks.deleteRun(existing.value.id)
243
+ isOpen.value = false
244
+ }
245
+ </script>
@@ -108,6 +108,7 @@
108
108
  </div>
109
109
 
110
110
  <div class="flex items-center justify-center gap-4">
111
+ <BookmarkButton />
111
112
  <Notifications :ui="{ content: 'mt-[64px] lg:mt-[72px]' }" />
112
113
  <DropdownMenu
113
114
  size="xl"
@@ -17,6 +17,7 @@
17
17
  </div>
18
18
 
19
19
  <div class="flex flex-1 items-center justify-end gap-4">
20
+ <BookmarkButton />
20
21
  <Notifications :ui="{ content: 'mt-[80px]' }" />
21
22
  <DropdownMenu
22
23
  :items="userMenuItems"
@@ -38,6 +38,11 @@ export const routes = {
38
38
  to: '/account/company-directory',
39
39
  icon: 'ph:building-office',
40
40
  },
41
+ bookmarks: {
42
+ label: 'Bookmarks',
43
+ to: '/account/bookmarks',
44
+ icon: 'ph:star',
45
+ },
41
46
  },
42
47
 
43
48
  announcements: {
@@ -276,5 +281,6 @@ export const sidebarProfile = [
276
281
  routes.account.profile,
277
282
  routes.account.myTeam,
278
283
  routes.account.companyDirectory,
284
+ routes.account.bookmarks,
279
285
  routes.logout,
280
286
  ]
@@ -0,0 +1,53 @@
1
+ export interface IBookmarkFolder {
2
+ id: string
3
+ name: string
4
+ created_at: string
5
+ updated_at: string
6
+ }
7
+
8
+ export interface IBookmark {
9
+ id: string
10
+ name: string
11
+ url: string
12
+ folder_id: string | null
13
+ favicon_url: string | null
14
+ image_url: string | null
15
+ created_at: string
16
+ updated_at: string
17
+ }
18
+
19
+ export interface IBookmarkPreview {
20
+ name: string
21
+ url: string
22
+ favicon_url: string
23
+ image_url: string
24
+ bookmark: IBookmark | null
25
+ }
26
+
27
+ export const useBookmarkFoldersLoader = () => {
28
+ const options = useRequestOptions()
29
+
30
+ return usePageLoader<IBookmarkFolder>({
31
+ baseURL: '/bookmark-folders',
32
+ getBaseRequestOptions: options.auth,
33
+ })
34
+ }
35
+
36
+ export const useBookmarksLoader = () => {
37
+ const options = useRequestOptions()
38
+
39
+ return usePageLoader<IBookmark>({
40
+ baseURL: '/bookmarks',
41
+ getBaseRequestOptions: options.auth,
42
+ })
43
+ }
44
+
45
+ export const useBookmarkPreviewLoader = () => {
46
+ const options = useRequestOptions()
47
+
48
+ return useObjectLoader<IBookmarkPreview>({
49
+ method: 'GET',
50
+ url: '/bookmarks/preview',
51
+ getRequestOptions: options.auth,
52
+ })
53
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@finema/finework-layer",
3
3
  "type": "module",
4
- "version": "2.0.15",
4
+ "version": "2.0.16",
5
5
  "main": "./nuxt.config.ts",
6
6
  "scripts": {
7
7
  "dev": "nuxi dev .playground -o",