From 434eab2a6ab130c7752716b74683d8c2624bc9b6 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 17 Jun 2026 17:04:52 +0330 Subject: [PATCH 1/6] refactor: enhance content alignment settings and update layout handling in ContentSection --- src/context/appearance.context.tsx | 11 ++ .../setting/tabs/appearance/appearance.tsx | 2 + .../components/content-alignment-settings.tsx | 33 +++++ src/pages/home/ui/content-section.tsx | 130 ++++++++++-------- 4 files changed, 117 insertions(+), 59 deletions(-) create mode 100644 src/layouts/setting/tabs/appearance/components/content-alignment-settings.tsx diff --git a/src/context/appearance.context.tsx b/src/context/appearance.context.tsx index 11ef93e2..f379b995 100644 --- a/src/context/appearance.context.tsx +++ b/src/context/appearance.context.tsx @@ -12,6 +12,7 @@ import { listenEvent } from '@/common/utils/call-event' type UI = 'SIMPLE' | 'ADVANCED' export interface AppearanceData { fontFamily: string + contentAlignment: 'center' | 'top' ui: UI } interface AppearanceContextContextType extends AppearanceData { @@ -21,14 +22,17 @@ interface AppearanceContextContextType extends AppearanceData { ) => void setFontFamily: (value: string) => void setUI: (value: UI) => void + contentAlignment: 'center' | 'top' canReOrderWidget: boolean toggleCanReOrderWidget: () => void ui: UI + setContentAlignment: (value: 'center' | 'top') => void } const DEFAULT_SETTINGS: AppearanceData = { fontFamily: 'Vazir', ui: 'ADVANCED', + contentAlignment: 'top', } export const AppearanceContext = createContext(null) @@ -120,6 +124,11 @@ export function AppearanceProvider({ children }: { children: React.ReactNode }) } } + const setContentAlignment = (value: 'center' | 'top') => { + updateSetting('contentAlignment', value) + Analytics.event(`set_content_alignment_${value}`) + } + const setUI = async (ui: UI) => { if (!isAuthenticated) return showToast( @@ -154,12 +163,14 @@ export function AppearanceProvider({ children }: { children: React.ReactNode }) const contextValue: AppearanceContextContextType = { fontFamily: settings.fontFamily, + contentAlignment: settings.contentAlignment, updateSetting, setFontFamily, canReOrderWidget, ui: settings.ui, setUI: setUI, toggleCanReOrderWidget, + setContentAlignment, } return ( diff --git a/src/layouts/setting/tabs/appearance/appearance.tsx b/src/layouts/setting/tabs/appearance/appearance.tsx index 924ce8b7..a0225bbe 100644 --- a/src/layouts/setting/tabs/appearance/appearance.tsx +++ b/src/layouts/setting/tabs/appearance/appearance.tsx @@ -4,6 +4,7 @@ import { BrowserTitleSelector } from './components/browserTitle-selector' import { FontSelector } from './components/font-selector' import { ThemeSelector } from './components/theme-selector' import { UISelector } from './components/ui-selector' +import { ContentAlignmentSettings } from './components/content-alignment-settings' export function AppearanceSettingTab() { const { isAuthenticated } = useAuth() const { data } = useGetUserInventory(isAuthenticated) @@ -17,6 +18,7 @@ export function AppearanceSettingTab() { fetched_browserTitles={data?.browser_titles || []} isAuthenticated={isAuthenticated} /> + ) } diff --git a/src/layouts/setting/tabs/appearance/components/content-alignment-settings.tsx b/src/layouts/setting/tabs/appearance/components/content-alignment-settings.tsx new file mode 100644 index 00000000..1ea16dd1 --- /dev/null +++ b/src/layouts/setting/tabs/appearance/components/content-alignment-settings.tsx @@ -0,0 +1,33 @@ +import { ItemSelector } from '@/components/item-selector' +import { SectionPanel } from '@/components/section-panel' +import { useAppearanceSetting } from '@/context/appearance.context' + +export function ContentAlignmentSettings() { + const { contentAlignment, setContentAlignment, ui } = useAppearanceSetting() + if (ui === 'SIMPLE') return null + return ( + +
+

موقعیت عمودی محتوا

+
+ setContentAlignment('center')} + label="وسط" + key="center" + className="w-1/2" + description="چیدمان محتوا در وسط صفحه قرار می‌گیرد." + /> + setContentAlignment('top')} + label="بالا" + key="top" + className="w-1/2" + description="چیدمان محتوا در بالا صفحه قرار می‌گیرد." + /> +
+
+
+ ) +} diff --git a/src/pages/home/ui/content-section.tsx b/src/pages/home/ui/content-section.tsx index 3eeda0c0..88f25384 100644 --- a/src/pages/home/ui/content-section.tsx +++ b/src/pages/home/ui/content-section.tsx @@ -70,7 +70,7 @@ function SortableWidget({ widget }: { widget: WidgetItem }) { } export function ContentSection() { - const { canReOrderWidget } = useAppearanceSetting() + const { canReOrderWidget, contentAlignment } = useAppearanceSetting() const { getSortedWidgets, reorderWidgets } = useWidgetVisibility() const sortedWidgets = getSortedWidgets().filter((widget) => !widget.disabled) @@ -114,70 +114,82 @@ export function ContentSection() {
4 ? 'pb-30!' : ''} justify-start flex-1 w-full px-1 md:px-4 py-1`} + className="flex flex-col flex-1 w-full overflow-y-auto scrollbar-none" > -
-
- -
- -
- - - - -
- -
- -
-
- - {sortedWidgets.length > 0 && ( -
- +
+
+ +
+ +
- widget.id)} - strategy={rectSortingStrategy} + + + + +
+ +
+ +
+
+ + {sortedWidgets.length > 0 && ( +
+ -
- {sortedWidgets.map((widget) => { - if (totalWidgetCount === 2) { - return ( -
widget.id)} + strategy={rectSortingStrategy} + > +
+ {sortedWidgets.map((widget) => { + if (totalWidgetCount === 2) { + return ( +
+ {canReOrderWidget ? ( + + ) : ( + widget.node + )} +
+ ) + } + return canReOrderWidget ? ( + - {canReOrderWidget ? ( - - ) : ( - widget.node - )} -
+ widget={widget} + /> + ) : ( + widget.node ) - } - return canReOrderWidget ? ( - - ) : ( - widget.node - ) - })} -
- - -
- )} + })} +
+ +
+
+ )} +
) From ca1fa4b9155f5afb9c7e7a6cc3d6a2468c9a0dd4 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 17 Jun 2026 17:05:01 +0330 Subject: [PATCH 2/6] refactor: update layout styles in BookmarkTitle component for improved responsiveness --- src/layouts/bookmark/components/bookmark/bookmark-title.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/layouts/bookmark/components/bookmark/bookmark-title.tsx b/src/layouts/bookmark/components/bookmark/bookmark-title.tsx index 3634ba5e..2cdf51de 100644 --- a/src/layouts/bookmark/components/bookmark/bookmark-title.tsx +++ b/src/layouts/bookmark/components/bookmark/bookmark-title.tsx @@ -7,10 +7,10 @@ export function BookmarkTitle({ customTextColor?: string }) { return ( -
+
{title} From 10f0ea56f1287a868a3e35f134c6c0b8dea7ddbb Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 17 Jun 2026 17:22:35 +0330 Subject: [PATCH 3/6] refactor: update width class in ContentSection for improved layout consistency --- src/pages/home/ui/content-section.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ui/content-section.tsx b/src/pages/home/ui/content-section.tsx index 88f25384..ea005112 100644 --- a/src/pages/home/ui/content-section.tsx +++ b/src/pages/home/ui/content-section.tsx @@ -163,7 +163,7 @@ export function ContentSection() { return (
{canReOrderWidget ? ( Date: Wed, 17 Jun 2026 18:53:51 +0330 Subject: [PATCH 4/6] fix font cache --- background/cache.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/background/cache.ts b/background/cache.ts index c64c3bb5..2bc9171c 100644 --- a/background/cache.ts +++ b/background/cache.ts @@ -53,15 +53,13 @@ export function setupCaching() { if (!isDev) { registerRoute( ({ request }) => - request.destination === 'script' || - request.destination === 'style' || - request.destination === 'font', + request.destination === 'script' || request.destination === 'style', new CacheFirst({ cacheName: 'static-assets-v1', plugins: [ new ExpirationPlugin({ maxEntries: 200, - maxAgeSeconds: 10 * 24 * 60 * 60, // 10 days + maxAgeSeconds: 10 * 24 * 60 * 60, purgeOnQuotaError: true, }), new CacheableResponsePlugin({ @@ -72,13 +70,13 @@ export function setupCaching() { ) registerRoute( - ({ request }) => request.destination === 'image', + ({ request }) => request.destination === 'font', new CacheFirst({ - cacheName: 'images-cache-v1', + cacheName: 'fonts-cache-v1', plugins: [ new ExpirationPlugin({ - maxEntries: 300, - maxAgeSeconds: 5 * 24 * 60 * 60, // 5 days + maxEntries: 50, + maxAgeSeconds: 2 * 60, purgeOnQuotaError: true, }), new CacheableResponsePlugin({ From f62e90b82b4a7090f64e0c69740093b2bbd6d954 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 17 Jun 2026 22:45:30 +0330 Subject: [PATCH 5/6] feat: add BookmarkIconPicker component for customizable bookmark icons --- .../components/bookmark-icon.picker.tsx | 106 ++++++++++++ .../components/bookmark/bookmark-icon.tsx | 2 +- .../components/modal/add-bookmark.modal.tsx | 70 ++------ .../components/modal/edit-bookmark.modal.tsx | 41 +---- src/layouts/bookmark/components/shared.tsx | 156 +----------------- 5 files changed, 130 insertions(+), 245 deletions(-) create mode 100644 src/layouts/bookmark/components/bookmark-icon.picker.tsx diff --git a/src/layouts/bookmark/components/bookmark-icon.picker.tsx b/src/layouts/bookmark/components/bookmark-icon.picker.tsx new file mode 100644 index 00000000..612e0fc3 --- /dev/null +++ b/src/layouts/bookmark/components/bookmark-icon.picker.tsx @@ -0,0 +1,106 @@ +import { showToast } from '@/common/toast' +import { getFaviconFromUrl } from '@/common/utils/icon' +import type React from 'react' +import { useRef, useState } from 'react' +import { FaUpload, FaImage } from 'react-icons/fa' +import { LuX } from 'react-icons/lu' + +type Props = { + value: File | string | null + url?: string | null + onChange: (file: File | null) => void +} + +export function BookmarkIconPicker({ value, url, onChange }: Props) { + const fileInputRef = useRef(null) + const [isDragging, setIsDragging] = useState(false) + const [error, setError] = useState(false) + + const openPicker = () => { + fileInputRef.current?.click() + } + + const handleFile = (file?: File) => { + if (!file || !file.type.startsWith('image/')) + return showToast('فرمت نامعتبر', 'error') + onChange(file) + } + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + setIsDragging(false) + handleFile(e.dataTransfer.files?.[0]) + } + + const handleUpload = (e: React.ChangeEvent) => { + handleFile(e.target.files?.[0]) + } + + const handleRemove = (e: React.MouseEvent) => { + e.stopPropagation() + onChange(null) + setError(false) + } + + const isFile = value instanceof File + let iconSrc: string | null = null + if (value && isFile) { + iconSrc = URL.createObjectURL(value) + } else { + if (value) { + iconSrc = value + } else if (url && url !== 'null') { + iconSrc = getFaviconFromUrl(url || '') + } + } + + return ( + <> + + +
{ + e.preventDefault() + setIsDragging(true) + }} + onDragLeave={() => setIsDragging(false)} + onDrop={handleDrop} + className={`relative w-12 h-12 flex items-center justify-center cursor-pointer border-2 rounded-xl transition + ${isDragging ? 'border-blue-400' : 'border-base-300'} + `} + > + {iconSrc && !error ? ( + setError(true)} + /> + ) : ( + + )} + +
+ +
+ + {isFile && ( + + )} +
+ + ) +} diff --git a/src/layouts/bookmark/components/bookmark/bookmark-icon.tsx b/src/layouts/bookmark/components/bookmark/bookmark-icon.tsx index 9dfb7267..71c9fb55 100644 --- a/src/layouts/bookmark/components/bookmark/bookmark-icon.tsx +++ b/src/layouts/bookmark/components/bookmark/bookmark-icon.tsx @@ -71,7 +71,7 @@ export function BookmarkIcon({ bookmark }: { bookmark: Bookmark }) { const colorClass = hasCustomColors ? '' : getColorFromTitle(bookmark.title) return ( -
+
{typeof displayIcon === 'string' && !imageError ? ( ('BOOKMARK') - const [iconSource, setIconSource] = useState('auto') const [showAdvanced, setShowAdvanced] = useState(false) const isAdding = useIsMutating({ mutationKey: ['addBookmark'] }) > 0 @@ -67,9 +60,6 @@ export function AddBookmarkModal({ structuredClone(empty) ) - const { fileInputRef, setIconLoadError, renderIconPreview, handleImageUpload } = - useBookmarkIcon() - const updateFormData: AddBookmarkUpdateFormData = < K extends keyof BookmarkCreateFormFields, >( @@ -83,10 +73,7 @@ export function AddBookmarkModal({ const newUrl = value.trim() updateFormData('url', newUrl) - if (iconSource === 'auto') { - setIconLoadError(false) - updateFormData('icon', null) - } + updateFormData('icon', null) if (formData.title.trim() === '' && newUrl !== '') { let hostName = '' @@ -130,7 +117,6 @@ export function AddBookmarkModal({ const resetForm = () => { setFormData(structuredClone(empty)) setType('BOOKMARK') - setIconSource('auto') setShowAdvanced(false) } @@ -146,11 +132,6 @@ export function AddBookmarkModal({ }) => { updateFormData('title', suggestion.title) updateFormData('url', suggestion.url) - - if (iconSource === 'auto') { - setIconLoadError(false) - updateFormData('icon', null) - } } const handleAdvancedModalClose = ( @@ -190,52 +171,27 @@ export function AddBookmarkModal({ size="md" title={`${type === 'FOLDER' ? 'پوشه جدید' : 'بوکمارک جدید'}`} direction="rtl" - className="!overflow-y-hidden" + className="overflow-y-hidden!" closeOnBackdropClick={false} >
-
- -
+
- {' '}
- {type === 'BOOKMARK' && ( - - )} - {renderIconPreview( - formData.icon || - (formData.url && getFaviconFromUrl(formData.url)), - type === 'FOLDER' ? 'upload' : iconSource, - setIconSource, - (value) => updateFormData('icon', value) - )} + updateFormData('icon', value)} + value={formData.icon} + url={formData.url} + />
- - handleImageUpload( - e, - (file) => updateFormData('icon', file), - setIconSource - ) - } - /> -
+
{type === 'BOOKMARK' && ( (null) const type = bookmark?.type - const { fileInputRef, renderIconPreview, handleImageUpload } = useBookmarkIcon() const updateFormData: UpdateBookmarkUpdateFormData = (key, value) => { if (key === 'icon') { @@ -141,6 +136,7 @@ export function EditBookmarkModal({ isDeletedIcon: false, }) setIconSource(bookmark.icon ? 'upload' : 'auto') + console.log(bookmark) if (bookmark.icon) { setIcon(bookmark.icon) } else if (bookmark.type === 'BOOKMARK' && bookmark.url) { @@ -164,19 +160,12 @@ export function EditBookmarkModal({
- {type === 'BOOKMARK' && ( - - )}
- {renderIconPreview( - icon, - type === 'FOLDER' ? 'upload' : iconSource, - setIconSource, - (value) => updateFormData('icon', value) - )} + updateFormData('icon', value)} + value={icon} + url={formData.url} + /> {iconSource === 'upload' && Boolean(icon) && (
- - handleImageUpload( - e, - (file) => updateFormData('icon', file), - setIconSource - ) - } - /> - - className="w-full! border-none!" + className="w-full! border-none! h-12" tabMode="simple" - size="small" + size="medium" tabs={[ { id: 'BOOKMARK', @@ -57,154 +53,6 @@ export function TypeSelector({ ) } -export function useBookmarkIcon() { - const fileInputRef = useRef(null) - const [isDragging, setIsDragging] = useState(false) - const [iconLoadError, setIconLoadError] = useState(false) - - const renderIconPreview = ( - icon: string | File | null, - iconSource: IconSourceType, - setIconSource: (source: IconSourceType) => void, - cb: (value: File | null) => void - ) => { - const handlePreviewClick = () => { - if (iconSource === 'upload') { - fileInputRef.current?.click() - } - } - - const handleRemoveCustomImage = (e: React.MouseEvent) => { - e.stopPropagation() - cb(null) - setIconSource('auto') - } - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault() - setIsDragging(true) - } - - const handleDragLeave = () => { - setIsDragging(false) - } - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault() - setIsDragging(false) - - const file = e.dataTransfer.files[0] - if (!file || !file.type.startsWith('image/')) return - cb(file) - setIconSource('upload') - } - if (icon && typeof icon !== 'string') { - return ( -
- Custom -
- -
- -
- ) - } - - if (icon) { - return ( -
- Favicon { - cb(null) - setIconLoadError(true) - }} - /> -
- -
- - {iconLoadError && ( -
- - آیکون بارگذاری نشد - -
- )} -
- ) - } - - return ( -
- -
- ) - } - const handleImageUpload = ( - e: React.ChangeEvent, - cb: (icon: File | null) => void, - setIconSource: (source: IconSourceType) => void - ) => { - const file = e.target.files?.[0] - if (!file) return - - if (!file.type.startsWith('image/')) { - showToast('لطفاً فقط فایل تصویری آپلود کنید', 'error') - return - } - - cb(file) - setIconSource('upload') - } - - return { - fileInputRef, - isDragging, - iconLoadError, - setIconLoadError, - renderIconPreview, - handleImageUpload, - } -} - interface ShowAdvancedButtonProps { showAdvanced: boolean setShowAdvanced: (show: boolean) => void From 6eebb8ab07f2dd4f32d8037a1cfdb9e1a313d725 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Thu, 18 Jun 2026 00:16:17 +0330 Subject: [PATCH 6/6] added search history --- src/common/constant/store.key.ts | 1 + src/components/tab-navigation.tsx | 2 +- src/layouts/search/history.portal.tsx | 82 +++++++++++++++---- src/layouts/search/hooks/useSearchHistory.ts | 70 ++++++++++++++++ src/layouts/search/search.tsx | 11 +++ src/layouts/search/suggestion/suggestions.tsx | 45 ++++++---- .../components/search-autocomplete.switch.tsx | 5 +- src/layouts/widgets/todos/todo.item.tsx | 2 +- 8 files changed, 184 insertions(+), 34 deletions(-) create mode 100644 src/layouts/search/hooks/useSearchHistory.ts diff --git a/src/common/constant/store.key.ts b/src/common/constant/store.key.ts index 5165f020..da0803b9 100644 --- a/src/common/constant/store.key.ts +++ b/src/common/constant/store.key.ts @@ -59,6 +59,7 @@ export interface StorageKV { updatedAt: number }[] calendarDrawerState: boolean + recent_searches: any pets: PetSettings clock: ClockSettings wigiPadDate: WigiPadDateSetting diff --git a/src/components/tab-navigation.tsx b/src/components/tab-navigation.tsx index 9eb8a4cd..114a8250 100644 --- a/src/components/tab-navigation.tsx +++ b/src/components/tab-navigation.tsx @@ -1,5 +1,5 @@ import React, { useId } from 'react' -import { motion, AnimatePresence } from 'framer-motion' +import { motion } from 'framer-motion' interface TabItem { id: T diff --git a/src/layouts/search/history.portal.tsx b/src/layouts/search/history.portal.tsx index b2788989..0485160e 100644 --- a/src/layouts/search/history.portal.tsx +++ b/src/layouts/search/history.portal.tsx @@ -1,11 +1,12 @@ -import { useState } from 'react' -import { LuSparkles } from 'react-icons/lu' +import { useState, useMemo } from 'react' import { useSearchSuggestions } from '@/services/hooks/search/getSuggestSearch.hook' import { SuggestionSkeleton } from './suggestion/suggestion.skeleton' import { useAuth } from '@/context/auth.context' import { AutocompleteConsentModal } from './suggestion/autocomplete-consent.modal' -import { Suggestions } from './suggestion/suggestions' import { Portal } from '@/components/portal/Portal' +import { useSearchHistory } from './hooks/useSearchHistory' +import { Suggestions } from './suggestion/suggestions' +import { MdSearch } from 'react-icons/md' interface SearchHistoryPortalProps { onClose: () => void @@ -24,6 +25,7 @@ export function SearchHistoryPortal({ }: SearchHistoryPortalProps) { const { isAuthenticated, user } = useAuth() const [showConsentModal, setShowConsentModal] = useState(false) + const { recentSearches, addSearch } = useSearchHistory() const { data: suggestions, isFetching } = useSearchSuggestions( searchQuery, @@ -31,10 +33,43 @@ export function SearchHistoryPortal({ ) const hasQuery = searchQuery.trim().length > 0 - const hasSuggestions = suggestions && suggestions.length > 0 const showSuggestions = user?.searchAutocompleteEnabled && hasQuery const showEnableButton = !user?.searchAutocompleteEnabled && isAuthenticated + const combinedSuggestions = useMemo(() => { + if (!hasQuery) return [] + + const combined: Array<{ text: string; isRecent: boolean }> = [] + const seen = new Set() + + if (user?.searchAutocompleteEnabled && suggestions) { + suggestions.forEach((s) => { + if (!seen.has(s.toLowerCase())) { + combined.push({ text: s, isRecent: false }) + seen.add(s.toLowerCase()) + } + }) + } + + recentSearches.forEach((item) => { + const lowerQuery = item.query.toLowerCase() + if (!seen.has(lowerQuery) && lowerQuery.includes(searchQuery.toLowerCase())) { + combined.push({ text: item.query, isRecent: true }) + seen.add(lowerQuery) + } + }) + + return combined + }, [suggestions, recentSearches, searchQuery, user?.searchAutocompleteEnabled]) + + const handleSearch = (query: string) => { + if (user?.searchAutocompleteEnabled) addSearch(query) + onSearch(query) + } + + const showLocalSearches = + user?.searchAutocompleteEnabled && recentSearches.length > 0 && !hasQuery + return ( <> @@ -44,21 +79,30 @@ export function SearchHistoryPortal({ className="z-20 -mt-12 overflow-hidden duration-300 shadow-2xl bg-content bg-glass h-60 rounded-b-2xl rounded-t-md animate-in fade-in slide-in-from-top-2" > {showSuggestions && - (isFetching && !hasSuggestions ? ( + hasQuery && + (isFetching ? ( - ) : hasSuggestions ? ( -
- -
+ ) : combinedSuggestions.length > 0 ? ( + ) : null)} + {!hasQuery && showLocalSearches && ( + ({ + isRecent: true, + text: f.query, + }))} + handleSearch={handleSearch} + /> + )} + {showEnableButton && (
- +

@@ -75,11 +119,21 @@ export function SearchHistoryPortal({ }} className="flex items-center gap-1.5 px-4 py-1.5 text-xs font-medium rounded-xl cursor-pointer transition-all bg-base-content/5 text-base-content/60 hover:text-primary hover:bg-primary/8 searchbox-item" > - فعال‌سازی

)} + + {!showSuggestions && + !showEnableButton && + !showLocalSearches && + hasQuery && ( +
+

+ نتیجه‌ای برای نمایش وجود ندارد +

+
+ )}
diff --git a/src/layouts/search/hooks/useSearchHistory.ts b/src/layouts/search/hooks/useSearchHistory.ts new file mode 100644 index 00000000..68bdf108 --- /dev/null +++ b/src/layouts/search/hooks/useSearchHistory.ts @@ -0,0 +1,70 @@ +import { getFromStorage, removeFromStorage, setToStorage } from '@/common/storage' +import { useState, useEffect, useCallback } from 'react' + +const STORAGE_KEY = 'recent_searches' +const MAX_SEARCHES = 8 + +export interface SearchHistoryItem { + query: string + timestamp: number + isRecent?: boolean +} + +export function useSearchHistory() { + const [recentSearches, setRecentSearches] = useState([]) + const [isLoaded, setIsLoaded] = useState(false) + + useEffect(() => { + const load = async () => { + try { + const stored = (await getFromStorage(STORAGE_KEY)) as SearchHistoryItem[] + if (stored) { + setRecentSearches(stored) + } + } catch {} + } + load() + setIsLoaded(true) + }, []) + + const addSearch = useCallback((query: string) => { + try { + setRecentSearches((prev) => { + const filtered = prev.filter((item) => item.query !== query) + + const updated = [ + { + query, + timestamp: Date.now(), + isRecent: true, + }, + ...filtered, + ] + + const limited = updated.slice(0, MAX_SEARCHES) + + try { + setToStorage(STORAGE_KEY, limited) + } catch {} + + return limited + }) + } catch {} + }, []) + + const clearHistory = useCallback(async () => { + try { + setRecentSearches([]) + await removeFromStorage(STORAGE_KEY) + } catch (error) { + console.error('Failed to clear search history:', error) + } + }, []) + + return { + recentSearches, + addSearch, + clearHistory, + isLoaded, + } +} diff --git a/src/layouts/search/search.tsx b/src/layouts/search/search.tsx index c6c4328b..da5fd953 100644 --- a/src/layouts/search/search.tsx +++ b/src/layouts/search/search.tsx @@ -9,6 +9,8 @@ import { ImageSearchButton } from './image/image-search.button' import { EngineSelector } from './select-engine/engine-selector' import { SearchHistoryPortal } from './history.portal' import type { EngineMeta } from '@/services/hooks/trends/getTrends' +import { useSearchHistory } from './hooks/useSearchHistory' +import { useAuth } from '@/context/auth.context' const DEFAULT_ENGINE: EngineMeta = { id: 'google', @@ -27,11 +29,16 @@ export function SearchLayout() { const inputRef = useRef(null) const [activePortal, setActivePortal] = useState<'voice' | 'image' | null>(null) const [portalStyles, setPortalStyles] = useState({}) + const { user } = useAuth() + + const { addSearch } = useSearchHistory() const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() const query = searchQuery.trim() if (query) { + if (user?.searchAutocompleteEnabled) addSearch(query) + SearchHandler({ content: query, engine: selectedEngine, @@ -56,6 +63,8 @@ export function SearchLayout() { const handleVoiceSearch = (query: string) => { if (query.trim()) { + if (user?.searchAutocompleteEnabled) addSearch(query.trim()) + SearchHandler({ content: query.trim(), engine: selectedEngine }) Analytics.event('voice_search_submitted') } @@ -132,6 +141,8 @@ export function SearchLayout() { const onSearchButtonClick = () => { const query = searchQuery.trim() if (query.trim()) { + if (user?.searchAutocompleteEnabled) addSearch(query.trim()) + SearchHandler({ content: query.trim(), engine: selectedEngine }) Analytics.event('search_button_submitted') } diff --git a/src/layouts/search/suggestion/suggestions.tsx b/src/layouts/search/suggestion/suggestions.tsx index 2ee02163..af053aec 100644 --- a/src/layouts/search/suggestion/suggestions.tsx +++ b/src/layouts/search/suggestion/suggestions.tsx @@ -1,21 +1,34 @@ +import { useGeneralSetting } from '@/context/general-setting.context' +import { LuHistory } from 'react-icons/lu' import { MdSearch } from 'react-icons/md' interface Prop { - suggestions: string[] - onSearch: (query: string) => void + combinedSuggestions: { text: string; isRecent: boolean }[] + handleSearch: (query: string) => void } -export function Suggestions({ suggestions, onSearch }: Prop) { - return suggestions.map((s, i) => ( - - )) +export function Suggestions({ combinedSuggestions, handleSearch }: Prop) { + const { blurMode } = useGeneralSetting() + return ( +
+ {combinedSuggestions.map((item, index) => ( + + ))} +
+ ) } diff --git a/src/layouts/setting/tabs/privacy/components/search-autocomplete.switch.tsx b/src/layouts/setting/tabs/privacy/components/search-autocomplete.switch.tsx index f6ec5d1b..997a6530 100644 --- a/src/layouts/setting/tabs/privacy/components/search-autocomplete.switch.tsx +++ b/src/layouts/setting/tabs/privacy/components/search-autocomplete.switch.tsx @@ -22,8 +22,9 @@ export function SearchAutocompleteSwitch() {

پیشنهادهای جستجو

- با فعال کردن این گزینه، هنگام تایپ در باکس جستجو، پیشنهادها مستقیما از - گوگل دریافت می‌شوند. هیچ اطلاعاتی ذخیره نمی‌شود. + با فعال کردن این گزینه، هنگام تایپ در باکس جستجو، پیشنهادها مستقیما + گوگل دریافت می‌شوند و تاریخچه جستجو در دستگاه شما ذخیره میشوند. هیچ + اطلاعاتی به سرور ارسال نمی‌شود.

diff --git a/src/layouts/widgets/todos/todo.item.tsx b/src/layouts/widgets/todos/todo.item.tsx index 2613eddd..3c0f05a0 100644 --- a/src/layouts/widgets/todos/todo.item.tsx +++ b/src/layouts/widgets/todos/todo.item.tsx @@ -115,7 +115,7 @@ export function TodoItem({ todo, blurMode = false, onEdit, onUpdated }: Prop) { const hasFriends = currentTodo?.friends && currentTodo?.friends?.length > 0 return (