feat: refactor data actions into new DataBar component

This commit is contained in:
Sam Chau
2025-01-09 01:47:56 +10:30
parent d93b57975c
commit 152423b65a
12 changed files with 255 additions and 228 deletions

View File

@@ -2,27 +2,24 @@ import React, {useState, useEffect} from 'react';
import {useNavigate} from 'react-router-dom';
import FormatCard from './FormatCard';
import FormatModal from './FormatModal';
import AddButton from '@ui/AddButton';
import {getGitStatus} from '@api/api';
import {CustomFormats} from '@api/data';
import FilterMenu from '@ui/FilterMenu';
import SortMenu from '@ui/SortMenu';
import {Loader, CheckSquare} from 'lucide-react';
import {Loader} from 'lucide-react';
import Alert from '@ui/Alert';
import SearchBar from '@ui/SearchBar';
import {useFormatModal} from '@hooks/useFormatModal';
import {useMassSelection} from '@hooks/useMassSelection';
import {useKeyboardShortcut} from '@hooks/useKeyboardShortcut';
import MassActionsBar from '@ui/MassActionsBar';
import ImportModal from '@ui/ImportModal';
import {importFormats} from '@api/import';
import DataBar from '@ui/DataBar/DataBar';
function FormatPage() {
// Basic state management
const [formats, setFormats] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedFormat, setSelectedFormat] = useState(null);
const [sortBy, setSortBy] = useState('title');
const [sortBy, setSortBy] = useState('name');
const [filterType, setFilterType] = useState('none');
const [filterValue, setFilterValue] = useState('');
const [allTags, setAllTags] = useState([]);
@@ -213,7 +210,6 @@ function FormatPage() {
);
}
// Rest of the sorting logic remains the same...
return filtered.sort((a, b) => {
switch (sortBy) {
case 'dateModified':
@@ -276,37 +272,21 @@ function FormatPage() {
return (
<div>
<div className='mb-4 flex items-center gap-4'>
<SearchBar
onSearch={setSearchQuery}
placeholder='Search by name or tag...'
/>
<div className='flex-none'>
<SortMenu sortBy={sortBy} setSortBy={setSortBy} />
</div>
<div className='flex-none'>
<FilterMenu
filterType={filterType}
setFilterType={setFilterType}
filterValue={filterValue}
setFilterValue={setFilterValue}
allTags={allTags}
/>
</div>
<div className='flex-none'>
<button
onClick={toggleSelectionMode}
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'
}`}
title='Toggle selection mode (Ctrl+A)'>
<CheckSquare className='w-4 h-4' />
<span className='text-sm'>Select</span>
</button>
</div>
</div>
<DataBar
onSearch={setSearchQuery}
searchPlaceholder='Search by name or tag...'
filterType={filterType}
setFilterType={setFilterType}
filterValue={filterValue}
setFilterValue={setFilterValue}
allTags={allTags}
sortBy={sortBy}
setSortBy={setSortBy}
isSelectionMode={isSelectionMode}
toggleSelectionMode={toggleSelectionMode}
onAdd={() => handleOpenModal()}
addButtonLabel='Add New Format'
/>
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4'>
{filteredFormats.map((format, index) => (
@@ -330,15 +310,6 @@ function FormatPage() {
))}
</div>
{!isSelectionMode && (
<AddButton
onClick={() => handleOpenModal()}
label='Add New Format'
top='5vh'
left='75vw'
/>
)}
{isSelectionMode && (
<MassActionsBar
selectedCount={selectedItems.size}

View File

@@ -2,26 +2,23 @@ import React, {useState, useEffect} from 'react';
import {useNavigate} from 'react-router-dom';
import ProfileCard from './ProfileCard';
import ProfileModal from './ProfileModal';
import AddButton from '@ui/AddButton';
import {getGitStatus} from '@api/api';
import {Profiles, CustomFormats} from '@api/data';
import FilterMenu from '@ui/FilterMenu';
import SortMenu from '@ui/SortMenu';
import {Loader, CheckSquare} from 'lucide-react';
import SearchBar from '@ui/SearchBar';
import {Loader} from 'lucide-react';
import Alert from '@ui/Alert';
import {useMassSelection} from '@hooks/useMassSelection';
import {useKeyboardShortcut} from '@hooks/useKeyboardShortcut';
import MassActionsBar from '@ui/MassActionsBar';
import ImportModal from '@ui/ImportModal';
import {importProfiles} from '@api/import';
import Alert from '@ui/Alert';
import DataBar from '@ui/DataBar/DataBar';
function ProfilePage() {
const [profiles, setProfiles] = useState([]);
const [formats, setFormats] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedProfile, setSelectedProfile] = useState(null);
const [sortBy, setSortBy] = useState('title');
const [sortBy, setSortBy] = useState('name');
const [filterType, setFilterType] = useState('none');
const [filterValue, setFilterValue] = useState('');
const [allTags, setAllTags] = useState([]);
@@ -107,7 +104,7 @@ function ProfilePage() {
try {
const response = await CustomFormats.getAll();
const formatsData = response.map(item => ({
id: item.content.name, // Use name as ID
id: item.content.name,
name: item.content.name,
description: item.content.description || '',
tags: item.content.tags || []
@@ -175,7 +172,6 @@ function ProfilePage() {
const handleMassImport = async arr => {
try {
// Use the identifier to find the correct profile
const selectedProfiles = Array.from(selectedItems).map(identifier =>
profiles.find(p => p.content.name === identifier)
);
@@ -193,48 +189,61 @@ function ProfilePage() {
}
};
const getFilteredAndSortedProfiles = () => {
return profiles
.filter(profile => {
// Apply search filter
if (searchQuery) {
return (
profile.content.name
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
profile.content.tags?.some(tag =>
tag
.toLowerCase()
.includes(searchQuery.toLowerCase())
)
);
}
// Apply existing filters
if (filterType === 'tag') {
return (
profile.content.tags &&
profile.content.tags.includes(filterValue)
);
}
if (filterType === 'date') {
const profileDate = new Date(profile.modified_date);
const filterDate = new Date(filterValue);
return (
profileDate.toDateString() === filterDate.toDateString()
);
}
return true;
})
.sort((a, b) => {
switch (sortBy) {
case 'dateModified':
return (
new Date(b.modified_date) -
new Date(a.modified_date)
);
case 'dateCreated':
return (
new Date(b.created_date) - new Date(a.created_date)
);
case 'name':
default:
return a.content.name.localeCompare(b.content.name);
}
});
};
const formatDate = dateString => {
return new Date(dateString).toLocaleString();
};
const sortedAndFilteredProfiles = profiles
.filter(profile => {
// Apply search filter
if (searchQuery) {
return (
profile.content.name
.toLowerCase()
.includes(searchQuery.toLowerCase()) ||
profile.content.tags?.some(tag =>
tag.toLowerCase().includes(searchQuery.toLowerCase())
)
);
}
// Apply existing filters
if (filterType === 'tag') {
return (
profile.content.tags &&
profile.content.tags.includes(filterValue)
);
}
if (filterType === 'date') {
const profileDate = new Date(profile.modified_date);
const filterDate = new Date(filterValue);
return profileDate.toDateString() === filterDate.toDateString();
}
return true;
})
.sort((a, b) => {
if (sortBy === 'name')
return a.content.name.localeCompare(b.content.name);
if (sortBy === 'dateCreated')
return new Date(b.created_date) - new Date(a.created_date);
if (sortBy === 'dateModified')
return new Date(b.modified_date) - new Date(a.modified_date);
return 0;
});
if (isLoading) {
return (
<div className='flex flex-col items-center justify-center h-screen'>
@@ -278,42 +287,28 @@ function ProfilePage() {
);
}
const filteredProfiles = getFilteredAndSortedProfiles();
return (
<div>
<div className='mb-4 flex items-center gap-4'>
<SearchBar
onSearch={setSearchQuery}
placeholder='Search by name or tag...'
/>
<div className='flex-none'>
<SortMenu sortBy={sortBy} setSortBy={setSortBy} />
</div>
<div className='flex-none'>
<FilterMenu
filterType={filterType}
setFilterType={setFilterType}
filterValue={filterValue}
setFilterValue={setFilterValue}
allTags={allTags}
/>
</div>
<div className='flex-none'>
<button
onClick={toggleSelectionMode}
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'
}`}
title='Toggle selection mode (Ctrl+A)'>
<CheckSquare className='w-4 h-4' />
<span className='text-sm'>Select</span>
</button>
</div>
</div>
<DataBar
onSearch={setSearchQuery}
searchPlaceholder='Search by name or tag...'
filterType={filterType}
setFilterType={setFilterType}
filterValue={filterValue}
setFilterValue={setFilterValue}
allTags={allTags}
sortBy={sortBy}
setSortBy={setSortBy}
isSelectionMode={isSelectionMode}
toggleSelectionMode={toggleSelectionMode}
onAdd={() => handleOpenModal()}
addButtonLabel='Add New Profile'
/>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4'>
{sortedAndFilteredProfiles.map((profile, index) => (
{filteredProfiles.map((profile, index) => (
<ProfileCard
key={profile.file_name}
profile={profile}
@@ -328,22 +323,13 @@ function ProfilePage() {
profile.content.name,
index,
e,
sortedAndFilteredProfiles
filteredProfiles
)
}
/>
))}
</div>
{!isSelectionMode && (
<AddButton
onClick={() => handleOpenModal()}
label='Add New Profile'
top='5vh'
left='75vw'
/>
)}
{isSelectionMode && (
<MassActionsBar
selectedCount={selectedItems.size}

View File

@@ -1,7 +1,7 @@
import React, {useState} from 'react';
import PropTypes from 'prop-types';
import {Search} from 'lucide-react';
import {SortDropdown} from '../ui/SortDropdown';
import {SortDropdown} from '../ui/DataBar/SortDropdown';
import TabViewer from '../ui/TabViewer';
const ProfileScoringTab = ({

View File

@@ -1,23 +1,19 @@
import React, {useState, useEffect} from 'react';
import RegexCard from './RegexCard';
import RegexModal from './RegexModal';
import AddNewCard from '../ui/AddNewCard';
import {RegexPatterns} from '@api/data';
import FilterMenu from '../ui/FilterMenu';
import SortMenu from '../ui/SortMenu';
import {Loader, CheckSquare} from 'lucide-react';
import {Loader} from 'lucide-react';
import Alert from '@ui/Alert';
import SearchBar from '@ui/SearchBar';
import AddButton from '@ui/AddButton';
import {useMassSelection} from '@hooks/useMassSelection';
import {useKeyboardShortcut} from '@hooks/useKeyboardShortcut';
import MassActionsBar from '@ui/MassActionsBar';
import DataBar from '@ui/DataBar/DataBar';
function RegexPage() {
const [patterns, setPatterns] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedPattern, setSelectedPattern] = useState(null);
const [sortBy, setSortBy] = useState('title');
const [sortBy, setSortBy] = useState('name');
const [filterType, setFilterType] = useState('none');
const [filterValue, setFilterValue] = useState('');
const [allTags, setAllTags] = useState([]);
@@ -37,7 +33,6 @@ function RegexPage() {
// Setup keyboard shortcut for selection mode (Ctrl+A)
useKeyboardShortcut('a', toggleSelectionMode, {ctrl: true});
// Load initial data
useEffect(() => {
loadPatterns();
}, []);
@@ -143,7 +138,6 @@ function RegexPage() {
);
}
// Rest of the sorting logic remains the same...
return filtered.sort((a, b) => {
switch (sortBy) {
case 'dateModified':
@@ -173,37 +167,21 @@ function RegexPage() {
return (
<div>
<div className='mb-4 flex items-center gap-4'>
<SearchBar
onSearch={setSearchQuery}
placeholder='Search by name or tag...'
/>
<div className='flex-none'>
<SortMenu sortBy={sortBy} setSortBy={setSortBy} />
</div>
<div className='flex-none'>
<FilterMenu
filterType={filterType}
setFilterType={setFilterType}
filterValue={filterValue}
setFilterValue={setFilterValue}
allTags={allTags}
/>
</div>
<div className='flex-none'>
<button
onClick={toggleSelectionMode}
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'
}`}
title='Toggle selection mode (Ctrl+A)'>
<CheckSquare className='w-4 h-4' />
<span className='text-sm'>Select</span>
</button>
</div>
</div>
<DataBar
onSearch={setSearchQuery}
searchPlaceholder='Search by name or tag...'
filterType={filterType}
setFilterType={setFilterType}
filterValue={filterValue}
setFilterValue={setFilterValue}
allTags={allTags}
sortBy={sortBy}
setSortBy={setSortBy}
isSelectionMode={isSelectionMode}
toggleSelectionMode={toggleSelectionMode}
onAdd={() => handleOpenModal()}
addButtonLabel='Add New Pattern'
/>
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4'>
{getFilteredAndSortedPatterns().map((pattern, index) => (
@@ -221,14 +199,6 @@ function RegexPage() {
}
/>
))}
{!isSelectionMode && (
<AddButton
onClick={() => handleOpenModal()}
label='Add New Pattern'
top='5vh'
left='75vw'
/>
)}
</div>
{isSelectionMode && (

View File

@@ -2,7 +2,7 @@ import React, {useState, useEffect} from 'react';
import {Loader} from 'lucide-react';
import ArrModal from './ArrModal';
import ArrCard from './ArrCard';
import AddButton from '@ui/AddButton';
import AddButton from '@ui/DataBar/AddButton';
import {getArrConfigs} from '@api/arr';
const ArrContainer = () => {

View File

@@ -1,42 +0,0 @@
import React from 'react';
import {Plus} from 'lucide-react';
const AddButton = ({onClick, label = 'Add New', top, right, bottom, left}) => {
const positionStyle = {
position: 'fixed',
zIndex: 50,
...(top !== undefined && {
top: typeof top === 'number' ? `${top}px` : top
}),
...(right !== undefined && {
right: typeof right === 'number' ? `${right}px` : right
}),
...(bottom !== undefined && {
bottom: typeof bottom === 'number' ? `${bottom}px` : bottom
}),
...(left !== undefined && {
left: typeof left === 'number' ? `${left}px` : left
})
};
return (
<div style={positionStyle}>
<div
onClick={onClick}
className='add-button-container flex items-center bg-gradient-to-br from-gray-800 to-gray-900
rounded-full border border-gray-700 shadow-xl hover:shadow-2xl
hover:border-blue-500/50 transition-all duration-300 cursor-pointer overflow-hidden'>
<div className='p-4 bg-blue-500/10 hover:bg-blue-500/20 transition-colors rounded-full'>
<Plus className='w-6 h-6 text-blue-400' />
</div>
<div className='add-button-label-wrapper'>
<span className='add-button-label font-medium text-gray-100 pl-5 pr-1 text-lg whitespace-nowrap'>
{label}
</span>
</div>
</div>
</div>
);
};
export default AddButton;

View File

@@ -0,0 +1,16 @@
import React from 'react';
import {Plus} from 'lucide-react';
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'
title={label}>
<Plus className='w-4 h-4' />
<span className='text-sm'>{label}</span>
</button>
);
};
export default AddButton;

View File

@@ -0,0 +1,102 @@
import React, {useState, useEffect} from 'react';
import SearchBar from './SearchBar';
import FilterMenu from './FilterMenu';
import {SortDropdown} from './SortDropdown';
import ToggleSelectButton from './ToggleSelectButton';
import AddButton from './AddButton';
const DataBar = ({
onSearch,
searchPlaceholder = 'Search by name or tag...',
filterType,
setFilterType,
filterValue,
setFilterValue,
allTags,
sortBy,
setSortBy,
isSelectionMode,
toggleSelectionMode,
onAdd,
addButtonLabel = 'Add New',
showAddButton = true
}) => {
const [isFloating, setIsFloating] = useState(false);
const [isVisible, setIsVisible] = useState(true);
const [lastScrollY, setLastScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => {
const currentScrollY = window.scrollY;
// Show/hide based on scroll direction
setIsVisible(currentScrollY <= 0 || currentScrollY < lastScrollY);
// Start floating once we scroll past the initial position
setIsFloating(currentScrollY > 64); // Approximately the height of the bar
setLastScrollY(currentScrollY);
};
window.addEventListener('scroll', handleScroll, {passive: true});
return () => window.removeEventListener('scroll', handleScroll);
}, [lastScrollY]);
return (
<div
className={`transition-all duration-300 ${
isFloating
? 'fixed top-0 left-0 right-0 z-50 px-6 py-4 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm shadow-md'
: 'relative w-full mb-4'
} ${isVisible ? 'translate-y-0' : '-translate-y-full'}`}>
<div className='flex items-center gap-4'>
<SearchBar
onSearch={onSearch}
placeholder={searchPlaceholder}
className='flex-1'
/>
<div className='flex-none'>
<SortDropdown
options={[
{key: 'name', label: 'Sort by Name'},
{key: 'dateCreated', label: 'Sort by Date Created'},
{
key: 'dateModified',
label: 'Sort by Date Modified'
}
]}
currentKey={sortBy}
currentDirection='desc'
onSort={key => setSortBy(key)}
/>
</div>
<div className='flex-none'>
<FilterMenu
filterType={filterType}
setFilterType={setFilterType}
filterValue={filterValue}
setFilterValue={setFilterValue}
allTags={allTags}
/>
</div>
<div className='flex-none'>
<ToggleSelectButton
isSelectionMode={isSelectionMode}
onClick={toggleSelectionMode}
/>
</div>
{showAddButton && !isSelectionMode && (
<div className='flex-none'>
<AddButton onClick={onAdd} label={addButtonLabel} />
</div>
)}
</div>
</div>
);
};
export default DataBar;

View File

@@ -0,0 +1,24 @@
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
}) => {
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'
}`}
title={`Toggle selection mode (Ctrl+${shortcutKey})`}>
<CheckSquare className='w-4 h-4' />
<span className='text-sm'>Select</span>
</button>
);
};
export default ToggleSelectButton;