Add active-only filter for project items

This commit is contained in:
Jurgis Sakalauskas
2025-10-18 10:53:08 +03:00
parent 97010a35f6
commit 16b65c3ff2
2 changed files with 54 additions and 13 deletions
@@ -1,27 +1,35 @@
interface ProjectItemsHeaderProps { interface ProjectItemsHeaderProps {
itemCount: number; itemCount: number;
totalItemCount: number;
isLoading: boolean; isLoading: boolean;
showDifferencesOnly: boolean; showDifferencesOnly: boolean;
hasAttributeDifferences: boolean; hasAttributeDifferences: boolean;
onToggleDifferences: (value: boolean) => void; onToggleDifferences: (value: boolean) => void;
showLargeImages: boolean; showLargeImages: boolean;
onToggleLargeImages: (value: boolean) => void; onToggleLargeImages: (value: boolean) => void;
showActiveOnly: boolean;
onToggleActiveOnly: (value: boolean) => void;
} }
export function ProjectItemsHeader({ export function ProjectItemsHeader({
itemCount, itemCount,
totalItemCount,
isLoading, isLoading,
showDifferencesOnly, showDifferencesOnly,
hasAttributeDifferences, hasAttributeDifferences,
onToggleDifferences, onToggleDifferences,
showLargeImages, showLargeImages,
onToggleLargeImages onToggleLargeImages,
showActiveOnly,
onToggleActiveOnly
}: ProjectItemsHeaderProps) { }: ProjectItemsHeaderProps) {
const subtitle = isLoading const subtitle = isLoading
? 'Loading items…' ? 'Loading items…'
: itemCount : totalItemCount === 0
? `${itemCount} item${itemCount === 1 ? '' : 's'} tracked.` ? 'No items yet add your first one below.'
: 'No items yet add your first one below.'; : showActiveOnly && itemCount !== totalItemCount
? `${itemCount} of ${totalItemCount} active item${itemCount === 1 ? '' : 's'} shown.`
: `${totalItemCount} item${totalItemCount === 1 ? '' : 's'} tracked.`;
return ( return (
<header className="mb-6 flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> <header className="mb-6 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
@@ -30,6 +38,15 @@ export function ProjectItemsHeader({
<p className="text-sm text-slate-500">{subtitle}</p> <p className="text-sm text-slate-500">{subtitle}</p>
</div> </div>
<div className="flex flex-col gap-3 text-xs font-medium uppercase tracking-wide text-slate-500 md:flex-row md:items-center md:gap-6"> <div className="flex flex-col gap-3 text-xs font-medium uppercase tracking-wide text-slate-500 md:flex-row md:items-center md:gap-6">
<label className="flex items-center gap-2">
<input
type="checkbox"
className="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
checked={showActiveOnly}
onChange={(event) => onToggleActiveOnly(event.target.checked)}
/>
Active only
</label>
<label className="flex items-center gap-2"> <label className="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
@@ -42,11 +42,19 @@ export function ProjectItemsSection({
const [showDifferencesOnly, setShowDifferencesOnly] = useState(false); const [showDifferencesOnly, setShowDifferencesOnly] = useState(false);
const [expandedItemId, setExpandedItemId] = useState<string | null>(null); const [expandedItemId, setExpandedItemId] = useState<string | null>(null);
const [showLargeImages, setShowLargeImages] = useState(false); const [showLargeImages, setShowLargeImages] = useState(false);
const [showActiveOnly, setShowActiveOnly] = useState(false);
const displayItems = useMemo(() => {
if (!showActiveOnly) {
return items;
}
return items.filter((item) => item.status === 'active');
}, [items, showActiveOnly]);
const mismatchedAttributes = useMemo(() => { const mismatchedAttributes = useMemo(() => {
const result = new Set<string>(); const result = new Set<string>();
if (items.length <= 1 || projectAttributes.length === 0) { if (displayItems.length <= 1 || projectAttributes.length === 0) {
return result; return result;
} }
@@ -59,7 +67,7 @@ export function ProjectItemsSection({
let referenceValue: unknown = null; let referenceValue: unknown = null;
let hasReference = false; let hasReference = false;
for (const item of items) { for (const item of displayItems) {
const attributes = (item.attributes ?? {}) as Record<string, unknown>; const attributes = (item.attributes ?? {}) as Record<string, unknown>;
const currentValue = attributes[attribute]; const currentValue = attributes[attribute];
@@ -77,7 +85,7 @@ export function ProjectItemsSection({
}); });
return result; return result;
}, [items, projectAttributes]); }, [displayItems, projectAttributes]);
useEffect(() => { useEffect(() => {
if (showDifferencesOnly && mismatchedAttributes.size === 0) { if (showDifferencesOnly && mismatchedAttributes.size === 0) {
@@ -89,11 +97,11 @@ export function ProjectItemsSection({
if (!expandedItemId) { if (!expandedItemId) {
return; return;
} }
const itemStillPresent = items.some((item) => item.id === expandedItemId); const itemStillPresent = displayItems.some((item) => item.id === expandedItemId);
if (!itemStillPresent) { if (!itemStillPresent) {
setExpandedItemId(null); setExpandedItemId(null);
} }
}, [items, expandedItemId]); }, [displayItems, expandedItemId]);
const visibleAttributes = useMemo(() => { const visibleAttributes = useMemo(() => {
if (!showDifferencesOnly) { if (!showDifferencesOnly) {
@@ -105,7 +113,7 @@ export function ProjectItemsSection({
const hasAttributeDifferences = mismatchedAttributes.size > 0; const hasAttributeDifferences = mismatchedAttributes.size > 0;
const showNoDifferencesMessage = const showNoDifferencesMessage =
!hasAttributeDifferences && projectAttributes.length > 0 && items.length > 1; !hasAttributeDifferences && projectAttributes.length > 0 && displayItems.length > 1;
const handleTogglePrices = (itemId: string) => { const handleTogglePrices = (itemId: string) => {
setExpandedItemId((current) => (current === itemId ? null : itemId)); setExpandedItemId((current) => (current === itemId ? null : itemId));
@@ -115,13 +123,29 @@ export function ProjectItemsSection({
<> <>
<section className="rounded-2xl border border-slate-200 bg-white p-8 shadow-sm"> <section className="rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
<ProjectItemsHeader <ProjectItemsHeader
itemCount={items.length} itemCount={displayItems.length}
totalItemCount={items.length}
isLoading={isLoading} isLoading={isLoading}
showDifferencesOnly={showDifferencesOnly} showDifferencesOnly={showDifferencesOnly}
hasAttributeDifferences={hasAttributeDifferences} hasAttributeDifferences={hasAttributeDifferences}
onToggleDifferences={(value) => setShowDifferencesOnly(value)} onToggleDifferences={(value) => setShowDifferencesOnly(value)}
showLargeImages={showLargeImages} showLargeImages={showLargeImages}
onToggleLargeImages={(value) => setShowLargeImages(value)} onToggleLargeImages={(value) => setShowLargeImages(value)}
showActiveOnly={showActiveOnly}
onToggleActiveOnly={(value) => {
setShowActiveOnly(value);
if (value) {
setExpandedItemId((current) => {
if (!current) {
return current;
}
const stillVisible = items.some(
(item) => item.id === current && item.status === 'active'
);
return stillVisible ? current : null;
});
}
}}
/> />
{error ? ( {error ? (
@@ -135,7 +159,7 @@ export function ProjectItemsSection({
) : null} ) : null}
<ProjectItemsDesktopTable <ProjectItemsDesktopTable
items={items} items={displayItems}
visibleAttributes={visibleAttributes} visibleAttributes={visibleAttributes}
expandedItemId={expandedItemId} expandedItemId={expandedItemId}
onTogglePrices={handleTogglePrices} onTogglePrices={handleTogglePrices}
@@ -147,7 +171,7 @@ export function ProjectItemsSection({
/> />
<ProjectItemsMobileList <ProjectItemsMobileList
items={items} items={displayItems}
visibleAttributes={visibleAttributes} visibleAttributes={visibleAttributes}
expandedItemId={expandedItemId} expandedItemId={expandedItemId}
onTogglePrices={handleTogglePrices} onTogglePrices={handleTogglePrices}