From 1db5fe57070dcd4829448d592753cfa5cfc8c428 Mon Sep 17 00:00:00 2001 From: Vishnu Date: Fri, 22 Aug 2025 11:49:11 +0530 Subject: [PATCH] feat: create portal component added to view child in new window --- .../components/icons/ExportCommonIcons.tsx | 21 ++ .../components/templates/CreateNewWindow.tsx | 182 +++++++++++++++ app/src/components/ui/log/LogList.tsx | 218 ++++++++++++------ app/src/styles/components/logs/logs.scss | 190 ++++++++------- 4 files changed, 448 insertions(+), 163 deletions(-) create mode 100644 app/src/components/templates/CreateNewWindow.tsx diff --git a/app/src/components/icons/ExportCommonIcons.tsx b/app/src/components/icons/ExportCommonIcons.tsx index 6f9a1fb..a037485 100644 --- a/app/src/components/icons/ExportCommonIcons.tsx +++ b/app/src/components/icons/ExportCommonIcons.tsx @@ -710,6 +710,27 @@ export const ExpandIcon = ({ isActive }: { isActive: boolean }) => { ); }; +export const ExpandIcon2 = () => { + return ( + + + + + ); +}; + export const StartIcon = () => { return ( ; + onClose?: () => void; + copyStyles?: boolean; + noopener?: boolean; + className?: string; + theme?: string | null; +}; + +export const RenderInNewWindow: React.FC = ({ + 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(null); + const containerElRef = useRef(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(` + + + + ${title} + + + +`); + 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); +}; diff --git a/app/src/components/ui/log/LogList.tsx b/app/src/components/ui/log/LogList.tsx index fc905dc..408764c 100644 --- a/app/src/components/ui/log/LogList.tsx +++ b/app/src/components/ui/log/LogList.tsx @@ -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 = ({ + selectedTab, + setSelectedTab, + clear, + filteredLogs, + formatTimestamp, +}) => { + return ( + <> +
+
+ {["all", "info", "warning", "error"].map((type) => ( + + ))} +
+ +
+ + {/* Log Entries */} +
+ {filteredLogs.length > 0 ? ( + filteredLogs.map((log) => ( +
+
{GetLogIcon(log.type)}
+
+
{log.message}
+
+ {formatTimestamp(log.timestamp)} +
+
+
+ )) + ) : ( +
+ There are no logs to display at the moment. +
+ )} +
+ + ); +}; + +// --- 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 -
setIsLogListVisible(false)} - > - {/* eslint-disable-next-line */} -
{ - e.stopPropagation(); - }} - > -
-
-
- -
-
Log List
-
- -
- - {/* Tabs */} -
-
- {["all", "info", "warning", "error"].map((type) => ( - - ))} -
- -
- - {/* Log Entries */} -
- {filteredLogs.length > 0 ? ( - filteredLogs.map((log) => ( -
-
{GetLogIcon(log.type)}
-
-
{log.message}
-
- {formatTimestamp(log.timestamp)} -
+
+
+
+
+
Log List
+
+
+ +
- )) - ) : ( -
- There are no logs to display at the moment.
- )} + + {/* Logs Section */} + +
-
-
+ ) : ( + setOpen(false)} + > +
+ +
+
+ )} + ); }; diff --git a/app/src/styles/components/logs/logs.scss b/app/src/styles/components/logs/logs.scss index 69d6e44..ab41771 100644 --- a/app/src/styles/components/logs/logs.scss +++ b/app/src/styles/components/logs/logs.scss @@ -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; + } +} \ No newline at end of file