diff --git a/app/src/app.tsx b/app/src/app.tsx index f5e19b0..0b6fd3e 100644 --- a/app/src/app.tsx +++ b/app/src/app.tsx @@ -3,23 +3,20 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import Dashboard from "./pages/Dashboard"; import Project from "./pages/Project"; import UserAuth from "./pages/UserAuth"; -import ToastProvider from "./components/templates/ToastProvider"; -import "./styles/main.scss" +import "./styles/main.scss"; +import { LoggerProvider } from "./components/ui/log/LoggerContext"; const App: React.FC = () => { return ( - + - } - /> + } /> } /> } /> - + ); }; diff --git a/app/src/components/icons/ExportCommonIcons.tsx b/app/src/components/icons/ExportCommonIcons.tsx index 6925853..4137498 100644 --- a/app/src/components/icons/ExportCommonIcons.tsx +++ b/app/src/components/icons/ExportCommonIcons.tsx @@ -814,3 +814,136 @@ export const SpeedIcon = () => { ); }; + +export const LogListIcon = () => { + return ( + + + + ); +}; + +export const LogTickIcon = () => { + return ( + + + + + + + + + + + ); +}; + +export const LogInfoIcon = () => { + return ( + + + + + + + ); +}; + +export const WarningIcon = () => { + return ( + + + + + + + ); +}; + +export const ErrorIcon = () => { + return ( + + + + ); +}; diff --git a/app/src/components/ui/footer/Footer.tsx b/app/src/components/ui/footer/Footer.tsx new file mode 100644 index 0000000..9f73284 --- /dev/null +++ b/app/src/components/ui/footer/Footer.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { HelpIcon } from "../../icons/DashboardIcon"; +import { useLogger } from "../log/LoggerContext"; +import { + LogInfoIcon, + ErrorIcon, + WarningIcon, +} from "../../icons/ExportCommonIcons"; // Adjust path as needed + +const getLogIcon = (type: string) => { + switch (type) { + case "info": + return ; + case "error": + return ; + case "warning": + return ; + case "log": + default: + return ; + } +}; + +const Footer = () => { + const { logs, setIsLogListVisible } = useLogger(); + const lastLog = logs.length > 0 ? logs[logs.length - 1] : null; + + return ( +
+
+
+
+
Selection
+
+
+
+
Rotate/Zoom
+
+
+
+
Pan/Context Menu
+
+
+ +
+
setIsLogListVisible(true)}> + {lastLog ? ( + <> + {getLogIcon(lastLog.type)} + {lastLog.message} + + ) : ( + "No logs yet." + )} +
+
+ V 0.01 +
+ +
+
+
+
+ ); +}; + +export default Footer; diff --git a/app/src/components/ui/log/LogList.tsx b/app/src/components/ui/log/LogList.tsx new file mode 100644 index 0000000..1b39e2f --- /dev/null +++ b/app/src/components/ui/log/LogList.tsx @@ -0,0 +1,88 @@ +// LogList.tsx +import React, { useState } from "react"; +import { + LogListIcon, + TickIcon, + LogInfoIcon, + WarningIcon, + ErrorIcon, + CloseIcon, +} from "../../icons/ExportCommonIcons"; // Adjust path as needed +import { useLogger } from "./LoggerContext"; + +const LogList: React.FC = () => { + const { logs, clear, setIsLogListVisible } = useLogger(); + const [selectedTab, setSelectedTab] = useState< + "all" | "info" | "warning" | "error" | "log" + >("all"); + + const getLogIcon = (type: string) => { + switch (type) { + case "info": + return ; + case "error": + return ; + case "warning": + return ; + case "log": + default: + return ; + } + }; + + const formatTimestamp = (date: Date) => new Date(date).toLocaleTimeString(); + + const filteredLogs = + selectedTab === "all" + ? [...logs].reverse() + : [...logs].filter((log) => log.type === selectedTab).reverse(); + + return ( +
+
+
+
+
+ +
+
Log List
+
+
setIsLogListVisible(false)}> + {/* */}X +
+
+ + {/* Tabs */} +
+ {["all", "info", "warning", "error"].map((type) => ( +
setSelectedTab(type as any)} + > + {`${type.charAt(0).toUpperCase() + type.slice(1)} Logs`} +
+ ))} +
+ + {/* Log Entries */} +
+ {filteredLogs.map((log) => ( +
+
+ +
+
{getLogIcon(log.type)}
+
+ [{formatTimestamp(log.timestamp)}] [{log.type.toUpperCase()}]{" "} + {log.message} +
+
+ ))} +
+
+
+ ); +}; + +export default LogList; diff --git a/app/src/components/ui/log/LoggerContext.tsx b/app/src/components/ui/log/LoggerContext.tsx new file mode 100644 index 0000000..f323b93 --- /dev/null +++ b/app/src/components/ui/log/LoggerContext.tsx @@ -0,0 +1,77 @@ +import React, { createContext, useContext, useState, useCallback } from "react"; + +export type LogType = "log" | "info" | "warning" | "error"; + +export interface LogEntry { + id: string; + type: LogType; + message: string; + timestamp: Date; +} + +interface LoggerContextValue { + logs: LogEntry[]; + setLogs: React.Dispatch>; + isLogListVisible: boolean; + setIsLogListVisible: React.Dispatch>; + log: (message: string) => void; + info: (message: string) => void; + warn: (message: string) => void; + error: (message: string) => void; + clear: () => void; +} + +const LoggerContext = createContext(undefined); + +export const LoggerProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [logs, setLogs] = useState([]); + const [isLogListVisible, setIsLogListVisible] = useState(false); + + const generateId = useCallback( + () => Math.random().toString(36).substring(2, 9), + [] + ); + + const addLog = useCallback( + (type: LogType, message: string) => { + const newLog: LogEntry = { + id: generateId(), + type, + message, + timestamp: new Date(), + }; + setLogs((prevLogs) => [...prevLogs, newLog]); + }, + [generateId] + ); + + const loggerMethods: LoggerContextValue = { + logs, + setLogs, + isLogListVisible, + setIsLogListVisible, + log: (message: string) => addLog("log", message), + info: (message: string) => addLog("info", message), + warn: (message: string) => addLog("warning", message), + error: (message: string) => addLog("error", message), + clear: () => { + setLogs([]); + }, + }; + + return ( + + {children} + + ); +}; + +export const useLogger = () => { + const context = useContext(LoggerContext); + if (!context) { + throw new Error("useLogger must be used within a LoggerProvider"); + } + return context; +}; diff --git a/app/src/components/ui/log/logger.ts b/app/src/components/ui/log/logger.ts new file mode 100644 index 0000000..1781e26 --- /dev/null +++ b/app/src/components/ui/log/logger.ts @@ -0,0 +1,71 @@ +type LogType = 'log' | 'info' | 'warning' | 'error'; + +interface LogEntry { + type: LogType; + message: string; + timestamp: Date; + context?: string; +} + +class Logger { + private static instance: Logger; + private logs: LogEntry[] = []; + private subscribers: Array<(log: LogEntry) => void> = []; + + private constructor() {} + + public static getInstance(): Logger { + if (!Logger.instance) { + Logger.instance = new Logger(); + } + return Logger.instance; + } + + private notifySubscribers(log: LogEntry) { + this.subscribers.forEach(callback => callback(log)); + } + + private addLog(type: LogType, message: string, context?: string) { + const log: LogEntry = { type, message, timestamp: new Date(), context }; + this.logs.push(log); + this.notifySubscribers(log); + + if (process.env.NODE_ENV === 'development') { + const logMessage = context ? `[${context}] ${message}` : message; + console[type === 'warning' ? 'warn' : type](logMessage); + } + } + + public log(message: string, context?: string) { + this.addLog('log', message, context); + } + + public info(message: string, context?: string) { + this.addLog('info', message, context); + } + + public warning(message: string, context?: string) { + this.addLog('warning', message, context); + } + + public error(message: string, context?: string) { + this.addLog('error', message, context); + } + + public getLogs(): LogEntry[] { + return [...this.logs]; + } + + public clear() { + this.logs = []; + } + + public subscribe(callback: (log: LogEntry) => void) { + this.subscribers.push(callback); + return () => { + this.subscribers = this.subscribers.filter(sub => sub !== callback); + }; + } +} + +export const logger = Logger.getInstance(); diff --git a/app/src/pages/Project.tsx b/app/src/pages/Project.tsx index 9bb7d0b..83e4497 100644 --- a/app/src/pages/Project.tsx +++ b/app/src/pages/Project.tsx @@ -27,9 +27,15 @@ import Scene from "../modules/scene/scene"; import { createHandleDrop } from "../modules/visualization/functions/handleUiDrop"; import { useSelectedZoneStore } from "../store/visualization/useZoneStore"; import { useFloatingWidget } from "../store/visualization/useDroppedObjectsStore"; +import { useLogger } from "../components/ui/log/LoggerContext"; +import Footer from "../components/ui/footer/Footer"; +import RenderOverlay from "../components/templates/Overlay"; +import LogList from "../components/ui/log/LogList"; const Project: React.FC = () => { let navigate = useNavigate(); + const echo = useLogger(); + const { activeModule, setActiveModule } = useModuleStore(); const { loadingProgress } = useLoadingProgress(); const { setUserName } = useUserName(); @@ -52,6 +58,7 @@ const Project: React.FC = () => { setOrganization(Organization); setUserName(name); } + echo.info("Log in success full"); } else { navigate("/"); } @@ -66,6 +73,7 @@ const Project: React.FC = () => { // collaboration store const { selectedUser } = useSelectedUserStore(); + const { isLogListVisible } = useLogger(); // real-time visualization store const { widgetSubOption } = useWidgetSubOption(); @@ -117,6 +125,12 @@ const Project: React.FC = () => { {selectedUser && } + {isLogListVisible && ( + + + + )} +