feat: scroll DataGrid buttons logic

This commit is contained in:
IlyaDoronin 2023-07-27 15:49:47 +03:00
parent 89d44b64fd
commit b1b267fdb7
6 changed files with 510 additions and 201 deletions

@ -1,6 +1,12 @@
import { Box } from "@mui/material"; import { useEffect, useRef, useState } from "react";
import { Box, useTheme, useMediaQuery } from "@mui/material";
import { DataGrid } from "@mui/x-data-grid"; import { DataGrid } from "@mui/x-data-grid";
import { scrollBlock } from "@root/utils/scrollBlock";
import forwardIcon from "@root/assets/icons/forward.svg";
import type { ChangeEvent } from "react";
import type { GridColDef } from "@mui/x-data-grid"; import type { GridColDef } from "@mui/x-data-grid";
const COLUMNS: GridColDef[] = [ const COLUMNS: GridColDef[] = [
@ -62,63 +68,174 @@ const ROWS = [
}, },
]; ];
export const PurchaseTab = () => ( export const PurchaseTab = () => {
<Box const [canScrollToRight, setCanScrollToRight] = useState<boolean>(true);
sx={{ const [canScrollToLeft, setCanScrollToLeft] = useState<boolean>(false);
height: "100%", const theme = useTheme();
padding: "0 25px", const smallScreen = useMediaQuery(theme.breakpoints.down(830));
"&:before": { const gridContainer = useRef<HTMLDivElement>(null);
content: '""',
height: "50px", useEffect(() => {
width: "100%", const handleScroll = (nativeEvent: unknown) => {
position: "absolute", const { target } = nativeEvent as ChangeEvent<HTMLDivElement>;
left: "0",
top: "0", if (target.scrollLeft > 0) {
background: "#F2F3F7", setCanScrollToLeft(true);
}, } else {
}} setCanScrollToLeft(false);
> }
<DataGrid
rows={ROWS} if (target.clientWidth + target.scrollLeft >= target.scrollWidth - 1) {
columns={COLUMNS} setCanScrollToRight(false);
pageSize={5} } else {
rowsPerPageOptions={[5]} setCanScrollToRight(true);
hideFooter }
disableColumnMenu };
disableSelectionOnClick
rowHeight={50} const addScrollEvent = () => {
headerHeight={50} const grid = gridContainer.current?.querySelector(
experimentalFeatures={{ newEditingApi: true }} ".MuiDataGrid-virtualScroller"
);
if (grid) {
grid.addEventListener("scroll", handleScroll);
return;
}
setTimeout(addScrollEvent, 100);
};
addScrollEvent();
return () => {
const grid = gridContainer.current?.querySelector(
".MuiDataGrid-virtualScroller"
);
grid?.removeEventListener("scroll", handleScroll);
};
}, []);
const scrollDataGrid = (toStart = false) => {
const grid = gridContainer.current?.querySelector(
".MuiDataGrid-virtualScroller"
);
if (grid) {
scrollBlock(grid, { left: toStart ? 0 : grid.scrollWidth });
}
};
return (
<Box
ref={gridContainer}
sx={{ sx={{
borderRadius: "0", height: "100%",
border: "none", padding: "0 25px",
"& .MuiDataGrid-columnHeaders": { "&:before": {
content: '""',
height: "50px",
width: "100%",
position: "absolute",
left: "0",
top: "0",
background: "#F2F3F7", background: "#F2F3F7",
borderBottom: "none",
},
"& .MuiDataGrid-main .MuiDataGrid-columnHeader": {
outline: "none",
},
"& .MuiDataGrid-columnHeaderTitle": {
fontWeight: "bold",
userSelect: "none",
},
"& .MuiDataGrid-virtualScrollerRenderZone": {
width: "100%",
},
"& .MuiDataGrid-iconSeparator": { display: "none" },
"& .MuiDataGrid-main .MuiDataGrid-cell": {
outline: "none",
border: "none",
justifyContent: "flex-start",
},
"& .MuiDataGrid-row": {
width: "100%",
"&:hover": {
background: "#F2F3F7",
},
}, },
}} }}
/> >
</Box> <DataGrid
); rows={ROWS}
columns={COLUMNS}
pageSize={5}
rowsPerPageOptions={[5]}
hideFooter
disableColumnMenu
disableSelectionOnClick
rowHeight={50}
headerHeight={50}
experimentalFeatures={{ newEditingApi: true }}
sx={{
borderRadius: "0",
border: "none",
"& .MuiDataGrid-columnHeaders": {
background: "#F2F3F7",
borderBottom: "none",
},
"& .MuiDataGrid-main .MuiDataGrid-columnHeader": {
outline: "none",
},
"& .MuiDataGrid-columnHeaderTitle": {
fontWeight: "bold",
userSelect: "none",
},
"& .MuiDataGrid-virtualScrollerRenderZone": {
width: "100%",
},
"& .MuiDataGrid-iconSeparator": { display: "none" },
"& .MuiDataGrid-main .MuiDataGrid-cell": {
outline: "none",
border: "none",
justifyContent: "flex-start",
},
"& .MuiDataGrid-row": {
width: "100%",
"&:hover": {
background: "#F2F3F7",
},
},
}}
/>
{smallScreen && (
<Box
sx={{
position: "absolute",
right: "15px",
bottom: "50px",
display: "flex",
columnGap: "5px",
}}
>
{canScrollToLeft && (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "30px",
height: "30px",
borderRadius: "8px",
border: "1px solid #7E2AEA",
background: "rgba(126, 42, 234, 0.07)",
}}
onClick={() => scrollDataGrid(true)}
>
<img
src={forwardIcon}
alt="forward"
style={{ transform: "rotate(180deg)" }}
/>
</Box>
)}
{canScrollToRight && (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "30px",
height: "30px",
borderRadius: "8px",
border: "1px solid #7E2AEA",
background: "rgba(126, 42, 234, 0.07)",
}}
onClick={() => scrollDataGrid(false)}
>
<img src={forwardIcon} alt="forward" />
</Box>
)}
</Box>
)}
</Box>
);
};

@ -1,6 +1,12 @@
import { useEffect, useRef, useState } from "react";
import { Typography, Box, useTheme, useMediaQuery } from "@mui/material";
import { DataGrid } from "@mui/x-data-grid"; import { DataGrid } from "@mui/x-data-grid";
import { Typography } from "@mui/material";
import { scrollBlock } from "@root/utils/scrollBlock";
import forwardIcon from "@root/assets/icons/forward.svg";
import type { ChangeEvent } from "react";
import type { GridColDef } from "@mui/x-data-grid"; import type { GridColDef } from "@mui/x-data-grid";
const COLUMNS: GridColDef[] = [ const COLUMNS: GridColDef[] = [
@ -86,49 +92,161 @@ const ROWS = [
}, },
]; ];
export const TransactionsTab = () => ( export const TransactionsTab = () => {
<DataGrid const [canScrollToRight, setCanScrollToRight] = useState<boolean>(true);
rows={ROWS} const [canScrollToLeft, setCanScrollToLeft] = useState<boolean>(false);
columns={COLUMNS} const theme = useTheme();
pageSize={5} const smallScreen = useMediaQuery(theme.breakpoints.down(1070));
rowsPerPageOptions={[5]} const gridContainer = useRef<HTMLDivElement>(null);
hideFooter
disableColumnMenu useEffect(() => {
disableSelectionOnClick const handleScroll = (nativeEvent: unknown) => {
rowHeight={50} const { target } = nativeEvent as ChangeEvent<HTMLDivElement>;
headerHeight={50}
experimentalFeatures={{ newEditingApi: true }} if (target.scrollLeft > 0) {
sx={{ setCanScrollToLeft(true);
borderRadius: "0", } else {
border: "none", setCanScrollToLeft(false);
"& .MuiDataGrid-columnHeaders": { }
padding: "0 25px",
background: "#F2F3F7", if (target.clientWidth + target.scrollLeft >= target.scrollWidth - 1) {
borderBottom: "none", setCanScrollToRight(false);
}, } else {
"& .MuiDataGrid-main .MuiDataGrid-columnHeader": { setCanScrollToRight(true);
outline: "none", }
}, };
"& .MuiDataGrid-columnHeaderTitle": {
fontWeight: "bold", const addScrollEvent = () => {
userSelect: "none", const grid = gridContainer.current?.querySelector(
}, ".MuiDataGrid-virtualScroller"
"& .MuiDataGrid-virtualScrollerRenderZone": { );
width: "100%",
}, if (grid) {
"& .MuiDataGrid-iconSeparator": { display: "none" }, grid.addEventListener("scroll", handleScroll);
"& .MuiDataGrid-main .MuiDataGrid-cell": {
outline: "none", return;
border: "none", }
justifyContent: "flex-start",
}, setTimeout(addScrollEvent, 100);
"& .MuiDataGrid-row": { };
padding: "0 25px",
width: "100%", addScrollEvent();
"&:hover": {
background: "#F2F3F7", return () => {
}, const grid = gridContainer.current?.querySelector(
}, ".MuiDataGrid-virtualScroller"
}} );
/>
); grid?.removeEventListener("scroll", handleScroll);
};
}, []);
const scrollDataGrid = (toStart = false) => {
const grid = gridContainer.current?.querySelector(
".MuiDataGrid-virtualScroller"
);
if (grid) {
scrollBlock(grid, { left: toStart ? 0 : grid.scrollWidth });
}
};
return (
<Box sx={{ height: "100%" }} ref={gridContainer}>
<DataGrid
rows={ROWS}
columns={COLUMNS}
pageSize={5}
rowsPerPageOptions={[5]}
hideFooter
disableColumnMenu
disableSelectionOnClick
rowHeight={50}
headerHeight={50}
experimentalFeatures={{ newEditingApi: true }}
sx={{
borderRadius: "0",
border: "none",
"& .MuiDataGrid-columnHeaders": {
padding: "0 25px",
background: "#F2F3F7",
borderBottom: "none",
},
"& .MuiDataGrid-main .MuiDataGrid-columnHeader": {
outline: "none",
},
"& .MuiDataGrid-columnHeaderTitle": {
fontWeight: "bold",
userSelect: "none",
},
"& .MuiDataGrid-virtualScrollerRenderZone": {
width: "100%",
},
"& .MuiDataGrid-iconSeparator": { display: "none" },
"& .MuiDataGrid-main .MuiDataGrid-cell": {
outline: "none",
border: "none",
justifyContent: "flex-start",
},
"& .MuiDataGrid-row": {
padding: "0 25px",
width: "100%",
"&:hover": {
background: "#F2F3F7",
},
},
}}
/>
{smallScreen && (
<Box
sx={{
position: "absolute",
right: "15px",
bottom: "50px",
display: "flex",
columnGap: "5px",
}}
>
{canScrollToLeft && (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "30px",
height: "30px",
borderRadius: "8px",
border: "1px solid #7E2AEA",
background: "rgba(126, 42, 234, 0.07)",
}}
onClick={() => scrollDataGrid(true)}
>
<img
src={forwardIcon}
alt="forward"
style={{ transform: "rotate(180deg)" }}
/>
</Box>
)}
{canScrollToRight && (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "30px",
height: "30px",
borderRadius: "8px",
border: "1px solid #7E2AEA",
background: "rgba(126, 42, 234, 0.07)",
}}
onClick={() => scrollDataGrid(false)}
>
<img src={forwardIcon} alt="forward" />
</Box>
)}
</Box>
)}
</Box>
);
};

@ -1,18 +1,6 @@
import { Box, Typography, useTheme, useMediaQuery } from "@mui/material"; import { Box, Typography, useTheme, useMediaQuery } from "@mui/material";
type UserTabProps = { export const UserTab = () => {
user: {
id: number;
registrationDate: string;
email: string;
phone: string;
type: string;
fullname: string;
walletBalance: string;
};
};
export const UserTab = ({ user }: UserTabProps) => {
const theme = useTheme(); const theme = useTheme();
const mobile = useMediaQuery(theme.breakpoints.down(700)); const mobile = useMediaQuery(theme.breakpoints.down(700));
@ -29,31 +17,31 @@ export const UserTab = ({ user }: UserTabProps) => {
<Typography sx={{ lineHeight: "20px" }}>ID</Typography> <Typography sx={{ lineHeight: "20px" }}>ID</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}> <Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>
{" "} {" "}
{user.id} 2810
</Typography> </Typography>
</Box> </Box>
<Box sx={{ marginBottom: "25px" }}> <Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>Дата регистрации</Typography> <Typography sx={{ lineHeight: "20px" }}>Дата регистрации</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}> <Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>
{user.registrationDate} 17.02.2023
</Typography> </Typography>
</Box> </Box>
<Box sx={{ marginBottom: "25px" }}> <Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>Email</Typography> <Typography sx={{ lineHeight: "20px" }}>Email</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}> <Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>
{user.email} emailexamle@gmail.com
</Typography> </Typography>
</Box> </Box>
<Box sx={{ marginBottom: "25px" }}> <Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>Телефон</Typography> <Typography sx={{ lineHeight: "20px" }}>Телефон</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}> <Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>
{user.phone} +7 123 456 78 90
</Typography> </Typography>
</Box> </Box>
<Box sx={{ marginBottom: "25px" }}> <Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>Тип:</Typography> <Typography sx={{ lineHeight: "20px" }}>Тип:</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}> <Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>
{user.type} НКО
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@ -61,7 +49,7 @@ export const UserTab = ({ user }: UserTabProps) => {
<Box sx={{ marginBottom: "25px" }}> <Box sx={{ marginBottom: "25px" }}>
<Typography sx={{ lineHeight: "20px" }}>ФИО:</Typography> <Typography sx={{ lineHeight: "20px" }}>ФИО:</Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}> <Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>
{user.fullname} Куликов Геннадий Викторович
</Typography> </Typography>
</Box> </Box>
<Box sx={{ marginBottom: "25px" }}> <Box sx={{ marginBottom: "25px" }}>
@ -69,7 +57,7 @@ export const UserTab = ({ user }: UserTabProps) => {
Внутренний кошелек Внутренний кошелек
</Typography> </Typography>
<Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}> <Typography sx={{ lineHeight: "20px", fontWeight: "bold" }}>
{user.walletBalance} 2 096 руб.
</Typography> </Typography>
</Box> </Box>
</Box> </Box>

@ -1,38 +1,155 @@
import { Box, Typography } from "@mui/material"; import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { Box, Typography, TextField, Button } from "@mui/material";
import type { File } from "./index"; import { authStore } from "@root/stores/auth";
type VerificationTabProps = { import type { ChangeEvent } from "react";
type File = {
name: "inn" | "rule" | "egrule" | "certificate";
url: string;
};
type Verification = {
_id: string;
accepted: boolean;
status: "org" | "nko";
updated_at: string;
comment: string;
files: File[]; files: File[];
}; };
export const VerificationTab = ({ files }: VerificationTabProps) => ( type PatchVerificationBody = {
<Box sx={{ padding: "25px" }}> id: string;
{files.map(({ name, url }, index) => ( status: "org" | "nko";
<Box sx={{ marginBottom: "25px" }}> comment: string;
<Typography sx={{ fontWeight: "bold", fontSize: "18px" }}> accepted: boolean;
{index + 1}.{" "} };
{name === "inn"
? "Скан ИНН организации (выписка из ЕГЮРЛ)" const baseUrl =
: name === "rule" process.env.NODE_ENV === "production" ? "" : "https://hub.pena.digital";
? "Устав организации"
: name === "certificate" export const VerificationTab = () => {
? "Свидетельство о регистрации НКО" const [user, setUser] = useState<Verification | null>(null);
: `Скан документа ${index + 1}`} const [comment, setComment] = useState<string>("");
</Typography> const { userId } = useParams();
<Typography> const { makeRequest } = authStore();
<a
style={{ const requestVefification = async () =>
color: "#7E2AEA", makeRequest<never, Verification>({
textDecoration: "none", method: "get",
fontSize: "18px", url: baseUrl + `/verification/verification/${userId}`,
}} }).then((verification) => {
href={url} setUser(verification);
setComment(verification.comment);
});
useEffect(() => {
requestVefification();
}, []);
const verify = async (accepted: boolean) => {
if (!user) {
return;
}
try {
await makeRequest<PatchVerificationBody, never>({
method: "patch",
useToken: true,
url: baseUrl + `/verification/verification`,
body: {
accepted,
comment,
id: user._id,
status: user.status,
},
});
await requestVefification();
} catch {}
};
return (
<Box sx={{ padding: "25px" }}>
<Typography
sx={{
marginBottom: "10px",
fontWeight: "bold",
color: user?.accepted ? "#0D9F00" : "#E02C2C",
}}
>
{user?.accepted ? "Верификация пройдена" : "Не верифицирован"}
</Typography>
{user?.files?.map(({ name, url }, index) => (
<Box sx={{ marginBottom: "25px" }} key={name + url}>
<Typography sx={{ fontWeight: "bold", fontSize: "18px" }}>
{index + 1}.{" "}
{name === "inn"
? "Скан ИНН организации (выписка из ЕГЮРЛ)"
: name === "rule"
? "Устав организации"
: name === "certificate"
? "Свидетельство о регистрации НКО"
: `Скан документа ${index + 1}`}
</Typography>
<Typography>
<a
style={{
color: "#7E2AEA",
textDecoration: "none",
fontSize: "18px",
}}
href={url}
>
{url.split("/").pop()?.split(".")?.[0]}
</a>
</Typography>
</Box>
))}
{user?.comment && (
<Box sx={{ marginBottom: "15px" }}>
<Typography
component="span"
sx={{ fontWeight: "bold", marginBottom: "10px" }}
> >
{url.split("/").pop()?.split(".")?.[0]} Комментарий:
</a> </Typography>
</Typography> <Typography component="span"> {user.comment}</Typography>
</Box>
)}
<TextField
multiline
value={comment}
rows={4}
label="Комментарий"
type=""
sx={{
width: "100%",
maxWidth: "500px",
marginBottom: "10px",
}}
onChange={(event: ChangeEvent<HTMLTextAreaElement>) =>
setComment(event.target.value)
}
/>
<Box sx={{ display: "flex", columnGap: "10px" }}>
<Button
variant="text"
sx={{ background: "#9A9AAF" }}
onClick={() => verify(false)}
>
Отклонить
</Button>
<Button
variant="text"
sx={{ background: "#9A9AAF" }}
onClick={() => verify(true)}
>
Подтвердить
</Button>
</Box> </Box>
))} </Box>
</Box> );
); };

@ -1,5 +1,5 @@
import { useEffect, useState } from "react"; import { useState } from "react";
import { useLinkClickHandler, useParams } from "react-router-dom"; import { useLinkClickHandler } from "react-router-dom";
import { import {
Box, Box,
Modal, Modal,
@ -17,8 +17,6 @@ import { PurchaseTab } from "./PurchaseTab";
import { TransactionsTab } from "./TransactionsTab"; import { TransactionsTab } from "./TransactionsTab";
import { VerificationTab } from "./VerificationTab"; import { VerificationTab } from "./VerificationTab";
import { authStore } from "@root/stores/auth";
import userIcon from "@root/assets/icons/user.svg"; import userIcon from "@root/assets/icons/user.svg";
import packageIcon from "@root/assets/icons/package.svg"; import packageIcon from "@root/assets/icons/package.svg";
import transactionsIcon from "@root/assets/icons/transactions.svg"; import transactionsIcon from "@root/assets/icons/transactions.svg";
@ -34,40 +32,13 @@ const TABS = [
{ name: "Верификация", icon: checkIcon }, { name: "Верификация", icon: checkIcon },
]; ];
const baseUrl =
process.env.NODE_ENV === "production" ? "" : "https://hub.pena.digital";
export type File = {
name: "inn" | "rule" | "egrule" | "certificate";
url: string;
};
export type Verification = {
_id: string;
accepted: boolean;
status: "org" | "nko";
updated_at: string;
comment: string;
files: File[];
};
const ModalUser = () => { const ModalUser = () => {
const [user, setUser] = useState<Verification | null>(null);
const [value, setValue] = useState<number>(0); const [value, setValue] = useState<number>(0);
const [openNavigation, setOpenNavigation] = useState<boolean>(false); const [openNavigation, setOpenNavigation] = useState<boolean>(false);
const { userId } = useParams();
const { makeRequest } = authStore();
const theme = useTheme(); const theme = useTheme();
const tablet = useMediaQuery(theme.breakpoints.down(1070)); const tablet = useMediaQuery(theme.breakpoints.down(1070));
const mobile = useMediaQuery(theme.breakpoints.down(700)); const mobile = useMediaQuery(theme.breakpoints.down(700));
useEffect(() => {
makeRequest<never, Verification>({
method: "get",
url: baseUrl + `/verification/verification/${userId}`,
}).then(setUser);
}, []);
return ( return (
<> <>
<Modal <Modal
@ -98,7 +69,7 @@ const ModalUser = () => {
boxShadow: 24, boxShadow: 24,
borderRadius: mobile ? "0" : "12px", borderRadius: mobile ? "0" : "12px",
outline: "none", outline: "none",
overflow: "hidden", overflowX: "hidden",
}} }}
> >
<Typography <Typography
@ -192,22 +163,10 @@ const ModalUser = () => {
boxShadow: "inset 30px 0px 40px 0px rgba(210, 208, 225, 0.2)", boxShadow: "inset 30px 0px 40px 0px rgba(210, 208, 225, 0.2)",
}} }}
> >
{value === 0 && ( {value === 0 && <UserTab />}
<UserTab
user={{
id: 2810,
registrationDate: "17.02.2023",
email: "emailexamle@gmail.com",
phone: "+7 123 456 78 90",
type: "НКО",
fullname: "Куликов Геннадий Викторович",
walletBalance: "2 096 руб.",
}}
/>
)}
{value === 1 && <PurchaseTab />} {value === 1 && <PurchaseTab />}
{value === 2 && <TransactionsTab />} {value === 2 && <TransactionsTab />}
{value === 3 && <VerificationTab files={user?.files || []} />} {value === 3 && <VerificationTab />}
</Box> </Box>
</Box> </Box>
</Box> </Box>

10
src/utils/scrollBlock.ts Normal file

@ -0,0 +1,10 @@
type Coordinates = {
top?: number;
left?: number;
};
export const scrollBlock = (
block: Element,
coordinates: Coordinates,
smooth = true
) => block.scroll({ ...coordinates, behavior: smooth ? "smooth" : "auto" });