From 1c6b32cba4d2b7ab7601be05159fcf5180f91e36 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Mon, 2 Feb 2026 01:06:22 +0330 Subject: [PATCH 01/26] remove subTitle --- .../notification-center/notification-center.tsx | 6 +----- src/services/hooks/extension/getWigiPadData.hook.ts | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/layouts/widgetify-card/notification-center/notification-center.tsx b/src/layouts/widgetify-card/notification-center/notification-center.tsx index dc379a68..e61c621f 100644 --- a/src/layouts/widgetify-card/notification-center/notification-center.tsx +++ b/src/layouts/widgetify-card/notification-center/notification-center.tsx @@ -99,9 +99,6 @@ export function NotificationCenter() {
{}} description={event.location || ''} @@ -138,10 +135,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/services/hooks/extension/getWigiPadData.hook.ts b/src/services/hooks/extension/getWigiPadData.hook.ts index 67e5da9f..85f39196 100644 --- a/src/services/hooks/extension/getWigiPadData.hook.ts +++ b/src/services/hooks/extension/getWigiPadData.hook.ts @@ -35,7 +35,6 @@ export interface UpcomingCalendarEvent { export interface NotificationItem { id?: string title: string - subTitle: string description?: string link?: string icon?: string @@ -43,6 +42,7 @@ export interface NotificationItem { type?: 'text' | 'url' | 'action' | 'page' goTo?: 'explorer' target?: string + ttl?: number } export interface WigiPadDataResponse { birthdays: WigiPadBirthday[] From 93dd17a82c0000ebae680e65375c6af2fb9e7035 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Mon, 2 Feb 2026 01:06:46 +0330 Subject: [PATCH 02/26] update mood ttl --- src/layouts/widgetify-card/daily-mood.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/layouts/widgetify-card/daily-mood.tsx b/src/layouts/widgetify-card/daily-mood.tsx index 61defbac..615e9e63 100644 --- a/src/layouts/widgetify-card/daily-mood.tsx +++ b/src/layouts/widgetify-card/daily-mood.tsx @@ -23,7 +23,7 @@ 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) => { @@ -66,7 +66,7 @@ export function DailyMoodNotification() { }) callEvent('remove_from_notifications', { id: 'notificationMood', - ttl: 1440, + ttl: 420, }) }, 1500) Analytics.event('notification_mood_clicked') From 0c74ca5066d3dfdd0b43bbfadb1eb7eabbc9a7d6 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Mon, 2 Feb 2026 01:11:44 +0330 Subject: [PATCH 03/26] fix icons --- src/layouts/widgets/wigiPad/info-panel/info-panel.tsx | 7 ++++--- src/pages/home/content-section.tsx | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/layouts/widgets/wigiPad/info-panel/info-panel.tsx b/src/layouts/widgets/wigiPad/info-panel/info-panel.tsx index 58a01c00..7ec42c27 100644 --- a/src/layouts/widgets/wigiPad/info-panel/info-panel.tsx +++ b/src/layouts/widgets/wigiPad/info-panel/info-panel.tsx @@ -5,11 +5,12 @@ import { BirthdayTab } from './tabs/birthday/birthday-tab' import { BsFillCalendar2WeekFill } from 'react-icons/bs' import { TabNavigation } from '@/components/tab-navigation' import { InfoWeather } from './infoWeather' +import { MdOutlineAccessAlarm, MdOutlineCloud, MdOutlineTab } from 'react-icons/md' const sections = [ - { id: 'all', label: 'ویجی تب', icon: '📋' }, - { id: 'weather', label: 'آب و هوا', icon: '⛅' }, - { id: 'birthdays', label: 'تولدها', icon: '🎂' }, + { id: 'all', label: 'ویجی تب', icon: }, + { id: 'weather', label: 'آب و هوا', icon: }, + { id: 'birthdays', label: 'تولدها', icon: }, ] export function InfoPanel() { const [activeSection, setActiveSection] = useState('all') diff --git a/src/pages/home/content-section.tsx b/src/pages/home/content-section.tsx index b5d7d4f9..696f3988 100644 --- a/src/pages/home/content-section.tsx +++ b/src/pages/home/content-section.tsx @@ -118,7 +118,7 @@ export function ContentSection() {
From 6c183d8b8f6c9d2b650ff1dce4c841ac5102f483 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Mon, 2 Feb 2026 01:14:38 +0330 Subject: [PATCH 04/26] fix --- src/layouts/search/browser-bookmark/browser-bookmark.tsx | 1 - 1 file changed, 1 deletion(-) 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)} From bbbfc7821956a1b37e3cb1fddb349943ba48a0f5 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Mon, 2 Feb 2026 01:31:31 +0330 Subject: [PATCH 05/26] notifications_analytics --- src/layouts/widgetify-card/daily-mood.tsx | 2 +- .../components/notification-item.tsx | 43 ++++++++++++++++--- .../notification-center.tsx | 2 + 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/layouts/widgetify-card/daily-mood.tsx b/src/layouts/widgetify-card/daily-mood.tsx index 615e9e63..c3c26773 100644 --- a/src/layouts/widgetify-card/daily-mood.tsx +++ b/src/layouts/widgetify-card/daily-mood.tsx @@ -29,7 +29,7 @@ export function DailyMoodNotification() { 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< 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 e61c621f..5c928a61 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({ @@ -91,6 +92,7 @@ export function NotificationCenter() { const filtered = notifications.filter((f) => f.id !== id) setNotifications([...filtered]) + Analytics.event('notifications_close') } return ( From 3db529aa6ef202d167d0ecf4a9832d466f808cb0 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Mon, 2 Feb 2026 01:54:46 +0330 Subject: [PATCH 06/26] note toggle expand --- .../widgets/notes/components/note-item.tsx | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/src/layouts/widgets/notes/components/note-item.tsx b/src/layouts/widgets/notes/components/note-item.tsx index 70f7154d..ec3f03be 100644 --- a/src/layouts/widgets/notes/components/note-item.tsx +++ b/src/layouts/widgets/notes/components/note-item.tsx @@ -1,24 +1,40 @@ +import { useState } from 'react' // اضافه شد import { PRIORITY_OPTIONS } from '@/common/constant/priority_options' import type { FetchedNote } from '@/services/hooks/note/note.interface' import moment from 'jalali-moment' import { FiCalendar } from 'react-icons/fi' +import { HiChevronDown } from 'react-icons/hi2' // آیکون برای dropdown +import Analytics from '@/analytics' interface Prop { note: FetchedNote handleNoteClick: any } + export function NoteItem({ note, handleNoteClick }: Prop) { const p = PRIORITY_OPTIONS.find((p) => p.value === note.priority) + const [isExpanded, setIsExpanded] = useState(false) + + const CHARACTER_LIMIT = 120 + + const toggleExpand = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + setIsExpanded(!isExpanded) + Analytics.event(`note_toggle_expand`) + } + + const shouldShowReadMore = note.body && note.body.length > CHARACTER_LIMIT return (
handleNoteClick(note.id)} > -
+

{note.title || 'بدون عنوان'} @@ -33,9 +49,29 @@ export function NoteItem({ note, handleNoteClick }: Prop) {

-

- {note.body} -

+
+

+ {note.body} +

+ + {shouldShowReadMore && ( + + )} +
) From 5c120b8551a65463030fed2d5a5ee91db5e234ed Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Mon, 2 Feb 2026 03:45:33 +0330 Subject: [PATCH 07/26] feat(search): add google image search and refactor voice search UI --- src/components/text-input.tsx | 2 +- .../search/image/image-search.button.tsx | 30 +++ .../search/image/image-search.modal.tsx | 219 ++++++++++++++++++ src/layouts/search/search.tsx | 20 +- .../search/voice/VoiceSearchButton.tsx | 60 ----- src/layouts/search/voice/VoiceSearchModal.tsx | 212 ----------------- .../search/voice/voice-search.button.tsx | 28 +++ .../search/voice/voice-search.portal.tsx | 115 +++++++++ 8 files changed, 411 insertions(+), 275 deletions(-) create mode 100644 src/layouts/search/image/image-search.button.tsx create mode 100644 src/layouts/search/image/image-search.modal.tsx delete mode 100644 src/layouts/search/voice/VoiceSearchButton.tsx delete mode 100644 src/layouts/search/voice/VoiceSearchModal.tsx create mode 100644 src/layouts/search/voice/voice-search.button.tsx create mode 100644 src/layouts/search/voice/voice-search.portal.tsx 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/layouts/search/image/image-search.button.tsx b/src/layouts/search/image/image-search.button.tsx new file mode 100644 index 00000000..251fd820 --- /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.modal.tsx b/src/layouts/search/image/image-search.modal.tsx new file mode 100644 index 00000000..1c473f4d --- /dev/null +++ b/src/layouts/search/image/image-search.modal.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..108c4dad 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.modal' +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..e91fe069 --- /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) => ( + + ))} +
+ )} +
+ + +
+
+
+ ) +} From d9c9982d115e0ce623394721568bf0b760e4c482 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Mon, 2 Feb 2026 03:53:07 +0330 Subject: [PATCH 08/26] fix --- src/layouts/widgetify-card/widgetify.layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/widgetify-card/widgetify.layout.tsx b/src/layouts/widgetify-card/widgetify.layout.tsx index b5f14dc2..6834a657 100644 --- a/src/layouts/widgetify-card/widgetify.layout.tsx +++ b/src/layouts/widgetify-card/widgetify.layout.tsx @@ -18,7 +18,7 @@ export const WidgetifyLayout = () => { useEffect(() => { if (isAuthenticated && !isLoadingUser) { if (user?.name) setUserName(user.name) - if (!user?.hasTodayMood) { + if (!user?.hasTodayMood && !user?.inCache) { callEvent('add_to_notifications', { id: 'notificationMood', node: , From 7c33aa849566d50e9f78df663f57601229c32db6 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Mon, 2 Feb 2026 04:09:58 +0330 Subject: [PATCH 09/26] fix and add cache --- background/cache.ts | 30 +++++++++++++++++++ .../widgetify-card/widgetify.layout.tsx | 2 +- .../widgets/todos/expandable-todo-input.tsx | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/background/cache.ts b/background/cache.ts index cbdb12df..dff065d1 100644 --- a/background/cache.ts +++ b/background/cache.ts @@ -3,6 +3,13 @@ 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', +] export function setupCaching() { if (typeof self !== 'undefined' && '__WB_MANIFEST' in self) { @@ -11,6 +18,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/layouts/widgetify-card/widgetify.layout.tsx b/src/layouts/widgetify-card/widgetify.layout.tsx index 6834a657..f119575a 100644 --- a/src/layouts/widgetify-card/widgetify.layout.tsx +++ b/src/layouts/widgetify-card/widgetify.layout.tsx @@ -18,7 +18,7 @@ export const WidgetifyLayout = () => { useEffect(() => { if (isAuthenticated && !isLoadingUser) { if (user?.name) setUserName(user.name) - if (!user?.hasTodayMood && !user?.inCache) { + if (user?.hasTodayMood === false && !user?.inCache) { callEvent('add_to_notifications', { id: 'notificationMood', node: , diff --git a/src/layouts/widgets/todos/expandable-todo-input.tsx b/src/layouts/widgets/todos/expandable-todo-input.tsx index d25e6528..9f2d8b0c 100644 --- a/src/layouts/widgets/todos/expandable-todo-input.tsx +++ b/src/layouts/widgets/todos/expandable-todo-input.tsx @@ -27,7 +27,7 @@ export function ExpandableTodoInput({ onAddTodo }: ExpandableTodoInputProps) { const [isExpanded, setIsExpanded] = useState(false) const [priority, setPriority] = useState(TodoPriority.Medium) const [category, setCategory] = useState('') - const { data: fetchedTags } = useGetTags(isAuthenticated) + const { data: fetchedTags } = useGetTags(isAuthenticated && isExpanded) const [isTagTooltipOpen, setIsTagTooltipOpen] = useState(false) const [selectedDate, setSelectedDate] = useState(today) const [isDatePickerOpen, setIsDatePickerOpen] = useState(false) From f7f19bf3579ae4c58d3495d10519aeb012b07dc1 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Mon, 2 Feb 2026 04:20:42 +0330 Subject: [PATCH 10/26] fix name --- src/common/storage.ts | 3 ++- src/components/badges/new.badge.tsx | 10 ++++++++++ src/layouts/search/image/image-search.button.tsx | 2 +- ...{image-search.modal.tsx => image-search.portal.tsx} | 0 .../notification-center/notification-center.tsx | 9 +++++++-- 5 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 src/components/badges/new.badge.tsx rename src/layouts/search/image/{image-search.modal.tsx => image-search.portal.tsx} (100%) 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/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/layouts/search/image/image-search.button.tsx b/src/layouts/search/image/image-search.button.tsx index 251fd820..88876b40 100644 --- a/src/layouts/search/image/image-search.button.tsx +++ b/src/layouts/search/image/image-search.button.tsx @@ -12,7 +12,7 @@ export function ImageSearchButton({ onClick }: { onClick: () => void }) {
- {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} + {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} +
-
- {option.label} -
-
- )} -
- ))} + )} +
+ ))}
diff --git a/src/layouts/widgets/calendar/components/day/toolTipContent.tsx b/src/layouts/widgets/calendar/components/day/toolTipContent.tsx index 3f069428..00cdf958 100644 --- a/src/layouts/widgets/calendar/components/day/toolTipContent.tsx +++ b/src/layouts/widgets/calendar/components/day/toolTipContent.tsx @@ -164,31 +164,33 @@ export const CalendarDayDetails: React.FC = ({
- {moodOptions.map((option) => ( - - ))} + {moodOptions + .filter((f) => f.label) + .map((option) => ( + + ))}
From b29a4a9d9d45e81bd64a0f4cb8df82bed2c787ef Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Thu, 5 Feb 2026 04:01:52 +0330 Subject: [PATCH 15/26] improve UI --- src/common/constant/store.key.ts | 2 + src/components/button/button.tsx | 2 + src/components/date-picker/index.ts | 2 +- src/components/dropdown/dropdown.tsx | 155 ++++--- .../filter-tooltip/filter-tooltip.tsx | 76 ++++ src/components/filter-tooltip/index.ts | 5 + src/context/todo.context.tsx | 42 +- .../components/bookmark-emptySlot.tsx | 2 +- .../bookmark/components/bookmark-folder.tsx | 91 ++-- .../bookmark/components/bookmark-item.tsx | 12 +- .../components/bookmark/bookmark-title.tsx | 2 +- .../comboWidget/combo-widget.layout.tsx | 17 +- .../widgets/notes/components/note-editor.tsx | 4 +- .../notes/components/note-navigation.tsx | 32 +- src/layouts/widgets/notes/notes.layout.tsx | 59 +-- .../todos/components/priority.dropdown.tsx | 83 ++++ .../widgets/todos/expandable-todo-input.tsx | 260 ++++++----- src/layouts/widgets/todos/todo.item.tsx | 41 +- src/layouts/widgets/todos/todos.tsx | 402 +++++++++++------- .../pomodoro/components/control-button.tsx | 2 +- .../widgets/tools/pomodoro/pomodoro-timer.tsx | 59 +-- src/layouts/widgets/tools/tools.layout.tsx | 16 +- 22 files changed, 825 insertions(+), 541 deletions(-) create mode 100644 src/components/filter-tooltip/filter-tooltip.tsx create mode 100644 src/components/filter-tooltip/index.ts create mode 100644 src/layouts/widgets/todos/components/priority.dropdown.tsx diff --git a/src/common/constant/store.key.ts b/src/common/constant/store.key.ts index 02b9063d..accf5f7a 100644 --- a/src/common/constant/store.key.ts +++ b/src/common/constant/store.key.ts @@ -89,5 +89,7 @@ export interface StorageKV { petState: boolean showNewBadgeForReOrderWidgets: boolean navbarVisible: boolean + todoFilter: string + todoSort: string [key: `removed_notification_${string}`]: string } 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..203e4289 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,7 +195,7 @@ 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 ${dropdownClassName} @@ -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..d5dcb8fa --- /dev/null +++ b/src/components/filter-tooltip/filter-tooltip.tsx @@ -0,0 +1,76 @@ +import { useState, useRef, ReactNode } from 'react' +import { FaFilter } from 'react-icons/fa' +import { ClickableTooltip } from '@/components/clickableTooltip' +import Tooltip from '../toolTip' + +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/context/todo.context.tsx b/src/context/todo.context.tsx index 04dbc6ea..ad9094d8 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') @@ -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/components/bookmark-emptySlot.tsx b/src/layouts/bookmark/components/bookmark-emptySlot.tsx index 98dd987a..90e9c8b3 100644 --- a/src/layouts/bookmark/components/bookmark-emptySlot.tsx +++ b/src/layouts/bookmark/components/bookmark-emptySlot.tsx @@ -25,7 +25,7 @@ export function EmptyBookmarkSlot({
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) {