ATC_SIMPLE/FRONTEND/src/components/DragTabs.tsx

373 lines
10 KiB
TypeScript

import {
ActionIcon,
Avatar,
Box,
CloseButton,
Flex,
Group,
Input,
Menu,
Tabs,
Text,
Tooltip,
UnstyledButton,
} from "@mantine/core";
import {
DndContext,
useSensor,
useSensors,
PointerSensor,
closestCenter,
type DragEndEvent,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
useSortable,
horizontalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { useEffect, useMemo, useState, type JSX } from "react";
import {
IconChevronRight,
IconEdit,
IconLogout,
IconSettingsPlus,
IconUsersGroup,
} from "@tabler/icons-react";
import classes from "./Component.module.css";
import type { TStation, TUser } from "../untils/types";
import type { Socket } from "socket.io-client";
interface DraggableTabsProps {
tabsData: TStation[];
panels: JSX.Element[];
storageKey?: string;
onChange: (activeTabId: string | null) => void;
w?: string | number;
isStationSettings?: boolean;
socket: Socket | null;
usersConnecting: TUser[];
setIsEditStation: (value: React.SetStateAction<boolean>) => void;
setIsOpenAddStation: (value: React.SetStateAction<boolean>) => void;
setStationEdit: (value: React.SetStateAction<TStation | undefined>) => void;
active: string;
setActive: (value: React.SetStateAction<string>) => void;
onSendCommand: (value: string) => void;
}
function SortableTab({
tab,
active,
onChange,
}: {
tab: TStation;
active: string | null;
onChange: (id: string) => void;
isStationSettings?: boolean;
}) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: tab.id.toString() });
return (
<Tabs.Tab
className={classes.tab}
ref={setNodeRef}
{...attributes}
{...listeners}
onPointerDown={(e) => {
listeners?.onPointerDown?.(e);
onChange(tab.id.toString());
}}
value={tab.id.toString()}
style={{
transform: CSS.Transform.toString(transform),
transition,
cursor: "grab",
userSelect: "none",
}}
color={active === tab.id.toString() ? "green" : ""}
fw={600}
fz="md"
c="#747474"
>
<Box className={classes.stationName}>
<Text fw={600} fz="md" className={classes.stationText}>
{tab.name}
</Text>
</Box>
</Tabs.Tab>
);
}
export default function DraggableTabs({
tabsData,
panels,
storageKey = "draggable-tabs-order",
onChange,
w,
isStationSettings = false,
socket,
usersConnecting,
setIsEditStation,
setIsOpenAddStation,
setStationEdit,
active,
setActive,
onSendCommand,
}: DraggableTabsProps) {
const user = useMemo(() => {
return localStorage.getItem("user") &&
typeof localStorage.getItem("user") === "string"
? JSON.parse(localStorage.getItem("user") || "")
: null;
}, []);
const [tabs, setTabs] = useState<TStation[]>(tabsData);
const [isChangeTab, setIsChangeTab] = useState<boolean>(false);
const [isSetActive, setIsSetActive] = useState<boolean>(false);
const [valueInput, setValueInput] = useState<string>("");
// const [active, setActive] = useState<string | null>(
// tabsData?.length > 0 ? tabsData[0]?.id.toString() : null
// );
const sensors = useSensors(useSensor(PointerSensor));
// Load saved order from localStorage
useEffect(() => {
if (isChangeTab) {
setTabs((pre) =>
pre
.map((t) => {
const updatedTab = tabsData.find((td) => td.id === t.id);
return updatedTab ? updatedTab : t;
})
.filter((t) => (tabsData.find((td) => td.id === t.id) ? true : false))
);
} else {
const saved = localStorage.getItem(storageKey);
let tabSelected =
tabsData?.length > 0 ? tabsData[0]?.id.toString() : null;
if (saved) {
try {
const order = JSON.parse(saved) as { id: string; index: number }[];
// Find the max index in saved order
const maxIndex = Math.max(...order.map((o) => o.index), 0);
const sorted = [...tabsData].sort((a, b) => {
const aOrder = order.find(
(o) => o.id.toString() === a.id.toString()
)?.index;
const bOrder = order.find(
(o) => o.id.toString() === b.id.toString()
)?.index;
// If not found, assign index after all existing ones
const aIndex = aOrder !== undefined ? aOrder : maxIndex + 1;
const bIndex = bOrder !== undefined ? bOrder : maxIndex + 1;
return aIndex - bIndex;
});
tabSelected = sorted?.length > 0 ? sorted[0]?.id.toString() : null;
setTabs(sorted);
} catch {
setTabs(tabsData);
}
} else {
setTabs(tabsData);
}
if (!isSetActive && tabSelected) {
setActive(tabSelected);
setTimeout(() => {
onChange(tabSelected);
}, 100);
setIsSetActive(true);
}
}
}, [tabsData, storageKey]);
// Handle reorder
const handleDragEnd = (event: DragEndEvent) => {
const { active: dragActive, over } = event;
if (dragActive.id !== over?.id && over?.id) {
const oldIndex = tabs.findIndex(
(t) => t.id.toString() === dragActive.id.toString()
);
const newIndex = tabs.findIndex(
(t) => t.id.toString() === over?.id.toString()
);
const newTabs = arrayMove(tabs, oldIndex, newIndex);
setTabs(newTabs);
const order = newTabs.map((t, i) => ({ id: t.id, index: i }));
localStorage.setItem(storageKey, JSON.stringify(order));
}
};
// Clean up
useEffect(() => {
return () => {
setIsChangeTab(false);
setIsSetActive(false);
setTabs([]);
setActive("0");
};
}, []);
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<Tabs
value={active}
onChange={(val) => {
setIsChangeTab(true);
onChange(val);
setActive(val || "0");
}}
w={w}
>
<Flex justify={"space-between"}>
<Flex style={{ width: "300px" }} align={"center"}>
<Input
style={{
width: "300px",
boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)",
}}
placeholder={"Chat to Port/All"}
value={valueInput}
onChange={(event) => {
const newValue = event.currentTarget.value;
setValueInput(newValue);
}}
onKeyDown={(event) => {
if (event.key === "Enter") {
onSendCommand(valueInput);
setValueInput("");
}
}}
rightSectionPointerEvents="all"
rightSection={
<CloseButton
aria-label="Clear input"
onClick={() => setValueInput("")}
style={{ display: valueInput ? undefined : "none" }}
/>
}
/>
</Flex>
<Tabs.List className={classes.list}>
<SortableContext
items={tabs}
strategy={horizontalListSortingStrategy}
>
{tabs.map((tab) => (
<SortableTab
key={tab.id}
tab={tab}
active={active}
onChange={(id) => {
setIsChangeTab(true);
onChange(id);
setActive(id);
}}
isStationSettings={isStationSettings}
/>
))}
</SortableContext>
<Flex gap={"md"} ms={"xs"} align={"center"}>
{Number(active) ? (
<ActionIcon
title="Edit Station"
variant="outline"
onClick={() => {
setStationEdit(
tabsData.find((el) => el.id === Number(active))
);
setIsOpenAddStation(true);
setIsEditStation(true);
}}
>
<IconEdit />
</ActionIcon>
) : (
""
)}
<ActionIcon
title="Add Station"
variant="outline"
color="green"
onClick={() => {
setIsOpenAddStation(true);
setIsEditStation(false);
setStationEdit(undefined);
}}
>
<IconSettingsPlus />
</ActionIcon>
</Flex>
</Tabs.List>
<Flex align={"center"}>
<Tooltip
withArrow
label={usersConnecting.map((el) => (
<Text key={el.userId}>{el.userName}</Text>
))}
>
<Avatar radius="xl" me={"sm"}>
<IconUsersGroup color="green" />
</Avatar>
</Tooltip>
<Menu withArrow>
<Menu.Target>
<UnstyledButton
style={{
padding: "var(--mantine-spacing-md)",
color: "var(--mantine-color-text)",
borderRadius: "var(--mantine-radius-sm)",
}}
>
<Group>
<div style={{ flex: 1 }}>
<Text size="sm" fw={500}>
{user?.userName || user?.user_name || ""}
</Text>
<Text c="dimmed" size="xs">
{user?.email}
</Text>
</div>
<IconChevronRight size={16} />
</Group>
</UnstyledButton>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
style={{ width: "150px" }}
onClick={() => {
localStorage.removeItem("user");
window.location.href = "/";
socket?.disconnect();
}}
color="red"
leftSection={<IconLogout size={16} stroke={1.5} />}
>
Logout
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Flex>
</Flex>
{panels}
</Tabs>
</DndContext>
);
}