feat: create portal component added to view child in new window
This commit is contained in:
@@ -710,6 +710,27 @@ export const ExpandIcon = ({ isActive }: { isActive: boolean }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const ExpandIcon2 = () => {
|
||||
return (
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.78365 2H6.6001C6.46676 2 6.33342 2.08334 6.33342 2.21668V2.71672C6.33342 2.85006 6.45009 3.00007 6.6001 3.00007H7.91686C8.06687 3.00007 8.15021 3.16675 8.03353 3.26675L5.20001 6.10028C5.1 6.20028 5.1 6.3503 5.20001 6.4503L5.55003 6.80033C5.65004 6.90033 5.80005 6.90033 5.90005 6.80033L8.73358 3.9668C8.83359 3.86679 9.00026 3.93346 9.00026 4.08347V5.40023C9.00026 5.53357 9.1336 5.68358 9.26695 5.68358H9.75031C9.88366 5.68358 10.0003 5.53357 10.0003 5.40023V2.23335C10.0003 2.08334 9.91699 2 9.78365 2Z"
|
||||
fill="var(--text-color)"
|
||||
/>
|
||||
<path
|
||||
d="M7.71705 5.91777L7.15035 6.50115C7.05034 6.60115 7.00034 6.71783 7.00034 6.85117V8.7513C7.00034 8.88464 6.88366 9.00132 6.75032 9.00132H3.25008C3.11674 9.00132 3.00007 8.88464 3.00007 8.7513V5.25106C3.00007 5.11772 3.11674 5.00104 3.25008 5.00104H5.16688C5.30022 5.00104 5.43357 4.95104 5.5169 4.85103L6.08361 4.28433C6.18362 4.18432 6.11695 4.00098 5.96694 4.00098H2.66671C2.30002 4.00098 2 4.301 2 4.66769V9.33467C2 9.70136 2.30002 10.0014 2.66671 10.0014H7.33369C7.70039 10.0014 8.00041 9.70136 8.00041 9.33467V6.03445C8.00041 5.88444 7.81706 5.81777 7.71705 5.91777Z"
|
||||
fill="var(--text-color)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const StartIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
|
||||
182
app/src/components/templates/CreateNewWindow.tsx
Normal file
182
app/src/components/templates/CreateNewWindow.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import React, { ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
type NewWindowProps = {
|
||||
children: ReactNode;
|
||||
title?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
left?: number;
|
||||
top?: number;
|
||||
center?: boolean;
|
||||
features?: Partial<{
|
||||
toolbar: boolean;
|
||||
menubar: boolean;
|
||||
scrollbars: boolean;
|
||||
resizable: boolean;
|
||||
location: boolean;
|
||||
status: boolean;
|
||||
}>;
|
||||
onClose?: () => void;
|
||||
copyStyles?: boolean;
|
||||
noopener?: boolean;
|
||||
className?: string;
|
||||
theme?: string | null;
|
||||
};
|
||||
|
||||
export const RenderInNewWindow: React.FC<NewWindowProps> = ({
|
||||
children,
|
||||
title = "New Window",
|
||||
width = 900,
|
||||
height = 700,
|
||||
left,
|
||||
top,
|
||||
center = true,
|
||||
features,
|
||||
onClose,
|
||||
copyStyles = true,
|
||||
noopener = true,
|
||||
className,
|
||||
theme = "light",
|
||||
}) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const childWindowRef = useRef<Window | null>(null);
|
||||
const containerElRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const screenLeft = window.screenLeft ?? window.screenX ?? 0;
|
||||
const screenTop = window.screenTop ?? window.screenY ?? 0;
|
||||
const availWidth = window.outerWidth ?? window.innerWidth;
|
||||
const availHeight = window.outerHeight ?? window.innerHeight;
|
||||
|
||||
const finalLeft =
|
||||
center && availWidth
|
||||
? Math.max(0, screenLeft + (availWidth - width) / 2)
|
||||
: left ?? 100;
|
||||
|
||||
const finalTop =
|
||||
center && availHeight
|
||||
? Math.max(0, screenTop + (availHeight - height) / 2)
|
||||
: top ?? 100;
|
||||
|
||||
const baseFeatures = [
|
||||
`width=${Math.floor(width)}`,
|
||||
`height=${Math.floor(height)}`,
|
||||
`left=${Math.floor(finalLeft)}`,
|
||||
`top=${Math.floor(finalTop)}`,
|
||||
];
|
||||
|
||||
const featureFlags = features ?? {
|
||||
toolbar: false,
|
||||
menubar: false,
|
||||
scrollbars: true,
|
||||
resizable: true,
|
||||
location: false,
|
||||
status: false,
|
||||
};
|
||||
|
||||
Object.entries(featureFlags).forEach(([k, v]) =>
|
||||
baseFeatures.push(`${k}=${v ? "yes" : "no"}`)
|
||||
);
|
||||
|
||||
const newWin = window.open("", "_blank", baseFeatures.join(","));
|
||||
if (!newWin) {
|
||||
console.warn("Popup blocked or failed to open window.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (noopener) {
|
||||
try {
|
||||
newWin.opener = null;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
newWin.document.open();
|
||||
newWin.document.write(`<!doctype html>
|
||||
<html data-theme=${theme}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>${title}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
<body style="margin:0;"></body>
|
||||
</html>`);
|
||||
newWin.document.close();
|
||||
|
||||
if (copyStyles) {
|
||||
const head = newWin.document.head;
|
||||
Array.from(document.styleSheets).forEach((styleSheet) => {
|
||||
try {
|
||||
if ((styleSheet as CSSStyleSheet).cssRules) {
|
||||
const newStyleEl = newWin.document.createElement("style");
|
||||
const rules = Array.from(
|
||||
(styleSheet as CSSStyleSheet).cssRules
|
||||
).map((r) => r.cssText);
|
||||
newStyleEl.appendChild(
|
||||
newWin.document.createTextNode(rules.join("\n"))
|
||||
);
|
||||
head.appendChild(newStyleEl);
|
||||
}
|
||||
} catch {
|
||||
const ownerNode = styleSheet.ownerNode as HTMLElement | null;
|
||||
if (ownerNode && ownerNode.tagName === "LINK") {
|
||||
const link = ownerNode as HTMLLinkElement;
|
||||
const newLink = newWin.document.createElement("link");
|
||||
newLink.rel = link.rel;
|
||||
newLink.href = link.href;
|
||||
newLink.media = link.media;
|
||||
head.appendChild(newLink);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const container = newWin.document.createElement("div");
|
||||
if (className) container.className = className;
|
||||
newWin.document.body.appendChild(container);
|
||||
|
||||
newWin.document.title = title;
|
||||
|
||||
// Handle child window close
|
||||
const handleChildUnload = () => {
|
||||
onClose?.();
|
||||
};
|
||||
newWin.addEventListener("beforeunload", handleChildUnload);
|
||||
|
||||
// 👇 Handle parent refresh/close → auto close child
|
||||
const handleParentUnload = () => {
|
||||
try {
|
||||
newWin.close();
|
||||
} catch {}
|
||||
};
|
||||
window.addEventListener("beforeunload", handleParentUnload);
|
||||
|
||||
childWindowRef.current = newWin;
|
||||
containerElRef.current = container;
|
||||
setMounted(true);
|
||||
|
||||
return () => {
|
||||
newWin.removeEventListener("beforeunload", handleChildUnload);
|
||||
window.removeEventListener("beforeunload", handleParentUnload);
|
||||
try {
|
||||
newWin.close();
|
||||
} catch {}
|
||||
childWindowRef.current = null;
|
||||
containerElRef.current = null;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const w = childWindowRef.current;
|
||||
if (w && !w.closed) {
|
||||
w.document.title = title;
|
||||
}
|
||||
}, [title]);
|
||||
|
||||
if (!mounted || !containerElRef.current) return null;
|
||||
|
||||
return createPortal(children, containerElRef.current);
|
||||
};
|
||||
@@ -1,9 +1,80 @@
|
||||
// LogList.tsx
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { LogListIcon, CloseIcon } from "../../icons/ExportCommonIcons"; // Adjust path as needed
|
||||
import { useLogger } from "./LoggerContext";
|
||||
import {
|
||||
LogListIcon,
|
||||
CloseIcon,
|
||||
ExpandIcon2,
|
||||
} from "../../icons/ExportCommonIcons"; // Adjust path as needed
|
||||
import { LogEntry, useLogger } from "./LoggerContext";
|
||||
import { GetLogIcon } from "../../footer/getLogIcons";
|
||||
import { RenderInNewWindow } from "../../templates/CreateNewWindow";
|
||||
|
||||
// --- Logs Component ---
|
||||
type LogsProps = {
|
||||
selectedTab: "all" | "info" | "warning" | "error";
|
||||
setSelectedTab: (tab: "all" | "info" | "warning" | "error") => void;
|
||||
clear: () => void;
|
||||
filteredLogs: LogEntry[];
|
||||
formatTimestamp: (date: Date) => string;
|
||||
};
|
||||
|
||||
const Logs: React.FC<LogsProps> = ({
|
||||
selectedTab,
|
||||
setSelectedTab,
|
||||
clear,
|
||||
filteredLogs,
|
||||
formatTimestamp,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="log-nav-container">
|
||||
<div className="log-nav-wrapper">
|
||||
{["all", "info", "warning", "error"].map((type) => (
|
||||
<button
|
||||
id="log-type"
|
||||
title="log-type"
|
||||
key={type}
|
||||
className={`log-nav ${selectedTab === type ? "active" : ""}`}
|
||||
onClick={() => setSelectedTab(type as any)}
|
||||
>
|
||||
{`${type.charAt(0).toUpperCase() + type.slice(1)} Logs`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
id="clean-btn"
|
||||
title="clear-btn"
|
||||
className="clear-button"
|
||||
onClick={clear}
|
||||
>
|
||||
clear
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Log Entries */}
|
||||
<div className="log-entry-wrapper">
|
||||
{filteredLogs.length > 0 ? (
|
||||
filteredLogs.map((log) => (
|
||||
<div key={log.id} className={`log-entry ${log.type}`}>
|
||||
<div className="log-icon">{GetLogIcon(log.type)}</div>
|
||||
<div className="log-entry-message-container">
|
||||
<div className="log-entry-message">{log.message}</div>
|
||||
<div className="message-time">
|
||||
{formatTimestamp(log.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="no-log">
|
||||
There are no logs to display at the moment.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// --- LogList Component ---
|
||||
const LogList: React.FC = () => {
|
||||
const {
|
||||
logs,
|
||||
@@ -15,6 +86,7 @@ const LogList: React.FC = () => {
|
||||
} = useLogger();
|
||||
|
||||
const formatTimestamp = (date: Date) => new Date(date).toLocaleTimeString();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const filteredLogs =
|
||||
selectedTab === "all"
|
||||
@@ -25,92 +97,86 @@ const LogList: React.FC = () => {
|
||||
if (isLogListVisible && logs.length > 0) {
|
||||
const lastLog = logs[logs.length - 1];
|
||||
const validTypes = ["all", "info", "warning", "error"];
|
||||
|
||||
if (validTypes.includes(lastLog.type)) {
|
||||
setSelectedTab(lastLog.type);
|
||||
setSelectedTab(lastLog.type as any);
|
||||
} else {
|
||||
setSelectedTab("all");
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [isLogListVisible]);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line
|
||||
<div
|
||||
className="log-list-container"
|
||||
onClick={() => setIsLogListVisible(false)}
|
||||
>
|
||||
{/* eslint-disable-next-line */}
|
||||
<div
|
||||
className="log-list-wrapper"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="log-header">
|
||||
<div className="log-header-wrapper">
|
||||
<div className="icon">
|
||||
<LogListIcon />
|
||||
</div>
|
||||
<div className="head">Log List</div>
|
||||
</div>
|
||||
<button
|
||||
id="close-btn"
|
||||
title="close-btn"
|
||||
className="close"
|
||||
onClick={() => setIsLogListVisible(false)}
|
||||
<>
|
||||
{!open ? (
|
||||
<div
|
||||
className="log-list-container"
|
||||
onClick={() => setIsLogListVisible(false)}
|
||||
>
|
||||
{/* eslint-disable-next-line */}
|
||||
<div
|
||||
className="log-list-wrapper"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="log-nav-container">
|
||||
<div className="log-nav-wrapper">
|
||||
{["all", "info", "warning", "error"].map((type) => (
|
||||
<button
|
||||
id="log-type"
|
||||
title="log-type"
|
||||
key={type}
|
||||
className={`log-nav ${selectedTab === type ? "active" : ""}`}
|
||||
onClick={() => setSelectedTab(type as any)}
|
||||
>
|
||||
{`${type.charAt(0).toUpperCase() + type.slice(1)} Logs`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
id="clean-btn"
|
||||
title="clear-btn"
|
||||
className="clear-button"
|
||||
onClick={clear}
|
||||
>
|
||||
clear
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Log Entries */}
|
||||
<div className="log-entry-wrapper">
|
||||
{filteredLogs.length > 0 ? (
|
||||
filteredLogs.map((log) => (
|
||||
<div key={log.id} className={`log-entry ${log.type}`}>
|
||||
<div className="log-icon">{GetLogIcon(log.type)}</div>
|
||||
<div className="log-entry-message-container">
|
||||
<div className="log-entry-message">{log.message}</div>
|
||||
<div className="message-time">
|
||||
{formatTimestamp(log.timestamp)}
|
||||
</div>
|
||||
<div className="log-header">
|
||||
<div className="log-header-wrapper">
|
||||
<div className="icon">
|
||||
<LogListIcon />
|
||||
</div>
|
||||
<div className="head">Log List</div>
|
||||
</div>
|
||||
<div className="action-buttons-container">
|
||||
<button
|
||||
id="expand-log-btn"
|
||||
title="open in new tab"
|
||||
className="expand-btn"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
<ExpandIcon2 />
|
||||
</button>
|
||||
<button
|
||||
id="close-btn"
|
||||
title="close"
|
||||
className="close"
|
||||
onClick={() => setIsLogListVisible(false)}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="no-log">
|
||||
There are no logs to display at the moment.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Logs Section */}
|
||||
<Logs
|
||||
selectedTab={selectedTab as any}
|
||||
setSelectedTab={setSelectedTab as any}
|
||||
clear={clear}
|
||||
filteredLogs={filteredLogs}
|
||||
formatTimestamp={formatTimestamp}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<RenderInNewWindow
|
||||
title="Log list"
|
||||
theme={localStorage.getItem("theme")}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<div className="log-list-new-window-wrapper">
|
||||
<Logs
|
||||
selectedTab={selectedTab as any}
|
||||
setSelectedTab={setSelectedTab as any}
|
||||
clear={clear}
|
||||
filteredLogs={filteredLogs}
|
||||
formatTimestamp={formatTimestamp}
|
||||
/>
|
||||
</div>
|
||||
</RenderInNewWindow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,86 @@
|
||||
@use "../../abstracts/variables.scss" as *;
|
||||
@use "../../abstracts/mixins.scss" as *;
|
||||
|
||||
.log-nav-container {
|
||||
@include flex-space-between;
|
||||
align-items: flex-end;
|
||||
.log-nav-wrapper {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
|
||||
.log-nav {
|
||||
padding: 8px 16px;
|
||||
border-radius: #{$border-radius-extra-large};
|
||||
}
|
||||
|
||||
.log-nav.active {
|
||||
background-color: var(--background-color-accent);
|
||||
color: var(--text-button-color);
|
||||
}
|
||||
}
|
||||
.clear-button {
|
||||
margin: 0 6px;
|
||||
padding: 4px 12px;
|
||||
color: var(--text-disabled);
|
||||
border-radius: #{$border-radius-large};
|
||||
&:hover {
|
||||
font-weight: 300;
|
||||
color: var(--text-color);
|
||||
background: var(--background-color-solid-gradient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.log-entry-wrapper {
|
||||
height: calc(100% - 80px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
background: var(--background-color);
|
||||
padding: 10px;
|
||||
border-radius: #{$border-radius-xlarge};
|
||||
outline: 1px solid var(--border-color);
|
||||
outline-offset: -1px;
|
||||
overflow: auto;
|
||||
|
||||
.log-entry {
|
||||
padding: 4px;
|
||||
border-radius: #{$border-radius-small};
|
||||
font-size: var(--font-size-small);
|
||||
display: flex;
|
||||
|
||||
.log-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
@include flex-center;
|
||||
}
|
||||
.log-entry-message-container {
|
||||
@include flex-space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
.message-time {
|
||||
font-size: var(--font-size-tiny);
|
||||
font-weight: 300;
|
||||
opacity: 0.8;
|
||||
text-wrap: nowrap;
|
||||
// height: 100%;
|
||||
}
|
||||
.log-entry-message {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(odd) {
|
||||
background: var(--background-color);
|
||||
}
|
||||
}
|
||||
.no-log {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.log-list-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
@@ -33,99 +113,35 @@
|
||||
scale: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
@include flex-center;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
cursor: pointer;
|
||||
border-radius: #{$border-radius-medium};
|
||||
svg {
|
||||
scale: 1.6;
|
||||
}
|
||||
&:hover {
|
||||
background: var(--background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
.log-nav-container {
|
||||
@include flex-space-between;
|
||||
align-items: flex-end;
|
||||
.log-nav-wrapper {
|
||||
.action-buttons-container {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
|
||||
.log-nav {
|
||||
padding: 8px 16px;
|
||||
border-radius: #{$border-radius-extra-large};
|
||||
}
|
||||
|
||||
.log-nav.active {
|
||||
background-color: var(--background-color-accent);
|
||||
color: var(--text-button-color);
|
||||
}
|
||||
}
|
||||
.clear-button{
|
||||
margin: 0 6px;
|
||||
padding: 4px 12px;
|
||||
color: var(--text-disabled);
|
||||
border-radius: #{$border-radius-large};
|
||||
&:hover{
|
||||
font-weight: 300;
|
||||
color: var(--text-color);
|
||||
background: var(--background-color-solid-gradient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.log-entry-wrapper {
|
||||
height: calc(100% - 80px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
background: var(--background-color);
|
||||
padding: 10px;
|
||||
border-radius: #{$border-radius-xlarge};
|
||||
outline: 1px solid var(--border-color);
|
||||
outline-offset: -1px;
|
||||
overflow: auto;
|
||||
|
||||
.log-entry {
|
||||
padding: 4px;
|
||||
border-radius: #{$border-radius-small};
|
||||
font-size: var(--font-size-small);
|
||||
display: flex;
|
||||
|
||||
.log-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
.close,
|
||||
.expand-btn {
|
||||
@include flex-center;
|
||||
}
|
||||
.log-entry-message-container {
|
||||
@include flex-space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
.message-time {
|
||||
font-size: var(--font-size-tiny);
|
||||
font-weight: 300;
|
||||
opacity: 0.8;
|
||||
text-wrap: nowrap;
|
||||
// height: 100%;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
cursor: pointer;
|
||||
border-radius: #{$border-radius-medium};
|
||||
svg {
|
||||
scale: 1.6;
|
||||
}
|
||||
.log-entry-message {
|
||||
width: 100%;
|
||||
&:hover {
|
||||
background: var(--background-color);
|
||||
border: 1px solid #ffffff29;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(odd) {
|
||||
background: var(--background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
.no-log{
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.log-list-new-window-wrapper{
|
||||
padding: 10px;
|
||||
background: var(--background-color-solid);
|
||||
height: 100vh;
|
||||
.log-nav-container{
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user