diff --git a/frontend/components/About.tsx b/frontend/components/About.tsx index 0d3b5d5..00c8bac 100644 --- a/frontend/components/About.tsx +++ b/frontend/components/About.tsx @@ -27,8 +27,8 @@ const About: React.FC = ({ isDarkMode = false }) => { }, []); return ( -
-
+
+

{t('about.title', 'About')} diff --git a/frontend/components/Areas.tsx b/frontend/components/Areas.tsx index 134c362..b776bdb 100644 --- a/frontend/components/Areas.tsx +++ b/frontend/components/Areas.tsx @@ -158,8 +158,8 @@ const Areas: React.FC = () => { }; return ( -
-
+
+
{/* Areas Header */}

{t('areas.title')}

diff --git a/frontend/components/Inbox/InboxItems.tsx b/frontend/components/Inbox/InboxItems.tsx index 2c0af82..b2c3170 100644 --- a/frontend/components/Inbox/InboxItems.tsx +++ b/frontend/components/Inbox/InboxItems.tsx @@ -455,8 +455,8 @@ const InboxItems: React.FC = () => { } return ( -
-
+
+
{/* Title row with info button on the right */}
diff --git a/frontend/components/Notes.tsx b/frontend/components/Notes.tsx index abfc8f8..b743b05 100644 --- a/frontend/components/Notes.tsx +++ b/frontend/components/Notes.tsx @@ -6,7 +6,6 @@ import { TrashIcon, FolderIcon, TagIcon as TagIconOutline, - FunnelIcon, ClockIcon, EllipsisVerticalIcon, XMarkIcon, @@ -18,6 +17,7 @@ import NoteModal from './Note/NoteModal'; import ConfirmDialog from './Shared/ConfirmDialog'; import DiscardChangesDialog from './Shared/DiscardChangesDialog'; import MarkdownRenderer from './Shared/MarkdownRenderer'; +import IconSortDropdown from './Shared/IconSortDropdown'; import TagInput from './Tag/TagInput'; import { Note } from '../entities/Note'; import { createNote, updateNote } from '../utils/notesService'; @@ -75,7 +75,6 @@ const Notes: React.FC = () => { const [showProjectDropdown, setShowProjectDropdown] = useState(false); const [showTagsInput, setShowTagsInput] = useState(false); const [showDiscardDialog, setShowDiscardDialog] = useState(false); - const [showSortDropdown, setShowSortDropdown] = useState(false); const [showNoteOptionsDropdown, setShowNoteOptionsDropdown] = useState(false); const hasAutoSelected = useRef(false); @@ -86,7 +85,6 @@ const Notes: React.FC = () => { ENABLE_NOTE_COLOR && previewNote ? previewNote.color : undefined; const activeNoteColor = (isEditing && editingNoteColor) || previewNoteColor || undefined; - const sortDropdownRef = useRef(null); const noteOptionsDropdownRef = useRef(null); // Get notes and projects from global store @@ -412,15 +410,9 @@ const Notes: React.FC = () => { } }, [sortedNotes, previewNote, uid]); - // Handle clicking outside sort dropdown to close it + // Handle clicking outside note options dropdown to close it useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if ( - sortDropdownRef.current && - !sortDropdownRef.current.contains(event.target as Node) - ) { - setShowSortDropdown(false); - } if ( noteOptionsDropdownRef.current && !noteOptionsDropdownRef.current.contains(event.target as Node) @@ -483,49 +475,17 @@ const Notes: React.FC = () => {

{/* Sort Filter Dropdown */} -
- - {showSortDropdown && ( -
-
- Sort by -
-
- {sortOptions.map((option) => ( - - ))} -
-
+ + title={t('notes.sortNotes', 'Sort notes')} + dropdownLabel={t('notes.sortBy', 'Sort by')} + /> + ); + const handleDeleteProject = async () => { if (!project?.uid) { return; @@ -1077,39 +1112,23 @@ const ProjectDetails: React.FC = () => { > - {/* Show Completed Toggle */} -
- - Show completed - - -
- - {/* Sort Filter */} -
)} @@ -1179,39 +1198,20 @@ const ProjectDetails: React.FC = () => { > - {/* Show Completed Toggle */} -
- - Show completed - - -
- - {/* Sort Filter */} -
)} diff --git a/frontend/components/Projects.tsx b/frontend/components/Projects.tsx index 081a65a..572919a 100644 --- a/frontend/components/Projects.tsx +++ b/frontend/components/Projects.tsx @@ -370,8 +370,8 @@ const Projects: React.FC = () => { } return ( -
-
+
+

{t('projects.title')} diff --git a/frontend/components/Shared/IconSortDropdown.tsx b/frontend/components/Shared/IconSortDropdown.tsx new file mode 100644 index 0000000..c260b0d --- /dev/null +++ b/frontend/components/Shared/IconSortDropdown.tsx @@ -0,0 +1,106 @@ +import React, { ReactNode, useEffect, useRef, useState } from 'react'; +import { FunnelIcon, CheckIcon } from '@heroicons/react/24/outline'; +import { SortOption } from './SortFilterButton'; + +interface IconSortDropdownProps { + options: SortOption[]; + value: string; + onChange: (value: string) => void; + ariaLabel?: string; + title?: string; + className?: string; + buttonClassName?: string; + dropdownLabel?: string; + align?: 'left' | 'right'; + extraContent?: ReactNode; +} + +const IconSortDropdown: React.FC = ({ + options, + value, + onChange, + ariaLabel = 'Sort items', + title = 'Sort items', + className = '', + buttonClassName = '', + dropdownLabel = 'Sort by', + align = 'right', + extraContent, +}) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + if (!isOpen) { + return; + } + + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => + document.removeEventListener('mousedown', handleClickOutside); + }, [isOpen]); + + return ( +
+ + {isOpen && ( +
+ {dropdownLabel && ( +
+ {dropdownLabel} +
+ )} +
+ {options.map((option) => ( + + ))} +
+ {extraContent && ( +
+ {extraContent} +
+ )} +
+ )} +
+ ); +}; + +export default IconSortDropdown; diff --git a/frontend/components/Shared/ViewOptionsBar.tsx b/frontend/components/Shared/ViewOptionsBar.tsx new file mode 100644 index 0000000..0f88b20 --- /dev/null +++ b/frontend/components/Shared/ViewOptionsBar.tsx @@ -0,0 +1,246 @@ +import React, { useRef, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + MagnifyingGlassIcon, + InformationCircleIcon, + FunnelIcon, +} from '@heroicons/react/24/outline'; +import { SortOption } from './SortFilterButton'; + +export interface ViewOptionsBarProps { + // Info/Description + showInfo?: boolean; + onToggleInfo?: () => void; + isInfoExpanded?: boolean; + + // Search + showSearch?: boolean; + onToggleSearch?: () => void; + isSearchExpanded?: boolean; + + // Show Completed Toggle + showCompletedToggle?: boolean; + showCompleted?: boolean; + onToggleCompleted?: () => void; + completedLabel?: string; + + // Sort/Filter Dropdown + showSort?: boolean; + sortOptions?: SortOption[]; + sortValue?: string; + onSortChange?: (value: string) => void; + sortLabel?: string; + + // Custom buttons/elements to add + customElements?: React.ReactNode; + + // Styling + className?: string; + position?: 'fixed' | 'relative'; // For upcoming view in Tasks + fixedPosition?: string; // Custom position classes for fixed positioning +} + +const ViewOptionsBar: React.FC = ({ + showInfo = false, + onToggleInfo, + isInfoExpanded = false, + showSearch = false, + onToggleSearch, + isSearchExpanded = false, + showCompletedToggle = false, + showCompleted = false, + onToggleCompleted, + completedLabel, + showSort = false, + sortOptions = [], + sortValue = '', + onSortChange, + sortLabel, + customElements, + className = '', + position = 'relative', + fixedPosition = '', +}) => { + const { t } = useTranslation(); + const [isSortDropdownOpen, setIsSortDropdownOpen] = React.useState(false); + const sortDropdownRef = useRef(null); + + // Close sort dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + sortDropdownRef.current && + !sortDropdownRef.current.contains(event.target as Node) + ) { + setIsSortDropdownOpen(false); + } + }; + + if (isSortDropdownOpen) { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + } + }, [isSortDropdownOpen]); + + const handleSortSelect = (value: string) => { + if (onSortChange) { + onSortChange(value); + } + setIsSortDropdownOpen(false); + }; + + const containerClasses = + position === 'fixed' + ? `${fixedPosition} z-20 ${className}` + : `flex items-center gap-2 ${className}`; + + return ( +
+ {/* Info Button */} + {showInfo && onToggleInfo && ( + + )} + + {/* Search Button */} + {showSearch && onToggleSearch && ( + + )} + + {/* Show Completed Toggle */} + {showCompletedToggle && onToggleCompleted && ( +
+ + {completedLabel || + t('common.showCompleted', 'Show completed')} + + +
+ )} + + {/* Sort/Filter Dropdown */} + {showSort && sortOptions.length > 0 && onSortChange && ( +
+ + {isSortDropdownOpen && ( +
+
+ {sortLabel || t('common.sortBy', 'Sort by')} +
+
+ {sortOptions.map((option) => ( + + ))} +
+
+ )} +
+ )} + + {/* Custom Elements */} + {customElements} +
+ ); +}; + +export default ViewOptionsBar; diff --git a/frontend/components/Tags.tsx b/frontend/components/Tags.tsx index e6feee5..277d969 100644 --- a/frontend/components/Tags.tsx +++ b/frontend/components/Tags.tsx @@ -124,8 +124,8 @@ const Tags: React.FC = () => { } return ( -
-
+
+
{/* Tags Header */}

diff --git a/frontend/components/Task/TasksToday.tsx b/frontend/components/Task/TasksToday.tsx index a2052de..85d191c 100644 --- a/frontend/components/Task/TasksToday.tsx +++ b/frontend/components/Task/TasksToday.tsx @@ -790,8 +790,8 @@ const TasksToday: React.FC = () => { } return ( -
-
+
+
{/* Today Header with Icons on the Right */}
diff --git a/frontend/components/Tasks.tsx b/frontend/components/Tasks.tsx index 0f8d325..c958612 100644 --- a/frontend/components/Tasks.tsx +++ b/frontend/components/Tasks.tsx @@ -5,7 +5,6 @@ import { useSidebar } from '../contexts/SidebarContext'; import TaskList from './Task/TaskList'; import GroupedTaskList from './Task/GroupedTaskList'; import NewTask from './Task/NewTask'; -import SortFilter from './Shared/SortFilter'; import { Task } from '../entities/Task'; import { getTitleAndIcon } from './Task/getTitleAndIcon'; import { getDescription } from './Task/getDescription'; @@ -17,14 +16,12 @@ import { import { useStore } from '../store/useStore'; import { useToast } from './Shared/ToastContext'; import { SortOption } from './Shared/SortFilterButton'; +import IconSortDropdown from './Shared/IconSortDropdown'; +import { TagIcon, XMarkIcon } from '@heroicons/react/24/solid'; import { - TagIcon, - XMarkIcon, - MagnifyingGlassIcon, -} from '@heroicons/react/24/solid'; -import { - InformationCircleIcon, QueueListIcon, + InformationCircleIcon, + MagnifyingGlassIcon, } from '@heroicons/react/24/outline'; import { getApiPath } from '../config/paths'; @@ -480,23 +477,17 @@ const Tasks: React.FC = () => { }; return ( -
-
+
+
{/* Title row with info button and filters dropdown on the right */}
-
+

{title}

@@ -517,7 +508,11 @@ const Tasks: React.FC = () => {
{/* Info expand/collapse button, search button, show completed toggle, and sort dropdown */}
)} -
- - Show completed - - -
- + + {t( + 'tasks.showCompleted', + 'Show completed' + )} + + + + + + } />
diff --git a/frontend/components/ViewDetail.tsx b/frontend/components/ViewDetail.tsx index 109b484..ed00178 100644 --- a/frontend/components/ViewDetail.tsx +++ b/frontend/components/ViewDetail.tsx @@ -19,7 +19,8 @@ import ProjectItem from './Project/ProjectItem'; import ConfirmDialog from './Shared/ConfirmDialog'; import { searchUniversal } from '../utils/searchService'; import { getApiPath } from '../config/paths'; -import SortFilterButton, { SortOption } from './Shared/SortFilterButton'; +import { SortOption } from './Shared/SortFilterButton'; +import IconSortDropdown from './Shared/IconSortDropdown'; interface View { id: number; @@ -473,11 +474,11 @@ const ViewDetail: React.FC = () => { } return ( -
-
+
+
{/* Header */} -
-
+
+
{isEditingName ? ( { ) : (

{view.name}

)}
-
+
-
- - Show completed - - -
- setShowCompleted((v) => !v)} + className="w-full flex items-center justify-between text-sm text-gray-700 dark:text-gray-300" + aria-pressed={showCompleted} + aria-label={ + showCompleted + ? t( + 'views.hideCompleted', + 'Hide completed tasks' + ) + : t( + 'views.showCompleted', + 'Show completed tasks' + ) + } + title={ + showCompleted + ? t( + 'views.hideCompleted', + 'Hide completed tasks' + ) + : t( + 'views.showCompleted', + 'Show completed tasks' + ) + } + > + + {t( + 'common.showCompleted', + 'Show completed' + )} + + + + + + } />