46ddc018a6
- 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.
144 lines
3.8 KiB
TypeScript
144 lines
3.8 KiB
TypeScript
"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,
|
|
};
|
|
};
|