style: enhance DataBar components with improved styling and animations

This commit is contained in:
Sam Chau
2025-01-09 15:57:26 +10:30
parent 2242bd2c4d
commit dbf3bf9801
7 changed files with 255 additions and 143 deletions

View File

@@ -5,10 +5,21 @@ const AddButton = ({onClick, label = 'Add New'}) => {
return (
<button
onClick={onClick}
className='flex items-center gap-2 px-3 py-2 rounded transition-colors bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
className='flex items-center gap-2 px-3 py-2 rounded-md
border border-gray-200 dark:border-gray-700
bg-white text-gray-700 dark:bg-gray-800 dark:text-gray-300
hover:bg-gray-50 dark:hover:bg-gray-750
hover:border-blue-500/50 hover:text-blue-500
dark:hover:border-blue-500/50 dark:hover:text-blue-400
transition-all duration-150 ease-in-out
group'
title={label}>
<Plus className='w-4 h-4' />
<span className='text-sm'>{label}</span>
<Plus
className='w-4 h-4 transition-transform duration-200 ease-out
group-hover:rotate-90 group-hover:scale-110
group-hover:text-blue-500 dark:group-hover:text-blue-400'
/>
<span className='text-sm font-medium'>{label}</span>
</button>
);
};

View File

@@ -91,14 +91,14 @@ const DataBar = ({
allTags={allTags}
/>
{showAddButton && !isSelectionMode && (
<AddButton onClick={onAdd} label={addButtonLabel} />
)}
<ToggleSelectButton
isSelectionMode={isSelectionMode}
onClick={toggleSelectionMode}
/>
{showAddButton && !isSelectionMode && (
<AddButton onClick={onAdd} label={addButtonLabel} />
)}
</div>
</>
);

View File

@@ -1,97 +1,142 @@
// FilterMenu.jsx
import { useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import React, {useRef, useEffect} from 'react';
import {Filter} from 'lucide-react';
function FilterMenu({ filterType, setFilterType, filterValue, setFilterValue, allTags }) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
function FilterMenu({
filterType,
setFilterType,
filterValue,
setFilterValue,
allTags
}) {
const [isOpen, setIsOpen] = React.useState(false);
const dropdownRef = useRef(null);
const options = [
{ value: 'none', label: 'No Filter' },
{ value: 'tag', label: 'Filter by Tag' },
{ value: 'date', label: 'Filter by Date' },
];
const options = [
{value: 'none', label: 'No Filter'},
{value: 'tag', label: 'Filter by Tag'},
{value: 'date', label: 'Filter by Date'}
];
useEffect(() => {
function handleClickOutside(event) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
useEffect(() => {
function handleClickOutside(event) {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target)
) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () =>
document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div className="flex items-center space-x-2">
<div className="relative inline-block text-left" ref={dropdownRef}>
<div>
<button
type="button"
className="inline-flex justify-between items-center w-full rounded-md border border-gray-600 shadow-sm px-4 py-2 bg-gray-700 text-sm font-medium text-white hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
onClick={() => setIsOpen(!isOpen)}
>
{options.find(option => option.value === filterType)?.label}
<svg className="-mr-1 ml-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
const hasActiveFilter = filterType !== 'none' && filterValue;
return (
<div className='relative' ref={dropdownRef}>
<button
type='button'
className={`
flex items-center gap-2 px-3 py-2 rounded-md
border border-gray-200 dark:border-gray-700
transition-all duration-150 ease-in-out
group
${
hasActiveFilter
? 'bg-blue-50 text-blue-600 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-800'
: 'bg-white text-gray-700 dark:bg-gray-800 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-750 hover:border-blue-500/50 hover:text-blue-500 dark:hover:border-blue-500/50 dark:hover:text-blue-400'
}
`}
onClick={() => setIsOpen(!isOpen)}>
<Filter
className={`w-4 h-4 transition-colors duration-200
${
hasActiveFilter
? ''
: 'group-hover:text-blue-500 dark:group-hover:text-blue-400 group-hover:animate-[wiggle_0.3s_ease-in-out]'
}
`}
style={{
'@keyframes wiggle': {
'0%': {transform: 'rotate(0deg)'},
'25%': {transform: 'rotate(-20deg)'},
'75%': {transform: 'rotate(20deg)'},
'100%': {transform: 'rotate(0deg)'}
}
}}
/>
<span className='text-sm font-medium'>
{filterType === 'none'
? 'Filter'
: options.find(option => option.value === filterType)
?.label}
</span>
</button>
{isOpen && (
<div className='absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg z-10'>
<div className='py-1' role='menu'>
{options.map(option => (
<button
key={option.value}
onClick={() => {
setFilterType(option.value);
setFilterValue('');
setIsOpen(false);
}}
className={`
block w-full text-left px-4 py-2 text-sm
${
filterType === option.value
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50'
}
`}
role='menuitem'>
{option.label}
</button>
))}
</div>
{filterType === 'tag' && (
<div className='border-t border-gray-200 dark:border-gray-700 p-2'>
<select
value={filterValue}
onChange={e => setFilterValue(e.target.value)}
className='w-full px-2 py-1.5 text-sm rounded-md
bg-gray-50 dark:bg-gray-700
text-gray-700 dark:text-gray-300
border border-gray-200 dark:border-gray-600
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500'>
<option value=''>Select a tag</option>
{allTags.map(tag => (
<option key={tag} value={tag}>
{tag}
</option>
))}
</select>
</div>
)}
{filterType === 'date' && (
<div className='border-t border-gray-200 dark:border-gray-700 p-2'>
<input
type='date'
value={filterValue}
onChange={e => setFilterValue(e.target.value)}
className='w-full px-2 py-1.5 text-sm rounded-md
bg-gray-50 dark:bg-gray-700
text-gray-700 dark:text-gray-300
border border-gray-200 dark:border-gray-600
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500'
/>
</div>
)}
</div>
)}
</div>
{isOpen && (
<div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-gray-700 ring-1 ring-black ring-opacity-5 z-50">
<div className="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
{options.map((option) => (
<button
key={option.value}
onClick={() => {
setFilterType(option.value);
setFilterValue('');
setIsOpen(false);
}}
className={`${
filterType === option.value ? 'bg-gray-600 text-white' : 'text-gray-200'
} block w-full text-left px-4 py-2 text-sm hover:bg-gray-600 hover:text-white`}
role="menuitem"
>
{option.label}
</button>
))}
</div>
</div>
)}
</div>
{filterType === 'tag' && (
<select
value={filterValue}
onChange={(e) => setFilterValue(e.target.value)}
className="appearance-none bg-gray-700 text-white py-2 px-4 pr-8 rounded-md border border-gray-600 leading-tight focus:outline-none focus:bg-gray-600 focus:border-white cursor-pointer"
>
<option value="">Select a tag</option>
{allTags.map(tag => (
<option key={tag} value={tag}>{tag}</option>
))}
</select>
)}
{filterType === 'date' && (
<input
type="date"
value={filterValue}
onChange={(e) => setFilterValue(e.target.value)}
className="bg-gray-700 text-white py-2 px-4 rounded-md border border-gray-600 leading-tight focus:outline-none focus:bg-gray-600 focus:border-white cursor-pointer"
/>
)}
</div>
);
);
}
FilterMenu.propTypes = {
filterType: PropTypes.string.isRequired,
setFilterType: PropTypes.func.isRequired,
filterValue: PropTypes.string.isRequired,
setFilterValue: PropTypes.func.isRequired,
allTags: PropTypes.arrayOf(PropTypes.string).isRequired,
};
export default FilterMenu;
export default FilterMenu;

View File

@@ -52,29 +52,47 @@ const SearchBar = ({
};
return (
<div className={`relative flex-1 min-w-0 ${className}`}>
<Search className='absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400' />
<div className={`relative flex-1 min-w-0 group ${className}`}>
<Search
className={`
absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4
transition-colors duration-200
${
isFocused
? 'text-blue-500'
: 'text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300'
}
`}
/>
<div
className={`
w-full h-10 pl-9 pr-8 rounded-md border
w-full h-10 pl-9 pr-8 rounded-md
transition-all duration-200 ease-in-out
border shadow-sm
${
isFocused
? 'ring-2 ring-blue-500 border-transparent'
: 'border-gray-300 dark:border-gray-700'
? 'border-blue-500 ring-2 ring-blue-500/20 bg-white/5'
: 'border-gray-300 dark:border-gray-700 hover:border-gray-400 dark:hover:border-gray-600'
}
bg-white dark:bg-gray-800
bg-white dark:bg-gray-800
flex items-center
transition-colors
`}>
{activeSearch ? (
<div className='flex items-center gap-1.5 px-2 py-0.5 bg-blue-500/10 text-blue-500 dark:bg-blue-500/20 dark:text-blue-400 rounded'>
<div
className='flex items-center gap-1.5 px-2 py-1
bg-blue-500/10 dark:bg-blue-500/20
border border-blue-500/20 dark:border-blue-400/20
text-blue-600 dark:text-blue-400
rounded-md shadow-sm
transition-all duration-200'>
<span className='text-sm font-medium leading-none'>
{activeSearch}
</span>
<button
onClick={clearSearch}
className='p-0.5 hover:bg-blue-500/20 rounded'>
className='p-0.5 hover:bg-blue-500/20 rounded-sm transition-colors'
aria-label='Clear search'>
<X className='h-3 w-3' />
</button>
</div>
@@ -87,8 +105,9 @@ const SearchBar = ({
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
placeholder={placeholder}
className='w-full bg-transparent text-gray-900 dark:text-gray-100
placeholder:text-gray-500 dark:placeholder:text-gray-400
className='w-full bg-transparent
text-gray-900 dark:text-gray-100
placeholder:text-gray-500 dark:placeholder:text-gray-400
focus:outline-none'
/>
)}
@@ -97,8 +116,13 @@ const SearchBar = ({
{searchTerm && !activeSearch && (
<button
onClick={() => setSearchTerm('')}
className='absolute right-3 top-1/2 -translate-y-1/2 p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700'>
<X className='h-4 w-4 text-gray-400' />
className='absolute right-3 top-1/2 -translate-y-1/2
p-1.5 rounded-full
text-gray-400 hover:text-gray-600
hover:bg-gray-100 dark:hover:bg-gray-700
transition-all duration-200'
aria-label='Clear search input'>
<X className='h-4 w-4' />
</button>
)}
</div>

View File

@@ -1,6 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import {ArrowDown} from 'lucide-react';
import {ArrowDownUp} from 'lucide-react';
export const SortDropdown = ({
options,
@@ -23,20 +22,41 @@ export const SortDropdown = ({
<div className='relative'>
<button
onClick={() => setIsOpen(!isOpen)}
className='flex items-center space-x-1 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded px-2 py-1 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors'>
<span>Sort</span>
<ArrowDown size={14} />
className='flex items-center gap-2 px-3 py-2 rounded-md
border border-gray-200 dark:border-gray-700
bg-white text-gray-700 dark:bg-gray-800 dark:text-gray-300
hover:bg-gray-50 dark:hover:bg-gray-750
hover:border-blue-500/50 hover:text-blue-500
dark:hover:border-blue-500/50 dark:hover:text-blue-400
transition-all duration-150 ease-in-out
group'>
<ArrowDownUp
className='w-4 h-4 transition-all duration-200
[transform-style:preserve-3d]
group-hover:[transform:rotateX(180deg)]
group-hover:text-blue-500 dark:group-hover:text-blue-400'
/>
<span className='text-sm font-medium'>Sort</span>
</button>
{isOpen && (
<div className='absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded shadow-lg z-10'>
<div
className='absolute right-0 mt-2 w-56 py-1
bg-white dark:bg-gray-800
border border-gray-200 dark:border-gray-700
rounded-md shadow-lg z-10'>
{options.map(option => (
<button
key={option.key}
onClick={() => handleSort(option.key)}
className='block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors'>
{option.label}
className='flex items-center justify-between w-full px-4 py-2 text-sm
text-gray-700 dark:text-gray-300
hover:bg-blue-50 dark:hover:bg-blue-900/20
hover:text-blue-500 dark:hover:text-blue-400
transition-colors duration-150'>
<span>{option.label}</span>
{currentKey === option.key && (
<span className='float-right'>
<span className='text-blue-500 dark:text-blue-400'>
{currentDirection === 'asc' ? '↑' : '↓'}
</span>
)}
@@ -48,14 +68,4 @@ export const SortDropdown = ({
);
};
SortDropdown.propTypes = {
options: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string.isRequired,
label: PropTypes.string.isRequired
})
).isRequired,
currentKey: PropTypes.string.isRequired,
currentDirection: PropTypes.string.isRequired,
onSort: PropTypes.func.isRequired
};
export default SortDropdown;

View File

@@ -1,22 +1,32 @@
import React from 'react';
import {CheckSquare} from 'lucide-react';
const ToggleSelectButton = ({
isSelectionMode,
onClick,
shortcutKey = 'A' // Default to 'A' since that's what's used in the pages
}) => {
const ToggleSelectButton = ({isSelectionMode, onClick, shortcutKey = 'A'}) => {
return (
<button
onClick={onClick}
className={`flex items-center gap-2 px-3 py-2 rounded transition-colors ${
isSelectionMode
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300'
: 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
className={`
flex items-center gap-2 px-3 py-2 rounded-md
border border-gray-200 dark:border-gray-700
transition-all duration-150 ease-in-out
group
${
isSelectionMode
? 'bg-blue-50 text-blue-600 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-800'
: 'bg-white text-gray-700 dark:bg-gray-800 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-750 hover:border-blue-500/50 hover:text-blue-500 dark:hover:border-blue-500/50 dark:hover:text-blue-400'
}
`}
title={`Toggle selection mode (Ctrl+${shortcutKey})`}>
<CheckSquare className='w-4 h-4' />
<span className='text-sm'>Select</span>
<CheckSquare
className={`w-4 h-4 transition-all duration-200
${
isSelectionMode
? ''
: 'group-hover:text-blue-500 dark:group-hover:text-blue-400 group-hover:animate-[check-bounce_0.3s_ease-in-out]'
}
`}
/>
<span className='text-sm font-medium'>Select</span>
</button>
);
};

View File

@@ -26,12 +26,24 @@ module.exports = {
opacity: '1',
transform: 'translate3d(0, 0, 0)'
}
},
wiggle: {
'0%, 100%': {transform: 'rotate(0deg)'},
'25%': {transform: 'rotate(-20deg)'},
'75%': {transform: 'rotate(20deg)'}
},
'check-bounce': {
'0%, 100%': {transform: 'scale(1) rotate(0deg)'},
'30%': {transform: 'scale(1.15) rotate(-10deg)'},
'60%': {transform: 'scale(0.9) rotate(5deg)'}
}
},
animation: {
'modal-open': 'modal-open 0.3s ease-out forwards',
'fade-in': 'fade-in 0.5s ease-in-out forwards',
'slide-down': 'slide-down 0.4s cubic-bezier(0.16, 1, 0.3, 1)'
'slide-down': 'slide-down 0.4s cubic-bezier(0.16, 1, 0.3, 1)',
wiggle: 'wiggle 0.3s ease-in-out',
'check-bounce': 'check-bounce 0.3s ease-in-out'
},
colors: {
'dark-bg': '#1a1c23',