Files
Rasadyar_Inspection_System/src/components/Tab/Tab.tsx
2026-01-26 10:14:10 +03:30

244 lines
6.8 KiB
TypeScript

import React, { useEffect, useMemo } from "react";
import { motion } from "framer-motion";
import { checkIsMobile } from "../../utils/checkIsMobile";
import { useTabStore } from "../../context/zustand-store/appStore";
type Tab = {
label: string;
visible?: boolean;
disabled?: boolean;
path?: string;
};
type TabsProps = {
tabs: Tab[];
onChange: (index: number) => void;
size?: "small" | "medium" | "large";
tabKey?: string;
};
const Tabs: React.FC<TabsProps> = ({
tabs,
onChange,
size = "medium",
tabKey = "default",
}) => {
const isMobile = checkIsMobile();
const { tabState, setActiveTab } = useTabStore();
const isTabVisible = (tab: Tab): boolean => {
if (tab.visible === false) return false;
return tab.visible === undefined || tab.visible === true;
};
const visibleTabs = useMemo(() => tabs.filter(isTabVisible), [tabs]);
const hasSingleVisibleTab = visibleTabs.length === 1;
const getFirstVisibleTabIndex = () => {
return tabs.findIndex(isTabVisible);
};
const isValidTabIndex = (index: number) => {
return index >= 0 && index < tabs.length && isTabVisible(tabs[index]);
};
const storedTab = tabState.activeTabs?.[tabKey];
const storedIndex = storedTab?.index ?? 0;
const firstVisibleIndex = getFirstVisibleTabIndex();
const selectedIndex = isValidTabIndex(storedIndex)
? storedIndex
: firstVisibleIndex !== -1
? firstVisibleIndex
: 0;
useEffect(() => {
if (hasSingleVisibleTab) {
const singleTabIndex = tabs.findIndex(isTabVisible);
if (singleTabIndex !== -1 && singleTabIndex !== selectedIndex) {
setActiveTab({
index: singleTabIndex,
path: tabs[singleTabIndex]?.path || window.location.pathname,
label: tabs[singleTabIndex].label,
tabKey,
});
onChange(singleTabIndex);
}
}
}, [hasSingleVisibleTab, tabs, tabKey]);
useEffect(() => {
if (!isValidTabIndex(selectedIndex)) {
const firstVisible = getFirstVisibleTabIndex();
if (firstVisible !== -1) {
setActiveTab({
index: firstVisible,
path: tabs[firstVisible]?.path || window.location.pathname,
label: tabs[firstVisible].label,
tabKey,
});
onChange(firstVisible);
}
} else {
onChange(selectedIndex);
}
}, [tabs, selectedIndex, tabKey]);
useEffect(() => {
if (typeof window === "undefined") return;
const currentPath = window.location.pathname;
if (storedTab) {
const storedTabStillExists = tabs.some(
(tab) =>
tab.label === storedTab.label &&
(tab.path === storedTab.path || !tab.path) &&
isTabVisible(tab)
);
if (storedTabStillExists) {
const storedIndex = tabs.findIndex(
(tab) =>
tab.label === storedTab.label &&
(tab.path === storedTab.path || !tab.path) &&
isTabVisible(tab)
);
if (storedIndex !== -1 && storedIndex !== selectedIndex) {
setActiveTab({
index: storedIndex,
path: tabs[storedIndex]?.path || currentPath,
label: tabs[storedIndex].label,
tabKey,
});
onChange(storedIndex);
}
return;
}
}
const pathMatchIndex = tabs.findIndex(
(tab) => tab.path === currentPath && isTabVisible(tab)
);
if (pathMatchIndex !== -1) {
setActiveTab({
index: pathMatchIndex,
path: currentPath,
label: tabs[pathMatchIndex].label,
tabKey,
});
onChange(pathMatchIndex);
return;
}
if (!isValidTabIndex(selectedIndex)) {
const firstVisible = getFirstVisibleTabIndex();
if (firstVisible !== -1) {
setActiveTab({
index: firstVisible,
path: tabs[firstVisible]?.path || currentPath,
label: tabs[firstVisible].label,
tabKey,
});
onChange(firstVisible);
}
}
}, [tabs, tabKey, storedTab, selectedIndex]);
const handleTabClick = (index: number) => {
if (!tabs[index].disabled) {
setActiveTab({
index,
path: tabs[index]?.path || window.location.pathname,
label: tabs[index].label,
tabKey,
});
onChange(index);
}
};
const sizeClasses: Record<NonNullable<TabsProps["size"]>, string> = {
small: "text-xs py-1 px-2",
medium: "text-sm py-1.5 px-3",
large: "text-base py-2 px-4",
};
if (hasSingleVisibleTab) {
return null;
}
if (isMobile) {
return (
<div className="flex flex-wrap justify-center gap-0.5 w-full">
{tabs.map((tab, index) =>
!isTabVisible(tab) ? null : (
<button
key={index}
onClick={() => handleTabClick(index)}
disabled={tab.disabled}
className={`text-center rounded-lg px-2 py-1 transition-colors duration-200 text-[11px] font-medium
${
tab.disabled
? "text-red-300 dark:text-red-400 bg-gray-200 opacity-60 dark:bg-dark-600 cursor-not-allowed"
: selectedIndex === index
? "bg-primary-100 text-primary-700"
: "bg-white text-gray-700 dark:bg-gray-200"
}`}
>
{tab.label}
</button>
)
)}
</div>
);
}
return (
<div className="w-full flex justify-center select-none">
<div className="relative flex gap-1 border-b border-gray-200">
{tabs.map((tab, index) =>
!isTabVisible(tab) ? null : (
<button
key={index}
onClick={() => handleTabClick(index)}
disabled={tab.disabled}
className={`relative transition-colors duration-200 rounded-t-md focus:outline-none
${
tab.disabled
? "text-gray-400 cursor-not-allowed"
: "cursor-pointer"
}
${
window.location.pathname === "/"
? "text-xs py-1 px-2"
: sizeClasses[size]
}`}
>
<span
className={`${
selectedIndex === index
? "text-primary-600 font-semibold"
: "text-gray-600 dark:text-gray-300 hover:text-primary-800"
}`}
>
{tab.label}
</span>
{selectedIndex === index && !tab.disabled && (
<motion.div
layoutId={`tab-underline-${tabKey}`}
className="absolute -bottom-px left-0 right-0 h-[2px] bg-primary-600 rounded-t"
transition={{ type: "spring", stiffness: 400, damping: 30 }}
/>
)}
</button>
)
)}
</div>
</div>
);
};
export default Tabs;