Replace overdue badge with arrow icon
This commit is contained in:
parent
96a8704788
commit
e9eabdec47
2 changed files with 119 additions and 65 deletions
|
|
@ -1,9 +1,10 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
MagnifyingGlassIcon,
|
||||
FolderIcon,
|
||||
Squares2X2Icon,
|
||||
Bars3Icon,
|
||||
ChevronDownIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import ConfirmDialog from './Shared/ConfirmDialog';
|
||||
import ProjectModal from './Project/ProjectModal';
|
||||
|
|
@ -35,6 +36,71 @@ const getPriorityStyles = (priority: PriorityType) => {
|
|||
}
|
||||
};
|
||||
|
||||
// Reusable dropdown component
|
||||
interface DropdownOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface DropdownProps {
|
||||
label: string;
|
||||
value: string;
|
||||
options: DropdownOption[];
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const Dropdown: React.FC<DropdownProps> = ({ label, value, options, onChange, placeholder }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
}, []);
|
||||
|
||||
const selectedOption = options.find(option => option.value === value);
|
||||
|
||||
return (
|
||||
<div className="w-full md:w-auto relative" ref={dropdownRef}>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{label}
|
||||
</label>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="inline-flex justify-between w-full px-3 py-2 bg-white dark:bg-gray-800 text-sm text-gray-900 dark:text-gray-100 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm focus:outline-none hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<span>{selectedOption?.label || placeholder}</span>
|
||||
<ChevronDownIcon className={`w-5 h-5 text-gray-500 dark:text-gray-300 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="absolute z-10 mt-2 w-full bg-white dark:bg-gray-700 shadow-lg rounded-md border border-gray-200 dark:border-gray-600 max-h-60 overflow-y-auto">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
onChange(option.value);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`flex items-center justify-between w-full px-4 py-2 text-sm text-gray-900 dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors ${
|
||||
option.value === value ? 'bg-blue-50 dark:bg-blue-900/20' : ''
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Projects: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
|
|
@ -64,6 +130,7 @@ const Projects: React.FC = () => {
|
|||
const [activeDropdown, setActiveDropdown] = useState<number | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [viewMode, setViewMode] = useState<'cards' | 'list'>('cards');
|
||||
const [isSearchExpanded, setIsSearchExpanded] = useState<boolean>(false);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const activeFilter = searchParams.get('active') || 'all';
|
||||
|
|
@ -100,6 +167,7 @@ const Projects: React.FC = () => {
|
|||
loadProjects();
|
||||
}, [activeFilter, areaFilter]);
|
||||
|
||||
|
||||
const handleSaveProject = async (project: Project) => {
|
||||
setProjectsLoading(true);
|
||||
try {
|
||||
|
|
@ -266,65 +334,54 @@ const Projects: React.FC = () => {
|
|||
>
|
||||
<Bars3Icon className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
{/* Search Toggle Button */}
|
||||
<button
|
||||
onClick={() => setIsSearchExpanded(!isSearchExpanded)}
|
||||
className={`p-2 rounded-md focus:outline-none transition-colors ${
|
||||
isSearchExpanded
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
aria-label={t('common.search', 'Search')}
|
||||
>
|
||||
<MagnifyingGlassIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-center md:space-x-4">
|
||||
<div className="w-full md:w-auto">
|
||||
<label
|
||||
htmlFor="activeFilter"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
{t('common.status')}
|
||||
</label>
|
||||
<select
|
||||
id="activeFilter"
|
||||
value={activeFilter}
|
||||
onChange={handleActiveFilterChange}
|
||||
className="block w-full p-2 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="true">
|
||||
{t('projects.filters.active')}
|
||||
</option>
|
||||
<option value="false">
|
||||
{t('projects.filters.inactive')}
|
||||
</option>
|
||||
<option value="all">
|
||||
{t('projects.filters.all')}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* Status Dropdown */}
|
||||
<Dropdown
|
||||
label={t('common.status')}
|
||||
value={activeFilter}
|
||||
options={[
|
||||
{ value: 'true', label: t('projects.filters.active') },
|
||||
{ value: 'false', label: t('projects.filters.inactive') },
|
||||
{ value: 'all', label: t('projects.filters.all') }
|
||||
]}
|
||||
onChange={(value) => handleActiveFilterChange({target: {value}} as any)}
|
||||
/>
|
||||
|
||||
<div className="w-full md:w-auto">
|
||||
<label
|
||||
htmlFor="areaFilter"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
{t('common.area')}
|
||||
</label>
|
||||
<select
|
||||
id="areaFilter"
|
||||
value={areaFilter}
|
||||
onChange={handleAreaFilterChange}
|
||||
className="block w-full p-2 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">
|
||||
{t('projects.filters.allAreas')}
|
||||
</option>
|
||||
{areas.map((area) => (
|
||||
<option
|
||||
key={area.id}
|
||||
value={area.id?.toString()}
|
||||
>
|
||||
{area.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{/* Area Dropdown */}
|
||||
<Dropdown
|
||||
label={t('common.area')}
|
||||
value={areaFilter}
|
||||
options={[
|
||||
{ value: '', label: t('projects.filters.allAreas') },
|
||||
...areas.map(area => ({
|
||||
value: area.id?.toString() || '',
|
||||
label: area.name
|
||||
}))
|
||||
]}
|
||||
onChange={(value) => handleAreaFilterChange({target: {value}} as any)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="mb-4">
|
||||
{/* Collapsible Search Bar */}
|
||||
<div className={`transition-all duration-300 ease-in-out overflow-hidden ${
|
||||
isSearchExpanded ? 'max-h-20 opacity-100 mb-4' : 'max-h-0 opacity-0 mb-0'
|
||||
}`}>
|
||||
<div className="flex items-center bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm p-2">
|
||||
<MagnifyingGlassIcon className="h-5 w-5 text-gray-500 dark:text-gray-400 mr-2" />
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
CalendarIcon,
|
||||
PlayIcon,
|
||||
ArrowPathIcon,
|
||||
ArrowRightIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { TagIcon, FolderIcon } from '@heroicons/react/24/solid';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
|
@ -128,12 +129,10 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
{task.name}
|
||||
</span>
|
||||
{isOverdue && (
|
||||
<span
|
||||
className="ml-2 px-2 py-0.5 text-xs bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 rounded-md"
|
||||
title="Task has been in today plan for a while"
|
||||
>
|
||||
overdue
|
||||
</span>
|
||||
<ArrowRightIcon
|
||||
className="ml-2 h-4 w-4 text-amber-600 dark:text-amber-400 opacity-60"
|
||||
title="This task was in your plan yesterday and wasn't completed."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* Project, tags, due date, and recurrence in same row, with spacing when they exist */}
|
||||
|
|
@ -278,12 +277,10 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
<div className="font-light text-md text-gray-900 dark:text-gray-100">
|
||||
<span className="break-words">{task.name}</span>
|
||||
{isOverdue && (
|
||||
<span
|
||||
className="ml-2 px-2 py-0.5 text-xs bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 rounded-md"
|
||||
title="Task has been in today plan for a while"
|
||||
>
|
||||
overdue
|
||||
</span>
|
||||
<ArrowRightIcon
|
||||
className="ml-2 h-4 w-4 text-amber-600 dark:text-amber-400 opacity-60"
|
||||
title="This task was in your plan yesterday and wasn't completed."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue