diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4614b6f..4f6f185 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,13 +13,16 @@ "@tanstack/react-query-devtools": "^5.91.1", "antd": "^6.0.0", "axios": "^1.13.2", + "prismjs": "^1.30.0", "react": "^19.1.1", "react-dom": "^19.1.1", - "react-router-dom": "^7.9.4" + "react-router-dom": "^7.9.4", + "react-simple-code-editor": "^0.14.1" }, "devDependencies": { "@eslint/js": "^9.36.0", "@types/node": "^24.9.1", + "@types/prismjs": "^1.26.5", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "@types/react-router-dom": "^5.3.3", @@ -2316,6 +2319,13 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/prismjs": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", @@ -5204,6 +5214,15 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -5305,6 +5324,16 @@ "react-dom": ">=18" } }, + "node_modules/react-simple-code-editor": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.14.1.tgz", + "integrity": "sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", diff --git a/frontend/package.json b/frontend/package.json index e0eef19..4f2f4fd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,13 +17,16 @@ "@tanstack/react-query-devtools": "^5.91.1", "antd": "^6.0.0", "axios": "^1.13.2", + "prismjs": "^1.30.0", "react": "^19.1.1", "react-dom": "^19.1.1", - "react-router-dom": "^7.9.4" + "react-router-dom": "^7.9.4", + "react-simple-code-editor": "^0.14.1" }, "devDependencies": { "@eslint/js": "^9.36.0", "@types/node": "^24.9.1", + "@types/prismjs": "^1.26.5", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "@types/react-router-dom": "^5.3.3", diff --git a/frontend/src/Layout/Sidebar.tsx b/frontend/src/Layout/Sidebar.tsx index 6127c0c..2856d7b 100644 --- a/frontend/src/Layout/Sidebar.tsx +++ b/frontend/src/Layout/Sidebar.tsx @@ -3,6 +3,7 @@ import { ApartmentOutlined, ApiOutlined, HomeOutlined, InfoCircleOutlined, Setti import { Layout, Menu } from 'antd'; import React from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; +import TerminalIcon from '../icons/TerminalIcon'; import { PATHS } from '../routes'; import '../theme/layout.less'; @@ -12,6 +13,11 @@ const menuItems = [ { key: PATHS.HOME, icon: , label: 'Home' }, { key: PATHS.NETWORK, icon: , label: 'Network' }, { key: PATHS.SNIFFING, icon: , label: 'Sniffing' }, + { + key: PATHS.SCRIPTING, + icon: , + label: 'Scripting', + }, { key: '/about', icon: , label: 'About' }, { key: '/settings', icon: , label: 'Settings' }, ]; diff --git a/frontend/src/appRouter.tsx b/frontend/src/appRouter.tsx index 57c46d8..e94dabf 100644 --- a/frontend/src/appRouter.tsx +++ b/frontend/src/appRouter.tsx @@ -3,6 +3,7 @@ import { Navigate, Route, Routes } from 'react-router-dom'; import App from './App'; // your layout component (has ) import Home from './pages/Home'; import Network from './pages/Network'; +import Scripting from './pages/Scripting'; import Sniffing from './pages/Sniffing'; import { PATHS } from './routes'; @@ -18,6 +19,7 @@ export default function AppRouter() { } /> } /> } /> + } /> {/* Fallback (renders inside layout too) */} } /> diff --git a/frontend/src/components/PythonEditor.tsx b/frontend/src/components/PythonEditor.tsx new file mode 100644 index 0000000..064d0d2 --- /dev/null +++ b/frontend/src/components/PythonEditor.tsx @@ -0,0 +1,54 @@ +import Prism from 'prismjs'; +import 'prismjs/components/prism-python'; +import 'prismjs/themes/prism.css'; +import { useCallback, useMemo } from 'react'; +import Editor from 'react-simple-code-editor'; + +// Props for the editor +export type PythonEditorProps = { + value: string; + onChange: (code: string) => void; + readOnly?: boolean; + className?: string; + height?: string | number; // e.g. '200px' or 200 +}; + +export default function PythonEditor({ + value, + onChange, + readOnly = false, + height = 480, + className = '', +}: PythonEditorProps) { + const highlight = useCallback((code: string) => { + try { + return Prism.highlight(code, Prism.languages.python, 'python'); + } catch (e) { + return code; + } + }, []); + + const editorStyle = useMemo( + () => ({ + fontFamily: '"JetBrains Mono", "Fira Code", monospace', + fontSize: 14, + height: typeof height === 'number' ? `${height}px` : height, + }), + [height], + ); + + return ( +
+ onChange(code)} + highlight={highlight} + padding={12} + preClassName="language-python" + textareaId="python-editor" + style={editorStyle} + readOnly={readOnly} + /> +
+ ); +} diff --git a/frontend/src/icons/TerminalIcon.tsx b/frontend/src/icons/TerminalIcon.tsx new file mode 100644 index 0000000..4298b6b --- /dev/null +++ b/frontend/src/icons/TerminalIcon.tsx @@ -0,0 +1,57 @@ +import React, { forwardRef } from 'react'; + +export type IconProps = React.SVGProps & { + /** + * Width/height of the icon. If a number is provided it will be used as px. + * Default: 24 + */ + size?: number | string; + /** + * Icon color — will be used as the fill for paths. + * Default: 'currentColor' so color can be controlled via CSS. + */ + color?: string; + /** + * Accessible title. If provided, title will be rendered and aria-hidden will be false. + */ + title?: string; +}; + +const TerminalIcon = forwardRef( + ({ size = 24, color = 'currentColor', title, ...rest }, ref) => { + // If user passed a numeric size, treat as px + const sizeValue = typeof size === 'number' ? `${size}px` : size; + + return ( + + {title ? {title} : null} + + + + + + ); + }, +); + +TerminalIcon.displayName = 'TerminalIcon'; + +export default TerminalIcon; diff --git a/frontend/src/index.less b/frontend/src/index.less index f787941..ce08f1e 100644 --- a/frontend/src/index.less +++ b/frontend/src/index.less @@ -76,10 +76,6 @@ a:hover { /* === Code blocks / preformatted text === */ pre, code { - background-color: #f0f0f0; - color: #262626; - padding: 0.25rem 0.5rem; - border-radius: 4px; font-family: "JetBrains Mono", "Fira Code", monospace; } diff --git a/frontend/src/pages/Scripting.tsx b/frontend/src/pages/Scripting.tsx new file mode 100644 index 0000000..18bd14f --- /dev/null +++ b/frontend/src/pages/Scripting.tsx @@ -0,0 +1,33 @@ +import { Card, Tabs } from 'antd'; +import { useState } from 'react'; +import PythonEditor from '../components/PythonEditor'; + +const TwoTabsPage: React.FC = () => { + const [code, setCode] = useState("print('hello world')\n"); + + const tabItems = [ + { + key: '1', + label: 'Saved Scripts', + children: <>, + }, + { + key: '2', + label: 'Custom Script', + children: ( +
+

Python Editor

+ +
+ ), + }, + ]; + + return ( + + + + ); +}; + +export default TwoTabsPage; diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 1215334..b0b80b9 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -3,4 +3,5 @@ export const PATHS = { HOME: '/home', NETWORK: '/network', SNIFFING: '/sniffing', + SCRIPTING: '/scripting', }; \ No newline at end of file