Feat main content revamp (#584)
* Move width of certain pages * fixup! Move width of certain pages
This commit is contained in:
parent
eb10ea8355
commit
0213f79b0a
13 changed files with 577 additions and 219 deletions
|
|
@ -27,8 +27,8 @@ const About: React.FC<AboutProps> = ({ isDarkMode = false }) => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex justify-center px-4 lg:px-2">
|
||||
<div className="w-full max-w-5xl">
|
||||
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
|
||||
<div className="w-full">
|
||||
<div className="flex items-center mb-4">
|
||||
<h2 className="text-2xl font-light">
|
||||
{t('about.title', 'About')}
|
||||
|
|
|
|||
|
|
@ -158,8 +158,8 @@ const Areas: React.FC = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-center px-4 lg:px-2">
|
||||
<div className="w-full max-w-5xl">
|
||||
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
|
||||
<div className="w-full">
|
||||
{/* Areas Header */}
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-2xl font-light">{t('areas.title')}</h2>
|
||||
|
|
|
|||
|
|
@ -455,8 +455,8 @@ const InboxItems: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center px-4 lg:px-2">
|
||||
<div className="w-full max-w-5xl">
|
||||
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
|
||||
<div className="w-full max-w-5xl mx-auto">
|
||||
{/* Title row with info button on the right */}
|
||||
<div className="flex items-center mb-8 justify-between">
|
||||
<div className="flex items-center">
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>(null);
|
||||
const noteOptionsDropdownRef = useRef<HTMLDivElement>(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 = () => {
|
|||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Sort Filter Dropdown */}
|
||||
<div className="relative" ref={sortDropdownRef}>
|
||||
<button
|
||||
onClick={() =>
|
||||
setShowSortDropdown(
|
||||
!showSortDropdown
|
||||
)
|
||||
}
|
||||
className="p-1 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors"
|
||||
aria-label="Sort notes"
|
||||
>
|
||||
<FunnelIcon className="h-5 w-5" />
|
||||
</button>
|
||||
{showSortDropdown && (
|
||||
<div className="absolute right-0 mt-1 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50">
|
||||
<div className="px-3 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
|
||||
Sort by
|
||||
</div>
|
||||
<div className="py-1">
|
||||
{sortOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
handleSortChange(
|
||||
option.value
|
||||
);
|
||||
setShowSortDropdown(
|
||||
false
|
||||
);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-sm transition-colors ${
|
||||
orderBy ===
|
||||
option.value
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<IconSortDropdown
|
||||
options={sortOptions}
|
||||
value={orderBy}
|
||||
onChange={handleSortChange}
|
||||
ariaLabel={t(
|
||||
'notes.sortNotes',
|
||||
'Sort notes'
|
||||
)}
|
||||
</div>
|
||||
title={t('notes.sortNotes', 'Sort notes')}
|
||||
dropdownLabel={t('notes.sortBy', 'Sort by')}
|
||||
/>
|
||||
<button
|
||||
onClick={handleNewNote}
|
||||
className="p-1 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors"
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ import { createNote } from '../../utils/notesService';
|
|||
import { isAuthError } from '../../utils/authUtils';
|
||||
import { getAutoSuggestNextActionsEnabled } from '../../utils/profileService';
|
||||
import AutoSuggestNextActionBox from './AutoSuggestNextActionBox';
|
||||
import SortFilterButton, { SortOption } from '../Shared/SortFilterButton';
|
||||
import { SortOption } from '../Shared/SortFilterButton';
|
||||
import IconSortDropdown from '../Shared/IconSortDropdown';
|
||||
import LoadingSpinner from '../Shared/LoadingSpinner';
|
||||
import { usePersistedModal } from '../../hooks/usePersistedModal';
|
||||
import BannerBadge from '../Shared/BannerBadge';
|
||||
|
|
@ -525,6 +526,40 @@ const ProjectDetails: React.FC = () => {
|
|||
saveProjectPreferences(showCompleted, newOrderBy);
|
||||
};
|
||||
|
||||
const renderShowCompletedToggle = () => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleShowCompletedChange(!showCompleted)}
|
||||
className="w-full flex items-center justify-between text-sm text-gray-700 dark:text-gray-300"
|
||||
aria-pressed={showCompleted}
|
||||
aria-label={
|
||||
showCompleted
|
||||
? t('projects.hideCompleted', 'Hide completed tasks')
|
||||
: t('projects.showCompleted', 'Show completed tasks')
|
||||
}
|
||||
title={
|
||||
showCompleted
|
||||
? t('projects.hideCompleted', 'Hide completed tasks')
|
||||
: t('projects.showCompleted', 'Show completed tasks')
|
||||
}
|
||||
>
|
||||
<span>{t('common.showCompleted', 'Show completed')}</span>
|
||||
<span
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
|
||||
showCompleted
|
||||
? 'bg-blue-600'
|
||||
: 'bg-gray-200 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
showCompleted ? 'translate-x-4' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
const handleDeleteProject = async () => {
|
||||
if (!project?.uid) {
|
||||
return;
|
||||
|
|
@ -1077,39 +1112,23 @@ const ProjectDetails: React.FC = () => {
|
|||
>
|
||||
<MagnifyingGlassIcon className="h-4 w-4 text-gray-600 dark:text-gray-200" />
|
||||
</button>
|
||||
{/* Show Completed Toggle */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap">
|
||||
Show completed
|
||||
</span>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleShowCompletedChange(
|
||||
!showCompleted
|
||||
)
|
||||
}
|
||||
className={`relative inline-flex h-4 w-7 items-center rounded-full transition-colors ${
|
||||
showCompleted
|
||||
? 'bg-blue-600'
|
||||
: 'bg-gray-200 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-3 w-3 transform rounded-full bg-white transition-transform ${
|
||||
showCompleted
|
||||
? 'translate-x-3.5'
|
||||
: 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sort Filter */}
|
||||
<SortFilterButton
|
||||
<IconSortDropdown
|
||||
options={sortOptions}
|
||||
value={orderBy}
|
||||
onChange={handleSortChange}
|
||||
size="mobile"
|
||||
ariaLabel={t(
|
||||
'projects.sortTasks',
|
||||
'Sort tasks'
|
||||
)}
|
||||
title={t(
|
||||
'projects.sortTasks',
|
||||
'Sort tasks'
|
||||
)}
|
||||
dropdownLabel={t(
|
||||
'tasks.sortBy',
|
||||
'Sort by'
|
||||
)}
|
||||
extraContent={renderShowCompletedToggle()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1179,39 +1198,20 @@ const ProjectDetails: React.FC = () => {
|
|||
>
|
||||
<MagnifyingGlassIcon className="h-5 w-5 text-gray-600 dark:text-gray-200" />
|
||||
</button>
|
||||
{/* Show Completed Toggle */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Show completed
|
||||
</span>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleShowCompletedChange(
|
||||
!showCompleted
|
||||
)
|
||||
}
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
|
||||
showCompleted
|
||||
? 'bg-blue-600'
|
||||
: 'bg-gray-200 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
showCompleted
|
||||
? 'translate-x-4'
|
||||
: 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sort Filter */}
|
||||
<SortFilterButton
|
||||
<IconSortDropdown
|
||||
options={sortOptions}
|
||||
value={orderBy}
|
||||
onChange={handleSortChange}
|
||||
size="desktop"
|
||||
ariaLabel={t(
|
||||
'projects.sortTasks',
|
||||
'Sort tasks'
|
||||
)}
|
||||
title={t(
|
||||
'projects.sortTasks',
|
||||
'Sort tasks'
|
||||
)}
|
||||
dropdownLabel={t('tasks.sortBy', 'Sort by')}
|
||||
extraContent={renderShowCompletedToggle()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -370,8 +370,8 @@ const Projects: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center px-4 lg:px-2">
|
||||
<div className="w-full max-w-5xl">
|
||||
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
|
||||
<div className="w-full">
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-2xl font-light">
|
||||
{t('projects.title')}
|
||||
|
|
|
|||
106
frontend/components/Shared/IconSortDropdown.tsx
Normal file
106
frontend/components/Shared/IconSortDropdown.tsx
Normal file
|
|
@ -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<IconSortDropdownProps> = ({
|
||||
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<HTMLDivElement>(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 (
|
||||
<div className={`relative ${className}`} ref={dropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
className={`p-1 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors ${buttonClassName}`}
|
||||
aria-label={ariaLabel}
|
||||
title={title}
|
||||
>
|
||||
<FunnelIcon className="h-5 w-5" />
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div
|
||||
className={`absolute ${align === 'left' ? 'left-0' : 'right-0'} mt-1 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50`}
|
||||
>
|
||||
{dropdownLabel && (
|
||||
<div className="px-3 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
|
||||
{dropdownLabel}
|
||||
</div>
|
||||
)}
|
||||
<div className="py-1">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
onChange(option.value);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-sm transition-colors ${
|
||||
value === option.value
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center justify-between">
|
||||
<span>{option.label}</span>
|
||||
{value === option.value && (
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{extraContent && (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
{extraContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconSortDropdown;
|
||||
246
frontend/components/Shared/ViewOptionsBar.tsx
Normal file
246
frontend/components/Shared/ViewOptionsBar.tsx
Normal file
|
|
@ -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<ViewOptionsBarProps> = ({
|
||||
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<HTMLDivElement>(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 (
|
||||
<div className={containerClasses}>
|
||||
{/* Info Button */}
|
||||
{showInfo && onToggleInfo && (
|
||||
<button
|
||||
onClick={onToggleInfo}
|
||||
className={`flex items-center hover:bg-blue-100/50 dark:hover:bg-blue-800/20 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset rounded-lg${isInfoExpanded ? ' bg-blue-50/70 dark:bg-blue-900/20' : ''} p-2`}
|
||||
aria-expanded={isInfoExpanded}
|
||||
aria-label={
|
||||
isInfoExpanded
|
||||
? t('common.hideInfo', 'Hide info')
|
||||
: t('common.showInfo', 'Show info')
|
||||
}
|
||||
title={
|
||||
isInfoExpanded
|
||||
? t('common.hideInfo', 'Hide info')
|
||||
: t('common.aboutView', 'About this view')
|
||||
}
|
||||
>
|
||||
<InformationCircleIcon className="h-5 w-5 text-blue-500" />
|
||||
<span className="sr-only">
|
||||
{isInfoExpanded
|
||||
? t('common.hideInfo', 'Hide info')
|
||||
: t('common.aboutView', 'About this view')}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Search Button */}
|
||||
{showSearch && onToggleSearch && (
|
||||
<button
|
||||
onClick={onToggleSearch}
|
||||
className={`flex items-center transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset rounded-lg p-2 ${
|
||||
isSearchExpanded
|
||||
? 'bg-blue-50/70 dark:bg-blue-900/20'
|
||||
: 'bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
aria-expanded={isSearchExpanded}
|
||||
aria-label={
|
||||
isSearchExpanded
|
||||
? t('common.hideSearch', 'Hide search')
|
||||
: t('common.showSearch', 'Show search')
|
||||
}
|
||||
title={
|
||||
isSearchExpanded
|
||||
? t('common.hideSearch', 'Hide search')
|
||||
: t('common.search', 'Search')
|
||||
}
|
||||
>
|
||||
<MagnifyingGlassIcon className="h-5 w-5 text-gray-600 dark:text-gray-200" />
|
||||
<span className="sr-only">
|
||||
{isSearchExpanded
|
||||
? t('common.hideSearch', 'Hide search')
|
||||
: t('common.search', 'Search')}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Show Completed Toggle */}
|
||||
{showCompletedToggle && onToggleCompleted && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{completedLabel ||
|
||||
t('common.showCompleted', 'Show completed')}
|
||||
</span>
|
||||
<button
|
||||
onClick={onToggleCompleted}
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
|
||||
showCompleted
|
||||
? 'bg-blue-600'
|
||||
: 'bg-gray-200 dark:bg-gray-600'
|
||||
}`}
|
||||
aria-pressed={showCompleted}
|
||||
aria-label={
|
||||
showCompleted
|
||||
? t('common.hideCompleted', 'Hide completed')
|
||||
: t('common.showCompleted', 'Show completed')
|
||||
}
|
||||
title={
|
||||
showCompleted
|
||||
? t('common.hideCompleted', 'Hide completed')
|
||||
: t('common.showCompleted', 'Show completed')
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
showCompleted
|
||||
? 'translate-x-4'
|
||||
: 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sort/Filter Dropdown */}
|
||||
{showSort && sortOptions.length > 0 && onSortChange && (
|
||||
<div className="relative" ref={sortDropdownRef}>
|
||||
<button
|
||||
onClick={() =>
|
||||
setIsSortDropdownOpen(!isSortDropdownOpen)
|
||||
}
|
||||
className="p-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
aria-label={
|
||||
sortLabel ||
|
||||
t('common.sortFilter', 'Sort and filter')
|
||||
}
|
||||
title={
|
||||
sortLabel ||
|
||||
t('common.sortFilter', 'Sort and filter')
|
||||
}
|
||||
>
|
||||
<FunnelIcon className="h-5 w-5" />
|
||||
</button>
|
||||
{isSortDropdownOpen && (
|
||||
<div className="absolute right-0 mt-1 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 z-50">
|
||||
<div className="px-3 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
|
||||
{sortLabel || t('common.sortBy', 'Sort by')}
|
||||
</div>
|
||||
<div className="py-1">
|
||||
{sortOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() =>
|
||||
handleSortSelect(option.value)
|
||||
}
|
||||
className={`w-full text-left px-3 py-2 text-sm transition-colors ${
|
||||
sortValue === option.value
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom Elements */}
|
||||
{customElements}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewOptionsBar;
|
||||
|
|
@ -124,8 +124,8 @@ const Tags: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center px-4 lg:px-2">
|
||||
<div className="w-full max-w-5xl">
|
||||
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
|
||||
<div className="w-full">
|
||||
{/* Tags Header */}
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-2xl font-light">
|
||||
|
|
|
|||
|
|
@ -790,8 +790,8 @@ const TasksToday: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center px-4 lg:px-2">
|
||||
<div className="w-full max-w-5xl">
|
||||
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
|
||||
<div className="w-full max-w-5xl mx-auto">
|
||||
<div className="flex flex-col">
|
||||
{/* Today Header with Icons on the Right */}
|
||||
<div className="mb-4">
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div
|
||||
className={
|
||||
isUpcomingView
|
||||
? 'w-full px-2 sm:px-4 lg:px-6'
|
||||
: 'flex justify-center px-4 lg:px-2'
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`w-full ${isUpcomingView ? 'max-w-none' : 'max-w-5xl'}`}
|
||||
>
|
||||
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
|
||||
<div className="w-full max-w-5xl mx-auto">
|
||||
{/* Title row with info button and filters dropdown on the right */}
|
||||
<div
|
||||
className={`flex flex-col sm:flex-row items-start sm:items-center justify-between ${isUpcomingView ? 'mb-4 sm:mb-6' : 'mb-8'}`}
|
||||
className={`flex items-center justify-between gap-2 min-w-0 ${
|
||||
isUpcomingView ? 'mb-4 sm:mb-6' : 'mb-8'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center mb-2 sm:mb-0">
|
||||
<div className="flex items-center flex-1 min-w-0 gap-2">
|
||||
<h2
|
||||
className={`${isUpcomingView ? 'text-lg sm:text-xl' : 'text-2xl'} font-light`}
|
||||
className={`${isUpcomingView ? 'text-lg sm:text-xl' : 'text-2xl'} font-light truncate`}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
|
|
@ -517,7 +508,11 @@ const Tasks: React.FC = () => {
|
|||
</div>
|
||||
{/* Info expand/collapse button, search button, show completed toggle, and sort dropdown */}
|
||||
<div
|
||||
className={`flex items-center gap-2 w-full sm:w-auto justify-end mt-2 sm:mt-0 ${isUpcomingView ? 'md:fixed md:right-4 md:top-20 md:px-3 md:py-2 md:z-20' : 'flex-wrap'}`}
|
||||
className={`flex items-center gap-2 flex-shrink-0 ${
|
||||
isUpcomingView
|
||||
? 'md:fixed md:right-4 md:top-20 md:px-3 md:py-2 md:z-20'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => setIsInfoExpanded((v) => !v)}
|
||||
|
|
@ -563,42 +558,65 @@ const Tasks: React.FC = () => {
|
|||
</span>
|
||||
</button>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Show completed
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setShowCompleted((v) => !v)}
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
|
||||
showCompleted
|
||||
? 'bg-blue-600'
|
||||
: 'bg-gray-200 dark:bg-gray-600'
|
||||
}`}
|
||||
aria-pressed={showCompleted}
|
||||
aria-label={
|
||||
showCompleted
|
||||
? 'Hide completed tasks'
|
||||
: 'Show completed tasks'
|
||||
}
|
||||
title={
|
||||
showCompleted
|
||||
? 'Hide completed tasks'
|
||||
: 'Show completed tasks'
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
<IconSortDropdown
|
||||
options={sortOptions}
|
||||
value={orderBy}
|
||||
onChange={handleSortChange}
|
||||
ariaLabel={t('tasks.sortTasks', 'Sort tasks')}
|
||||
title={t('tasks.sortTasks', 'Sort tasks')}
|
||||
dropdownLabel={t('tasks.sortBy', 'Sort by')}
|
||||
extraContent={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => 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
|
||||
? 'translate-x-4'
|
||||
: 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<SortFilter
|
||||
sortOptions={sortOptions}
|
||||
sortValue={orderBy}
|
||||
onSortChange={handleSortChange}
|
||||
? t(
|
||||
'tasks.hideCompleted',
|
||||
'Hide completed tasks'
|
||||
)
|
||||
: t(
|
||||
'tasks.showCompleted',
|
||||
'Show completed tasks'
|
||||
)
|
||||
}
|
||||
title={
|
||||
showCompleted
|
||||
? t(
|
||||
'tasks.hideCompleted',
|
||||
'Hide completed tasks'
|
||||
)
|
||||
: t(
|
||||
'tasks.showCompleted',
|
||||
'Show completed tasks'
|
||||
)
|
||||
}
|
||||
>
|
||||
<span>
|
||||
{t(
|
||||
'tasks.showCompleted',
|
||||
'Show completed'
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
|
||||
showCompleted
|
||||
? 'bg-blue-600'
|
||||
: 'bg-gray-200 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
showCompleted
|
||||
? 'translate-x-4'
|
||||
: 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="flex justify-center px-4 lg:px-2">
|
||||
<div className="w-full max-w-5xl">
|
||||
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
|
||||
<div className="w-full max-w-5xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-8">
|
||||
<div className="flex items-center flex-1 mb-2 sm:mb-0">
|
||||
<div className="flex items-center justify-between gap-2 flex-wrap sm:flex-nowrap mb-8">
|
||||
<div className="flex items-center flex-1 min-w-0 gap-2">
|
||||
{isEditingName ? (
|
||||
<input
|
||||
ref={titleInputRef}
|
||||
|
|
@ -497,13 +498,13 @@ const ViewDetail: React.FC = () => {
|
|||
) : (
|
||||
<h2
|
||||
onClick={handleEditName}
|
||||
className="text-2xl font-light text-gray-900 dark:text-white cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
|
||||
className="text-2xl font-light text-gray-900 dark:text-white cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 transition-colors truncate"
|
||||
>
|
||||
{view.name}
|
||||
</h2>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 w-full sm:w-auto justify-end">
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => setIsSearchExpanded((v) => !v)}
|
||||
className={`flex items-center transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset rounded-lg p-2 ${
|
||||
|
|
@ -525,38 +526,65 @@ const ViewDetail: React.FC = () => {
|
|||
>
|
||||
<MagnifyingGlassIcon className="h-5 w-5 text-gray-600 dark:text-gray-200" />
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Show completed
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setShowCompleted((v) => !v)}
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
|
||||
showCompleted
|
||||
? 'bg-blue-600'
|
||||
: 'bg-gray-200 dark:bg-gray-600'
|
||||
}`}
|
||||
aria-pressed={showCompleted}
|
||||
aria-label={
|
||||
showCompleted
|
||||
? 'Hide completed tasks'
|
||||
: 'Show completed tasks'
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
showCompleted
|
||||
? 'translate-x-4'
|
||||
: 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<SortFilterButton
|
||||
<IconSortDropdown
|
||||
options={sortOptions}
|
||||
value={orderBy}
|
||||
onChange={setOrderBy}
|
||||
size="desktop"
|
||||
ariaLabel={t('views.sortTasks', 'Sort tasks')}
|
||||
title={t('views.sortTasks', 'Sort tasks')}
|
||||
dropdownLabel={t('tasks.sortBy', 'Sort by')}
|
||||
extraContent={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => 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'
|
||||
)
|
||||
}
|
||||
>
|
||||
<span>
|
||||
{t(
|
||||
'common.showCompleted',
|
||||
'Show completed'
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
|
||||
showCompleted
|
||||
? 'bg-blue-600'
|
||||
: 'bg-gray-200 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
showCompleted
|
||||
? 'translate-x-4'
|
||||
: 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<div className="relative" ref={criteriaDropdownRef}>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -122,8 +122,8 @@ const Views: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center px-4 lg:px-2">
|
||||
<div className="w-full max-w-5xl">
|
||||
<div className="w-full px-2 sm:px-4 lg:px-6 pt-4 pb-8">
|
||||
<div className="w-full">
|
||||
{/* Views Header */}
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-2xl font-light">{t('views.title')}</h2>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue