diff --git a/background/cache.ts b/background/cache.ts index cbdb12df..c24f1d8d 100644 --- a/background/cache.ts +++ b/background/cache.ts @@ -3,6 +3,14 @@ import { ExpirationPlugin } from 'workbox-expiration' import { registerRoute } from 'workbox-routing' import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies' import { NavigationRoute } from 'workbox-routing' +const allowedPaths = [ + '/extension/wigi-pad-data', + '/date/events', + '/news/rss', + '/currencies', + '/wallpapers', + '/contents', +] export function setupCaching() { if (typeof self !== 'undefined' && '__WB_MANIFEST' in self) { @@ -11,6 +19,29 @@ export function setupCaching() { }) } + registerRoute( + ({ url, request }) => { + if (request.method !== 'GET') return false + if (url.origin !== 'https://api.widgetify.ir') return false + + return allowedPaths.some((path) => url.pathname.startsWith(path)) + }, + + new StaleWhileRevalidate({ + cacheName: 'widgetify-public-api', + plugins: [ + new CacheableResponsePlugin({ + statuses: [200], + }), + new ExpirationPlugin({ + maxEntries: 150, + maxAgeSeconds: 60 * 60 * 5, + purgeOnQuotaError: true, + }), + ], + }) + ) + const isDev = import.meta.env.DEV if (!isDev) { diff --git a/src/assets/images/no-internet.png b/src/assets/images/no-internet.png new file mode 100644 index 00000000..d8c8cffd Binary files /dev/null and b/src/assets/images/no-internet.png differ diff --git a/src/common/constant/moods.ts b/src/common/constant/moods.ts index 2a242520..f873d26b 100644 --- a/src/common/constant/moods.ts +++ b/src/common/constant/moods.ts @@ -6,6 +6,13 @@ export const moodOptions = [ colorClass: 'error', borderClass: 'border-error/50', }, + { + value: 'normal', + emoji: '😴', + label: '', + colorClass: 'warning', + borderClass: 'border-yellow-400/50', + }, { value: 'tired', emoji: '😴', diff --git a/src/common/constant/store.key.ts b/src/common/constant/store.key.ts index 02b9063d..ea42e2bc 100644 --- a/src/common/constant/store.key.ts +++ b/src/common/constant/store.key.ts @@ -1,6 +1,5 @@ import type { CurrencyColorMode } from '@/context/currency.context' import type { Theme } from '@/context/theme.context' -import type { TodoOptions } from '@/context/todo.context' import type { WidgetItem } from '@/context/widget-visibility.context' import type { Bookmark } from '@/layouts/bookmark/types/bookmark.types' import type { PetSettings } from '@/layouts/widgetify-card/pets/pet.context' @@ -63,7 +62,6 @@ export interface StorageKV { }[] calendarDrawerState: boolean pets: PetSettings - todoOptions: TodoOptions youtubeSettings: { username: string | null subscriptionStyle: 'short' | 'long' @@ -89,5 +87,7 @@ export interface StorageKV { petState: boolean showNewBadgeForReOrderWidgets: boolean navbarVisible: boolean + todoFilter: string + todoSort: string [key: `removed_notification_${string}`]: string } diff --git a/src/common/storage.ts b/src/common/storage.ts index 7b3a565f..e6f9f4e0 100644 --- a/src/common/storage.ts +++ b/src/common/storage.ts @@ -63,7 +63,8 @@ export async function setWithExpiry( value: StorageKV[K], minutes: number ) { - const expiry = Date.now() + minutes * 60000 + const safeMinutes = Math.max(1, minutes) + const expiry = Date.now() + safeMinutes * 60_000 const data = { value, expiry } await setToStorage(key, data as any) diff --git a/src/common/toast.tsx b/src/common/toast.tsx index b6447921..372675e8 100644 --- a/src/common/toast.tsx +++ b/src/common/toast.tsx @@ -1,4 +1,4 @@ -import type React from 'react' +import React from 'react' import toast from 'react-hot-toast' import { playAlarm } from './playAlarm' @@ -165,17 +165,18 @@ export function showToast( ) } } else { - toast.custom((t) => ( -
-
-
-
{message}
+ if (React.isValidElement(message)) + toast.custom((t) => ( +
+
+
+
{message}
+
-
- )) + )) } if (options?.alarmSound) { diff --git a/src/common/utils/icon.ts b/src/common/utils/icon.ts index 11cf5133..bb27d14f 100644 --- a/src/common/utils/icon.ts +++ b/src/common/utils/icon.ts @@ -24,7 +24,7 @@ export const getFaviconFromUrl = (url: string): string => { } const urlObj = new URL(processedUrl) - return `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=64` + return `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=64&fallback_opts=404` } catch { return '' } diff --git a/src/components/badges/new.badge.tsx b/src/components/badges/new.badge.tsx new file mode 100644 index 00000000..90e8c437 --- /dev/null +++ b/src/components/badges/new.badge.tsx @@ -0,0 +1,10 @@ +interface Prop { + className: string +} +export function NewBadge({ className }: Prop) { + return ( + + ) +} diff --git a/src/components/blur-mode/blur-mode.button.tsx b/src/components/blur-mode/blur-mode.button.tsx new file mode 100644 index 00000000..75ae566f --- /dev/null +++ b/src/components/blur-mode/blur-mode.button.tsx @@ -0,0 +1,25 @@ +import { useGeneralSetting } from '@/context/general-setting.context' +import Tooltip from '../toolTip' +import { Button } from '../button/button' +import { FaEye, FaEyeSlash } from 'react-icons/fa' + +export function BlurModeButton() { + const { blurMode, updateSetting } = useGeneralSetting() + + const handleBlurModeToggle = () => { + const newBlurMode = !blurMode + updateSetting('blurMode', newBlurMode) + } + + return ( + + + + ) +} diff --git a/src/components/button/button.tsx b/src/components/button/button.tsx index ed9e19da..984e42b7 100644 --- a/src/components/button/button.tsx +++ b/src/components/button/button.tsx @@ -15,6 +15,7 @@ interface ButtonProps { children?: React.ReactNode isPrimary?: boolean size: 'xs' | 'sm' | 'md' | 'lg' | 'xl' + ref?: any } export function Button(prop: ButtonProps) { const sizes: Record = { @@ -32,6 +33,7 @@ export function Button(prop: ButtonProps) { disabled={prop.disabled} className={`btn cursor-pointer ${prop.fullWidth ? 'full-width' : ''} ${prop.className} ${prop.rounded ? `rounded-${prop.rounded}` : ''} ${prop.isPrimary ? 'btn-primary text-white' : ''} ${sizes[prop.size] || 'btn-md'} active:!translate-y-0`} style={prop.style} + ref={prop.ref} > {prop.loading ? prop.loadingText || ( diff --git a/src/components/date-picker/index.ts b/src/components/date-picker/index.ts index 0396ad21..6bc7ab0d 100644 --- a/src/components/date-picker/index.ts +++ b/src/components/date-picker/index.ts @@ -1 +1 @@ -export { DatePicker as SimpleDatePicker } from './date-picker' +export { DatePicker } from './date-picker' diff --git a/src/components/dropdown/dropdown.tsx b/src/components/dropdown/dropdown.tsx index d29eb577..5a636724 100644 --- a/src/components/dropdown/dropdown.tsx +++ b/src/components/dropdown/dropdown.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from 'react' import { Portal } from '../portal/Portal' import { useDropdown } from './useDropdown' +import { useEffect, useState } from 'react' export interface DropdownOption { id: string @@ -38,6 +39,7 @@ export function Dropdown({ placeholder, }: DropdownProps) { const { isOpen, toggle, close, dropdownRef, dropdownContentRef } = useDropdown() + const [dropdownPosition, setDropdownPosition] = useState({ top: '0px', left: '0px' }) const handleOptionClick = (option: DropdownOption) => { if (option.disabled) return @@ -50,72 +52,108 @@ export function Dropdown({ } } - const getDropdownPosition = () => { - if (!dropdownRef.current || !isOpen) return {} - - const rect = dropdownRef.current.getBoundingClientRect() - const viewportHeight = window.innerHeight - const viewportWidth = window.innerWidth - const scrollY = window.scrollY - const scrollX = window.scrollX - - const dropdownWidth = - width === 'auto' - ? 250 - : width === 'full' - ? Math.min(400, viewportWidth - 32) - : parseInt(width as string, 10) || 250 - - const dropdownHeight = 300 - - let top: number - let left: number - - switch (position) { - case 'bottom-left': - case 'top-left': - left = rect.left + scrollX - break - case 'bottom-right': - case 'top-right': - left = rect.right + scrollX - dropdownWidth - break - case 'bottom-center': - left = rect.left + scrollX + rect.width / 2 - dropdownWidth / 2 - break - default: - left = rect.left + scrollX - } + useEffect(() => { + if (!isOpen || !dropdownRef.current) return + + const calculatePosition = () => { + if (!dropdownRef.current) return + + const rect = dropdownRef.current.getBoundingClientRect() + const viewportHeight = window.innerHeight + const viewportWidth = window.innerWidth + + const dropdownWidth = + width === 'auto' + ? 250 + : width === 'full' + ? Math.min(400, viewportWidth - 32) + : parseInt(width as string, 10) || 250 + + const dropdownHeight = dropdownContentRef.current + ? dropdownContentRef.current.offsetHeight + : parseInt(maxHeight, 10) || 300 - // Calculate vertical position - if (position.startsWith('bottom')) { - top = rect.bottom + scrollY + 4 - if (rect.bottom + dropdownHeight > viewportHeight) { - top = rect.top + scrollY - dropdownHeight - 4 + let top: number + let left: number + + switch (position) { + case 'bottom-left': + case 'top-left': + left = rect.left + break + case 'bottom-right': + case 'top-right': + left = rect.right - dropdownWidth + break + case 'bottom-center': + left = rect.left + rect.width / 2 - dropdownWidth / 2 + break + default: + left = rect.left + } + + if (position.startsWith('bottom')) { + top = rect.bottom + 4 + if (top + dropdownHeight > viewportHeight - 16) { + top = rect.top - dropdownHeight - 4 + } + } else { + top = rect.top - dropdownHeight - 4 + if (top < 16) { + top = rect.bottom + 4 + } } - } else { - top = rect.top + scrollY - dropdownHeight - 4 - if (rect.top - dropdownHeight < 0) { - top = rect.bottom + scrollY + 4 + + const padding = 16 + if (left < padding) left = padding + if (left + dropdownWidth > viewportWidth - padding) { + left = viewportWidth - dropdownWidth - padding } + + if (top < padding) top = padding + if (top + dropdownHeight > viewportHeight - padding) { + top = viewportHeight - dropdownHeight - padding + } + + setDropdownPosition({ + top: `${Math.max(0, top)}px`, + left: `${Math.max(0, left)}px`, + }) } - const padding = 16 - if (left < padding) left = padding - if (left + dropdownWidth > viewportWidth - padding) { - left = viewportWidth - dropdownWidth - padding + // محاسبه اولیه با تاخیر کوچک + const initialTimeout = setTimeout(calculatePosition, 10) + + // Event listeners + window.addEventListener('resize', calculatePosition) + window.addEventListener('scroll', calculatePosition, true) + + // ResizeObserver برای track کردن تغییرات سایز + const resizeObserver = new ResizeObserver(() => { + // استفاده از requestAnimationFrame فقط برای این callback + // تا smooth باشد اما loop نداشته باشیم + requestAnimationFrame(calculatePosition) + }) + + // Track trigger و همه parent هاش + let element: HTMLElement | null = dropdownRef.current + while (element) { + resizeObserver.observe(element) + element = element.parentElement } - if (top < padding) top = padding - if (top + dropdownHeight > viewportHeight - padding) { - top = viewportHeight - dropdownHeight - padding + // Track خود dropdown هم + if (dropdownContentRef.current) { + resizeObserver.observe(dropdownContentRef.current) } - return { - top: `${Math.max(0, top)}px`, - left: `${Math.max(0, left)}px`, + return () => { + clearTimeout(initialTimeout) + window.removeEventListener('resize', calculatePosition) + window.removeEventListener('scroll', calculatePosition, true) + resizeObserver.disconnect() } - } + }, [isOpen, position, width, maxHeight]) const dropdownContent = children || ( <> @@ -150,7 +188,6 @@ export function Dropdown({ {trigger}
- {/* Dropdown Menu Portal */} {isOpen && !disabled && (
@@ -158,9 +195,9 @@ export function Dropdown({ ref={dropdownContentRef} className={` fixed z-[9999] border border-content rounded-xl - bg-content shadow-xl + bg-content shadow-xl overflow-hidden pointer-events-auto - animate-in fade-in-0 zoom-in-95 duration-100 + animate-in fade-in-0 zoom-in-95 duration-100 bg-glass ${dropdownClassName} `} style={{ @@ -172,7 +209,7 @@ export function Dropdown({ ? '100%' : width, minWidth: width === 'auto' ? '150px' : undefined, - ...getDropdownPosition(), + ...dropdownPosition, }} >
diff --git a/src/components/filter-tooltip/filter-tooltip.tsx b/src/components/filter-tooltip/filter-tooltip.tsx new file mode 100644 index 00000000..dfc7a4b7 --- /dev/null +++ b/src/components/filter-tooltip/filter-tooltip.tsx @@ -0,0 +1,78 @@ +import { useState, useRef, ReactNode } from 'react' +import { FaFilter } from 'react-icons/fa' +import { ClickableTooltip } from '@/components/clickableTooltip' +import Tooltip from '../toolTip' +import { Button } from '../button/button' + +export interface FilterOption { + value: string + label: string +} + +export interface FilterTooltipProps { + options: FilterOption[] + value: string + onChange: (value: string) => void + placeholder?: string + className?: string + buttonClassName?: string + tooltipClassName?: string + icon: ReactNode +} + +export function FilterTooltip({ + options, + value, + onChange, + placeholder = 'فیلتر', + className, + buttonClassName, + tooltipClassName, + icon, +}: FilterTooltipProps) { + const [showTooltip, setShowTooltip] = useState(false) + const filterButtonRef = useRef(null) + + const handleFilterSelect = (selectedValue: string) => { + onChange(selectedValue) + setShowTooltip(false) + } + + return ( +
+ + + + + {options.map((option) => ( + + ))} +
+ } + /> +
+ ) +} diff --git a/src/components/filter-tooltip/index.ts b/src/components/filter-tooltip/index.ts new file mode 100644 index 00000000..27d286d1 --- /dev/null +++ b/src/components/filter-tooltip/index.ts @@ -0,0 +1,5 @@ +export { + FilterTooltip, + type FilterOption, + type FilterTooltipProps, +} from './filter-tooltip' diff --git a/src/components/text-input.tsx b/src/components/text-input.tsx index 9cc61cdf..a093b619 100644 --- a/src/components/text-input.tsx +++ b/src/components/text-input.tsx @@ -19,7 +19,7 @@ interface TextInputProps { disabled?: boolean name?: string type?: string - direction?: 'rtl' | 'ltr' | '' + direction?: 'rtl' | 'ltr' | 'auto' | '' className?: string ref?: React.RefObject debounce?: boolean diff --git a/src/context/todo.context.tsx b/src/context/todo.context.tsx index 04dbc6ea..27a195ce 100644 --- a/src/context/todo.context.tsx +++ b/src/context/todo.context.tsx @@ -25,13 +25,11 @@ export enum TodoPriority { Medium = 'medium', High = 'high', } -export interface TodoOptions { - viewMode: TodoViewType -} + export interface AddTodoInput { text: string date: string - priority?: TodoPriority + priority?: TodoPriority | null category?: string notes?: string } @@ -40,12 +38,10 @@ interface TodoContextType { todos: Todo[] setTodos: (todos: Todo[]) => void addTodo: (input: AddTodoInput) => void - todoOptions: TodoOptions refetchTodos: any toggleTodo: (id: string) => void updateTodo: (id: string, updates: Partial>) => void clearCompleted: (date?: string) => void - updateOptions: (options: Partial) => void reorderTodos: (todos: Todo[]) => Promise isPending: boolean } @@ -55,23 +51,15 @@ const TodoContext = createContext(null) export function TodoProvider({ children }: { children: React.ReactNode }) { const { isAuthenticated } = useAuth() const [todos, setTodos] = useState(null) - const [todoOptions, setTodoOptions] = useState({ - viewMode: TodoViewType.All, - }) const { data: fetchedTodos, refetch, isPending } = useGetTodos(isAuthenticated) const { mutateAsync: addTodoAsync, isPending: isAdding } = useAddTodo() const { mutateAsync: updateTodoAsync, isPending: isUpdating } = useUpdateTodo() const { mutateAsync: reorderTodosAsync } = useReorderTodos() - const didLoadInitialOptions = useRef(false) - useEffect(() => { async function load() { - const [todos, todoOptions] = await Promise.all([ - getFromStorage('todos'), - getFromStorage('todoOptions'), - ]) + const todos = await getFromStorage('todos') const migratedTodos = (todos || []).map((todo: Todo, index: number) => ({ ...todo, @@ -79,15 +67,6 @@ export function TodoProvider({ children }: { children: React.ReactNode }) { })) setTodos(migratedTodos) - if (todoOptions) { - setTodoOptions(todoOptions) - } else { - setToStorage('todoOptions', { - viewMode: TodoViewType.All, - }) - } - - didLoadInitialOptions.current = true } const todosChangedEvent = listenEvent('todosChanged', (todoList: Todo[]) => { @@ -133,19 +112,6 @@ export function TodoProvider({ children }: { children: React.ReactNode }) { setToStorage('todos', todos) }, [todos]) - useEffect(() => { - if (!didLoadInitialOptions.current) return - - setToStorage('todoOptions', todoOptions) - }, [todoOptions]) - - function updateOptions(options: Partial) { - setTodoOptions((prev) => ({ - ...prev, - ...options, - })) - } - const addTodo = async (input: AddTodoInput) => { if (!isAuthenticated) { showToast('برای اضافه کردن وظیفه باید وارد حساب کاربری شوید.', 'error') @@ -163,7 +129,7 @@ export function TodoProvider({ children }: { children: React.ReactNode }) { text: input.text, completed: false, date: input.date, - priority: input.priority || TodoPriority.Low, + priority: input.priority, category: input.category || '', order: maxOrder + 1, description: input.notes || '', @@ -324,8 +290,6 @@ export function TodoProvider({ children }: { children: React.ReactNode }) { toggleTodo, updateTodo, clearCompleted, - updateOptions, - todoOptions, reorderTodos, refetchTodos: refetch, isPending: isPending || isAdding || isUpdating, diff --git a/src/layouts/bookmark/bookmarks.tsx b/src/layouts/bookmark/bookmarks.tsx index ddc81699..99be839b 100644 --- a/src/layouts/bookmark/bookmarks.tsx +++ b/src/layouts/bookmark/bookmarks.tsx @@ -30,7 +30,6 @@ export function BookmarksList() { const { isAuthenticated } = useAuth() const [showAddBookmarkModal, setShowAddBookmarkModal] = useState(false) - const [folderPath, setFolderPath] = useState([]) const sensors = useSensors( @@ -51,10 +50,7 @@ export function BookmarksList() { ) const { active, over } = event - - if (!over || active.id === over.id) { - return - } + if (!over || active.id === over.id) return const allBookmarks = [...bookmarks] const currentItems = getCurrentFolderItems(currentFolderId) @@ -63,9 +59,7 @@ export function BookmarksList() { (b) => b.id === active.id || b.onlineId === active.id ) - if (!sourceBookmark) { - return - } + if (!sourceBookmark) return const sourceIndex = currentItems.findIndex( (item) => item.id === active.id || item.onlineId === active.id @@ -74,9 +68,7 @@ export function BookmarksList() { (item) => item.id === over.id || item.onlineId === over.id ) - if (sourceIndex === -1 || sourceIndex === targetIndex) { - return - } + if (sourceIndex === -1 || sourceIndex === targetIndex) return const actualSourceIndex = allBookmarks.findIndex( (b) => @@ -109,7 +101,6 @@ export function BookmarksList() { setBookmarks(updatedBookmarks) let folderIdToSend = currentFolderId - const isFolderIdValidUuid = validate(currentFolderId) if (isFolderIdValidUuid) { const foundedFolder = bookmarks.find((b) => b.id === currentFolderId) @@ -139,7 +130,6 @@ export function BookmarksList() { setCurrentFolderId(null) return } - const newPath = folderPath.slice(0, depth + 1) setFolderPath(newPath) setCurrentFolderId(folderId) @@ -153,15 +143,12 @@ export function BookmarksList() { const fillersCount = Math.max(0, TOTAL_BOOKMARKS - currentFolderItems.length) const fillers = new Array(fillersCount).fill(null) const addButton = currentFolderItems.length < TOTAL_BOOKMARKS ? [null] : [] - return [...baseItems, ...fillers, ...addButton].slice(0, TOTAL_BOOKMARKS) } - const bookmarkCount = currentFolderItems.length const minBookmarks = 10 const needsFillers = bookmarkCount < minBookmarks const fillersCount = needsFillers ? minBookmarks - bookmarkCount : 0 - return [...currentFolderItems, ...new Array(fillersCount).fill(null), null] } @@ -177,7 +164,7 @@ export function BookmarksList() {
)} - setFolderPath(path)} - openAddBookmarkModal={() => setShowAddBookmarkModal(true)} - /> +
+ setFolderPath(path)} + openAddBookmarkModal={() => setShowAddBookmarkModal(true)} + /> +
{showAddBookmarkModal && !isAuthenticated ? ( diff --git a/src/layouts/bookmark/components/bookmark-emptySlot.tsx b/src/layouts/bookmark/components/bookmark-emptySlot.tsx index 98dd987a..4ac84034 100644 --- a/src/layouts/bookmark/components/bookmark-emptySlot.tsx +++ b/src/layouts/bookmark/components/bookmark-emptySlot.tsx @@ -1,5 +1,4 @@ import { TbBookmarkPlus } from 'react-icons/tb' -import Tooltip from '@/components/toolTip' export function EmptyBookmarkSlot({ onClick, @@ -22,29 +21,27 @@ export function EmptyBookmarkSlot({ } return ( - - - +
+ + {canAdd && ( +
+ )} + ) } diff --git a/src/layouts/bookmark/components/bookmark-folder.tsx b/src/layouts/bookmark/components/bookmark-folder.tsx index dca4c6b5..03f45c98 100644 --- a/src/layouts/bookmark/components/bookmark-folder.tsx +++ b/src/layouts/bookmark/components/bookmark-folder.tsx @@ -2,10 +2,13 @@ import { useState } from 'react' import { FaFolder, FaFolderOpen } from 'react-icons/fa' import { SlOptions } from 'react-icons/sl' import { addOpacityToColor } from '@/common/color' -import Tooltip from '@/components/toolTip' import type { Bookmark } from '../types/bookmark.types' import { RenderStickerPattern } from './bookmark/bookmark-sticker' import { BookmarkTitle } from './bookmark/bookmark-title' +import { useBookmarkStore } from '../context/bookmark.context' +import { getFaviconFromUrl } from '@/common/utils/icon' +import { BookmarkIcon } from './bookmark/bookmark-icon' +import noInternet from '@/assets/images/no-internet.png' export function FolderBookmarkItem({ bookmark, @@ -18,25 +21,54 @@ export function FolderBookmarkItem({ isDragging?: boolean onMenuClick?: (e: React.MouseEvent) => void }) { + const { getCurrentFolderItems } = useBookmarkStore() + const [isHovered, setIsHovered] = useState(false) - const getFolderStyle = () => { - return 'border border-primary/0 hover:border-primary/40 bg-content bg-glass hover:bg-primary/20' - } + const folderItems = getCurrentFolderItems(bookmark.id) + .filter((item) => item.type === 'BOOKMARK') + .slice(0, 6) + + const renderFolderIcons = () => { + if (bookmark.icon) { + return + } + + if (folderItems.length > 0) { + return ( +
+ {folderItems.map((child, index) => ( +
+ { + ;(e.target as HTMLImageElement).src = noInternet + }} + /> +
+ ))} +
+ ) + } - const displayIcon = - bookmark.icon || - (isHovered ? ( - + return isHovered ? ( + ) : ( - - )) + + ) + } const customStyles = bookmark.customBackground - ? { + ? ({ + '--custom-bg': bookmark.customBackground, + '--custom-border': addOpacityToColor(bookmark.customBackground, 0.2), backgroundColor: bookmark.customBackground, borderColor: addOpacityToColor(bookmark.customBackground, 0.2), - } + } as React.CSSProperties) : {} const handleMouseDown = (e: React.MouseEvent) => { @@ -45,63 +77,58 @@ export function FolderBookmarkItem({ } } + const getFolderStyle = () => { + return 'bg-content bg-glass hover:bg-primary/20' + } + return ( -
- - - + {onMenuClick && ( +
{ + e.stopPropagation() + onMenuClick(e) + }} + className={ + 'absolute cursor-pointer top-1.5 right-1.5 p-1 rounded-full opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-base-content/10 z-10' + } + > + +
+ )} +
) } diff --git a/src/layouts/bookmark/components/bookmark-item.tsx b/src/layouts/bookmark/components/bookmark-item.tsx index d22238b0..225760fe 100644 --- a/src/layouts/bookmark/components/bookmark-item.tsx +++ b/src/layouts/bookmark/components/bookmark-item.tsx @@ -1,6 +1,5 @@ import { SlOptions } from 'react-icons/sl' import { addOpacityToColor } from '@/common/color' -import Tooltip from '@/components/toolTip' import type { Bookmark } from '../types/bookmark.types' import { BookmarkIcon } from './bookmark/bookmark-icon' import { RenderStickerPattern } from './bookmark/bookmark-sticker' @@ -20,10 +19,6 @@ export function BookmarkItem({ isDragging = false, onMenuClick, }: BookmarkItemProps) { - const getBookmarkStyle = () => { - return 'bg-content hover:bg-base-300 text-content backdrop-blur-sm bg-glass' - } - const customStyles = bookmark.customBackground ? { backgroundColor: bookmark.customBackground, @@ -39,45 +34,49 @@ export function BookmarkItem({ return (
- - - +
+
) } diff --git a/src/layouts/bookmark/components/bookmark/bookmark-icon.tsx b/src/layouts/bookmark/components/bookmark/bookmark-icon.tsx index 465daab5..61371a66 100644 --- a/src/layouts/bookmark/components/bookmark/bookmark-icon.tsx +++ b/src/layouts/bookmark/components/bookmark/bookmark-icon.tsx @@ -1,9 +1,11 @@ import { FaFolder } from 'react-icons/fa' import { getFaviconFromUrl } from '@/common/utils/icon' import type { Bookmark } from '../../types/bookmark.types' +import noInternet from '@/assets/images/no-internet.png' export function BookmarkIcon({ bookmark }: { bookmark: Bookmark }) { let displayIcon: string | React.ReactNode + if (bookmark.icon) { displayIcon = bookmark.icon } else if (bookmark.type === 'BOOKMARK') { @@ -16,14 +18,27 @@ export function BookmarkIcon({ bookmark }: { bookmark: Bookmark }) { displayIcon = } + const handleImageAnalysis = (e: React.SyntheticEvent) => { + const img = e.target as HTMLImageElement + + if (img.naturalWidth < 32 || img.naturalHeight < 32) { + img.src = 'https://cdn.widgetify.ir/system/bookmark.png' + } + } + return ( -
+
{typeof displayIcon === 'string' ? ( {bookmark.title} { + const target = e.target as HTMLImageElement + if (target.src !== noInternet) target.src = noInternet + }} /> ) : ( displayIcon diff --git a/src/layouts/bookmark/components/bookmark/bookmark-title.tsx b/src/layouts/bookmark/components/bookmark/bookmark-title.tsx index 7ddfb7bf..3634ba5e 100644 --- a/src/layouts/bookmark/components/bookmark/bookmark-title.tsx +++ b/src/layouts/bookmark/components/bookmark/bookmark-title.tsx @@ -7,14 +7,13 @@ export function BookmarkTitle({ customTextColor?: string }) { return ( - - {title} - +
+ + {title} + +
) } diff --git a/src/layouts/bookmark/components/folder-header.tsx b/src/layouts/bookmark/components/folder-header.tsx index e803e451..d2a0e681 100644 --- a/src/layouts/bookmark/components/folder-header.tsx +++ b/src/layouts/bookmark/components/folder-header.tsx @@ -6,6 +6,8 @@ import Modal from '@/components/modal' import Tooltip from '@/components/toolTip' import type { FolderPathItem } from '../types/bookmark.types' import { FolderPath } from './folder-path' +import { IoHome } from 'react-icons/io5' +import { LuX } from 'react-icons/lu' interface FolderHeaderProps { folderPath: FolderPathItem[] @@ -36,6 +38,15 @@ export function FolderHeader({ folderPath, onNavigate }: FolderHeaderProps) { + + +
)} {renderIconPreview( - formData.icon, + formData.icon || + (formData.url && getFaviconFromUrl(formData.url)), type === 'FOLDER' ? 'upload' : iconSource, setIconSource, (value) => updateFormData('icon', value) diff --git a/src/layouts/bookmark/components/modal/advanced.modal.tsx b/src/layouts/bookmark/components/modal/advanced.modal.tsx index 6bfdd5fd..c45ebc29 100644 --- a/src/layouts/bookmark/components/modal/advanced.modal.tsx +++ b/src/layouts/bookmark/components/modal/advanced.modal.tsx @@ -27,6 +27,7 @@ interface AdvancedModalProps { type: BookmarkType title: string url: string | null + icon: any } } @@ -290,23 +291,25 @@ export function AdvancedModal({ title, onClose, isOpen, bookmark }: AdvancedModa backgroundPosition: 'center', }} > - {}} - /> +
+ {}} + /> +
diff --git a/src/layouts/bookmark/components/modal/edit-bookmark.modal.tsx b/src/layouts/bookmark/components/modal/edit-bookmark.modal.tsx index e79d04d2..e12e5aad 100644 --- a/src/layouts/bookmark/components/modal/edit-bookmark.modal.tsx +++ b/src/layouts/bookmark/components/modal/edit-bookmark.modal.tsx @@ -30,6 +30,7 @@ export interface BookmarkUpdateFormFields { customTextColor: string | null sticker: string | null icon: File | null + isDeletedIcon: boolean } const empty: BookmarkUpdateFormFields = { @@ -42,6 +43,7 @@ const empty: BookmarkUpdateFormFields = { customTextColor: '', sticker: '', icon: null, + isDeletedIcon: false, } type UpdateBookmarkUpdateFormData = ( key: K, @@ -65,11 +67,11 @@ export function EditBookmarkModal({ const [icon, setIcon] = useState(null) const type = bookmark?.type - const { fileInputRef, renderIconPreview, handleImageUpload } = useBookmarkIcon() const updateFormData: UpdateBookmarkUpdateFormData = (key, value) => { if (key === 'icon') { + // @ts-expect-error setIcon(value || null) } setFormData((prev) => ({ ...prev, [key]: value })) @@ -88,10 +90,18 @@ export function EditBookmarkModal({ icon: formData.icon, id: bookmark.id, onlineId: bookmark.onlineId || undefined, + isDeletedIcon: formData.isDeletedIcon, }) // onClose() } + const onRemoveIcon = () => { + setIcon(null) + updateFormData('icon', null) + updateFormData('isDeletedIcon', true) + setIconSource('auto') + } + const handleAdvancedModalClose = ( data: { background: string | null @@ -128,6 +138,7 @@ export function EditBookmarkModal({ icon: null, sticker: bookmark.sticker, url: bookmark.url, + isDeletedIcon: false, }) setIconSource(bookmark.icon ? 'upload' : 'auto') if (bookmark.icon) { @@ -152,22 +163,30 @@ export function EditBookmarkModal({ >
-
+
{type === 'BOOKMARK' && ( )} - {renderIconPreview( - icon, - type === 'FOLDER' ? 'upload' : iconSource, - setIconSource, - (value) => updateFormData('icon', value) - )} -

- برای آپلود تصویر کلیک کنید یا فایل را بکشید و رها کنید -

+
+ {renderIconPreview( + icon, + type === 'FOLDER' ? 'upload' : iconSource, + setIconSource, + (value) => updateFormData('icon', value) + )} + {iconSource === 'upload' && Boolean(icon) && ( + + )} +
@@ -136,7 +135,7 @@ export function useBookmarkIcon() { Favicon { cb(null) setIconLoadError(true) diff --git a/src/layouts/bookmark/context/bookmark.context.tsx b/src/layouts/bookmark/context/bookmark.context.tsx index b57a5a24..eaeae81a 100644 --- a/src/layouts/bookmark/context/bookmark.context.tsx +++ b/src/layouts/bookmark/context/bookmark.context.tsx @@ -259,6 +259,7 @@ export const BookmarkProvider: React.FC<{ children: React.ReactNode }> = ({ title: input.title?.trim(), icon: input.icon || null, url: input.url, + isDeletedIcon: input.isDeletedIcon, }) ) diff --git a/src/layouts/search/browser-bookmark/browser-bookmark.tsx b/src/layouts/search/browser-bookmark/browser-bookmark.tsx index 9a919d60..b75230f1 100644 --- a/src/layouts/search/browser-bookmark/browser-bookmark.tsx +++ b/src/layouts/search/browser-bookmark/browser-bookmark.tsx @@ -125,7 +125,6 @@ export function BrowserBookmark() {
- {/* پاپ‌اور با مختصات هوشمند */} setIsOpen(false)} diff --git a/src/layouts/search/image/image-search.button.tsx b/src/layouts/search/image/image-search.button.tsx new file mode 100644 index 00000000..88876b40 --- /dev/null +++ b/src/layouts/search/image/image-search.button.tsx @@ -0,0 +1,30 @@ +import Analytics from '@/analytics' +import Tooltip from '@/components/toolTip' + +export function ImageSearchButton({ onClick }: { onClick: () => void }) { + const onClickHandle = () => { + Analytics.event('searchbox_open_image_search') + onClick() + } + + return ( + + + + ) +} diff --git a/src/layouts/search/image/image-search.portal.tsx b/src/layouts/search/image/image-search.portal.tsx new file mode 100644 index 00000000..ab66cd86 --- /dev/null +++ b/src/layouts/search/image/image-search.portal.tsx @@ -0,0 +1,219 @@ +import { useRef, useState } from 'react' +import { MdClose, MdLink, MdOutlinePrivacyTip } from 'react-icons/md' +import { Button } from '@/components/button/button' +import { showToast } from '@/common/toast' +import { TextInput } from '@/components/text-input' +import Analytics from '@/analytics' +import { RequireAuth } from '@/components/auth/require-auth' +import { getMainClient } from '@/services/api' +import { translateError } from '@/utils/translate-error' + +export function ImageSearchPortal({ onClose }: { onClose: () => void }) { + const [isUploading, setIsUploading] = useState(false) + const [imageUrl, setImageUrl] = useState('') + const fileInputRef = useRef(null) + const [uploadProgress, setUploadProgress] = useState(0) + const handleUpload = async (file: File) => { + if (!file.type.startsWith('image/')) { + showToast('لطفاً فقط فایل تصویری انتخاب کنید', 'error') + return + } + + if (file.size > 1 * 1024 * 1024) { + showToast('حجم فایل نباید بیشتر از ۱ مگابایت باشد', 'error') + return + } + + setIsUploading(true) + setUploadProgress(0) + Analytics.event('searchbox_image_file') + + try { + const formData = new FormData() + formData.append('image', file) + + const client = await getMainClient() + const response = await client.post('/users/@me/upload/search', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + onUploadProgress: (progressEvent) => { + const percentCompleted = Math.round( + (progressEvent.loaded * 100) / (progressEvent.total || 1) + ) + setUploadProgress(percentCompleted) + }, + }) + + const data = response.data + window.open( + `https://www.google.com/searchbyimage?image_url=${encodeURIComponent(data.url)}&client=chrome`, + '_blank' + ) + onClose() + } catch (er) { + showToast(translateError(er) as string, 'error') + } finally { + setIsUploading(false) + } + } + + const handleUrlSearch = () => { + if (!imageUrl) return + window.open( + `https://www.google.com/searchbyimage?image_url=${encodeURIComponent(imageUrl)}&client=chrome`, + '_blank' + ) + Analytics.event('searchbox_image_url') + + onClose() + } + + return ( +
+
+ + جستجوی تصویر با گوگل + +
+ + + + +
+
+ +
+ +
e.preventDefault()} + onDrop={(e) => { + e.preventDefault() + const file = e.dataTransfer.files[0] + if (file) handleUpload(file) + }} + className="relative flex flex-col items-center justify-center py-8 transition-all border-2 border-dashed cursor-pointer group border-base-content/10 rounded-2xl hover:border-primary/40 hover:bg-primary/5" + onClick={() => fileInputRef.current?.click()} + > +
+ + + + + + +
+

+ یک تصویر را اینجا بکشید یا{' '} + + فایل را انتخاب کنید + +

+ {isUploading && ( +
+ + +
+ + +
+ + {uploadProgress < 100 + ? 'در حال ارسال تصویر...' + : 'در حال جستجو در گوگل...'} + + + {uploadProgress}% + +
+ +
+
+
+
+
+ )} +
+
+
+
+ +
+ setImageUrl(v)} + placeholder="لینک تصویر را پیست کنید..." + className="flex-1 py-2 text-xs bg-transparent border-none! outline-none! ring-transparent! focus:placeholder:opacity-50" + onKeyDown={(e) => e.key === 'Enter' && handleUrlSearch()} + direction={imageUrl ? 'auto' : 'rtl'} + /> + +
+
+ + e.target.files?.[0] && handleUpload(e.target.files[0])} + /> +
+ ) +} diff --git a/src/layouts/search/search.tsx b/src/layouts/search/search.tsx index 47f8bfe9..6a924a36 100644 --- a/src/layouts/search/search.tsx +++ b/src/layouts/search/search.tsx @@ -2,8 +2,11 @@ import { useEffect, useRef, useState } from 'react' import { MdOutlineClear } from 'react-icons/md' import Analytics from '@/analytics' import { BrowserBookmark } from './browser-bookmark/browser-bookmark' -import { VoiceSearchButton } from './voice/VoiceSearchButton' +import { VoiceSearchButton } from './voice/voice-search.button' import { FcGoogle } from 'react-icons/fc' +import { ImageSearchPortal } from './image/image-search.portal' +import { VoiceSearchPortal } from './voice/voice-search.portal' +import { ImageSearchButton } from './image/image-search.button' export function SearchLayout() { const [searchQuery, setSearchQuery] = useState('') @@ -11,6 +14,7 @@ export function SearchLayout() { const searchRef = useRef(null) const inputRef = useRef(null) const trendingRef = useRef(null) + const [activePortal, setActivePortal] = useState<'voice' | 'image' | null>(null) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() @@ -64,6 +68,15 @@ export function SearchLayout() { return (
+ {activePortal === 'voice' && ( + setActivePortal(null)} + onSearch={handleVoiceSearch} + /> + )} + {activePortal === 'image' && ( + setActivePortal(null)} /> + )}
- +
+ setActivePortal('voice')} /> + setActivePortal('image')} /> +
void - className?: string -} - -export function VoiceSearchButton({ onSearch, className = '' }: VoiceSearchButtonProps) { - const [isModalOpen, setIsModalOpen] = useState(false) - - const handleClick = () => { - setIsModalOpen(true) - Analytics.event('voice_search_button_clicked') - } - - return ( - <> - -
- - - - - - -
-
- - setIsModalOpen(false)} - onSearch={onSearch} - /> - - ) -} diff --git a/src/layouts/search/voice/VoiceSearchModal.tsx b/src/layouts/search/voice/VoiceSearchModal.tsx deleted file mode 100644 index 870f3754..00000000 --- a/src/layouts/search/voice/VoiceSearchModal.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { useEffect, useMemo, useState } from 'react' -import { FaSearch } from 'react-icons/fa' -import { FiMic, FiMicOff, FiSettings } from 'react-icons/fi' -import Analytics from '@/analytics' -import { Button } from '@/components/button/button' -import Modal from '@/components/modal' -import { useVoiceSearch } from './useVoiceSearch' - -interface VoiceSearchModalProps { - isOpen: boolean - onClose: () => void - onSearch?: (query: string) => void -} -export type Language = 'fa-IR' | 'en-US' -const languages = [ - { code: 'fa-IR' as Language, name: 'فارسی', flag: '🇮🇷' }, - { code: 'en-US' as Language, name: 'انگلیسی', flag: '🇺🇸' }, -] - -export function VoiceSearchModal({ isOpen, onClose, onSearch }: VoiceSearchModalProps) { - const [selectedLanguage, setSelectedLanguage] = useState('fa-IR') - const [isLanguageMenuOpen, setIsLanguageMenuOpen] = useState(false) - - const barHeights = useMemo(() => { - return [...Array(6)].map(() => Math.random() * 15 + 8) - }, []) - - const { - isListening, - currentTranscript, - startVoiceSearch, - stopVoiceSearch, - clearTranscript, - } = useVoiceSearch(() => {}, selectedLanguage) - - useEffect(() => { - if (isOpen && !isListening) { - startVoiceSearch() - } else if (!isOpen && isListening) { - stopVoiceSearch() - } - }, [isOpen, isListening, startVoiceSearch, stopVoiceSearch]) - - const handleClose = () => { - stopVoiceSearch() - clearTranscript() - onClose() - } - - const handleLanguageChange = (language: Language) => { - setSelectedLanguage(language) - setIsLanguageMenuOpen(false) - if (isListening) { - stopVoiceSearch() - setTimeout(() => startVoiceSearch(), 100) - } - } - - const handleSearch = () => { - if (currentTranscript.trim() && onSearch) { - Analytics.event('voice_search_submitted') - stopVoiceSearch() - onSearch(currentTranscript.trim()) - onClose() - } - } - - const getLanguageName = (lang: Language) => { - return lang === 'fa-IR' ? 'فارسی' : 'انگلیسی' - } - - return ( - -
-
- {isListening && ( -
- {barHeights.slice(0, 3).map((height, i) => ( -
- ))} -
- )} - -
- {isListening ? : } -
- - {isListening && ( -
- {barHeights.slice(3, 6).map((height, i) => ( -
- ))} -
- )} -
- -
-

- {currentTranscript - ? currentTranscript - : isListening - ? 'در حال گوش دادن...' - : 'آماده برای گوش دادن'} -

-

- {isListening - ? `زبان: ${getLanguageName(selectedLanguage)}` - : 'برای شروع گوش دادن کلیک کنید'} -

-
- -
-
- - - {isLanguageMenuOpen && ( -
- {languages.map((lang) => ( - - ))} -
- )} -
-
- -
- - -
-
- - ) -} diff --git a/src/layouts/search/voice/voice-search.button.tsx b/src/layouts/search/voice/voice-search.button.tsx new file mode 100644 index 00000000..bdb732ec --- /dev/null +++ b/src/layouts/search/voice/voice-search.button.tsx @@ -0,0 +1,28 @@ +import Analytics from '@/analytics' +import Tooltip from '@/components/toolTip' + +export function VoiceSearchButton({ onClick }: { onClick: () => void }) { + const onClickHandle = () => { + Analytics.event('searchbox_open_voice_search') + onClick() + } + return ( + +
onClickHandle()} + className="flex items-center justify-center transition-all duration-300 rounded-full cursor-pointer h-9 w-9 shrink-0 hover:bg-base-300 group" + > + + + +
+
+ ) +} diff --git a/src/layouts/search/voice/voice-search.portal.tsx b/src/layouts/search/voice/voice-search.portal.tsx new file mode 100644 index 00000000..68f04f14 --- /dev/null +++ b/src/layouts/search/voice/voice-search.portal.tsx @@ -0,0 +1,115 @@ +import { useEffect, useState } from 'react' +import { MdClose, MdMic, MdSettings } from 'react-icons/md' +import { useVoiceSearch } from './useVoiceSearch' + +interface VoiceSearchPortalProps { + onClose: () => void + onSearch: (query: string) => void +} + +export type Language = 'fa-IR' | 'en-US' +const languages = [ + { code: 'fa-IR' as Language, name: 'فارسی' }, + { code: 'en-US' as Language, name: 'English' }, +] + +export function VoiceSearchPortal({ onClose, onSearch }: VoiceSearchPortalProps) { + const [selectedLanguage, setSelectedLanguage] = useState('fa-IR') + const [isLanguageMenuOpen, setIsLanguageMenuOpen] = useState(false) + + const { isListening, currentTranscript, startVoiceSearch, stopVoiceSearch } = + useVoiceSearch((result) => { + if (result.trim()) { + onSearch(result) + onClose() + } + }, selectedLanguage) + + useEffect(() => { + startVoiceSearch() + return () => stopVoiceSearch() + }, [selectedLanguage]) + + return ( +
+
+
+ + جستجوی صوتی + +
+ {[...Array(4)].map((_, i) => ( +
+ ))} +
+
+ +
+ +
+
+

+ {currentTranscript || + (selectedLanguage === 'fa-IR' + ? 'در حال گوش دادن...' + : 'Listening...')} +

+
+ +
+
+ + + {isLanguageMenuOpen && ( +
+ {languages.map((lang) => ( + + ))} +
+ )} +
+ + +
+
+
+ ) +} diff --git a/src/layouts/widgetify-card/daily-mood.tsx b/src/layouts/widgetify-card/daily-mood.tsx index 61defbac..7a292596 100644 --- a/src/layouts/widgetify-card/daily-mood.tsx +++ b/src/layouts/widgetify-card/daily-mood.tsx @@ -23,13 +23,13 @@ export function DailyMoodNotification() { const isAdding = useIsMutating({ mutationKey: ['upsertMoodLog'] }) > 0 const onRemoveNotif = () => { - callEvent('remove_from_notifications', { id: 'notificationMood', ttl: 1440 }) + callEvent('remove_from_notifications', { id: 'notificationMood', ttl: 420 }) } const handleMoodChange = async (value: string) => { if (isAdding) return if (value === '') return - + Analytics.event('notifications_daily_moods') const currentGregorian = today.clone().doAsGregorian() const [error, response] = await safeAwait< @@ -66,7 +66,7 @@ export function DailyMoodNotification() { }) callEvent('remove_from_notifications', { id: 'notificationMood', - ttl: 1440, + ttl: 420, }) }, 1500) Analytics.event('notification_mood_clicked') @@ -95,30 +95,34 @@ export function DailyMoodNotification() {
- {moodOptions.map((option) => ( -
!isAdding && handleMoodChange(option.value)} - className={`p-1.5 w-full shadow-sm rounded-xl transition-all cursor-pointer ${ - mood === option.value - ? `bg-${option.colorClass} text-${option.colorClass}-content scale-105` - : `bg-base-300 hover:bg-base-300/70 opacity-80 hover:opacity-100 hover:scale-95` - }`} - > - {isAdding ? ( -
- ) : ( -
-
- {option.emoji} -
-
- {option.label} + {moodOptions + .filter((f) => f.label) + .map((option) => ( +
+ !isAdding && handleMoodChange(option.value) + } + className={`p-1.5 w-full shadow-sm rounded-xl transition-all cursor-pointer ${ + mood === option.value + ? `bg-${option.colorClass} text-${option.colorClass}-content scale-105` + : `bg-base-300 hover:bg-base-300/70 opacity-80 hover:opacity-100 hover:scale-95` + }`} + > + {isAdding ? ( +
+ ) : ( +
+
+ {option.emoji} +
+
+ {option.label} +
-
- )} -
- ))} + )} +
+ ))}
diff --git a/src/layouts/widgetify-card/notification-center/components/notification-item.tsx b/src/layouts/widgetify-card/notification-center/components/notification-item.tsx index 5e6df67d..64e87995 100644 --- a/src/layouts/widgetify-card/notification-center/components/notification-item.tsx +++ b/src/layouts/widgetify-card/notification-center/components/notification-item.tsx @@ -1,6 +1,8 @@ import { callEvent } from '@/common/utils/call-event' import type { NotificationItem } from '@/services/hooks/extension/getWigiPadData.hook' -import { HiXMark } from 'react-icons/hi2' // آیکون مدرن‌تر +import { HiChevronDown, HiXMark } from 'react-icons/hi2' // آیکون مدرن‌تر +import { useState } from 'react' +import Analytics from '@/analytics' interface NotificationItemProps extends NotificationItem { onClose(e: any, id: string): any @@ -21,8 +23,10 @@ function Wrapper({ link, children, className, type, goTo, target }: Prop) { const handleClick = () => { if (type === 'page' && goTo) { callEvent('go_to_page', goTo) + Analytics.event('notifications_page') } else if (type === 'action' && target) { callEvent(target as any) + Analytics.event('notifications_action') } } @@ -53,13 +57,25 @@ export function NotificationCardItem(prop: NotificationItemProps) { const { link, icon, title, closeable, id, onClose, description, target, goTo, type } = prop + const [isExpanded, setIsExpanded] = useState(false) + const CHARACTER_LIMIT = 85 + + const toggleExpand = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + setIsExpanded(!isExpanded) + Analytics.event('notifications_toggle_expand') + } + + const shouldShowReadMore = description && description.length > CHARACTER_LIMIT + return ( {icon && (
@@ -77,9 +93,26 @@ export function NotificationCardItem(prop: NotificationItemProps) {
{description && ( -

- {description} -

+
+

+ {description} +

+ + {shouldShowReadMore && ( + + )} +
)}
diff --git a/src/layouts/widgetify-card/notification-center/notification-center.tsx b/src/layouts/widgetify-card/notification-center/notification-center.tsx index dc379a68..8ee25000 100644 --- a/src/layouts/widgetify-card/notification-center/notification-center.tsx +++ b/src/layouts/widgetify-card/notification-center/notification-center.tsx @@ -9,6 +9,7 @@ import { listenEvent } from '@/common/utils/call-event' import { getWithExpiry, setToStorage, setWithExpiry } from '@/common/storage' import type { ReactNode } from 'react' import { useGetNotifications } from '@/services/hooks/extension/getNotifications.hook' +import Analytics from '@/analytics' export function NotificationCenter() { const { data: fetchedNotifications } = useGetNotifications({ @@ -34,7 +35,12 @@ export function NotificationCenter() { ) if (!notifFromStorage) { setPushed((prev: any) => { - if (prev.some((item: NotificationItem) => item.id === notif.id)) { + if ( + prev.some( + (item: { id: string; node: ReactNode }) => + item.id === notif.id + ) + ) { return prev } return [...prev, notif] @@ -46,7 +52,7 @@ export function NotificationCenter() { const removeEvent = listenEvent( 'remove_from_notifications', async ({ id, ttl }) => { - setNotifications((prev) => prev.filter((item) => item.id !== id)) + setPushed((prev) => prev.filter((item) => item.id !== id)) if (ttl) { await setWithExpiry(`removed_notification_${id}`, 'true', ttl) } else { @@ -91,6 +97,7 @@ export function NotificationCenter() { const filtered = notifications.filter((f) => f.id !== id) setNotifications([...filtered]) + Analytics.event('notifications_close') } return ( @@ -99,9 +106,6 @@ export function NotificationCenter() {
{}} description={event.location || ''} @@ -138,10 +142,9 @@ export function NotificationCenter() { closeable={item.closeable} key={index} title={item.title} - subTitle={item.subTitle} description={item.description} link={item.link} - onClose={(e) => onClose(e, item.id || '')} + onClose={(e) => onClose(e, item.id || '', item.ttl)} icon={item.icon} goTo={item.goTo} target={item.target} diff --git a/src/layouts/widgetify-card/overviewCards/todo-overviewCard.tsx b/src/layouts/widgetify-card/overviewCards/todo-overviewCard.tsx deleted file mode 100644 index 4c72515d..00000000 --- a/src/layouts/widgetify-card/overviewCards/todo-overviewCard.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { motion } from 'framer-motion' -import { FiClipboard } from 'react-icons/fi' -import { useGeneralSetting } from '@/context/general-setting.context' -import { TodoViewType, useTodoStore } from '@/context/todo.context' -import { formatDateStr, getCurrentDate } from '@/layouts/widgets/calendar/utils' - -export function TodoOverviewCard() { - const { selected_timezone: timezone, blurMode } = useGeneralSetting() - - const { todos, todoOptions } = useTodoStore() - const today = getCurrentDate(timezone.value) - - const todayStr = formatDateStr(today) - const todayTodos = todos.filter((todo) => { - if (todoOptions.viewMode === TodoViewType.Day) { - return todo.date === todayStr - } - if (todoOptions.viewMode === TodoViewType.Monthly) { - const month = today.format('jMM') - return todo.date.startsWith(`${today.year()}-${month}`) - } - return true - }) - const completedTodos = todayTodos.filter((todo) => todo.completed) - const pendingTodos = todayTodos.filter((todo) => !todo.completed) - - const getTodoLabel = (mode: 'full' | 'short') => { - if (mode === 'full') { - return todoOptions.viewMode === TodoViewType.All - ? 'همه وظایف' - : todoOptions.viewMode === TodoViewType.Day - ? 'وظایف امروز' - : `وظایف ${today.format('jMMMM')} ماه` - } - - return todoOptions.viewMode === TodoViewType.All - ? '' - : todoOptions.viewMode === TodoViewType.Day - ? 'برای امروز' - : `برای ${today.format('jMMMM')} ماه` - } - - return ( - -
- 0 ? 'text-green-500' : 'opacity-50'} - /> -
-

{getTodoLabel('full')}

- {blurMode ? ( -

حالت مخفی فعال است.

- ) : ( -

- {pendingTodos.length > 0 - ? `${completedTodos.length} از ${todayTodos.length} وظیفه تکمیل شده` - : todayTodos.length > 0 - ? `تمام وظایف ${getTodoLabel('short')} تکمیل شده‌اند 👏` - : `هیچ وظیفه‌ای ${getTodoLabel('short')} تعریف نشده است`} -

- )} -
-
-
- ) -} diff --git a/src/layouts/widgetify-card/widgetify.layout.tsx b/src/layouts/widgetify-card/widgetify.layout.tsx index b5f14dc2..32b63c88 100644 --- a/src/layouts/widgetify-card/widgetify.layout.tsx +++ b/src/layouts/widgetify-card/widgetify.layout.tsx @@ -9,6 +9,7 @@ import { Pet } from './pets/pet' import { PetProvider } from './pets/pet.context' import { callEvent } from '@/common/utils/call-event' import { DailyMoodNotification } from './daily-mood' +import { BlurModeButton } from '@/components/blur-mode/blur-mode.button' export const WidgetifyLayout = () => { const { user, isAuthenticated, isLoadingUser } = useAuth() const { blurMode, updateSetting } = useGeneralSetting() @@ -18,7 +19,7 @@ export const WidgetifyLayout = () => { useEffect(() => { if (isAuthenticated && !isLoadingUser) { if (user?.name) setUserName(user.name) - if (!user?.hasTodayMood) { + if (user?.hasTodayMood === false && !user?.inCache) { callEvent('add_to_notifications', { id: 'notificationMood', node: , @@ -44,18 +45,12 @@ export const WidgetifyLayout = () => {
-

+

سلام {userName || '👋'}

- -
- {blurMode ? : } -
-
+ +
= ({ ) if (error) { const msg = translateError(error) - showToast(msg as any, 'error') + showToast(typeof msg === 'string' ? msg : 'خطایی رخ داد!', 'error') return } @@ -164,31 +164,33 @@ export const CalendarDayDetails: React.FC = ({
- {moodOptions.map((option) => ( - - ))} + {moodOptions + .filter((f) => f.label) + .map((option) => ( + + ))}
diff --git a/src/layouts/widgets/comboWidget/combo-widget.layout.tsx b/src/layouts/widgets/comboWidget/combo-widget.layout.tsx index 1ae2e8d0..2f2b805d 100644 --- a/src/layouts/widgets/comboWidget/combo-widget.layout.tsx +++ b/src/layouts/widgets/comboWidget/combo-widget.layout.tsx @@ -1,5 +1,4 @@ import { useEffect, useState } from 'react' -import { FaGear } from 'react-icons/fa6' import Analytics from '@/analytics' import { getFromStorage, setToStorage } from '@/common/storage' import { callEvent } from '@/common/utils/call-event' @@ -10,6 +9,7 @@ import { WidgetContainer } from '../widget-container' import { WigiArzLayout } from '../wigiArz/wigi_arz.layout' import { TabNavigation } from '@/components/tab-navigation' import { HiOutlineCurrencyBangladeshi, HiOutlineNewspaper } from 'react-icons/hi2' +import { CgOptions } from 'react-icons/cg' export type ComboTabType = 'news' | 'currency' @@ -48,8 +48,8 @@ export function ComboWidget() { if (!activeTab) return null return ( - -
+ +
-
+
diff --git a/src/layouts/widgets/notes/components/note-editor.tsx b/src/layouts/widgets/notes/components/note-editor.tsx index 1662239e..7360e8e6 100644 --- a/src/layouts/widgets/notes/components/note-editor.tsx +++ b/src/layouts/widgets/notes/components/note-editor.tsx @@ -91,7 +91,7 @@ export function NoteEditor({ note }: NoteEditorProps) {