feat(tables): implement sortable columns across various table components
- Added TableSortProvider to manage sorting state in AdminsTable, CmsTable, CompaniesTable, CustomersTable, TendersTable, and CompanyCategoriesTable. - Enhanced TableHead component to support sortable columns with visual indicators for sort direction. - Introduced columnSortKeys and columnDefaultOrder mappings in respective table presenters to define sortable fields and default sorting behavior. - Updated useCompanyListPresenter, useCmsTablePresenter, useCustomerListPresenter, useTenderListPresenter, and useCompanyCategoriesPresenter to handle sorting logic and state management. - Improved user experience by allowing dynamic sorting of table data based on user interactions.
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
import gsap from "gsap";
|
||||
import { useCallback, useContext, useLayoutEffect, useRef } from "react";
|
||||
import { SortDirection, TableSortContext } from "./tableSortContext";
|
||||
|
||||
const prefersReducedMotion = () =>
|
||||
typeof window !== "undefined" &&
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
/**
|
||||
* Presenter for a single sortable column header.
|
||||
*
|
||||
* Owns the interaction logic (which direction to request next) and the GSAP
|
||||
* choreography for the animated sort indicator, keeping the `table.tsx` view
|
||||
* free of side effects per the architecture guidelines.
|
||||
*/
|
||||
export const useSortableHeadPresenter = (
|
||||
sortKey: string,
|
||||
defaultSortOrder: SortDirection = "desc",
|
||||
) => {
|
||||
const {
|
||||
sortKey: activeKey,
|
||||
sortOrder,
|
||||
onSortChange,
|
||||
} = useContext(TableSortContext);
|
||||
|
||||
const isActive = activeKey === sortKey;
|
||||
const currentOrder: SortDirection = isActive
|
||||
? sortOrder ?? defaultSortOrder
|
||||
: defaultSortOrder;
|
||||
|
||||
const rootRef = useRef<HTMLButtonElement>(null);
|
||||
const pillRef = useRef<HTMLSpanElement>(null);
|
||||
const upRef = useRef<HTMLSpanElement>(null);
|
||||
const downRef = useRef<HTMLSpanElement>(null);
|
||||
const sweepRef = useRef<HTMLSpanElement>(null);
|
||||
const didMount = useRef(false);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!onSortChange) return;
|
||||
const nextOrder: SortDirection = isActive
|
||||
? currentOrder === "asc"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: defaultSortOrder;
|
||||
onSortChange(sortKey, nextOrder);
|
||||
}, [onSortChange, isActive, currentOrder, defaultSortOrder, sortKey]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const reduced = prefersReducedMotion();
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
const up = upRef.current;
|
||||
const down = downRef.current;
|
||||
const pill = pillRef.current;
|
||||
const sweep = sweepRef.current;
|
||||
if (!up || !down || !pill) return;
|
||||
|
||||
const activeArrow = currentOrder === "asc" ? up : down;
|
||||
const idleArrow = currentOrder === "asc" ? down : up;
|
||||
|
||||
const activeOpacity = isActive ? 1 : 0.45;
|
||||
const idleOpacity = isActive ? 0.2 : 0.45;
|
||||
|
||||
if (reduced) {
|
||||
gsap.set(idleArrow, { opacity: idleOpacity, y: 0, scale: 1 });
|
||||
gsap.set(activeArrow, { opacity: activeOpacity, y: 0, scale: 1 });
|
||||
return;
|
||||
}
|
||||
|
||||
// Resting state for whichever chevron is not the requested direction.
|
||||
gsap.to(idleArrow, {
|
||||
opacity: idleOpacity,
|
||||
scale: 0.85,
|
||||
y: 0,
|
||||
duration: 0.3,
|
||||
ease: "power2.out",
|
||||
});
|
||||
|
||||
// First paint: settle without the celebratory burst.
|
||||
if (!didMount.current) {
|
||||
didMount.current = true;
|
||||
gsap.set(activeArrow, {
|
||||
opacity: activeOpacity,
|
||||
scale: isActive ? 1 : 0.85,
|
||||
y: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
gsap.to(activeArrow, {
|
||||
opacity: activeOpacity,
|
||||
scale: isActive ? 1 : 0.85,
|
||||
y: 0,
|
||||
duration: 0.35,
|
||||
ease: "power2.out",
|
||||
});
|
||||
|
||||
if (!isActive) return;
|
||||
|
||||
// Pop the pill and slide the chosen chevron in from its direction.
|
||||
gsap.fromTo(
|
||||
pill,
|
||||
{ scale: 0.78 },
|
||||
{ scale: 1, duration: 0.55, ease: "back.out(3.2)" },
|
||||
);
|
||||
gsap.fromTo(
|
||||
activeArrow,
|
||||
{ y: currentOrder === "asc" ? 7 : -7, scale: 0.4, opacity: 0 },
|
||||
{
|
||||
y: 0,
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: 0.55,
|
||||
ease: "back.out(2.6)",
|
||||
},
|
||||
);
|
||||
|
||||
// A light sweep races across the indicator on every change.
|
||||
if (sweep) {
|
||||
gsap.fromTo(
|
||||
sweep,
|
||||
{ xPercent: -160, opacity: 0.9 },
|
||||
{ xPercent: 160, opacity: 0, duration: 0.7, ease: "power2.inOut" },
|
||||
);
|
||||
}
|
||||
}, rootRef);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, [isActive, currentOrder]);
|
||||
|
||||
return {
|
||||
rootRef,
|
||||
pillRef,
|
||||
upRef,
|
||||
downRef,
|
||||
sweepRef,
|
||||
isActive,
|
||||
currentOrder,
|
||||
handleClick,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user