diff --git a/src/components/gallery/gallery-asset-purchase-modal.tsx b/src/components/gallery/gallery-asset-purchase-modal.tsx
new file mode 100644
index 000000000..c3fd17eb3
--- /dev/null
+++ b/src/components/gallery/gallery-asset-purchase-modal.tsx
@@ -0,0 +1,175 @@
+import { Button, Modal } from '@/components/ui'
+import { UserCoin } from '@/layouts/setting/tabs/account/components/user-coin'
+import { callEvent } from '@/common/utils/call-event'
+import { showToast } from '@/common/toast'
+import type { GalleryAsset } from '@/services/hooks/gallery/get-gallery-assets.hook'
+import { usePurchaseGalleryAsset } from '@/services/hooks/gallery/get-gallery-assets.hook'
+
+interface GalleryAssetPurchaseModalProps {
+ isOpen: boolean
+ onClose: () => void
+ asset: GalleryAsset | null
+ userCoins: number
+ isVip: boolean
+ onPurchaseSuccess: (asset: GalleryAsset) => void
+ onSelectDirectly?: (asset: GalleryAsset) => void
+}
+
+export function GalleryAssetPurchaseModal({
+ isOpen,
+ onClose,
+ asset,
+ userCoins,
+ isVip,
+ onPurchaseSuccess,
+ onSelectDirectly,
+}: GalleryAssetPurchaseModalProps) {
+ const { mutate: purchase, isPending } = usePurchaseGalleryAsset()
+
+ if (!asset) return null
+
+ const isVipUnlocked = isVip && asset.accessVip
+ const canAfford = userCoins >= asset.price
+
+ const handlePurchase = () => {
+ if (!canAfford) return
+
+ purchase(asset.id, {
+ onSuccess: (response) => {
+ showToast(`${asset.title || 'آیتم'} برای همیشه خریداری شد`, 'success')
+ const updatedAsset = response?.data?.asset || {
+ ...asset,
+ isOwned: true,
+ isUnlocked: true,
+ }
+ onPurchaseSuccess(updatedAsset)
+ },
+ onError: () => {
+ showToast('خطا در دریافت آیتم', 'error')
+ },
+ })
+ }
+
+ const handleUseWithVip = () => {
+ if (onSelectDirectly) {
+ onSelectDirectly(asset)
+ }
+ onClose()
+ }
+
+ const handleOpenCoins = () => {
+ onClose()
+ callEvent('openMarketModal')
+ setTimeout(() => {
+ callEvent('market_change_tab', 'coins')
+ }, 100)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {asset.title || 'تصویر گالری'}
+
+ {asset.price > 0 && (
+
+ )}
+
+
+ {isVipUnlocked
+ ? 'چون اشتراک پرو داری میتونی این تصویر رو رایگان فعال کنی یا با ویجکوین دائمی بخریش'
+ : 'این تصویر رو با ویجکوین باز کن و همیشه ازش استفاده کن'}
+
+
+
+ {!isVipUnlocked && !canAfford && (
+
+
+ موجودی ویجکوین ناکافیه ({asset.price - userCoins} ویجکوین کسری
+ داری)
+
+
+ خرید ویجکوین
+
+
+ )}
+
+
+ {isVipUnlocked ? (
+
+
+ استفاده رایگان با پرو
+
+ {asset.price > 0 && (
+
+ خرید دائمی
+
+ )}
+
+ ) : (
+
+
+ خرید دائمی
+
+
+ انصراف
+
+
+ )}
+
+
+
+ )
+}
diff --git a/src/components/gallery/gallery-picker-modal.tsx b/src/components/gallery/gallery-picker-modal.tsx
new file mode 100644
index 000000000..a7a08936d
--- /dev/null
+++ b/src/components/gallery/gallery-picker-modal.tsx
@@ -0,0 +1,263 @@
+import { useState, useRef } from 'react'
+import { Modal, Chip, VipBadge } from '@/components/ui'
+import { Icon } from '@/src/icons'
+import { playNativeToastSound, showToast } from '@/common/toast'
+import { useAuth } from '@/context/auth.context'
+import { callEvent } from '@/common/utils/call-event'
+import { UserCoin } from '@/layouts/setting/tabs/account/components/user-coin'
+import { useLazyLoad } from '@/layouts/setting/tabs/wallpapers/hooks/use-lazy-load'
+import {
+ useGetGalleryAssets,
+ useGetGalleryCategories,
+ type GalleryAsset,
+ type GalleryAssetType,
+} from '@/services/hooks/gallery/get-gallery-assets.hook'
+import { GalleryAssetPurchaseModal } from './gallery-asset-purchase-modal'
+import toast from 'react-hot-toast'
+
+interface GalleryPickerModalProps {
+ isOpen: boolean
+ onClose: () => void
+ type: GalleryAssetType
+ title?: string
+ onSelect: (asset: GalleryAsset) => void
+ selectedAssetId?: string
+}
+
+export function GalleryPickerModal({
+ isOpen,
+ onClose,
+ type,
+ title = 'گالری تصاویر',
+ onSelect,
+ selectedAssetId,
+}: GalleryPickerModalProps) {
+ const [selectedCategory, setSelectedCategory] = useState
('ALL')
+ const [purchasingAsset, setPurchasingAsset] = useState(null)
+
+ const { user, isVip } = useAuth()
+ const { data: categories = [] } = useGetGalleryCategories(type, isOpen)
+
+ const { data, isLoading } = useGetGalleryAssets(
+ {
+ type,
+ limit: 100,
+ category: selectedCategory === 'ALL' ? undefined : selectedCategory,
+ },
+ isOpen
+ )
+
+ const assets = data?.data?.assets ?? []
+
+ const handleItemClick = (asset: GalleryAsset) => {
+ if (asset.isOwned || asset.price === 0) {
+ onSelect(asset)
+ onClose()
+ playNativeToastSound('success')
+ return
+ }
+
+ if (!user) {
+ callEvent('openProfile')
+ showToast('برای استفاده از این مورد اول وارد حسابت شو', 'error')
+ return
+ }
+
+ if (asset.accessVip && !isVip && asset.price === 0) {
+ callEvent('openSettings', 'vip')
+ return
+ }
+
+ setPurchasingAsset(asset)
+ }
+
+ const handlePurchaseSuccess = (asset: GalleryAsset) => {
+ setPurchasingAsset(null)
+ onSelect({ ...asset, isOwned: true, isUnlocked: true })
+ onClose()
+ }
+
+ const handleSelectDirectly = (asset: GalleryAsset) => {
+ setPurchasingAsset(null)
+ onSelect(asset)
+ onClose()
+ }
+
+ return (
+ <>
+
+
+ {categories.length > 0 && (
+
+ setSelectedCategory('ALL')}
+ className="text-xs"
+ >
+ همه
+
+ {categories.map((cat) => (
+ setSelectedCategory(cat)}
+ className="text-xs"
+ >
+ {cat}
+
+ ))}
+
+ )}
+
+
+ {isLoading ? (
+
+ {[...Array(8)].map((_, idx) => (
+
+ ))}
+
+ ) : assets.length === 0 ? (
+
+
+
هیچ تصویری پیدا نشد
+
+ ) : (
+
+ {assets.map((asset) => (
+ handleItemClick(asset)}
+ />
+ ))}
+
+ )}
+
+
+
+
+ setPurchasingAsset(null)}
+ asset={purchasingAsset}
+ userCoins={user?.coins ?? 0}
+ isVip={Boolean(isVip)}
+ onPurchaseSuccess={handlePurchaseSuccess}
+ onSelectDirectly={handleSelectDirectly}
+ />
+ >
+ )
+}
+
+function GalleryPinterestItem({
+ asset,
+ isSelected,
+ onClick,
+}: {
+ asset: GalleryAsset
+ isSelected: boolean
+ onClick: () => void
+}) {
+ const [loaded, setLoaded] = useState(false)
+ const [error, setError] = useState(false)
+ const imgRef = useRef(null)
+
+ const loadContent = () => {
+ if (imgRef.current) {
+ imgRef.current.src = asset.previewUrl || asset.url
+ }
+ }
+
+ const elementRef = useLazyLoad(loadContent)
+
+ const itemOutlineStyle = isSelected
+ ? 'ring-2 ring-primary ring-offset-2 ring-offset-base-100'
+ : 'ring-1 ring-base-content/10 hover:ring-primary/70'
+
+ return (
+
+ {!loaded && (
+
+ )}
+
+ {error && (
+
+ )}
+
+
{
+ setLoaded(true)
+ setError(false)
+ }}
+ onError={() => {
+ setLoaded(true)
+ setError(true)
+ }}
+ className="object-cover w-full h-auto transition-transform duration-300 group-hover:scale-103"
+ style={{ opacity: loaded && !error ? 1 : 0 }}
+ />
+
+ {loaded && !error && (
+ <>
+
+
+ {asset.title || ''}
+
+
+ {asset.price > 0 && !asset.isOwned ? (
+
+
+
+ ) : null}
+
+
+ {isSelected && (
+
+
+
+ )}
+
+ {asset.accessVip && !asset.isOwned && (
+
+
+
+ )}
+
+ {asset.isOwned && !isSelected && (
+
+
+ خریداری شده
+
+ )}
+ >
+ )}
+
+ )
+}
diff --git a/src/components/gallery/index.ts b/src/components/gallery/index.ts
new file mode 100644
index 000000000..a9ac1f6e7
--- /dev/null
+++ b/src/components/gallery/index.ts
@@ -0,0 +1,2 @@
+export * from './gallery-picker-modal'
+export * from './gallery-asset-purchase-modal'
diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts
index e67bae401..1d2241883 100644
--- a/src/components/ui/index.ts
+++ b/src/components/ui/index.ts
@@ -43,6 +43,8 @@ export * from './offline-indicator/offline-indicator'
export * from './pagination/pagination'
+export * from './popover-menu'
+
export * from './portal/portal'
export * from './section-panel/section-panel'
diff --git a/src/components/ui/modal/modal.tsx b/src/components/ui/modal/modal.tsx
index 848bccab3..5e28d6bc6 100644
--- a/src/components/ui/modal/modal.tsx
+++ b/src/components/ui/modal/modal.tsx
@@ -45,9 +45,9 @@ export function Modal({
if (!dialog) return
if (isOpen) {
- if (!dialog.open) dialog.showModal()
- } else if (dialog.open) {
- dialog.close()
+ dialog.setAttribute('open', '')
+ } else {
+ dialog.removeAttribute('open')
}
}, [isOpen])
@@ -55,6 +55,7 @@ export function Modal({
const dialog = dialogRef.current
if (!dialog) return
const handleCancel = (e: Event) => {
+ if (e.target !== dialog) return
e.preventDefault() // keep it mounted so the exit animation can play
onClose()
}
diff --git a/src/components/ui/modal/modal.variants.ts b/src/components/ui/modal/modal.variants.ts
index 8e906ecce..01db276e5 100644
--- a/src/components/ui/modal/modal.variants.ts
+++ b/src/components/ui/modal/modal.variants.ts
@@ -60,6 +60,7 @@ export const modalDialogVariants = cva([
'modal-middle',
'p-2',
'md:p-4',
+ 'z-[1000]',
])
export type ModalDialogVariant = typeof modalDialogVariants
diff --git a/src/components/ui/popover-menu/index.ts b/src/components/ui/popover-menu/index.ts
new file mode 100644
index 000000000..376fa7f08
--- /dev/null
+++ b/src/components/ui/popover-menu/index.ts
@@ -0,0 +1,3 @@
+export * from './popover-menu'
+export * from './popover-menu-item'
+export * from './popover-menu.variants'
diff --git a/src/components/ui/popover-menu/popover-menu-item.tsx b/src/components/ui/popover-menu/popover-menu-item.tsx
new file mode 100644
index 000000000..a24409e07
--- /dev/null
+++ b/src/components/ui/popover-menu/popover-menu-item.tsx
@@ -0,0 +1,54 @@
+import { cn } from '@/common/utils/cn'
+import { popoverMenuItemVariants } from './popover-menu.variants'
+
+export interface PopoverMenuItemProps {
+ icon?: React.ReactNode
+ label: string
+ badge?: React.ReactNode
+ onClick?: () => void
+ variant?: 'default' | 'danger' | 'primary'
+ disabled?: boolean
+ className?: string
+}
+
+export function PopoverMenuItem({
+ icon,
+ label,
+ badge,
+ onClick,
+ variant = 'default',
+ disabled = false,
+ className,
+}: PopoverMenuItemProps) {
+ return (
+ {
+ e.stopPropagation()
+ if (!disabled) {
+ onClick?.()
+ }
+ }}
+ className={cn(popoverMenuItemVariants({ variant }), className)}
+ >
+
+ {icon && {icon} }
+ {label}
+
+ {badge && {badge} }
+
+ )
+}
+
+export function PopoverMenuDivider() {
+ return
+}
+
+export function PopoverMenuHeader({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/ui/popover-menu/popover-menu.tsx b/src/components/ui/popover-menu/popover-menu.tsx
new file mode 100644
index 000000000..3fb4c4640
--- /dev/null
+++ b/src/components/ui/popover-menu/popover-menu.tsx
@@ -0,0 +1,143 @@
+import { useEffect, useRef, useState } from 'react'
+import { Portal } from '@/components/ui/portal/portal'
+import { cn } from '@/common/utils/cn'
+import { popoverMenuVariants } from './popover-menu.variants'
+
+export interface PopoverMenuProps {
+ isOpen: boolean
+ onClose: () => void
+ position?: { x: number; y: number } | null
+ triggerRef?: React.RefObject
+ children: React.ReactNode
+ className?: string
+ width?: number | string
+ placement?:
+ | 'bottom-start'
+ | 'bottom-end'
+ | 'bottom-center'
+ | 'top-start'
+ | 'top-end'
+ | 'top-center'
+ offset?: number
+}
+
+export function PopoverMenu({
+ isOpen,
+ onClose,
+ position,
+ triggerRef,
+ children,
+ className,
+ width = 208,
+ placement = 'bottom-center',
+ offset = 8,
+}: PopoverMenuProps) {
+ const menuRef = useRef(null)
+ const [coords, setCoords] = useState<{ top: number; left: number } | null>(null)
+
+ useEffect(() => {
+ if (!isOpen) {
+ setCoords(null)
+ return
+ }
+
+ const computePosition = () => {
+ if (position) {
+ const numericWidth = typeof width === 'number' ? width : 208
+ const left = Math.min(
+ Math.max(12, position.x),
+ window.innerWidth - numericWidth - 12
+ )
+ const top = Math.min(Math.max(12, position.y), window.innerHeight - 260)
+ setCoords({ top, left })
+ return
+ }
+
+ if (triggerRef?.current) {
+ const rect = triggerRef.current.getBoundingClientRect()
+ const numericWidth = typeof width === 'number' ? width : 208
+ let left = rect.left + rect.width / 2 - numericWidth / 2
+ let top = rect.bottom + offset
+
+ if (placement === 'bottom-start') {
+ left = rect.left
+ } else if (placement === 'bottom-end') {
+ left = rect.right - numericWidth
+ } else if (placement.startsWith('top')) {
+ top = rect.top - offset - 100
+ if (placement === 'top-start') left = rect.left
+ if (placement === 'top-end') left = rect.right - numericWidth
+ }
+
+ left = Math.min(Math.max(12, left), window.innerWidth - numericWidth - 12)
+ top = Math.min(Math.max(12, top), window.innerHeight - 180)
+
+ setCoords({ top, left })
+ }
+ }
+
+ computePosition()
+
+ const handleClickOutside = (e: MouseEvent) => {
+ if (
+ menuRef.current &&
+ !menuRef.current.contains(e.target as Node) &&
+ triggerRef?.current &&
+ !triggerRef.current.contains(e.target as Node)
+ ) {
+ onClose()
+ } else if (
+ menuRef.current &&
+ !menuRef.current.contains(e.target as Node) &&
+ !triggerRef
+ ) {
+ onClose()
+ }
+ }
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ onClose()
+ }
+ }
+
+ const handleScroll = () => {
+ onClose()
+ }
+
+ document.addEventListener('mousedown', handleClickOutside)
+ document.addEventListener('keydown', handleKeyDown)
+ window.addEventListener('scroll', handleScroll, true)
+ window.addEventListener('resize', computePosition)
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside)
+ document.removeEventListener('keydown', handleKeyDown)
+ window.removeEventListener('scroll', handleScroll, true)
+ window.removeEventListener('resize', computePosition)
+ }
+ }, [isOpen, position, triggerRef, width, placement, offset, onClose])
+
+ if (!isOpen || !coords) return null
+
+ return (
+
+ e.stopPropagation()}
+ onContextMenu={(e) => e.preventDefault()}
+ >
+ {children}
+
+
+ )
+}
diff --git a/src/components/ui/popover-menu/popover-menu.variants.ts b/src/components/ui/popover-menu/popover-menu.variants.ts
new file mode 100644
index 000000000..9223a3e40
--- /dev/null
+++ b/src/components/ui/popover-menu/popover-menu.variants.ts
@@ -0,0 +1,50 @@
+import { cva } from 'class-variance-authority'
+
+export const popoverMenuVariants = cva([
+ 'bg-content',
+ 'bg-glass',
+ 'rounded-3xl',
+ 'shadow-2xl',
+ 'border',
+ 'border-base-content/10',
+ 'p-2',
+ 'text-right',
+ 'text-xs',
+ 'flex',
+ 'flex-col',
+ 'gap-1',
+ 'animate-in',
+ 'fade-in',
+ 'zoom-in-95',
+ 'duration-150',
+])
+
+export const popoverMenuItemVariants = cva(
+ [
+ 'flex',
+ 'items-center',
+ 'justify-between',
+ 'w-full',
+ 'px-2.5',
+ 'py-2',
+ 'rounded-xl',
+ 'font-medium',
+ 'transition-colors',
+ 'text-right',
+ 'cursor-pointer',
+ 'disabled:opacity-40',
+ 'disabled:cursor-not-allowed',
+ ],
+ {
+ variants: {
+ variant: {
+ default: 'text-content hover:bg-base-content/10 active:bg-base-300',
+ danger: 'text-error hover:bg-error/10 active:bg-error/20',
+ primary: 'text-primary hover:bg-primary/10 active:bg-primary/20',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ }
+)
diff --git a/src/components/ui/portal/portal.tsx b/src/components/ui/portal/portal.tsx
index ffc3378a7..15b548c44 100644
--- a/src/components/ui/portal/portal.tsx
+++ b/src/components/ui/portal/portal.tsx
@@ -40,23 +40,20 @@ export function raiseToTopLayer(host: HTMLElement | null) {
if (host.parentElement !== parent) {
parent.appendChild(host)
-
- if (parent !== document.body) {
- parent.addEventListener('close', () => raiseToTopLayer(host), {
- once: true,
- })
- }
}
if (typeof host.showPopover !== 'function') return
try {
- host.hidePopover()
- } catch {}
-
- try {
- host.showPopover()
- } catch {}
+ if (!host.matches(':popover-open')) {
+ host.showPopover()
+ }
+ } catch {
+ try {
+ host.hidePopover()
+ host.showPopover()
+ } catch {}
+ }
}
type TopLayerHostProps = Pick
diff --git a/src/layouts/bookmark/components/bookmark-icon.picker.tsx b/src/layouts/bookmark/components/bookmark-icon.picker.tsx
index 0e02e2028..649138831 100644
--- a/src/layouts/bookmark/components/bookmark-icon.picker.tsx
+++ b/src/layouts/bookmark/components/bookmark-icon.picker.tsx
@@ -3,30 +3,37 @@ import { getFaviconFromUrl } from '@/common/utils/icon'
import { Icon } from '@/src/icons'
import type React from 'react'
import { useRef, useState } from 'react'
+import { PopoverMenu, PopoverMenuItem, PopoverMenuDivider } from '@/components/ui'
+import { GalleryPickerModal } from '@/components/gallery/gallery-picker-modal'
+import type { GalleryAsset } from '@/services/hooks/gallery/get-gallery-assets.hook'
type Props = {
value: File | string | null
url?: string | null
- onChange: (file: File | null) => void
+ onChange: (value: File | string | null) => void
+ size?: 'md' | 'lg'
}
-export function BookmarkIconPicker({ value, url, onChange }: Props) {
+export function BookmarkIconPicker({ value, url, onChange, size = 'md' }: Props) {
+ const triggerRef = useRef(null)
const fileInputRef = useRef(null)
+ const [isMenuOpen, setIsMenuOpen] = useState(false)
+ const [isGalleryOpen, setIsGalleryOpen] = useState(false)
const [isDragging, setIsDragging] = useState(false)
const [error, setError] = useState(false)
const openPicker = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
- if (fileInputRef.current) {
- fileInputRef.current.value = ''
- fileInputRef.current.click()
- }
+ setIsMenuOpen(true)
}
const handleFile = (file?: File) => {
if (!file || !file.type.startsWith('image/'))
- return showToast('فرمت نامعتبر است', 'error')
+ return showToast('فرمت فایل نامعتبر است', 'error')
+ if (file.size > 250 * 1024) {
+ return showToast('حجم فایل آیکون نباید بیشتر از ۲۵۰ کیلوبایت باشد', 'error')
+ }
setError(false)
onChange(file)
}
@@ -52,18 +59,27 @@ export function BookmarkIconPicker({ value, url, onChange }: Props) {
}
}
+ const handleGallerySelect = (asset: GalleryAsset) => {
+ setIsGalleryOpen(false)
+ setError(false)
+ onChange(asset.url)
+ }
+
const isFile = value instanceof File
let iconSrc: string | null = null
if (value && isFile) {
iconSrc = URL.createObjectURL(value)
- } else {
- if (typeof value === 'string' && value) {
- iconSrc = value
- } else if (url && url !== 'null') {
- iconSrc = getFaviconFromUrl(url || '')
- }
+ } else if (typeof value === 'string' && value) {
+ iconSrc = value
+ } else if (url && url !== 'null') {
+ iconSrc = getFaviconFromUrl(url || '')
}
+ const isLarge = size === 'lg'
+ const containerSizeClasses = isLarge
+ ? 'w-16 h-16 rounded-2xl'
+ : 'w-12 h-12 rounded-xl'
+
return (
<>
- {
- e.preventDefault()
- setIsDragging(true)
- }}
- onDragLeave={() => setIsDragging(false)}
- onDrop={handleDrop}
- className={`relative w-12 h-11 shrink-0 flex items-center justify-center cursor-pointer border-2 rounded-xl transition-all ${
- isDragging
- ? 'border-primary bg-primary/10'
- : 'border-base-content/15 hover:border-primary/40 bg-base-200/80'
- }`}
- title="انتخاب آیکون"
- >
- {iconSrc && !error ? (
- setError(true)}
- />
- ) : (
-
- )}
+
+
{
+ e.preventDefault()
+ setIsDragging(true)
+ }}
+ onDragLeave={() => setIsDragging(false)}
+ onDrop={handleDrop}
+ className={`relative shrink-0 flex items-center justify-center cursor-pointer border-2 transition-all duration-200 ${containerSizeClasses} ${
+ isDragging
+ ? 'border-primary bg-primary/10 shadow-lg'
+ : 'border-base-content/15 hover:border-primary/50 bg-base-200/80 hover:bg-base-200'
+ }`}
+ title="انتخاب یا تغییر آیکون"
+ >
+ {iconSrc && !error ? (
+ setError(true)}
+ />
+ ) : (
+
+ )}
-
-
-
+
+
+
+
- {isFile && (
+ {Boolean(value) && (
- ✕
+ ✕
)}
-
+
+
+ setIsMenuOpen(false)}
+ triggerRef={triggerRef}
+ width={190}
+ placement="bottom-start"
+ >
+ }
+ label="بارگذاری از دستگاه"
+ onClick={() => {
+ setIsMenuOpen(false)
+ if (fileInputRef.current) {
+ fileInputRef.current.value = ''
+ fileInputRef.current.click()
+ }
+ }}
+ />
+ }
+ label="انتخاب از گالری"
+ onClick={() => {
+ setIsMenuOpen(false)
+ setIsGalleryOpen(true)
+ }}
+ />
+ {Boolean(value) && (
+ <>
+
+ }
+ label="حذف آیکون"
+ variant="danger"
+ onClick={() => {
+ setIsMenuOpen(false)
+ handleRemove({
+ preventDefault: () => {},
+ stopPropagation: () => {},
+ } as any)
+ }}
+ />
+ >
+ )}
+
+
+ {isGalleryOpen && (
+ setIsGalleryOpen(false)}
+ type="BOOKMARK_ICON"
+ title="گالری آیکون بوکمارک"
+ onSelect={handleGallerySelect}
+ />
+ )}
>
)
}
diff --git a/src/layouts/bookmark/components/bookmark/bookmark-icon.tsx b/src/layouts/bookmark/components/bookmark/bookmark-icon.tsx
index ed767cbe1..fdd1c60a1 100644
--- a/src/layouts/bookmark/components/bookmark/bookmark-icon.tsx
+++ b/src/layouts/bookmark/components/bookmark/bookmark-icon.tsx
@@ -69,7 +69,6 @@ export function BookmarkIcon({ bookmark }: { bookmark: Bookmark }) {
? bookmark.customTextColor
: 'rgba(255, 255, 255, 1)'
const colorClass = hasCustomColors ? '' : getColorFromTitle(bookmark.title)
-
return (
{typeof displayIcon === 'string' && !imageError ? (
diff --git a/src/layouts/bookmark/components/modal/add-bookmark.modal.tsx b/src/layouts/bookmark/components/modal/add-bookmark.modal.tsx
index 60a59b2ef..08835b81c 100644
--- a/src/layouts/bookmark/components/modal/add-bookmark.modal.tsx
+++ b/src/layouts/bookmark/components/modal/add-bookmark.modal.tsx
@@ -28,7 +28,7 @@ export interface BookmarkCreateFormFields {
customBackground: string | null
customTextColor: string | null
sticker: string | null
- icon: File | null
+ icon: File | string | null
widgetId?: string | null
}
@@ -100,7 +100,7 @@ export function AddBookmarkModal({
setFormData((prev) => ({
...prev,
url: newUrl,
- icon: null,
+ icon: prev.icon,
title: prev.title.trim() === '' && hostName ? hostName : prev.title,
}))
}
@@ -146,7 +146,7 @@ export function AddBookmarkModal({
...prev,
title: suggestion.title,
url: suggestion.url,
- icon: null,
+ icon: suggestion.icon || null,
}))
}
@@ -194,48 +194,44 @@ export function AddBookmarkModal({