mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: gantt sidebar and main content scroll sync
This commit is contained in:
parent
3ef0570f6a
commit
5ca137fb92
@ -1,4 +1,4 @@
|
|||||||
import { FC } from "react";
|
import { FC, useEffect, useRef } from "react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { useIssueDetail } from "hooks/store";
|
||||||
import { useChart } from "../hooks";
|
import { useChart } from "../hooks";
|
||||||
@ -12,7 +12,7 @@ import { IBlockUpdateData, IGanttBlock } from "../types";
|
|||||||
export type GanttChartBlocksProps = {
|
export type GanttChartBlocksProps = {
|
||||||
itemsContainerWidth: number;
|
itemsContainerWidth: number;
|
||||||
blocks: IGanttBlock[] | null;
|
blocks: IGanttBlock[] | null;
|
||||||
blockToRender: (data: any) => React.ReactNode;
|
blockToRender: (data: any, textDisplacement: number) => React.ReactNode;
|
||||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||||
enableBlockLeftResize: boolean;
|
enableBlockLeftResize: boolean;
|
||||||
enableBlockRightResize: boolean;
|
enableBlockRightResize: boolean;
|
||||||
@ -31,9 +31,12 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
|
|||||||
enableBlockMove,
|
enableBlockMove,
|
||||||
showAllBlocks,
|
showAllBlocks,
|
||||||
} = props;
|
} = props;
|
||||||
|
// refs
|
||||||
const { activeBlock, dispatch } = useChart();
|
const blocksContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
// store hooks
|
||||||
const { peekIssue } = useIssueDetail();
|
const { peekIssue } = useIssueDetail();
|
||||||
|
// chart hook
|
||||||
|
const { activeBlock, dispatch, scrollTop, updateScrollTop } = useChart();
|
||||||
|
|
||||||
// update the active block on hover
|
// update the active block on hover
|
||||||
const updateActiveBlock = (block: IGanttBlock | null) => {
|
const updateActiveBlock = (block: IGanttBlock | null) => {
|
||||||
@ -75,10 +78,28 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBlocksScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
|
||||||
|
updateScrollTop(e.currentTarget.scrollTop);
|
||||||
|
|
||||||
|
const sidebarScrollContainer = document.getElementById("gantt-sidebar-scroll-container") as HTMLDivElement;
|
||||||
|
if (!sidebarScrollContainer) return;
|
||||||
|
|
||||||
|
sidebarScrollContainer.scrollTop = e.currentTarget.scrollTop;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const blocksContainer = blocksContainerRef.current;
|
||||||
|
if (!blocksContainer) return;
|
||||||
|
|
||||||
|
blocksContainer.scrollTop = scrollTop;
|
||||||
|
}, [scrollTop]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={blocksContainerRef}
|
||||||
className="relative z-[5] mt-[72px] h-full overflow-hidden overflow-y-auto"
|
className="relative z-[5] mt-[72px] h-full overflow-hidden overflow-y-auto"
|
||||||
style={{ width: `${itemsContainerWidth}px` }}
|
style={{ width: `${itemsContainerWidth}px` }}
|
||||||
|
onScroll={handleBlocksScroll}
|
||||||
>
|
>
|
||||||
{blocks &&
|
{blocks &&
|
||||||
blocks.length > 0 &&
|
blocks.length > 0 &&
|
||||||
@ -91,26 +112,26 @@ export const GanttChartBlocks: FC<GanttChartBlocksProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`block-${block.id}`}
|
key={`block-${block.id}`}
|
||||||
className={cn(
|
className={cn("relative h-11", {
|
||||||
"h-11",
|
"rounded bg-custom-background-80": activeBlock?.id === block.id,
|
||||||
{ "rounded bg-custom-background-80": activeBlock?.id === block.id },
|
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
|
||||||
{
|
peekIssue?.issueId === block.data.id,
|
||||||
"rounded-l border border-r-0 border-custom-primary-70 hover:border-custom-primary-70":
|
})}
|
||||||
peekIssue?.issueId === block.data.id,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
onMouseEnter={() => updateActiveBlock(block)}
|
onMouseEnter={() => updateActiveBlock(block)}
|
||||||
onMouseLeave={() => updateActiveBlock(null)}
|
onMouseLeave={() => updateActiveBlock(null)}
|
||||||
>
|
>
|
||||||
{!isBlockVisibleOnChart && <ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />}
|
{isBlockVisibleOnChart ? (
|
||||||
<ChartDraggable
|
<ChartDraggable
|
||||||
block={block}
|
block={block}
|
||||||
blockToRender={blockToRender}
|
blockToRender={blockToRender}
|
||||||
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
|
handleBlock={(...args) => handleChartBlockPosition(block, ...args)}
|
||||||
enableBlockLeftResize={enableBlockLeftResize}
|
enableBlockLeftResize={enableBlockLeftResize}
|
||||||
enableBlockRightResize={enableBlockRightResize}
|
enableBlockRightResize={enableBlockRightResize}
|
||||||
enableBlockMove={enableBlockMove}
|
enableBlockMove={enableBlockMove}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<ChartAddBlock block={block} blockUpdateHandler={blockUpdateHandler} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useRef, useState } from "react";
|
||||||
// icons
|
// icons
|
||||||
// components
|
// components
|
||||||
import { GanttChartBlocks } from "components/gantt-chart";
|
import { GanttChartBlocks } from "components/gantt-chart";
|
||||||
@ -39,7 +39,7 @@ type ChartViewRootProps = {
|
|||||||
loaderTitle: string;
|
loaderTitle: string;
|
||||||
blocks: IGanttBlock[] | null;
|
blocks: IGanttBlock[] | null;
|
||||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||||
blockToRender: (data: any) => React.ReactNode;
|
blockToRender: (data: any, textDisplacement: number) => React.ReactNode;
|
||||||
sidebarToRender: (props: any) => React.ReactNode;
|
sidebarToRender: (props: any) => React.ReactNode;
|
||||||
enableBlockLeftResize: boolean;
|
enableBlockLeftResize: boolean;
|
||||||
enableBlockRightResize: boolean;
|
enableBlockRightResize: boolean;
|
||||||
@ -69,8 +69,11 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
|
|||||||
const [itemsContainerWidth, setItemsContainerWidth] = useState<number>(0);
|
const [itemsContainerWidth, setItemsContainerWidth] = useState<number>(0);
|
||||||
const [fullScreenMode, setFullScreenMode] = useState<boolean>(false);
|
const [fullScreenMode, setFullScreenMode] = useState<boolean>(false);
|
||||||
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null); // blocks state management starts
|
const [chartBlocks, setChartBlocks] = useState<IGanttBlock[] | null>(null); // blocks state management starts
|
||||||
|
// refs
|
||||||
|
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||||
// hooks
|
// hooks
|
||||||
const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft } = useChart();
|
const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft, scrollTop, updateScrollTop } =
|
||||||
|
useChart();
|
||||||
|
|
||||||
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
|
const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) =>
|
||||||
blocks && blocks.length > 0
|
blocks && blocks.length > 0
|
||||||
@ -202,17 +205,25 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
|
|||||||
|
|
||||||
const scrollWidth: number = scrollContainer?.scrollWidth;
|
const scrollWidth: number = scrollContainer?.scrollWidth;
|
||||||
const clientVisibleWidth: number = scrollContainer?.clientWidth;
|
const clientVisibleWidth: number = scrollContainer?.clientWidth;
|
||||||
const currentScrollPosition: number = scrollContainer?.scrollLeft;
|
const currentLeftScrollPosition: number = scrollContainer?.scrollLeft;
|
||||||
|
|
||||||
updateScrollLeft(currentScrollPosition);
|
updateScrollLeft(currentLeftScrollPosition);
|
||||||
|
|
||||||
const approxRangeLeft: number = scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth;
|
const approxRangeLeft: number = scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth;
|
||||||
const approxRangeRight: number = scrollWidth - (approxRangeLeft + clientVisibleWidth);
|
const approxRangeRight: number = scrollWidth - (approxRangeLeft + clientVisibleWidth);
|
||||||
|
|
||||||
if (currentScrollPosition >= approxRangeRight) updateCurrentViewRenderPayload("right", currentView);
|
if (currentLeftScrollPosition >= approxRangeRight) updateCurrentViewRenderPayload("right", currentView);
|
||||||
if (currentScrollPosition <= approxRangeLeft) updateCurrentViewRenderPayload("left", currentView);
|
if (currentLeftScrollPosition <= approxRangeLeft) updateCurrentViewRenderPayload("left", currentView);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onSidebarScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => updateScrollTop(e.currentTarget.scrollTop);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// const sidebarContainer = sidebarRef.current;
|
||||||
|
// if (!sidebarContainer) return;
|
||||||
|
// sidebarContainer.scrollTop = scrollTop;
|
||||||
|
// }, [scrollTop]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
@ -289,7 +300,14 @@ export const ChartViewRoot: FC<ChartViewRootProps> = (props) => {
|
|||||||
<h6>Duration</h6>
|
<h6>Duration</h6>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })}
|
<div
|
||||||
|
id="gantt-sidebar-scroll-container"
|
||||||
|
className="max-h-full mt-[12px] overflow-y-auto pl-2.5"
|
||||||
|
onScroll={onSidebarScroll}
|
||||||
|
ref={sidebarRef}
|
||||||
|
>
|
||||||
|
{sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="horizontal-scroll-enable relative flex h-full w-full flex-1 flex-col overflow-hidden overflow-x-auto"
|
className="horizontal-scroll-enable relative flex h-full w-full flex-1 flex-col overflow-hidden overflow-x-auto"
|
||||||
|
@ -24,6 +24,7 @@ const chartReducer = (state: ChartContextData, action: ChartContextActionPayload
|
|||||||
const initialView = "month";
|
const initialView = "month";
|
||||||
|
|
||||||
export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
// states;
|
||||||
const [state, dispatch] = useState<ChartContextData>({
|
const [state, dispatch] = useState<ChartContextData>({
|
||||||
currentView: initialView,
|
currentView: initialView,
|
||||||
currentViewData: currentViewDataWithView(initialView),
|
currentViewData: currentViewDataWithView(initialView),
|
||||||
@ -31,23 +32,23 @@ export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
allViews: allViewsWithData,
|
allViews: allViewsWithData,
|
||||||
activeBlock: null,
|
activeBlock: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [scrollLeft, setScrollLeft] = useState(0);
|
const [scrollLeft, setScrollLeft] = useState(0);
|
||||||
|
const [scrollTop, setScrollTop] = useState(0);
|
||||||
|
|
||||||
const handleDispatch = (action: ChartContextActionPayload): ChartContextData => {
|
const handleDispatch = (action: ChartContextActionPayload): ChartContextData => {
|
||||||
const newState = chartReducer(state, action);
|
const newState = chartReducer(state, action);
|
||||||
|
|
||||||
dispatch(() => newState);
|
dispatch(() => newState);
|
||||||
|
|
||||||
return newState;
|
return newState;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateScrollLeft = (scrollLeft: number) => {
|
const updateScrollLeft = (scrollLeft: number) => setScrollLeft(scrollLeft);
|
||||||
setScrollLeft(scrollLeft);
|
|
||||||
};
|
const updateScrollTop = (scrollTop: number) => setScrollTop(scrollTop);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContext.Provider value={{ ...state, scrollLeft, updateScrollLeft, dispatch: handleDispatch }}>
|
<ChartContext.Provider
|
||||||
|
value={{ ...state, scrollLeft, updateScrollLeft, scrollTop, updateScrollTop, dispatch: handleDispatch }}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</ChartContext.Provider>
|
</ChartContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -7,7 +7,7 @@ import { IGanttBlock } from "../types";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: IGanttBlock;
|
block: IGanttBlock;
|
||||||
blockToRender: (data: any) => React.ReactNode;
|
blockToRender: (data: any, textDisplacement: number) => React.ReactNode;
|
||||||
handleBlock: (totalBlockShifts: number, dragDirection: "left" | "right" | "move") => void;
|
handleBlock: (totalBlockShifts: number, dragDirection: "left" | "right" | "move") => void;
|
||||||
enableBlockLeftResize: boolean;
|
enableBlockLeftResize: boolean;
|
||||||
enableBlockRightResize: boolean;
|
enableBlockRightResize: boolean;
|
||||||
@ -223,6 +223,8 @@ export const ChartDraggable: React.FC<Props> = (props) => {
|
|||||||
scrollLeft > block.position.marginLeft + block.position.width;
|
scrollLeft > block.position.marginLeft + block.position.width;
|
||||||
const isBlockHiddenOnRight = posFromLeft && window && posFromLeft > window.innerWidth;
|
const isBlockHiddenOnRight = posFromLeft && window && posFromLeft > window.innerWidth;
|
||||||
|
|
||||||
|
const textDisplacement = scrollLeft - (block.position?.marginLeft ?? 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* move to left side hidden block button */}
|
{/* move to left side hidden block button */}
|
||||||
@ -272,7 +274,7 @@ export const ChartDraggable: React.FC<Props> = (props) => {
|
|||||||
className={`relative z-[2] flex h-8 w-full items-center rounded ${isMoving ? "pointer-events-none" : ""}`}
|
className={`relative z-[2] flex h-8 w-full items-center rounded ${isMoving ? "pointer-events-none" : ""}`}
|
||||||
onMouseDown={handleBlockMove}
|
onMouseDown={handleBlockMove}
|
||||||
>
|
>
|
||||||
{blockToRender(block.data)}
|
{blockToRender(block.data, textDisplacement)}
|
||||||
</div>
|
</div>
|
||||||
{/* right resize drag handle */}
|
{/* right resize drag handle */}
|
||||||
{enableBlockRightResize && (
|
{enableBlockRightResize && (
|
||||||
|
@ -12,7 +12,7 @@ type GanttChartRootProps = {
|
|||||||
loaderTitle: string;
|
loaderTitle: string;
|
||||||
blocks: IGanttBlock[] | null;
|
blocks: IGanttBlock[] | null;
|
||||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||||
blockToRender: (data: any) => React.ReactNode;
|
blockToRender: (data: any, textDisplacement: number) => React.ReactNode;
|
||||||
sidebarToRender: (props: any) => React.ReactNode;
|
sidebarToRender: (props: any) => React.ReactNode;
|
||||||
enableBlockLeftResize?: boolean;
|
enableBlockLeftResize?: boolean;
|
||||||
enableBlockRightResize?: boolean;
|
enableBlockRightResize?: boolean;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
|
import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd";
|
||||||
import { MoreVertical } from "lucide-react";
|
import { MoreVertical } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
@ -43,9 +42,6 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
|||||||
showAllBlocks = false,
|
showAllBlocks = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { cycleId } = router.query;
|
|
||||||
|
|
||||||
const { activeBlock, dispatch } = useChart();
|
const { activeBlock, dispatch } = useChart();
|
||||||
const { peekIssue } = useIssueDetail();
|
const { peekIssue } = useIssueDetail();
|
||||||
|
|
||||||
@ -105,12 +101,7 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
|||||||
<DragDropContext onDragEnd={handleOrderChange}>
|
<DragDropContext onDragEnd={handleOrderChange}>
|
||||||
<Droppable droppableId="gantt-sidebar">
|
<Droppable droppableId="gantt-sidebar">
|
||||||
{(droppableProvided) => (
|
{(droppableProvided) => (
|
||||||
<div
|
<div id={`gantt-sidebar-${viewId}`} ref={droppableProvided.innerRef} {...droppableProvided.droppableProps}>
|
||||||
id={`gantt-sidebar-${cycleId}`}
|
|
||||||
className="mt-[12px] max-h-full overflow-y-auto pl-2.5"
|
|
||||||
ref={droppableProvided.innerRef}
|
|
||||||
{...droppableProvided.droppableProps}
|
|
||||||
>
|
|
||||||
<>
|
<>
|
||||||
{blocks ? (
|
{blocks ? (
|
||||||
blocks.map((block, index) => {
|
blocks.map((block, index) => {
|
||||||
|
@ -54,6 +54,8 @@ export type ChartContextActionPayload =
|
|||||||
export interface ChartContextReducer extends ChartContextData {
|
export interface ChartContextReducer extends ChartContextData {
|
||||||
scrollLeft: number;
|
scrollLeft: number;
|
||||||
updateScrollLeft: (scrollLeft: number) => void;
|
updateScrollLeft: (scrollLeft: number) => void;
|
||||||
|
scrollTop: number;
|
||||||
|
updateScrollTop: (scrollTop: number) => void;
|
||||||
dispatch: (action: ChartContextActionPayload) => void;
|
dispatch: (action: ChartContextActionPayload) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +69,9 @@ export const BaseGanttRoot: React.FC<IBaseGanttRoot> = observer((props: IBaseGan
|
|||||||
loaderTitle="Issues"
|
loaderTitle="Issues"
|
||||||
blocks={issues ? renderIssueBlocksStructure(issues as TIssue[]) : null}
|
blocks={issues ? renderIssueBlocksStructure(issues as TIssue[]) : null}
|
||||||
blockUpdateHandler={updateIssueBlockStructure}
|
blockUpdateHandler={updateIssueBlockStructure}
|
||||||
blockToRender={(data: TIssue) => <IssueGanttBlock data={data} />}
|
blockToRender={(data: TIssue, textDisplacement) => (
|
||||||
|
<IssueGanttBlock data={data} textDisplacement={textDisplacement} />
|
||||||
|
)}
|
||||||
sidebarToRender={(props) => (
|
sidebarToRender={(props) => (
|
||||||
<IssueGanttSidebar
|
<IssueGanttSidebar
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
|
// hooks
|
||||||
|
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui";
|
import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
|
|
||||||
|
|
||||||
export const IssueGanttBlock = ({ data }: { data: TIssue }) => {
|
export const IssueGanttBlock = ({ data, textDisplacement }: { data: TIssue; textDisplacement: number }) => {
|
||||||
// hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
router: { workspaceSlug },
|
router: { workspaceSlug },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
@ -43,7 +44,15 @@ export const IssueGanttBlock = ({ data }: { data: TIssue }) => {
|
|||||||
}
|
}
|
||||||
position="top-left"
|
position="top-left"
|
||||||
>
|
>
|
||||||
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100">{data?.name}</div>
|
<div className="relative w-full truncate px-2.5 py-1 text-sm text-custom-text-100 overflow-hidden">
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
...(textDisplacement > 0 ? { paddingLeft: `${textDisplacement}px` } : {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data?.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user