Skip to content

Commit 4e017c4

Browse files
committed
feat(bookmark): improve bookmark management with breadcrumb navigation & image handling
- Replace back button with proper breadcrumb navigation pattern - Add ability to navigate directly to any folder level in path - Improve image handling with drag & drop support and better compression - Add "Open in new tab" option to bookmark context menu - Enhance favicon loading with fallback sources and error states - Improve UI feedback and accessibility throughout Closes #12
1 parent 1ac7a65 commit 4e017c4

File tree

5 files changed

+346
-68
lines changed

5 files changed

+346
-68
lines changed

src/context/bookmark.context.tsx

+84-24
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { toast } from 'react-hot-toast'
33
import { getFromStorage, setToStorage } from '../common/storage'
44
import type { Bookmark } from '../layouts/search/bookmarks/types/bookmark.types'
55

6-
const MAX_BOOKMARK_SIZE = 1024 * 1024
6+
const MAX_BOOKMARK_SIZE = 1.5 * 1024 * 1024
77

88
export interface BookmarkStoreContext {
99
bookmarks: Bookmark[]
@@ -61,50 +61,103 @@ export const BookmarkProvider: React.FC<{ children: React.ReactNode }> = ({
6161

6262
const getBookmarkDataSize = (bookmark: Bookmark): number => {
6363
try {
64+
if (bookmark.customImage) {
65+
const base64Data = bookmark.customImage.split(',')[1] || bookmark.customImage
66+
67+
const imageSize = Math.ceil((base64Data.length * 3) / 4)
68+
69+
const bookmarkWithoutImage = { ...bookmark }
70+
bookmarkWithoutImage.customImage = undefined
71+
const jsonSize = new Blob([JSON.stringify(bookmarkWithoutImage)]).size
72+
73+
return imageSize + jsonSize
74+
}
75+
6476
const json = JSON.stringify(bookmark)
6577
return new Blob([json]).size
6678
} catch (e) {
79+
console.error('Error calculating bookmark size:', e)
6780
return Number.POSITIVE_INFINITY
6881
}
6982
}
7083

71-
const compressImageData = (imageData: string): string => {
84+
const compressImageData = async (imageData: string): Promise<string> => {
7285
if (!imageData.startsWith('data:image')) {
7386
return imageData
7487
}
7588

76-
const base64 = imageData.split(',')[1]
77-
const binaryString = window.atob(base64)
78-
const length = binaryString.length
79-
80-
if (length > 2 * 1024 * 1024) {
81-
throw new Error('Image is too large to process')
89+
if (imageData.startsWith('data:image/gif') && imageData.length < MAX_BOOKMARK_SIZE) {
90+
return imageData
8291
}
8392

84-
const img = new Image()
85-
const canvas = document.createElement('canvas')
86-
const ctx = canvas.getContext('2d')
87-
const maxDimension = 48
93+
try {
94+
const base64 = imageData.split(',')[1]
95+
const binaryString = window.atob(base64)
96+
const length = binaryString.length
8897

89-
canvas.width = maxDimension
90-
canvas.height = maxDimension
91-
img.src = imageData
98+
if (length > 3 * 1024 * 1024) {
99+
throw new Error('Image is too large to process')
100+
}
92101

93-
try {
94-
ctx?.drawImage(img, 0, 0, maxDimension, maxDimension)
95-
return canvas.toDataURL('image/webp', 0.6)
102+
return new Promise((resolve) => {
103+
const img = new Image()
104+
img.onload = () => {
105+
const canvas = document.createElement('canvas')
106+
const ctx = canvas.getContext('2d')
107+
const maxDimension = 128
108+
109+
let width = img.width
110+
let height = img.height
111+
112+
if (width > height) {
113+
if (width > maxDimension) {
114+
height = Math.round(height * (maxDimension / width))
115+
width = maxDimension
116+
}
117+
} else {
118+
if (height > maxDimension) {
119+
width = Math.round(width * (maxDimension / height))
120+
height = maxDimension
121+
}
122+
}
123+
124+
canvas.width = width
125+
canvas.height = height
126+
127+
ctx?.drawImage(img, 0, 0, width, height)
128+
129+
const format = imageData.startsWith('data:image/gif')
130+
? 'image/png'
131+
: 'image/webp'
132+
const quality = 0.7
133+
134+
const compressed = canvas.toDataURL(format, quality)
135+
resolve(compressed)
136+
}
137+
138+
img.onerror = () => {
139+
console.warn('Image compression failed, using original')
140+
resolve(imageData)
141+
}
142+
143+
img.src = imageData
144+
})
96145
} catch (e) {
97-
return imageData.substring(0, 50000)
146+
console.error('Error in image compression:', e)
147+
return imageData
98148
}
99149
}
100150

101-
const prepareBookmarkForStorage = (bookmark: Bookmark): Bookmark => {
151+
const prepareBookmarkForStorage = async (bookmark: Bookmark): Promise<Bookmark> => {
102152
const processedBookmark = { ...bookmark, isLocal: true }
103153

104154
if (processedBookmark.customImage && processedBookmark.customImage.length > 50000) {
105155
try {
106-
processedBookmark.customImage = compressImageData(processedBookmark.customImage)
156+
processedBookmark.customImage = await compressImageData(
157+
processedBookmark.customImage,
158+
)
107159
} catch (err) {
160+
console.error('Image processing error:', err)
108161
toast.error('خطا در پردازش تصویر. از تصویر پیش‌فرض استفاده می‌شود.')
109162

110163
if (processedBookmark.type === 'BOOKMARK') {
@@ -119,12 +172,18 @@ export const BookmarkProvider: React.FC<{ children: React.ReactNode }> = ({
119172
const addBookmark = async (bookmark: Bookmark) => {
120173
try {
121174
const bookmarkSize = getBookmarkDataSize(bookmark)
122-
if (bookmarkSize > MAX_BOOKMARK_SIZE) {
123-
toast.error('تصویر انتخاب شده خیلی بزرگ است. لطفاً تصویر کوچکتری انتخاب کنید.')
175+
176+
const isGif = bookmark.customImage?.startsWith('data:image/gif')
177+
const sizeLimit = isGif ? 1.5 * MAX_BOOKMARK_SIZE : MAX_BOOKMARK_SIZE
178+
179+
if (bookmarkSize > sizeLimit) {
180+
toast.error(
181+
`تصویر انتخاب شده (${(bookmarkSize / 1024).toFixed(1)} کیلوبایت) بزرگتر از حداکثر مجاز است.`,
182+
)
124183
return
125184
}
126185

127-
const newBookmark = prepareBookmarkForStorage(bookmark)
186+
const newBookmark = await prepareBookmarkForStorage(bookmark)
128187
const updatedBookmarks = [...bookmarks, newBookmark]
129188

130189
try {
@@ -144,6 +203,7 @@ export const BookmarkProvider: React.FC<{ children: React.ReactNode }> = ({
144203
const localBookmarks = updatedBookmarks.filter((b) => b.isLocal)
145204
await setToStorage('bookmarks', localBookmarks)
146205
} catch (error) {
206+
console.error('Error adding bookmark:', error)
147207
toast.error('خطا در افزودن بوکمارک')
148208
}
149209
}

src/layouts/search/bookmarks/bookmarks.tsx

+17-6
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,22 @@ export function BookmarksComponent() {
100100
}
101101
}
102102

103-
const handleBackClick = () => {
104-
if (folderPath.length === 0) return
103+
const handleNavigate = (folderId: string | null, depth: number) => {
104+
if (depth === -1) {
105+
setFolderPath([])
106+
setCurrentFolderId(null)
107+
return
108+
}
105109

106-
const newPath = [...folderPath]
107-
newPath.pop()
110+
const newPath = folderPath.slice(0, depth + 1)
108111
setFolderPath(newPath)
109-
setCurrentFolderId(newPath[newPath.length - 1]?.id ?? null)
112+
setCurrentFolderId(folderId)
113+
}
114+
115+
function onOpenInNewTab(bookmark: Bookmark) {
116+
if (bookmark && bookmark.type === 'BOOKMARK') {
117+
window.open(bookmark.url, '_blank')
118+
}
110119
}
111120

112121
const currentFolderItems = getCurrentFolderItems(currentFolderId)
@@ -144,12 +153,14 @@ export function BookmarksComponent() {
144153
deleteBookmark(selectedBookmark.id)
145154
setSelectedBookmark(null)
146155
}}
156+
isFolder={selectedBookmark.type === 'FOLDER'}
157+
onOpenInNewTab={() => onOpenInNewTab(selectedBookmark)}
147158
theme={theme}
148159
/>
149160
)}
150161
</div>
151162

152-
<FolderPath folderPath={folderPath} onBackClick={handleBackClick} theme={theme} />
163+
<FolderPath folderPath={folderPath} onNavigate={handleNavigate} theme={theme} />
153164

154165
<AddBookmarkModal
155166
isOpen={showAddBookmarkModal}

0 commit comments

Comments
 (0)